Merge branch 'beta' into chillyRecep_expri_re

This commit is contained in:
DustinLin 2024-08-06 11:36:58 -04:00
commit 082d723c94
390 changed files with 26687 additions and 27386 deletions

View File

@ -1,18 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the Feature**
<!-- A clear and concise description of what you want to happen. -->
<!-- Add a link to the feature if it is an existing move/ability/etc -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

View File

@ -0,0 +1,39 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
- type: textarea
id: relation
attributes:
label: Is your feature request related to a problem? Please describe.
description: Clear and concise description of what the problem is.
placeholder: E.g. "I'm always frustrated when [...]"
validations:
required: true
- type: markdown
attributes:
value: |
---
- type: textarea
id: description
attributes:
label: Describe the Feature
description: A clear and concise description of what you want to happen. Add a link to the feature if it is an existing move/ability/etc.
validations:
required: true
- type: markdown
attributes:
value: |
---
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: true

View File

@ -5,12 +5,12 @@
## What are the changes?
<!-- Summarize what are the changes from a user perspective on the application -->
## Why am I doing these changes?
## Why am I doing these changes the user will see?
<!-- Explain why you decided to introduce these changes -->
<!-- Does it come from an issue or another PR? Please link it -->
<!-- Explain why you believe this can enhance user experience -->
## What did change?
## What are the changes from a developer perspective?
<!-- Explicitly state what are the changes introduced by the PR -->
<!-- You can make use of a comparison between what was the state before and after your PR changes -->
@ -30,6 +30,7 @@
- [ ] The PR is self-contained and cannot be split into smaller PRs?
- [ ] Have I provided a clear explanation of the changes?
- [ ] Have I considered writing automated tests for the issue?
- [ ] If I have text, did I add placeholders for them in locales?
- [ ] Have I tested the changes (manually)?
- [ ] Are all unit tests still passing? (`npm run test`)
- [ ] Are the changes visual?

224
docs/enemy-ai.md Normal file
View File

@ -0,0 +1,224 @@
# EnemyCommandPhase: How Enemy Pokémon Decide What to Do
## Step 1: Should the Enemy Pokémon Switch?
When battling an enemy Trainer, the first decision the enemy needs to make is whether or not to switch an active Pokémon with another Pokémon in their party. This decision is primarily made by comparing **matchup scores** between each Pokémon in the enemy's party.
### Calculating Matchup Scores
The core function for matchup score calculation can be found in `src/field/pokemon.ts`, within the `Pokemon` class:
```ts
getMatchupScore(pokemon: Pokemon): number;
```
This computes the source Pokémon's matchup score against the Pokémon passed by argument using the formula
$$\text{MUScore} = (\text{atkScore}+\text{defScore}) * \text{hpDiffRatio} $$
where
- $\text{atkScore}$ is the combined effectiveness of the source Pokémon's types against the opposing Pokémon's defensive typing: $\prod_{\text{types}} \text{typeEffectiveness}(\text{type}, \text{oppPokemon})$. $\text{typeEffectiveness}$ is 1 when the type deals neutral damage to the opposing Pokémon's defensive typing, 2 when the type deals super effective damage, and so on. $atkScore$ is also increased by 25 percent if the source Pokémon has a higher Speed stat than the opposing Pokémon.
- $\text{defScore}$ is the inverse of the opposing Pokémon's $\text{atkScore}$ against the source Pokémon's defensive typing, or $(\prod_{\text{types}} \text{typeEffectiveness}(\text{type}, \text{sourcePokemon}))^{-1}$. Unlike $\text{atkScore}$, $\text{defScore}$ is capped at a maximum score of 4.
- $\text{hpDiffRatio}= \text{sourceHpRatio}-\text{oppHpRatio}+1$. This is further multiplied by 1.5 if the source Pokémon has a higher Speed stat than the opposing Pokémon; however, $\text{hpDiffRatio}$ cannot be higher than 1.
The maximum possible matchup score a Pokémon could have against a single opponent is $(16+16)\times 2=64$, which occurs when
- the Pokémon hits its opponent for 4x super effective damage with both of its types.
- the Pokémon is immune to or resists both of the opponent's types by 4x.
- the Pokémon is at max HP while the opponent's HP ratio is near zero.
In most situations, though, a Pokémon's matchup score against an opponent will be at most 16, which is equivalent to having two super effective types and resisting both of the opponent's types with the same HP ratios as before.
The minimum possible matchup score a Pokémon could have against a single opponent is near zero, which occurs when the Pokémon's HP ratio is near zero while the opponent is at max HP. However, a Pokémon's matchup score can also be very low when its type(s) are 4x weak to and/or resisted by its opponent's types.
### Determining Switches in EnemyCommandPhase
The `EnemyCommandPhase` follows this process to determine whether or not an enemy Pokémon should switch on each turn during a Trainer battle.
1. If the Pokémon has a move already queued (e.g. they are recharging after using Hyper Beam), or they are trapped (e.g. by Bind or Arena Trap), skip to resolving a `FIGHT` command (see next section).
2. For each Pokémon in the enemy's party, [compute their matchup scores](#calculating-matchup-scores) against the active player Pokémon. If there are two active player Pokémon in the battle, add their matchup scores together.
3. Take the party member with the highest matchup score and apply a multiplier to the score that reduces the score based on how frequently the enemy trainer has switched Pokémon in the current battle.
- The multiplier scales off of a counter that increments when the enemy trainer chooses to switch a Pokémon and decrements when they choose to use a move.
4. Compare the result of Step 3 with the active enemy Pokémon's matchup score. If the party member's matchup score is at least three times that of the active Pokémon, switch to that party member.
- "Boss" trainers only require the party member's matchup score to be at least two times that of the active Pokémon, so they are more likely to switch than other trainers. The full list of boss trainers in the game is as follows:
- All gym leaders, Elite 4 members, and Champions
- All Evil Team leaders
- The last three Rival Fights (on waves 95, 145, and 195)
5. If the enemy decided to switch, send a switch `turnCommand` and end this `EnemyCommandPhase`; otherwise, move on to resolving a `FIGHT` enemy command.
## Step 2: Selecting a Move
At this point, the enemy (a wild or trainer Pokémon) has decided against switching and instead will use a move from its moveset. However, it still needs to figure out which move to use and, if applicable, which target to use the move against. The logic for determining an enemy's next move and target is contained within two methods: `EnemyPokemon.getNextMove()` and `EnemyPokemon.getNextTargets()` in `src/field/pokemon.ts`.
### Choosing a Move with `getNextMove()`
In `getNextMove()`, the enemy Pokémon chooses a move to use in the following steps:
1. If the Pokémon has a move in its Move Queue (e.g. the second turn of a charging move), and the queued move is still usable, use that move against the given target.
2. Filter out any moves it can't use within its moveset. The remaining moves make up the enemy's **move pool** for the turn.
1. A move can be unusable if it has no PP left or it has been disabled by another move or effect
2. If the enemy's move pool is empty, use Struggle.
3. Calculate the **move score** of each move in the enemy's move pool.
1. A move's move score is equivalent to the move's maximum **target score** among all of the move's possible targets on the field ([more on this later](#calculating-move-and-target-scores)).
2. A move's move score is set to -20 if at least one of these conditions are met:
- The move is unimplemented (or, more precisely, the move's name ends with "&nbsp;(N)").
- Conditions for the move to succeed are not met (unless the move is Sucker Punch, Upper Hand, or Thunderclap, as those moves' conditions can't be resolved before the turn starts).
- The move's target scores are 0 or `NaN` for each target. In this case, the game assumes the target score calculation for that move is unimplemented.
4. Sort the move pool in descending order of move scores.
5. From here, the enemy's move selection varies based on its `aiType`. If the enemy is a Boss Pokémon or has a Trainer, it uses the `SMART` AI type; otherwise, it uses the `SMART_RANDOM` AI type.
1. Let $m_i$ be the *i*-th move in the sorted move pool $M$:
- If `aiType === SMART_RANDOM`, the enemy has a 5/8 chance of selecting $m_0$ and a 3/8 chance of advancing to the next best move $m_1$, where it then repeats this roll. This process stops when a move is selected or the last move in the move pool is reached.
- If `aiType === SMART`, a similar loop is used to decide between selecting the move $m_i$ and advancing to the next iteration with the move $m_{i+1}$. However, instead of using a flat probability, the following conditions need to be met to advance from selecting $m_i$ to $m_{i+1}$:
- $\text{sign}(s_i) = \text{sign}(s_{i+1})$, where $s_i$ is the move score of $m_i$.
- $\text{randInt}(0, 100) < \text{round}(\frac{s_{i+1}}{s_i}\times 50)$. In other words: if the scores of $m_i$ and $m_{i+1}$ have the same sign, the chance to advance to the next iteration with $m_{i+1}$ is proportional to how close the scores are to each other. The probability to advance to the next iteration is at most 50 percent (when $s_i$ and $s_{i+1}$ are equal).
6. The enemy will use the move selected in Step 5 against the target(s) with the highest [**target selection score (TSS)**](#choosing-targets-with-getnexttargets)
### Calculating Move and Target Scores
As part of the move selection process, the enemy Pokémon must compute a **target score (TS)** for each legal target for each move in its move pool. The base target score for all moves is a combination of the move's **user benefit score (UBS)** and **target benefit score (TBS)**.
![equation](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BTS%7D=%5Ctext%7BUBS%7D&plus;%5Ctext%7BTBS%7D%5Ctimes%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D-1&%5Ctext%7Bif%20target%20is%20an%20opponent%7D%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
A move's UBS and TBS are computed with the respective functions in the `Move` class:
```ts
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer;
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer;
```
Logically, these functions are very similar &ndash; they add up their respective benefit scores from each of the move's attributes (as determined by `attr.getUserBenefitScore`, and `attr.getTargetBenefitScore`, respectively) and return the total benefit score. However, there are two key functional differences in how the UBS and TBS of a move are handled:
1. In addition to influencing move selection, a move's TBS also influences target selection for that move, whereas UBS has no influence.
2. When evaluating the target score of a move against an opposing Pokémon, the move's TBS is multiplied by -1, whereas the move's UBS does not change. For this reason, move attributes return negative values for their TBS to reward using the move against an enemy.
#### Calculating Target Benefit Score (TBS) for Attack Moves
In addition to the base score from `Move.getTargetBenefitScore()`, attack moves calculate an `attackScore` which influences the move's TBS based on the following properties:
- The move's power (after the move's `VariablePowerAttrs` are applied)
- The move's type effectiveness against the target (note that this also accounts for type immunities from abilities such as Levitate and field effects such as Strong Winds).
- The move's category (Physical/Special), and whether the user has a higher Attack or Special Attack stat.
More specifically, the following steps are taken to compute the move's `attackScore`:
1. Compute a multiplier based on the move's type effectiveness:
![typeMultEqn](https://latex.codecogs.com/png.image?%5Cdpi%7B110%7D%5Cbg%7Bwhite%7D%5Ctext%7BtypeMult%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D2&&%5Ctext%7Bif%20move%20is%20super%20effective(or%20better)%7D%5C%5C-2&&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
2. Compute a multiplier based on the move's category and the user's offensive stats:
1. Compute the user's offensive stat ratio:
![statRatioEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BstatRatio%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%5Cfrac%7B%5Ctext%7BuserSpAtk%7D%7D%7B%5Ctext%7BuserAtk%7D%7D&%5Ctext%7Bif%20move%20is%20physical%7D%5C%5C%5Cfrac%7B%5Ctext%7BuserAtk%7D%7D%7B%5Ctext%7BuserSpAtk%7D%7D&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
2. Compute the stat-based multiplier:
![statMultEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BstatMult%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D2&%5Ctext%7Bif%20statRatio%7D%5Cle%200.75%5C%5C1.5&%5Ctext%7Bif%5C;%7D0.75%5Cle%5Ctext%7BstatRatio%7D%5Cle%200.875%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
3. Calculate the move's `attackScore`:
$\text{attackScore} = (\text{typeMult}\times \text{statMult})+\lfloor \frac{\text{power}}{5} \rfloor$
The maximum total multiplier in `attackScore` ($\text{typeMult}\times \text{statMult}$) is 4, which occurs for attacks that are super effective against the target and are categorically aligned with the user's offensive stats (e.g. the move is physical, and the user has much higher Attack than Sp. Atk). The minimum total multiplier of -4 occurs (somewhat confusingly) for attacks that are not super effective but are categorically aligned with the user's offensive stats.
The attack move's total TBS, then, is $\text{TBS}=\text{baseScore}-\text{attackScore}$, where $\text{baseScore}$ is the result of `Move.getTargetBenefitScore()`.
#### Calculating Target Score (TS) for Attack Moves
The final step to calculate an attack move's target score (TS) is to multiply the base target score by the move's type effectiveness and STAB (if it applies):
- If the target is an enemy, the corresponding TS is multiplied by the move's type effectiveness against the enemy (e.g. 2 if the move is super effective), then by 1.5 if the move shares a type with the user.
- If the target is an ally, the TS is divided by these factors instead.
- If $\text{TS}=0$ after these multipliers are applied, the TS is set to -20 for the current target.
### Choosing Targets with `getNextTargets()`
The enemy's target selection for single-target moves works in a very similar way to its move selection. Each potential target is given a **target selection score (TSS)** which is based on the move's [target benefit score](#calculating-move-and-target-scores) for that target:
![TSSEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BTSS%7D=%5Ctext%7BTBS%7D%5Ctimes%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D-1&%5Ctext%7Bif%20target%20is%20an%20opponent%7D%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
Once the TSS is calculated for each target, the target is selected as follows:
1. Sort the targets (indexes) in decreasing order of their target selection scores (or weights). Let $t_i$ be the index of the *i*-th target in the sorted list, and let $w_i$ be that target's corresponding TSS.
2. Normalize the weights. Let $w_n$ be the lowest-weighted target in the sorted list, then:
![normWeightEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7DW_i=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7Dw_i&plus;%7Cw_n%7C&%5Ctext%7Bif%5C;%7Dw_n%5C;%5Ctext%7Bis%20negative%7D%5C%5Cw_i&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.)
3. Remove all weights from the list such that $W_i < \frac{W_0}{2}$
4. Generate a random integer $R=\text{rand}(0, W_{\text{total}})$ where $W_{\text{total}}$ is the sum of all the remaining weights after Step 3.
5. For each target $(t_i, W_i)$,
1. if $R \le \sum_{j=0}^{i} W_i$, or if $t_i$ is the last target in the list, **return** $t_i$
2. otherwise, advance to the next target $t_{i+1}$ and repeat this check.
Once the target is selected, the enemy has successfully determined its next action for the turn, and its corresponding `EnemyCommandPhase` ends. From here, the `TurnStartPhase` processes the enemy's commands alongside the player's commands and begins to resolve the turn.
## An Example in Battle
Suppose you enter a single battle against an enemy trainer with the following Pokémon in their party:
1. An [Excadrill](https://bulbapedia.bulbagarden.net/wiki/Excadrill_(Pok%C3%A9mon)) with the Ability Sand Force and the following moveset
1. Earthquake
2. Iron Head
3. Crush Claw
4. Swords Dance
2. A [Heatmor](https://bulbapedia.bulbagarden.net/wiki/Heatmor_(Pok%C3%A9mon)) with the Ability Flash Fire and the following moveset
1. Fire Lash
2. Inferno
3. Hone Claws
4. Shadow Claw
The enemy trainer leads with their Heatmor, and you lead with a [Dachsbun](https://bulbapedia.bulbagarden.net/wiki/Dachsbun_(Pok%C3%A9mon)) with the Ability Well-Baked Body. We'll cover the enemy's behavior over the next two turns.
### Turn 1
To determine whether the enemy should switch Pokémon, it first calculates each party member's matchup scores against the player's Dachsbun:
$$\text{MUScore} = (\text{atkScore}+\text{defScore}) * \text{hpDiffRatio} $$
- Defensively, Heatmor's Fire typing resists Dachsbun's Fairy typing, so its `defScore` is 2. However, because of Dachsbun's Fire immunity granted by Well-Baked Body, Heatmor's `atkScore` against Dachsbun is 0. With both Pokémon at maximum HP, Heatmor's total matchup score is 2.
- Excadrill's Steel typing also resists Fairy, so its `defScore` is also 2. In this case, though, Steel is also super effective against Fairy, so Excadrill's base `atkScore` is 2. If Excadrill outspeeds Dachsbun (possibly due to it having a +Spd nature or holding a Carbos), its `atkScore` is further increased to 2.5. Since both Pokémon are at maximum HP, Excadrill's total matchup score is 4 (or 4.5 if it outspeeds).
Based on the enemy party's matchup scores, whether or not the trainer switches out Heatmor for Excadrill depends on the trainer's type. The difference in matchup scores is enough to cause a switch to Excadrill for boss trainers (e.g. gym leaders) but not for regular trainers. For this example, we'll assume the trainer is a boss and, therefore, decides to switch to Excadrill on this turn.
### Turn 2
Now that the enemy Pokémon with the best matchup score is on the field (assuming it survives Dachsbun's attack on the last turn), the enemy will now decide to have Excadrill use one of its moves. Assuming all of its moves are usable, we'll go through the target score calculations for each move:
- **Earthquake**: In a single battle, this move is just a 100-power Ground-type physical attack with no additional effects. With no additional benefit score from attributes, the move's base target score against the player's Dachsbun is just the `attackScore` from `AttackMove.getTargetBenefitScore()`. In this case, Earthquake's `attackScore` is given by
$\text{attackScore}=(\text{typeMult}\times \text{statMult}) + \lfloor \frac{\text{power}}{5} \rfloor = -2\times 2 + 20 = 16$
Here, `typeMult` is -2 because the move is not super effective, and `statMult` is 2 because Excadrill's Attack is significantly higher than its Sp. Atk. Accounting for STAB thanks to Excadrill's typing, the final target score for this move is **24**
- **Iron Head**: This move is an 80-power Steel-type physical attack with an additional chance to cause the target to flinch. With these properties, Iron Head has a user benefit score of 0 and a target benefit score given by
$\text{TBS}=\text{getTargetBenefitScore(FlinchAttr)}-\text{attackScore}$
Under its current implementation, the target benefit score of `FlinchAttr` is -5. Calculating the move's `attackScore`, we get:
$\text{attackScore}=(\text{typeMult}\times \text{statMult}) + \lfloor \frac{\text{power}}{5} \rfloor = 2\times 2 + 16 = 20$
Note that `typeMult` in this case is 2 because Iron Head is super effective (or better) against Dachsbun. With the move's UBS at 0, the base target score calculation against Dachsbun simplifies to
$\text{TS}=-\text{TBS}=-(-5-20)=25$
We then need to apply a 2x multiplier for the move's type effectiveness and a 1.5x multiplier since STAB applies. After applying these multipliers, the final score for this move is **75**.
- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to
$\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$
where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score).
- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
$\text{TBS}=\text{getTargetBenefitScore(StatChangeAttr)}-\text{attackScore}$
$\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$
$\text{TS}=-\text{TBS}=13$
This move is neutral against Dachsbun and isn't boosted by STAB from Excadrill, so we don't need to apply any extra multipliers. The final score for this move is **13**.
We now have a sorted move pool in decreasing order of move scores:
1. Iron Head (**75**)
2. Earthquake (**24**)
3. Crush Claw (**13**)
4. Swords Dance (**6**)
Since no other score is at least half that of Iron Head's score, the enemy AI automatically chooses to use Iron Head against Dachsbun at this point.
## Guidelines for Implementing Benefit Scores
When implementing a new move attribute, it's important to override `MoveAttr`'s `getUserBenefitScore` and `getTargetBenefitScore` functions to ensure that the enemy AI can accurately determine when and how to use moves with that attribute. Here are a few basic specifications you should adhere to when implementing benefit scores for a new attribute:
- A move's **user benefit score (UBS)** incentivizes (or discourages) the move's usage in general. A positive UBS gives the move more incentive to be used, while a negative UBS gives the move less incentive.
- A move's **target benefit score (TBS)** incentivizes (or discourages) the move's usage on a specific target. A positive TBS indicates the move is better used on the user or its allies, while a negative TBS indicates the move is better used on enemies.
- **The total benefit score (UBS + TBS) of a move should never be 0.** The move selection algorithm assumes the move's benefit score is unimplemented if the total score is 0 and penalizes the move's usage as a result. With status moves especially, it's important to have some form of implementation among the move's attributes to avoid this scenario.
- **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making.

View File

@ -1,6 +1,7 @@
import tseslint from '@typescript-eslint/eslint-plugin';
import stylisticTs from '@stylistic/eslint-plugin-ts'
import parser from '@typescript-eslint/parser';
import imports from 'eslint-plugin-import';
// import imports from 'eslint-plugin-import'; // Disabled due to not being compatible with eslint v9
export default [
{
@ -10,7 +11,8 @@ export default [
parser: parser
},
plugins: {
imports: imports.configs.recommended,
// imports: imports.configs.recommended // Disabled due to not being compatible with eslint v9
'@stylistic/ts': stylisticTs,
'@typescript-eslint': tseslint
},
rules: {
@ -25,12 +27,12 @@ export default [
"ignoreRestSiblings": true // Allows unused variables that are part of a rest property in object destructuring. Useful for excluding certain properties from an object while using the rest.
}],
"eol-last": ["error", "always"], // Enforces at least one newline at the end of files
"@typescript-eslint/semi": ["error", "always"], // Requires semicolons for TypeScript-specific syntax
"@stylistic/ts/semi": ["error", "always"], // Requires semicolons for TypeScript-specific syntax
"semi": "off", // Disables the general semi rule for TypeScript files
"no-extra-semi": ["error"], // Disallows unnecessary semicolons for TypeScript-specific syntax
"brace-style": "off", // Note: you must disable the base rule as it can report incorrect errors
"curly": ["error", "all"], // Enforces the use of curly braces for all control statements
"@typescript-eslint/brace-style": ["error", "1tbs"],
"@stylistic/ts/brace-style": ["error", "1tbs"],
"no-trailing-spaces": ["error", { // Disallows trailing whitespace at the end of lines
"skipBlankLines": false, // Enforces the rule even on blank lines
"ignoreComments": false // Enforces the rule on lines containing comments

View File

@ -33,6 +33,7 @@ body {
display: flex;
justify-content: space-around;
}
#app {
display: flex;
justify-content: center;
@ -206,6 +207,17 @@ input:-internal-autofill-selected {
}
}
#tnc-links {
font-size: xx-small;
position: relative;
}
a {
color: #328cea;
margin-right: 4px;
margin-left: 4px;
}
/* Firefox old*/
@-moz-keyframes blink {
0% {

View File

@ -114,15 +114,15 @@
</div>
</div>
</div>
<div id="tnc-links">
<a href="#" class="termly-display-preferences" style="display: none;" target="_blank" rel="noreferrer noopener">Consent Preferences</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=bc96778b-3f04-4d25-bafc-0deba53e8bec" target="_blank" rel="noreferrer noopener">Privacy Policy</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=8b523c05-7ec2-4646-9534-5bd61b386e2a" target="_blank" rel="noreferrer noopener">Cookie Disclaimer</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=b01e092a-9721-477f-8356-45576702ff9e" target="_blank" rel="noreferrer noopener">Terms & Conditions</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=3b5d1928-3f5b-4ee1-b8df-2d6c276b0bcc" target="_blank" rel="noreferrer noopener">Acceptable Use Policy</a>
</div>
<script type="module" src="./src/main.ts"></script>
<script src="./src/touch-controls.ts" type="module"></script>
<script src="./src/debug.js" type="module"></script>
<div id="links">
<a href="#" class="termly-display-preferences" style="display: none;">Consent Preferences</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=bc96778b-3f04-4d25-bafc-0deba53e8bec">Privacy Policy</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=8b523c05-7ec2-4646-9534-5bd61b386e2a">Cookie Disclaimer</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=b01e092a-9721-477f-8356-45576702ff9e">Terms & Conditions</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=3b5d1928-3f5b-4ee1-b8df-2d6c276b0bcc">Acceptable Use Policy</a>
</div>
</body>
</html>

6319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,34 +21,29 @@
},
"devDependencies": {
"@eslint/js": "^9.3.0",
"@hpcc-js/wasm": "^2.18.0",
"@stylistic/eslint-plugin-ts": "^2.6.0-beta.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.12.13",
"@typescript-eslint/eslint-plugin": "^7.10.0",
"@typescript-eslint/parser": "^7.10.0",
"@vitest/coverage-istanbul": "^1.4.0",
"axios": "^1.6.2",
"axios-cache-interceptor": "^1.3.2",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"@typescript-eslint/eslint-plugin": "^8.0.0-alpha.54",
"@typescript-eslint/parser": "^8.0.0-alpha.54",
"@vitest/coverage-istanbul": "^2.0.4",
"dependency-cruiser": "^16.3.10",
"eslint": "^9.7.0",
"jsdom": "^24.0.0",
"json-beautify": "^1.1.1",
"lefthook": "^1.6.12",
"phaser3spectorjs": "^0.0.8",
"pokenode-ts": "^1.20.0",
"typedoc": "^0.26.4",
"typescript": "^5.5.3",
"typescript-eslint": "^7.10.0",
"vite": "^4.5.0",
"vite-plugin-fs": "^0.4.4",
"typescript-eslint": "^8.0.0-alpha.54",
"vite": "^5.3.5",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.4.0",
"vitest": "^2.0.4",
"vitest-canvas-mock": "^0.3.3"
},
"dependencies": {
"@hpcc-js/wasm": "^2.16.2",
"@material/material-color-utilities": "^0.2.7",
"@types/jsdom": "^21.1.7",
"crypto-js": "^4.2.0",
"dependency-cruiser": "^16.3.3",
"i18next": "^23.11.1",
"i18next-browser-languagedetector": "^7.2.1",
"i18next-korean-postposition-processor": "^1.0.0",

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,272 +1,140 @@
{
"textures": [
"textures":[
{
"image": "1001.png",
"format": "RGBA8888",
"size": {
"w": 237,
"h": 237
},
"size": { "w": 161, "h": 157 },
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 79,
"h": 79
},
"frame": {
"x": 0,
"y": 0,
"w": 79,
"h": 79
}
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0002.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 1,
"w": 79,
"h": 78
},
"frame": {
"x": 79,
"y": 0,
"w": 79,
"h": 78
}
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 1,
"w": 79,
"h": 78
},
"frame": {
"x": 79,
"y": 0,
"w": 79,
"h": 78
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0003.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 3,
"w": 79,
"h": 76
},
"frame": {
"x": 158,
"y": 0,
"w": 79,
"h": 76
}
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 3,
"w": 79,
"h": 76
},
"frame": {
"x": 158,
"y": 0,
"w": 79,
"h": 76
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0004.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 79,
"h": 75
},
"frame": {
"x": 158,
"y": 76,
"w": 79,
"h": 75
}
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 79,
"h": 75
},
"frame": {
"x": 158,
"y": 76,
"w": 79,
"h": 75
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0005.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 79,
"h": 74
},
"frame": {
"x": 79,
"y": 78,
"w": 79,
"h": 74
}
},
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 79,
"h": 74
},
"frame": {
"x": 79,
"y": 78,
"w": 79,
"h": 74
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
{
"filename": "0006.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 6,
"w": 79,
"h": 73
},
"frame": {
"x": 0,
"y": 79,
"w": 79,
"h": 73
}
},
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 6,
"w": 79,
"h": 73
},
"frame": {
"x": 0,
"y": 79,
"w": 79,
"h": 73
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
{
"filename": "0007.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
"spriteSourceSize": {
"x": 0,
"y": 7,
"w": 79,
"h": 72
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
"frame": {
"x": 158,
"y": 151,
"w": 79,
"h": 72
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0013.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
},
{
"filename": "0014.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
},
{
"filename": "0015.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
},
{
"filename": "0016.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:2873276355a5ff8fe57d616764a11cb5:5516cfd39964108d480df461b020785f:c8a3fc07f857e38a4f887e43523aab92$"
]}],
"meta": {"app": "https://www.aseprite.org/","version": "1.3.7-x64"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,272 +1,140 @@
{
"textures": [
"textures":[
{
"image": "1001.png",
"format": "RGBA8888",
"size": {
"w": 237,
"h": 237
},
"size": { "w": 161, "h": 157 },
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 79,
"h": 79
},
"frame": {
"x": 0,
"y": 0,
"w": 79,
"h": 79
}
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0002.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 1,
"w": 79,
"h": 78
},
"frame": {
"x": 79,
"y": 0,
"w": 79,
"h": 78
}
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 1,
"w": 79,
"h": 78
},
"frame": {
"x": 79,
"y": 0,
"w": 79,
"h": 78
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0003.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 3,
"w": 79,
"h": 76
},
"frame": {
"x": 158,
"y": 0,
"w": 79,
"h": 76
}
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 3,
"w": 79,
"h": 76
},
"frame": {
"x": 158,
"y": 0,
"w": 79,
"h": 76
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0004.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 79,
"h": 75
},
"frame": {
"x": 158,
"y": 76,
"w": 79,
"h": 75
}
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 79,
"h": 75
},
"frame": {
"x": 158,
"y": 76,
"w": 79,
"h": 75
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 79 },
"frame": { "x": 80, "y": 78, "w": 79, "h": 79 }
},
{
"filename": "0005.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 79,
"h": 74
},
"frame": {
"x": 79,
"y": 78,
"w": 79,
"h": 74
}
},
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 79,
"h": 74
},
"frame": {
"x": 79,
"y": 78,
"w": 79,
"h": 74
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
{
"filename": "0006.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 6,
"w": 79,
"h": 73
},
"frame": {
"x": 0,
"y": 79,
"w": 79,
"h": 73
}
},
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
},
"spriteSourceSize": {
"x": 0,
"y": 6,
"w": 79,
"h": 73
},
"frame": {
"x": 0,
"y": 79,
"w": 79,
"h": 73
}
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
{
"filename": "0007.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 79,
"h": 79
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
"spriteSourceSize": {
"x": 0,
"y": 7,
"w": 79,
"h": 72
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 80, "h": 78 },
"frame": { "x": 0, "y": 79, "w": 80, "h": 78 }
},
"frame": {
"x": 158,
"y": 151,
"w": 79,
"h": 72
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 1, "w": 81, "h": 78 },
"frame": { "x": 80, "y": 0, "w": 81, "h": 78 }
},
{
"filename": "0013.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
},
{
"filename": "0014.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
},
{
"filename": "0015.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
},
{
"filename": "0016.png",
"rotated": false,
"trimmed": true,
"sourceSize": { "w": 81, "h": 79 },
"spriteSourceSize": { "x": 0, "y": 0, "w": 80, "h": 79 },
"frame": { "x": 0, "y": 0, "w": 80, "h": 79 }
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:83b74b0673ef1ae8346efab60ae89872:f76c930177bf7a296d3f47eb6294ae4f:c8a3fc07f857e38a4f887e43523aab92$"
]}],
"meta": {"app": "https://www.aseprite.org/","version": "1.3.7-x64"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 303 B

View File

Before

Width:  |  Height:  |  Size: 342 B

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

View File

Before

Width:  |  Height:  |  Size: 846 B

After

Width:  |  Height:  |  Size: 846 B

View File

Before

Width:  |  Height:  |  Size: 938 B

After

Width:  |  Height:  |  Size: 938 B

View File

Before

Width:  |  Height:  |  Size: 510 B

After

Width:  |  Height:  |  Size: 510 B

View File

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 513 B

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 541 B

View File

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

View File

Before

Width:  |  Height:  |  Size: 697 B

After

Width:  |  Height:  |  Size: 697 B

View File

Before

Width:  |  Height:  |  Size: 661 B

After

Width:  |  Height:  |  Size: 661 B

View File

@ -3265,6 +3265,11 @@
1,
1
],
"217": [
1,
1,
1
],
"229": [
0,
1,
@ -6658,6 +6663,11 @@
1,
1
],
"217": [
1,
1,
1
],
"229": [
0,
1,

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,38 @@
{
"0": {
"422919": "112114",
"7b7b8c": "7b7b8c",
"101010": "101010",
"945221": "2f6324",
"ffffff": "ffffff",
"634229": "1d3d26",
"b5b5bd": "b5b5bd",
"f7c563": "fecd85",
"c59c4a": "cd9343",
"dedede": "dedede"
},
"1": {
"422919": "2d0e1f",
"7b7b8c": "7b7b8c",
"101010": "101010",
"945221": "8c2a37",
"ffffff": "ffffff",
"634229": "6b1d38",
"b5b5bd": "b5b5bd",
"f7c563": "f2cab8",
"c59c4a": "c48e81",
"dedede": "dedede"
},
"2": {
"422919": "111433",
"7b7b8c": "7b7b8c",
"101010": "101010",
"945221": "323760",
"ffffff": "ffffff",
"634229": "1e2249",
"b5b5bd": "b5b5bd",
"f7c563": "5ccaf2",
"c59c4a": "45a2f9",
"dedede": "dedede"
}
}

View File

@ -0,0 +1,50 @@
{
"0": {
"7b7b8c": "7b7b8c",
"101010": "101010",
"634229": "1d3d26",
"ffffff": "ffffff",
"945221": "2f6324",
"422919": "112114",
"dedede": "dedede",
"bd7342": "6a8a46",
"ffef84": "f7ffa5",
"b5b5bd": "b5b5bd",
"c59c4a": "ceb552",
"f7c563": "f7de7b",
"841931": "a52942",
"de3a5a": "ef526b"
},
"1": {
"7b7b8c": "7b7b8c",
"101010": "101010",
"634229": "6b1d38",
"ffffff": "ffffff",
"945221": "8c2a37",
"422919": "2d0e1f",
"dedede": "dedede",
"bd7342": "b74543",
"ffef84": "f9eddb",
"b5b5bd": "b5b5bd",
"c59c4a": "c48e81",
"f7c563": "f2cab8",
"841931": "841931",
"de3a5a": "de3a5a"
},
"2": {
"7b7b8c": "7b7b8c",
"101010": "101010",
"634229": "1e2249",
"ffffff": "ffffff",
"945221": "323760",
"422919": "111433",
"dedede": "dedede",
"bd7342": "46527a",
"ffef84": "adf2f7",
"b5b5bd": "b5b5bd",
"c59c4a": "45a2f9",
"f7c563": "5ccaf2",
"841931": "a52942",
"de3a5a": "ef526b"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "aqua_admin_f.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:efd07ff3ed1e610150a4b8ca18974343:d9b85b9eb11182e9e4669e2bd8b08694:72b7b50231708a9486d5f315824e4df1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "aqua_admin_m.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:831f5748dad92911b10a1cb358ee2dae:a3bf81bbaa3b49cad5e0e549cf94563b:bb6befc9383c9c08837183ae2a7a80c1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "flare_admin_f.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:831f5748dad92911b10a1cb358ee2dae:a3bf81bbaa3b49cad5e0e549cf94563b:bb6befc9383c9c08837183ae2a7a80c1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "flare_admin_m.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:c30bf82452209a923f4becf13d275a9a:a6355b09f92c9c0388d0b919010f587f:0638dbf213f8a974eb5af76eb1e5ddeb$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "galactic_admin_f.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:831f5748dad92911b10a1cb358ee2dae:a3bf81bbaa3b49cad5e0e549cf94563b:bb6befc9383c9c08837183ae2a7a80c1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "galactic_admin_m.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:3012867f03f02c4ee67a8ab3ad5a000e:77a5f60f1adc158664b3b2ee17bf30fe:7e8259b5177c0a76e5d02d6bdc66affe$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "magma_admin_f.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:831f5748dad92911b10a1cb358ee2dae:a3bf81bbaa3b49cad5e0e549cf94563b:bb6befc9383c9c08837183ae2a7a80c1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "magma_admin_m.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:f63ad48affc076f60fae78992c96a2bf:80928b32710abcb28c07c6fc5a425d99:3b961d8852b62aaf24ceb2030c036515$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "rocket_admin_f.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:831f5748dad92911b10a1cb358ee2dae:a3bf81bbaa3b49cad5e0e549cf94563b:bb6befc9383c9c08837183ae2a7a80c1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "rocket_admin_m.png",
"format": "RGBA8888",
"size": {
"w": 80,
"h": 80
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
},
"frame": {
"x": 0,
"y": 0,
"w": 80,
"h": 80
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:831f5748dad92911b10a1cb358ee2dae:a3bf81bbaa3b49cad5e0e549cf94563b:bb6befc9383c9c08837183ae2a7a80c1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

View File

@ -1,4 +1,4 @@
import { enConfig } from "#app/locales/en/config.js";
import { type enConfig } from "#app/locales/en/config.js";
// Module declared to make referencing keys in the localization files type-safe.
declare module "i18next" {

View File

@ -1,6 +1,6 @@
import Phaser from "phaser";
import UI from "./ui/ui";
import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase } from "./phases";
import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase, SummonPhase, ToggleDoublePositionPhase } from "./phases";
import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon";
import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species";
import { Constructor } from "#app/utils";
@ -14,7 +14,7 @@ import { Arena, ArenaBase } from "./field/arena";
import { GameData } from "./system/game-data";
import { TextStyle, addTextObject, getTextColor } from "./ui/text";
import { allMoves } from "./data/move";
import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getPartyLuckValue } from "./modifier/modifier-type";
import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, modifierTypes } from "./modifier/modifier-type";
import AbilityBar from "./ui/ability-bar";
import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability";
import { allAbilities } from "./data/ability";
@ -68,6 +68,8 @@ import { UiTheme } from "#enums/ui-theme";
import { TimedEventManager } from "#app/timed-event-manager.js";
import i18next from "i18next";
import {TrainerType} from "#enums/trainer-type";
import { battleSpecDialogue } from "./data/dialogue";
import { LoadingScene } from "./loading-scene";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -230,7 +232,7 @@ export default class BattleScene extends SceneBase {
private fieldOverlay: Phaser.GameObjects.Rectangle;
private shopOverlay: Phaser.GameObjects.Rectangle;
private shopOverlayShown: boolean = false;
private shopOverlayOpacity: number = .80;
private shopOverlayOpacity: number = .8;
public modifiers: PersistentModifier[];
private enemyModifiers: PersistentModifier[];
@ -319,6 +321,7 @@ export default class BattleScene extends SceneBase {
}
create() {
this.scene.remove(LoadingScene.KEY);
initGameSpeed.apply(this);
this.inputController = new InputsController(this);
this.uiInputs = new UiInputs(this, this.inputController);
@ -369,7 +372,7 @@ export default class BattleScene extends SceneBase {
this.fieldUI = fieldUI;
const transition = (this.make as any).rexTransitionImagePack({
const transition = this.make.rexTransitionImagePack({
x: 0,
y: 0,
scale: 6,
@ -377,11 +380,14 @@ export default class BattleScene extends SceneBase {
origin: { x: 0, y: 0 }
}, true);
//@ts-ignore (the defined types in the package are incromplete...)
transition.transit({
mode: "blinds",
ease: "Cubic.easeInOut",
duration: 1250,
oncomplete: () => transition.destroy()
});
transition.once("complete", () => {
transition.destroy();
});
this.add.existing(transition);
@ -1056,7 +1062,7 @@ export default class BattleScene extends SceneBase {
playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, doubleChance));
doubleTrainer = !Utils.randSeedInt(doubleChance.value);
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance
if (trainerConfigs[trainerType].trainerTypeDouble && !(trainerType === TrainerType.TATE || trainerType === TrainerType.LIZA)) {
if (trainerConfigs[trainerType].trainerTypeDouble && ![ TrainerType.TATE, TrainerType.LIZA ].includes(trainerType)) {
doubleTrainer = false;
}
}
@ -1133,7 +1139,7 @@ export default class BattleScene extends SceneBase {
}
if (resetArenaState) {
this.arena.resetArenaEffects();
playerField.forEach((_, p) => this.unshiftPhase(new ReturnPhase(this, p)));
playerField.forEach((_, p) => this.pushPhase(new ReturnPhase(this, p)));
for (const pokemon of this.getParty()) {
// Only trigger form change when Eiscue is in Noice form
@ -1146,7 +1152,7 @@ export default class BattleScene extends SceneBase {
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
}
this.unshiftPhase(new ShowTrainerPhase(this));
this.pushPhase(new ShowTrainerPhase(this));
}
for (const pokemon of this.getParty()) {
@ -1786,8 +1792,10 @@ export default class BattleScene extends SceneBase {
return 13.950;
case "battle_johto_champion": //B2W2 Johto Champion Battle
return 23.498;
case "battle_hoenn_champion": //B2W2 Hoenn Champion Battle
case "battle_hoenn_champion_g5": //B2W2 Hoenn Champion Battle
return 11.328;
case "battle_hoenn_champion_g6": //ORAS Hoenn Champion Battle
return 11.762;
case "battle_sinnoh_champion": //B2W2 Sinnoh Champion Battle
return 12.235;
case "battle_champion_alder": //BW Unova Champion Battle
@ -2619,4 +2627,33 @@ export default class BattleScene extends SceneBase {
};
(window as any).gameInfo = gameInfo;
}
/**
* Initialized the 2nd phase of the final boss (e.g. form-change for Eternatus)
* @param pokemon The (enemy) pokemon
*/
initFinalBossPhaseTwo(pokemon: Pokemon): void {
if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) {
this.fadeOutBgm(Utils.fixedInt(2000), false);
this.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, null, () => {
this.addEnemyModifier(getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as PersistentModifier, false, true);
pokemon.generateAndPopulateMoveset(1);
this.setFieldScale(0.75);
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);
this.currentBattle.double = true;
const availablePartyMembers = this.getParty().filter((p) => p.isAllowedInBattle());
if (availablePartyMembers.length > 1) {
this.pushPhase(new ToggleDoublePositionPhase(this, true));
if (!availablePartyMembers[1].isOnField()) {
this.pushPhase(new SummonPhase(this, 1));
}
}
this.shiftPhase();
});
return;
}
this.shiftPhase();
}
}

View File

@ -294,7 +294,7 @@ export default class Battle {
if (pokemon.species.speciesId === Species.TAPU_KOKO || pokemon.species.speciesId === Species.TAPU_LELE || pokemon.species.speciesId === Species.TAPU_BULU || pokemon.species.speciesId === Species.TAPU_FINI) {
return "battle_legendary_tapu";
}
if (pokemon.species.speciesId === Species.COSMOG || pokemon.species.speciesId === Species.COSMOEM || pokemon.species.speciesId === Species.SOLGALEO || pokemon.species.speciesId === Species.LUNALA) {
if ([ Species.COSMOG, Species.COSMOEM, Species.SOLGALEO, Species.LUNALA ].includes(pokemon.species.speciesId)) {
return "battle_legendary_sol_lun";
}
if (pokemon.species.speciesId === Species.NECROZMA) {
@ -308,7 +308,7 @@ export default class Battle {
return "battle_legendary_ultra_nec";
}
}
if (pokemon.species.speciesId === Species.NIHILEGO || pokemon.species.speciesId === Species.BUZZWOLE || pokemon.species.speciesId === Species.PHEROMOSA || pokemon.species.speciesId === Species.XURKITREE || pokemon.species.speciesId === Species.CELESTEELA || pokemon.species.speciesId === Species.KARTANA || pokemon.species.speciesId === Species.GUZZLORD || pokemon.species.speciesId === Species.POIPOLE || pokemon.species.speciesId === Species.NAGANADEL || pokemon.species.speciesId === Species.STAKATAKA || pokemon.species.speciesId === Species.BLACEPHALON) {
if ([ Species.NIHILEGO, Species.BUZZWOLE, Species.PHEROMOSA, Species.XURKITREE, Species.CELESTEELA, Species.KARTANA, Species.GUZZLORD, Species.POIPOLE, Species.NAGANADEL, Species.STAKATAKA, Species.BLACEPHALON ].includes(pokemon.species.speciesId)) {
return "battle_legendary_ub";
}
if (pokemon.species.speciesId === Species.ZACIAN || pokemon.species.speciesId === Species.ZAMAZENTA) {
@ -425,7 +425,13 @@ export class FixedBattleConfig {
}
}
function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[]): GetTrainerFunc {
/**
* Helper function to generate a random trainer for evil team trainers and the elite 4/champion
* @param trainerPool The TrainerType or list of TrainerTypes that can possibly be generated
* @param randomGender whether or not to randomly (50%) generate a female trainer (for use with evil team grunts)
* @returns the generated trainer
*/
function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[], randomGender: boolean = false): GetTrainerFunc {
return (scene: BattleScene) => {
const rand = Utils.randSeedInt(trainerPool.length);
const trainerTypes: TrainerType[] = [];
@ -435,11 +441,20 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[]): Get
: trainerPoolEntry;
trainerTypes.push(trainerType);
}
// If the trainer type has a double variant, there's a 33% chance of it being a double battle (for now we only allow tate&liza to be double)
if (trainerConfigs[trainerTypes[rand]].trainerTypeDouble && (trainerTypes[rand] === TrainerType.TATE || trainerTypes[rand] === TrainerType.LIZA)) {
return new Trainer(scene, trainerTypes[rand], Utils.randSeedInt(3) ? TrainerVariant.DOUBLE : TrainerVariant.DEFAULT);
let trainerGender = TrainerVariant.DEFAULT;
if (randomGender) {
trainerGender = (Utils.randInt(2) === 0) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT;
}
return new Trainer(scene, trainerTypes[rand], TrainerVariant.DEFAULT);
/* 1/3 chance for evil team grunts to be double battles */
const evilTeamGrunts = [TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT];
const isEvilTeamGrunt = evilTeamGrunts.includes(trainerTypes[rand]);
if (trainerConfigs[trainerTypes[rand]].hasDouble && isEvilTeamGrunt) {
return new Trainer(scene, trainerTypes[rand], (Utils.randInt(3) === 0) ? TrainerVariant.DOUBLE : trainerGender);
}
return new Trainer(scene, trainerTypes[rand], trainerGender);
};
}
@ -449,7 +464,8 @@ export interface FixedBattleConfigs {
/**
* Youngster/Lass on 5
* Rival on 8, 55, 95, 145, 195
* Evil team grunts on 35, 62, 64, 66, 112, 114
* Evil team grunts on 35, 62, 64, and 112
* Evil team admin on 66 and 114
* Evil leader on 115, 165
* E4 on 182, 184, 186, 188
* Champion on 190
@ -462,21 +478,21 @@ export const classicFixedBattles: FixedBattleConfigs = {
[25]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_2, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)),
[35]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ])),
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ], true)),
[55]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_3, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)),
[62]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ])),
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ], true)),
[64]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ])),
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ], true)),
[66]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ])),
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_ADMIN, TrainerType.MAGMA_ADMIN, TrainerType.AQUA_ADMIN, TrainerType.GALACTIC_ADMIN, TrainerType.PLASMA_SAGE, TrainerType.FLARE_ADMIN ], true)),
[95]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_4, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)),
[112]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ])),
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ], true)),
[114]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT ])),
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_ADMIN, TrainerType.MAGMA_ADMIN, TrainerType.AQUA_ADMIN, TrainerType.GALACTIC_ADMIN, TrainerType.PLASMA_SAGE, TrainerType.FLARE_ADMIN ], true)),
[115]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_1, TrainerType.MAXIE, TrainerType.ARCHIE, TrainerType.CYRUS, TrainerType.GHETSIS, TrainerType.LYSANDRE ])),
[145]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)

View File

@ -1,4 +1,4 @@
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { Type } from "./type";
import { Constructor } from "#app/utils";
import * as Utils from "../utils";
@ -286,7 +286,7 @@ export class BlockItemTheftAbAttr extends AbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]) {
return i18next.t("abilityTriggers:blockItemTheft", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName
});
}
}
@ -405,7 +405,7 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr {
if (!simulated) {
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / 4), 1), i18next.t("abilityTriggers:typeImmunityHeal", {pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName}), true));
Math.max(Math.floor(pokemon.getMaxHp() / 4), 1), i18next.t("abilityTriggers:typeImmunityHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
}
}
return true;
@ -485,7 +485,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:nonSuperEffectiveImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName
});
}
}
@ -777,7 +777,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:postDefendTypeChange", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName,
typeName: i18next.t(`pokemonInfo:Type.${Type[pokemon.getTypes(true)[0]]}`)
});
}
@ -901,7 +901,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:postDefendContactDamage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName
});
}
}
@ -941,14 +941,19 @@ export class PostDefendPerishSongAbAttr extends PostDefendAbAttr {
export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr {
private weatherType: WeatherType;
protected condition: PokemonDefendCondition | null;
constructor(weatherType: WeatherType) {
constructor(weatherType: WeatherType, condition?: PokemonDefendCondition) {
super();
this.weatherType = weatherType;
this.condition = condition ?? null;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (this.condition !== null && !this.condition(pokemon, attacker, move)) {
return false;
}
if (!pokemon.scene.arena.weather?.isImmutable()) {
return pokemon.scene.arena.trySetWeather(this.weatherType, true);
}
@ -999,7 +1004,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:postDefendAbilityGive", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName
});
}
}
@ -1083,9 +1088,8 @@ export class MoveEffectChanceMultiplierAbAttr extends AbAttr {
* [1]: {@linkcode Moves } Move used by the ability user.
*/
apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
//Disable showAbility during getTargetBenefitScore
const showAbility = args[4];
this.showAbility = showAbility;
// Disable showAbility during getTargetBenefitScore
this.showAbility = args[4];
if ((args[0] as Utils.NumberHolder).value <= 0 || (args[1] as Move).id === Moves.ORDER_UP) {
return false;
}
@ -1532,23 +1536,50 @@ export class BattleStatMultiplierAbAttr extends AbAttr {
}
export class PostAttackAbAttr extends AbAttr {
private attackCondition: PokemonAttackCondition;
/** The default attackCondition requires that the selected move is a damaging move */
constructor(attackCondition: PokemonAttackCondition = (user, target, move) => (move.category !== MoveCategory.STATUS)) {
super();
this.attackCondition = attackCondition;
}
/**
* Please override {@link applyPostAttackAfterMoveTypeCheck} instead of this method. By default, this method checks that the move used is a damaging attack before
* applying the effect of any inherited class. This can be changed by providing a different {@link attackCondition} to the constructor. See {@link ConfusionOnStatusEffectAbAttr}
* for an example of an effect that does not require a damaging move.
*/
applyPostAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise<boolean> {
// When attackRequired is true, we require the move to be an attack move and to deal damage before checking secondary requirements.
// If attackRequired is false, we always defer to the secondary requirements.
if (this.attackCondition(pokemon, defender, move)) {
return this.applyPostAttackAfterMoveTypeCheck(pokemon, passive, defender, move, hitResult, args);
} else {
return false;
}
}
/**
* This method is only called after {@link applyPostAttack} has already been applied. Use this for handling checks specific to the ability in question.
*/
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise<boolean> {
return false;
}
}
export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
private condition: PokemonAttackCondition;
private stealCondition: PokemonAttackCondition;
constructor(condition?: PokemonAttackCondition) {
constructor(stealCondition?: PokemonAttackCondition) {
super();
this.condition = condition;
this.stealCondition = stealCondition;
}
applyPostAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise<boolean> {
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, defender, move))) {
if (hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, defender, move))) {
const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferrable);
if (heldItems.length) {
const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)];
@ -1584,7 +1615,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
this.effects = effects;
}
applyPostAttack(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
/**Status inflicted by abilities post attacking are also considered additional effects.*/
if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) {
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)];
@ -1615,7 +1646,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr {
this.effects = effects;
}
applyPostAttack(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
/**Battler tags inflicted by abilities post attacking are also considered additional effects.*/
if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance(attacker, pokemon, move) && !pokemon.status) {
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)];
@ -1797,7 +1828,7 @@ export class IntimidateImmunityAbAttr extends AbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:intimidateImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName
});
}
}
@ -2148,6 +2179,49 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr {
}
}
/**
* Removes supplied status effects from the user's field.
*/
export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAttr {
private statusEffect: StatusEffect[];
/**
* @param statusEffect - The status effects to be removed from the user's field.
*/
constructor(...statusEffect: StatusEffect[]) {
super(false);
this.statusEffect = statusEffect;
}
/**
* Removes supplied status effect from the user's field when user of the ability is summoned.
*
* @param pokemon - The Pokémon that triggered the ability.
* @param passive - n/a
* @param args - n/a
* @returns A boolean or a promise that resolves to a boolean indicating the result of the ability application.
*/
applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
const party = pokemon instanceof PlayerPokemon ? pokemon.scene.getPlayerField() : pokemon.scene.getEnemyField();
const allowedParty = party.filter(p => p.isAllowedInBattle());
if (allowedParty.length < 1) {
return false;
}
for (const pokemon of allowedParty) {
if (this.statusEffect.includes(pokemon.status?.effect)) {
pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
pokemon.resetStatus(false);
pokemon.updateInfo();
}
}
return true;
}
}
/** Attempt to copy the stat changes on an ally pokemon */
export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
@ -2354,8 +2428,8 @@ export class ProtectStatAbAttr extends PreStatChangeAbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:protectStat", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
statName: this.protectedStat !== undefined ? getBattleStatName(this.protectedStat) : "stats", // TODO : Change "stats" to i18next.t("battle:stats") after PR#2600 merged to 'main'
abilityName,
statName: this.protectedStat !== undefined ? getBattleStatName(this.protectedStat) : i18next.t("battle:stats")
});
}
}
@ -2372,7 +2446,8 @@ export class ConfusionOnStatusEffectAbAttr extends PostAttackAbAttr {
private effects: StatusEffect[];
constructor(...effects: StatusEffect[]) {
super();
/** This effect does not require a damaging move */
super((user, target, move) => true);
this.effects = effects;
}
/**
@ -2385,7 +2460,7 @@ export class ConfusionOnStatusEffectAbAttr extends PostAttackAbAttr {
* @param args [0] {@linkcode StatusEffect} applied by move
* @returns true if defender is confused
*/
applyPostAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (this.effects.indexOf(args[0]) > -1 && !defender.isFainted()) {
return defender.addTag(BattlerTagType.CONFUSED, pokemon.randSeedInt(3,2), move.id, defender.id);
}
@ -2399,17 +2474,33 @@ export class PreSetStatusAbAttr extends AbAttr {
}
}
export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
/**
* Provides immunity to status effects to specified targets.
*/
export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
private immuneEffects: StatusEffect[];
/**
* @param immuneEffects - The status effects to which the Pokémon is immune.
*/
constructor(...immuneEffects: StatusEffect[]) {
super();
this.immuneEffects = immuneEffects;
}
/**
* Applies immunity to supplied status effects.
*
* @param pokemon - The Pokémon to which the status is being applied.
* @param passive - n/a
* @param effect - The status effect being applied.
* @param cancelled - A holder for a boolean value indicating if the status application was cancelled.
* @param args - n/a
* @returns A boolean indicating the result of the status application.
*/
applyPreSetStatus(pokemon: Pokemon, passive: boolean, effect: StatusEffect, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!this.immuneEffects.length || this.immuneEffects.indexOf(effect) > -1) {
if (this.immuneEffects.length < 1 || this.immuneEffects.includes(effect)) {
cancelled.value = true;
return true;
}
@ -2421,24 +2512,40 @@ export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
return this.immuneEffects.length ?
i18next.t("abilityTriggers:statusEffectImmunityWithName", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName,
statusEffectName: getStatusEffectDescriptor(args[0] as StatusEffect)
}) :
i18next.t("abilityTriggers:statusEffectImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
abilityName
});
}
}
/**
* Provides immunity to status effects to the user.
* @extends PreSetStatusEffectImmunityAbAttr
*/
export class StatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { }
/**
* Provides immunity to status effects to the user's field.
* @extends PreSetStatusEffectImmunityAbAttr
*/
export class UserFieldStatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { }
export class PreApplyBattlerTagAbAttr extends AbAttr {
applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise<boolean> {
return false;
}
}
export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
/**
* Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets.
*/
export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
private immuneTagType: BattlerTagType;
private battlerTag: BattlerTag;
constructor(immuneTagType: BattlerTagType) {
super();
@ -2449,6 +2556,7 @@ export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (tag.tagType === this.immuneTagType) {
cancelled.value = true;
this.battlerTag = tag;
return true;
}
@ -2458,12 +2566,24 @@ export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:battlerTagImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
battlerTagName: (args[0] as BattlerTag).getDescriptor()
abilityName,
battlerTagName: this.battlerTag.getDescriptor()
});
}
}
/**
* Provides immunity to BattlerTags {@linkcode BattlerTag} to the user.
* @extends PreApplyBattlerTagImmunityAbAttr
*/
export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { }
/**
* Provides immunity to BattlerTags {@linkcode BattlerTag} to the user's field.
* @extends PreApplyBattlerTagImmunityAbAttr
*/
export class UserFieldBattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { }
export class BlockCritAbAttr extends AbAttr {
apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
(args[0] as Utils.BooleanHolder).value = true;
@ -2844,7 +2964,7 @@ export class PostWeatherLapseHealAbAttr extends PostWeatherLapseAbAttr {
const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / (16 / this.healFactor)), 1), i18next.t("abilityTriggers:postWeatherLapseHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }), true));
Math.max(Math.floor(pokemon.getMaxHp() / (16 / this.healFactor)), 1), i18next.t("abilityTriggers:postWeatherLapseHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
return true;
}
@ -2863,8 +2983,11 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr {
applyPostWeatherLapse(pokemon: Pokemon, passive: boolean, weather: Weather, args: any[]): boolean {
const scene = pokemon.scene;
if (pokemon.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
return false;
}
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }));
scene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }));
pokemon.damageAndUpdate(Math.ceil(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER);
return true;
}
@ -2938,7 +3061,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / 8), 1), i18next.t("abilityTriggers:poisonHeal", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName: abilityName}), true));
Math.max(Math.floor(pokemon.getMaxHp() / 8), 1), i18next.t("abilityTriggers:poisonHeal", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName }), true));
return true;
}
}
@ -3031,10 +3154,11 @@ export class PostTurnLootAbAttr extends PostTurnAbAttr {
) as BerryModifier | undefined;
if (!berryModifier) {
const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1);
if (pokemon.isPlayer()) {
pokemon.scene.addModifier(new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1));
pokemon.scene.addModifier(newBerry);
} else {
pokemon.scene.addEnemyModifier(new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1));
pokemon.scene.addEnemyModifier(newBerry);
}
} else if (berryModifier.stackCount < berryModifier.getMaxHeldItemCount(pokemon)) {
berryModifier.stackCount++;
@ -3095,7 +3219,7 @@ export class PostTurnHealAbAttr extends PostTurnAbAttr {
const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / 16), 1), i18next.t("abilityTriggers:postTurnHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }), true));
Math.max(Math.floor(pokemon.getMaxHp() / 16), 1), i18next.t("abilityTriggers:postTurnHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }), true));
return true;
}
@ -3139,7 +3263,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
applyPostTurn(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
let hadEffect: boolean = false;
for (const opp of pokemon.getOpponents()) {
if (opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) {
if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
opp.damageAndUpdate(Math.floor(Math.max(1, opp.getMaxHp() / 8)), HitResult.OTHER);
pokemon.scene.queueMessage(i18next.t("abilityTriggers:badDreams", {pokemonName: getPokemonNameWithAffix(opp)}));
hadEffect = true;
@ -3347,7 +3471,7 @@ export class HealFromBerryUseAbAttr extends AbAttr {
pokemon.scene,
pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() * this.healPercent), 1),
i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }),
i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }),
true
)
);
@ -3422,7 +3546,7 @@ export class ArenaTrapAbAttr extends CheckTrappedAbAttr {
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:arenaTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName });
return i18next.t("abilityTriggers:arenaTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName });
}
}
@ -3528,7 +3652,7 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
const cancelled = new Utils.BooleanHolder(false);
pokemon.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled));
if (cancelled.value) {
if (cancelled.value || attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
return false;
}
attacker.damageAndUpdate(Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
@ -3540,7 +3664,7 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:postFaintContactDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName });
return i18next.t("abilityTriggers:postFaintContactDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName });
}
}
@ -3560,7 +3684,7 @@ export class PostFaintHPDamageAbAttr extends PostFaintAbAttr {
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:postFaintHpDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName });
return i18next.t("abilityTriggers:postFaintHpDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName });
}
}
@ -3918,7 +4042,7 @@ export class IceFaceBlockPhysicalAbAttr extends ReceivedMoveDamageMultiplierAbAt
* @returns {string} - The trigger message.
*/
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:iceFaceAvoidedDamage", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName: abilityName });
return i18next.t("abilityTriggers:iceFaceAvoidedDamage", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName });
}
}
@ -3993,7 +4117,7 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
pokemon.scene.setPhaseQueueSplice();
let result = applyFunc(attr, passive);
// TODO Remove this when promises get reworked PR#924
// TODO Remove this when promises get reworked
if (result instanceof Promise) {
result = await result;
}
@ -4184,7 +4308,7 @@ export const allAbilities = [ new Ability(Abilities.NONE, 3) ];
export function initAbilities() {
allAbilities.push(
new Ability(Abilities.STENCH, 3)
.attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => (move.category !== MoveCategory.STATUS && !move.hasAttr(FlinchAttr)) ? 10 : 0, BattlerTagType.FLINCHED),
.attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) ? 10 : 0, BattlerTagType.FLINCHED),
new Ability(Abilities.DRIZZLE, 3)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN),
@ -4719,10 +4843,10 @@ export function initAbilities() {
new Ability(Abilities.REFRIGERATE, 6)
.attr(MoveTypeChangeAttr, Type.ICE, 1.2, (user, target, move) => move.type === Type.NORMAL),
new Ability(Abilities.SWEET_VEIL, 6)
.attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
.ignorable()
.partial(),
.partial(), // Mold Breaker ally should not be affected by Sweet Veil
new Ability(Abilities.STANCE_CHANGE, 6)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
@ -4973,7 +5097,7 @@ export function initAbilities() {
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.hasFlag(MoveFlags.SOUND_BASED), 0.5)
.ignorable(),
new Ability(Abilities.SAND_SPIT, 8)
.attr(PostDefendWeatherChangeAbAttr, WeatherType.SANDSTORM),
.attr(PostDefendWeatherChangeAbAttr, WeatherType.SANDSTORM, (target, user, move) => move.category !== MoveCategory.STATUS),
new Ability(Abilities.ICE_SCALES, 8)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.category === MoveCategory.SPECIAL, 0.5)
.ignorable(),
@ -5017,7 +5141,8 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonNeutralizingGas", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.partial(),
new Ability(Abilities.PASTEL_VEIL, 8)
.attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.ignorable(),
new Ability(Abilities.HUNGER_SWITCH, 8)
.attr(PostTurnFormChangeAbAttr, p => p.getFormKey ? 0 : 1)

View File

@ -1,689 +0,0 @@
import { MainClient, NamedAPIResource } from "pokenode-ts";
import { MoveTarget, allMoves } from "./move";
import * as Utils from "../utils";
import fs from "vite-plugin-fs/browser";
import PokemonSpecies, { PokemonForm, SpeciesFormKey, allSpecies } from "./pokemon-species";
import { GrowthRate } from "./exp";
import { Type } from "./type";
import { allAbilities } from "./ability";
import { pokemonFormLevelMoves } from "./pokemon-level-moves";
import { tmSpecies } from "./tms";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
const targetMap = {
"specific-move": MoveTarget.ATTACKER,
"selected-pokemon-me-first": MoveTarget.NEAR_ENEMY,
"ally": MoveTarget.NEAR_ALLY,
"users-field": MoveTarget.USER_SIDE,
"user-or-ally": MoveTarget.USER_OR_NEAR_ALLY,
"opponents-field": MoveTarget.ENEMY_SIDE,
"user": MoveTarget.USER,
"random-opponent": MoveTarget.RANDOM_NEAR_ENEMY,
"all-other-pokemon": MoveTarget.ALL_NEAR_OTHERS,
"selected-pokemon": MoveTarget.NEAR_OTHER,
"all-opponents": MoveTarget.ALL_NEAR_ENEMIES,
"entire-field": MoveTarget.BOTH_SIDES,
"user-and-allies": MoveTarget.USER_AND_ALLIES,
"all-pokemon": MoveTarget.ALL,
"all-allies": MoveTarget.NEAR_ALLY,
"fainting-pokemon": MoveTarget.NEAR_OTHER
};
const generationMap = {
"generation-i": 1,
"generation-ii": 2,
"generation-iii": 3,
"generation-iv": 4,
"generation-v": 5,
"generation-vi": 6,
"generation-vii": 7,
"generation-viii": 8,
"generation-ix": 9
};
const growthRateMap = {
"slow-then-very-fast": GrowthRate.ERRATIC,
"fast": GrowthRate.FAST,
"medium": GrowthRate.MEDIUM_FAST,
"medium-slow": GrowthRate.MEDIUM_SLOW,
"slow": GrowthRate.SLOW,
"fast-then-very-slow": GrowthRate.FLUCTUATING
};
const regionalForms = [ "alola", "galar", "hisui", "paldea" ];
const ignoredForms = [ "gmax", "totem", "cap", "starter" ];
const generationDexNumbers = {
1: 151,
2: 251,
3: 386,
4: 494,
5: 649,
6: 721,
7: 809,
8: 905,
9: 1010
};
const versions = [ "scarlet-violet", "sword-shield", "sun-moon" ];
type LevelMove = [level: integer, moveId: integer];
interface SpeciesLevelMoves {
[key: string]: LevelMove[]
}
interface FormLevelMoves {
[key: integer]: LevelMove[]
}
interface SpeciesFormLevelMoves {
[key: string]: FormLevelMoves
}
interface TmSpecies {
[key: string]: Array<string | string[]>
}
export async function printPokemon() {
const api = new MainClient();
const useExistingTmList = true;
let enumStr = "export enum Species {\n";
let pokemonSpeciesStr = "\tallSpecies.push(\n";
const speciesLevelMoves: SpeciesLevelMoves = {};
const speciesFormLevelMoves: SpeciesFormLevelMoves = {};
const moveTmSpecies: TmSpecies = {};
let pokemonArr: NamedAPIResource[] = [];
const offset = 0;
const pokemonResponse = await api.pokemon.listPokemons(offset, 2000);
pokemonArr = pokemonResponse.results;
const types = Utils.getEnumKeys(Type).map(t => t.toLowerCase());
const abilities = Utils.getEnumKeys(Abilities).map(a => a.toLowerCase().replace(/\_/g, "-"));
const pokemonSpeciesList: PokemonSpecies[] = [];
for (const p of pokemonArr) {
const pokemon = await api.pokemon.getPokemonByName(p.name);
let region: string = "";
if (pokemon.id > 10000) {
const dexIdMatch = /\/(\d+)\//.exec(pokemon.species.url);
if (!dexIdMatch) {
continue;
}
const matchingSpecies = pokemonSpeciesList[parseInt(dexIdMatch[1]) - 1];
if (!matchingSpecies) {
continue;
}
const speciesKey = (matchingSpecies as any).key as string;
const formName = pokemon.name.slice(speciesKey.length + 1);
if (ignoredForms.filter(f => formName.indexOf(f) > -1).length) {
continue;
}
const shortFormName = formName.indexOf("-") > -1
? formName.slice(0, formName.indexOf("-"))
: formName;
if (regionalForms.indexOf(shortFormName) > -1) {
region = shortFormName.toUpperCase();
} else {
const formBaseStats: integer[] = [];
let formBaseTotal = 0;
// Assume correct stat order in API result
for (const stat of pokemon.stats) {
formBaseStats.push(stat.base_stat);
formBaseTotal += stat.base_stat;
}
const [ formType1, formType2 ] = [ types.indexOf(pokemon.types.find(t => t.slot === 1).type.name), types.indexOf(pokemon.types.find(t => t.slot === 2)?.type.name) ];
const [ formAbility1, formAbility2, formAbilityHidden ] = [
Math.max(abilities.indexOf(pokemon.abilities.find(a => a.slot === 1)?.ability.name), 0),
Math.max(abilities.indexOf(pokemon.abilities.find(a => a.slot === 2)?.ability.name), 0),
Math.max(abilities.indexOf(pokemon.abilities.find(a => a.slot === 3)?.ability.name), 0)
];
const pokemonForm = new PokemonForm(formName, formName, formType1 as Type, formType2 > -1 ? formType2 as Type : null, pokemon.height / 10, pokemon.weight / 10,
formAbility1 as Abilities, formAbility2 as Abilities, formAbilityHidden as Abilities, formBaseTotal, formBaseStats[0], formBaseStats[1], formBaseStats[2], formBaseStats[3], formBaseStats[4], formBaseStats[5],
matchingSpecies.catchRate, matchingSpecies.baseFriendship, matchingSpecies.baseExp, matchingSpecies.genderDiffs);
pokemonForm.speciesId = matchingSpecies.speciesId;
pokemonForm.formIndex = matchingSpecies.forms.length;
pokemonForm.generation = matchingSpecies.generation;
let moveVer: string;
if (!speciesFormLevelMoves.hasOwnProperty(speciesKey)) {
speciesFormLevelMoves[speciesKey] = [];
}
speciesFormLevelMoves[speciesKey][pokemonForm.formIndex] = [];
for (const version of versions) {
if (pokemon.moves.find(m => m.version_group_details.find(v => v.version_group.name === version && v.move_learn_method.name === "level-up"))) {
moveVer = version;
break;
}
}
if (moveVer) {
pokemon.moves.forEach(moveData => {
moveData.version_group_details.filter(v => versions.indexOf(v.version_group.name) > -1).forEach(verData => {
const isMoveVer = verData.version_group.name === moveVer;
const moveName = moveData.move.name.toUpperCase().replace(/\_/g, "").replace(/\-/g, "_");
const moveId = Math.max(Utils.getEnumKeys(Moves).indexOf(moveName), 0);
const learnMethod = verData.move_learn_method.name;
if (isMoveVer && learnMethod === "level-up") {
speciesFormLevelMoves[speciesKey][pokemonForm.formIndex].push([ verData.level_learned_at, moveId ]);
}
if ([ "machine", "tutor" ].indexOf(learnMethod) > -1 || (useExistingTmList && tmSpecies.hasOwnProperty(moveId as Moves) && learnMethod === "level-up")) {
if (!moveTmSpecies.hasOwnProperty(moveId)) {
moveTmSpecies[moveId] = [];
}
const speciesIndex = moveTmSpecies[moveId].findIndex(s => s[0] === speciesKey);
if (speciesIndex === -1) {
moveTmSpecies[moveId].push([ speciesKey, formName ]);
} else {
(moveTmSpecies[moveId][speciesIndex] as string[]).push(formName);
}
}
});
});
if (JSON.stringify(speciesLevelMoves[speciesKey]) === JSON.stringify(speciesFormLevelMoves[speciesKey][pokemonForm.formIndex])) {
delete speciesFormLevelMoves[speciesKey][pokemonForm.formIndex];
if (!Object.keys(speciesFormLevelMoves[speciesKey]).length) {
delete speciesFormLevelMoves[speciesKey];
}
}
}
matchingSpecies.forms.push(pokemonForm);
continue;
}
}
const species = await api.pokemon.getPokemonSpeciesByName(pokemon.species.name);
let speciesKey = species.name.toUpperCase().replace(/\-/g, "_");
const matchingExistingSpecies = allSpecies.find(s => Species[s.speciesId] === speciesKey);
let dexId = species.id;
if (region) {
dexId += (regionalForms.indexOf(region.toLowerCase()) + 1) * 2000;
speciesKey = `${region}_${speciesKey}`;
}
let generationIndex = 0;
if (!region) {
while (++generationIndex < 9 && dexId > generationDexNumbers[generationIndex]) {}
} else {
generationIndex = regionalForms.indexOf(region.toLowerCase()) + 6;
}
const baseStats: integer[] = [];
let baseTotal = 0;
// Assume correct stat order in API result
for (const stat of pokemon.stats) {
baseStats.push(stat.base_stat);
baseTotal += stat.base_stat;
}
console.log(pokemon);
const [ type1, type2 ] = [ types.indexOf(pokemon.types.find(t => t.slot === 1).type.name), types.indexOf(pokemon.types.find(t => t.slot === 2)?.type.name) ];
const [ ability1, ability2, abilityHidden ] = [
Math.max(abilities.indexOf(pokemon.abilities.find(a => a.slot === 1)?.ability.name), 0),
Math.max(abilities.indexOf(pokemon.abilities.find(a => a.slot === 2)?.ability.name), 0),
Math.max(abilities.indexOf(pokemon.abilities.find(a => a.slot === 3)?.ability.name), 0)
];
const pokemonSpecies = new PokemonSpecies(dexId, generationIndex, species.is_legendary && baseTotal < 660, species.is_legendary && baseTotal >= 660, species.is_mythical,
species.genera.find(g => g.language.name === "en")?.genus, type1 as Type, type2 > -1 ? type2 as Type : null, pokemon.height / 10, pokemon.weight / 10, ability1 as Abilities, ability2 as Abilities, abilityHidden as Abilities,
baseTotal, baseStats[0], baseStats[1], baseStats[2], baseStats[3], baseStats[4], baseStats[5], species.capture_rate, species.base_happiness, pokemon.base_experience, growthRateMap[species.growth_rate.name],
species.gender_rate < 9 ? 100 - (species.gender_rate * 12.5) : null, species.has_gender_differences, species.forms_switchable);
(pokemonSpecies as any).key = speciesKey;
pokemonSpeciesList.push(pokemonSpecies);
let moveVer: string;
speciesLevelMoves[speciesKey] = [];
for (const version of versions) {
if (pokemon.moves.find(m => m.version_group_details.find(v => v.version_group.name === version && v.move_learn_method.name === "level-up"))) {
moveVer = version;
break;
}
}
const speciesTmMoves: integer[] = [];
if (moveVer) {
pokemon.moves.forEach(moveData => {
const verData = moveData.version_group_details.find(v => v.version_group.name === moveVer);
if (!verData) {
return;
}
const moveName = moveData.move.name.toUpperCase().replace(/\_/g, "").replace(/\-/g, "_");
const moveId = Math.max(Utils.getEnumKeys(Moves).indexOf(moveName), 0);
switch (verData.move_learn_method.name) {
case "level-up":
speciesLevelMoves[speciesKey].push([ verData.level_learned_at, moveId ]);
break;
case "machine":
case "tutor":
if (moveId > 0) {
if (!moveTmSpecies.hasOwnProperty(moveId)) {
moveTmSpecies[moveId] = [];
}
if (moveTmSpecies[moveId].indexOf(speciesKey) === -1) {
moveTmSpecies[moveId].push(speciesKey);
}
speciesTmMoves.push(moveId);
}
break;
}
});
}
for (const f of pokemon.forms) {
const form = await api.pokemon.getPokemonFormByName(f.name);
const formIndex = pokemonSpecies.forms.length;
const matchingForm = matchingExistingSpecies && matchingExistingSpecies.forms.length > formIndex
? matchingExistingSpecies.forms.find(f2 => f2.formKey === form.form_name || f2.formName === form.form_name) || matchingExistingSpecies.forms[formIndex]
: null;
const formName = matchingForm
? matchingForm.formName
: form.form_names.find(fn => fn.language.name === "en")?.name || form.form_name;
const formKey = matchingForm
? matchingForm.formKey
: form.form_name;
const [ formType1, formType2 ] = [ types.indexOf(form.types.find(t => t.slot === 1).type.name), types.indexOf(form.types.find(t => t.slot === 2)?.type.name) ];
const pokemonForm = new PokemonForm(formName, formKey, formType1 as Type, formType2 > -1 ? formType2 as Type : null,
pokemonSpecies.height, pokemonSpecies.weight, pokemonSpecies.ability1, pokemonSpecies.ability2, pokemonSpecies.abilityHidden, baseTotal, baseStats[0], baseStats[1], baseStats[2], baseStats[3], baseStats[4], baseStats[5],
pokemonSpecies.catchRate, pokemonSpecies.baseFriendship, pokemonSpecies.baseExp, pokemonSpecies.genderDiffs);
pokemonForm.speciesId = pokemonSpecies.speciesId;
pokemonForm.formIndex = formIndex;
pokemonForm.generation = pokemonSpecies.generation;
if (!pokemonForm.formIndex && speciesTmMoves.length) {
for (const moveId of speciesTmMoves) {
const speciesIndex = moveTmSpecies[moveId].findIndex(s => s === speciesKey);
moveTmSpecies[moveId][speciesIndex] = [
speciesKey,
formKey
];
}
}
pokemonSpecies.forms.push(pokemonForm);
}
console.log(pokemonSpecies.name, pokemonSpecies);
}
for (const pokemonSpecies of pokemonSpeciesList) {
const speciesKey = (pokemonSpecies as any).key as string;
enumStr += ` ${speciesKey}${pokemonSpecies.speciesId >= 2000 ? ` = ${pokemonSpecies.speciesId}` : ""},\n`;
pokemonSpeciesStr += ` new PokemonSpecies(Species.${speciesKey}, "${pokemonSpecies.name}", ${pokemonSpecies.generation}, ${pokemonSpecies.subLegendary}, ${pokemonSpecies.legendary}, ${pokemonSpecies.mythical}, "${pokemonSpecies.species}", Type.${Type[pokemonSpecies.type1]}, ${pokemonSpecies.type2 ? `Type.${Type[pokemonSpecies.type2]}` : "null"}, ${pokemonSpecies.height}, ${pokemonSpecies.weight}, Abilities.${Abilities[pokemonSpecies.ability1]}, Abilities.${Abilities[pokemonSpecies.ability2]}, Abilities.${Abilities[pokemonSpecies.abilityHidden]}, ${pokemonSpecies.baseTotal}, ${pokemonSpecies.baseStats[0]}, ${pokemonSpecies.baseStats[1]}, ${pokemonSpecies.baseStats[2]}, ${pokemonSpecies.baseStats[3]}, ${pokemonSpecies.baseStats[4]}, ${pokemonSpecies.baseStats[5]}, ${pokemonSpecies.catchRate}, ${pokemonSpecies.baseFriendship}, ${pokemonSpecies.baseExp}, GrowthRate.${GrowthRate[pokemonSpecies.growthRate]}, ${pokemonSpecies.malePercent}, ${pokemonSpecies.genderDiffs}`;
if (pokemonSpecies.forms.length > 1) {
pokemonSpeciesStr += `, ${pokemonSpecies.canChangeForm},`;
for (const form of pokemonSpecies.forms) {
pokemonSpeciesStr += `\n new PokemonForm("${form.formName}", "${form.formName}", Type.${Type[form.type1]}, ${form.type2 ? `Type.${Type[form.type2]}` : "null"}, ${form.height}, ${form.weight}, Abilities.${Abilities[form.ability1]}, Abilities.${Abilities[form.ability2]}, Abilities.${Abilities[form.abilityHidden]}, ${form.baseTotal}, ${form.baseStats[0]}, ${form.baseStats[1]}, ${form.baseStats[2]}, ${form.baseStats[3]}, ${form.baseStats[4]}, ${form.baseStats[5]}, ${form.catchRate}, ${form.baseFriendship}, ${form.baseExp}${form.genderDiffs ? ", true" : ""}),`;
}
pokemonSpeciesStr += "\n ";
}
pokemonSpeciesStr += "),\n";
}
let speciesLevelMovesStr = "export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = {\n";
let speciesFormLevelMovesStr = "export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = {\n";
let tmSpeciesStr = "export const tmSpecies: TmSpecies = {\n";
for (const species of Object.keys(speciesLevelMoves)) {
speciesLevelMovesStr += ` [Species.${species}]: [\n`;
const orderedLevelMoves = speciesLevelMoves[species].sort((a: LevelMove, b: LevelMove) => {
if (a[0] !== b[0]) {
return a[0] < b[0] ? -1 : 1;
}
return a[1] < b[1] ? -1 : 1;
});
for (const lm of orderedLevelMoves) {
speciesLevelMovesStr += ` [ ${lm[0]}, Moves.${Moves[lm[1]]} ],\n`;
}
speciesLevelMovesStr += " ],\n";
}
for (const species of Object.keys(speciesFormLevelMoves)) {
speciesFormLevelMovesStr += ` [Species.${species}]: {\n`;
for (const f of Object.keys(speciesFormLevelMoves[species])) {
speciesFormLevelMovesStr += ` ${f}: [\n`;
const orderedLevelMoves = speciesFormLevelMoves[species][f].sort((a: LevelMove, b: LevelMove) => {
if (a[0] !== b[0]) {
return a[0] < b[0] ? -1 : 1;
}
return a[1] < b[1] ? -1 : 1;
});
for (const lm of orderedLevelMoves) {
speciesFormLevelMovesStr += ` [ ${lm[0]}, Moves.${Moves[lm[1]]} ],\n`;
}
speciesFormLevelMovesStr += " ],\n";
}
speciesFormLevelMovesStr += " },\n";
}
for (const moveId of Object.keys(moveTmSpecies)) {
tmSpeciesStr += ` [Moves.${Moves[parseInt(moveId)]}]: [\n`;
for (const species of moveTmSpecies[moveId]) {
if (typeof species === "string") {
tmSpeciesStr += ` Species.${species},\n`;
} else {
const matchingExistingSpecies = allSpecies.find(s => Species[s.speciesId] === species[0]);
const forms = (species as string[]).slice(1);
if (matchingExistingSpecies && (!pokemonFormLevelMoves.hasOwnProperty(matchingExistingSpecies.speciesId) || matchingExistingSpecies.forms.length <= 1 || (matchingExistingSpecies.forms.length === 2 && matchingExistingSpecies.forms[1].formKey.indexOf(SpeciesFormKey.MEGA) > -1) || matchingExistingSpecies.forms.length === forms.length)) {
tmSpeciesStr += ` Species.${species[0]},\n`;
} else {
tmSpeciesStr += ` [\n Species.${species[0]},\n`;
for (const form of forms) {
tmSpeciesStr += ` '${form}',\n`;
}
tmSpeciesStr += " ],\n";
}
}
}
tmSpeciesStr += " ],\n";
}
enumStr += "\n};";
pokemonSpeciesStr += " );";
speciesLevelMovesStr += "\n};";
speciesFormLevelMovesStr += "\n};";
tmSpeciesStr += "\n};";
console.log(enumStr);
console.log(pokemonSpeciesStr);
console.log(speciesLevelMovesStr);
console.log(speciesFormLevelMovesStr);
console.log(tmSpeciesStr);
console.log(moveTmSpecies);
}
export async function printAbilities() {
const replaceText = true;
let abilityContent: string = await fs.readFile("./src/data/ability.ts");
const api = new MainClient();
let enumStr = "export enum Abilities {\n NONE,";
let abilityStr = " allAbilities.push(";
abilityContent = abilityContent.slice(abilityContent.indexOf(abilityStr));
let abilities: NamedAPIResource[] = [];
const offset = 0;
const abilitiesResponse = await api.pokemon.listAbilities(offset, 2000);
abilities = abilitiesResponse.results;
for (const a of abilities) {
const ability = await api.pokemon.getAbilityByName(a.name);
const abilityEnumName = ability.name.toUpperCase().replace(/\_/g, "").replace(/\-/g, "_");
enumStr += `\n ${abilityEnumName},`;
console.log(ability.name, ability);
const matchingLineIndex = abilityContent.search(new RegExp(`new Ability\\\(Abilities.${abilityEnumName},`));
let matchingLine = matchingLineIndex > -1 ? abilityContent.slice(matchingLineIndex) : null;
if (matchingLine) {
matchingLine = matchingLine.slice(0, matchingLine.search(/,(?: \/\/.*?)?(?:\r)?\n[ \t]+(?:new|\);)/));
}
let abilityName = ability.names.find(ln => ln.language.name === "en").name;
[ "N", "P" ].every(s => {
if (!matchingLine || matchingLine.indexOf(` (${s})`) > -1) {
abilityName += ` (${s})`;
return false;
}
return true;
});
let flavorText: string;
if (!matchingLine || replaceText) {
for (const version of versions) {
if ((flavorText = ability.flavor_text_entries.find(fte => fte.language.name === "en" && fte.version_group.name === version)?.flavor_text) || "") {
if (flavorText.indexOf("forgotten") > -1) {
continue;
}
break;
}
}
} else if (matchingLine) {
flavorText = allAbilities[ability.id].description;
}
abilityStr += `\n new Ability(Abilities.${abilityEnumName}, "${abilityName}", "${flavorText?.replace(/\n/g, "\\n").replace(/ /g, " ").replace(//g, "'") || ""}", ${generationMap[ability.generation.name]})`;
if (matchingLine && matchingLine.length > 1) {
const newLineIndex = matchingLine.indexOf("\n");
if (newLineIndex > -1) {
abilityStr += matchingLine.slice(newLineIndex);
}
}
abilityStr += ",";
}
enumStr += "\n};";
abilityStr += "\n);";
console.log(enumStr);
console.log(abilityStr);
}
export async function printMoves() {
const replaceText = true;
let moveContent: string = await fs.readFile("./src/data/move.ts");
const api = new MainClient();
let enumStr = "export enum Moves {\n NONE,";
let moveStr = " allMoves.push(";
moveContent = moveContent.slice(moveContent.indexOf(moveStr));
let moves: NamedAPIResource[] = [];
const offset = 0;
const movesResponse = await api.move.listMoves(offset, 2000);
moves = movesResponse.results;
console.log(moves);
for (const m of moves) {
const move = await api.move.getMoveByName(m.name);
const moveEnumName = move.name.toUpperCase().replace(/\_/g, "").replace(/\-/g, "_");
enumStr += `\n ${moveEnumName},`;
console.log(move.name, move);
const matchingLineIndex = moveContent.search(new RegExp(`new (?:Attack|(?:Self)?Status)Move\\\(Moves.${Moves[move.id]},`));
let matchingLine = matchingLineIndex > -1 ? moveContent.slice(matchingLineIndex) : null;
if (matchingLine) {
matchingLine = matchingLine.slice(0, matchingLine.search(/,(?: \/\/.*?)?(?:\r)?\n[ \t]+(?:new|\);)/));
}
let moveName = move.names.find(ln => ln.language.name === "en").name;
[ "N", "P" ].every(s => {
if (!matchingLine || matchingLine.indexOf(` (${s})`) > -1) {
moveName += ` (${s})`;
return false;
}
return true;
});
let flavorText: string;
if (!matchingLine || replaceText) {
for (const version of versions) {
if ((flavorText = move.flavor_text_entries.find(fte => fte.language.name === "en" && fte.version_group.name === version)?.flavor_text) || "") {
if (flavorText.indexOf("forgotten") > -1) {
continue;
}
break;
}
}
} else if (matchingLine) {
flavorText = allMoves[move.id].effect;
}
const moveTarget = targetMap[move.target.name];
moveStr += `\n new ${move.damage_class.name !== "status" ? "Attack" : (moveTarget === MoveTarget.USER ? "Self" : "") + "Status"}Move(Moves.${moveEnumName}, "${moveName}", Type.${move.type.name.toUpperCase()}${move.damage_class.name !== "status" ? `, MoveCategory.${move.damage_class.name.toUpperCase()}` : ""}${move.damage_class.name !== "status" ? `, ${move.power || -1}` : ""}, ${move.accuracy || -1}, ${move.pp}, "${flavorText?.replace(/\n/g, "\\n").replace(/ /g, " ").replace(//g, "'") || ""}", ${move.effect_chance || -1}, ${move.priority}, ${generationMap[move.generation.name]})`;
const expectedTarget = move.damage_class.name !== "status" || moveTarget !== MoveTarget.USER ? MoveTarget.NEAR_OTHER : MoveTarget.USER;
if (matchingLine && matchingLine.length > 1) {
const newLineIndex = matchingLine.indexOf("\n");
if (newLineIndex > -1) {
console.log(matchingLine.slice(newLineIndex).replace(/(?:\r)?\n[ \t]+.target\(.*?\)/g, ""), newLineIndex);
moveStr += matchingLine.slice(newLineIndex).replace(/(?:\r)?\n[ \t]+.target\(.*?\)/g, "");
}
}
if (moveTarget !== expectedTarget) {
moveStr += `\n .target(MoveTarget.${MoveTarget[moveTarget]})`;
}
moveStr += ",";
}
enumStr += "\n};";
moveStr += "\n);";
console.log(enumStr);
console.log(moveStr);
}
export async function printTmSpecies() {
const moveTmSpecies: TmSpecies = {};
const api = new MainClient();
const moveIds = Object.keys(tmSpecies).map(k => parseInt(k) as Moves);
for (const moveId of moveIds) {
const move = await api.move.getMoveById(moveId);
moveTmSpecies[moveId] = [];
for (const species of move.learned_by_pokemon) {
const dexIdMatch = /\/(\d+)\//.exec(species.url);
if (!dexIdMatch) {
continue;
}
const dexId = parseInt(dexIdMatch[1]);
let matchingSpecies: PokemonSpecies;
let formKey = "";
console.log(species.name);
if (dexId < 10000) {
matchingSpecies = allSpecies[dexId - 1];
} else {
const pokemon = await api.pokemon.getPokemonById(dexId);
const speciesDexIdMatch = /\/(\d+)\//.exec(pokemon.species.url);
if (!speciesDexIdMatch) {
continue;
}
const speciesDexId = parseInt(speciesDexIdMatch[1]);
const speciesKey = Species[allSpecies[speciesDexId - 1].speciesId];
formKey = species.name.slice(speciesKey.length + 1);
const regionKey = regionalForms.find(r => formKey.indexOf(r) > -1);
if (regionKey) {
formKey = formKey.slice(regionKey.length + 1);
matchingSpecies = allSpecies.find(s => Species[s.speciesId] === `${regionKey.toUpperCase()}_${speciesKey}`);
} else {
matchingSpecies = allSpecies[speciesDexId - 1];
}
}
if (!matchingSpecies) {
console.log("NO MATCH", species.name);
continue;
}
const speciesKey = Species[matchingSpecies.speciesId];
const matchingIndex = moveTmSpecies[moveId].findIndex(s => Array.isArray(s) ? s[0] === speciesKey : s === speciesKey);
if (matchingIndex === -1) {
moveTmSpecies[moveId].push(!formKey ? speciesKey : [ speciesKey, formKey ]);
} else {
if (!Array.isArray(moveTmSpecies[moveId][matchingIndex])) {
moveTmSpecies[moveId][matchingIndex] = [ moveTmSpecies[moveId][matchingIndex] as string, "" ];
}
(moveTmSpecies[moveId][matchingIndex] as string[]).push(formKey);
}
}
}
let tmSpeciesStr = "export const tmSpecies: TmSpecies = {\n";
for (const moveId of Object.keys(moveTmSpecies)) {
tmSpeciesStr += ` [Moves.${Moves[parseInt(moveId)]}]: [\n`;
for (const species of moveTmSpecies[moveId]) {
if (typeof species === "string") {
tmSpeciesStr += ` Species.${species},\n`;
} else {
const matchingExistingSpecies = allSpecies.find(s => Species[s.speciesId] === species[0]);
const forms = (species as string[]).slice(1);
if (matchingExistingSpecies && (!pokemonFormLevelMoves.hasOwnProperty(matchingExistingSpecies.speciesId) || matchingExistingSpecies.forms.length <= 1 || (matchingExistingSpecies.forms.length === 2 && matchingExistingSpecies.forms[1].formKey.indexOf(SpeciesFormKey.MEGA) > -1) || matchingExistingSpecies.forms.length === forms.length)) {
tmSpeciesStr += ` Species.${species[0]},\n`;
} else {
tmSpeciesStr += ` [\n Species.${species[0]},\n`;
for (const form of forms) {
tmSpeciesStr += ` '${form}',\n`;
}
tmSpeciesStr += " ],\n";
}
}
}
tmSpeciesStr += " ],\n";
}
tmSpeciesStr += "\n};";
console.log(tmSpeciesStr);
}

View File

@ -2,7 +2,7 @@ import { Arena } from "../field/arena";
import { Type } from "./type";
import * as Utils from "../utils";
import { MoveCategory, allMoves, MoveTarget } from "./move";
import { getPokemonMessage, getPokemonNameWithAffix } from "../messages";
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
import { MoveEffectPhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases";
import { StatusEffect } from "./status-effect";
@ -46,7 +46,7 @@ export abstract class ArenaTag {
onRemove(arena: Arena, quiet: boolean = false): void {
if (!quiet) {
arena.scene.queueMessage(`${this.getMoveName()}\'s effect wore off${this.side === ArenaTagSide.PLAYER ? "\non your side" : this.side === ArenaTagSide.ENEMY ? "\non the foe's side" : ""}.`);
arena.scene.queueMessage(i18next.t(`arenaTag:arenaOnRemove${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`, { moveName: this.getMoveName() }));
}
}
@ -77,14 +77,14 @@ export class MistTag extends ArenaTag {
const source = arena.scene.getPokemonById(this.sourceId);
if (!quiet) {
arena.scene.queueMessage(getPokemonMessage(source, "'s team became\nshrouded in mist!"));
arena.scene.queueMessage(i18next.t("arenaTag:mistOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(source) }));
}
}
apply(arena: Arena, args: any[]): boolean {
(args[0] as Utils.BooleanHolder).value = true;
arena.scene.queueMessage("The mist prevented\nthe lowering of stats!");
arena.scene.queueMessage(i18next.t("arenaTag:mistApply"));
return true;
}
@ -144,7 +144,7 @@ class ReflectTag extends WeakenMoveScreenTag {
onAdd(arena: Arena, quiet: boolean = false): void {
if (!quiet) {
arena.scene.queueMessage(`Reflect reduced the damage of physical moves${this.side === ArenaTagSide.PLAYER ? "\non your side" : this.side === ArenaTagSide.ENEMY ? "\non the foe's side" : ""}.`);
arena.scene.queueMessage(i18next.t(`arenaTag:reflectOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
}
}
}
@ -160,7 +160,7 @@ class LightScreenTag extends WeakenMoveScreenTag {
onAdd(arena: Arena, quiet: boolean = false): void {
if (!quiet) {
arena.scene.queueMessage(`Light Screen reduced the damage of special moves${this.side === ArenaTagSide.PLAYER ? "\non your side" : this.side === ArenaTagSide.ENEMY ? "\non the foe's side" : ""}.`);
arena.scene.queueMessage(i18next.t(`arenaTag:lightScreenOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
}
}
}
@ -176,7 +176,7 @@ class AuroraVeilTag extends WeakenMoveScreenTag {
onAdd(arena: Arena, quiet: boolean = false): void {
if (!quiet) {
arena.scene.queueMessage(`Aurora Veil reduced the damage of moves${this.side === ArenaTagSide.PLAYER ? "\non your side" : this.side === ArenaTagSide.ENEMY ? "\non the foe's side" : ""}.`);
arena.scene.queueMessage(i18next.t(`arenaTag:auroraVeilOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
}
}
}
@ -198,7 +198,7 @@ abstract class ConditionalProtectTag extends ArenaTag {
}
onAdd(arena: Arena): void {
arena.scene.queueMessage(`${super.getMoveName()} protected${this.side === ArenaTagSide.PLAYER ? " your" : this.side === ArenaTagSide.ENEMY ? " the\nopposing" : ""} team!`);
arena.scene.queueMessage(i18next.t(`arenaTag:conditionalProtectOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`, { moveName: super.getMoveName() }));
}
// Removes default message for effect removal
@ -223,7 +223,7 @@ abstract class ConditionalProtectTag extends ArenaTag {
&& this.protectConditionFunc(...args.slice(2))) {
(args[0] as Utils.BooleanHolder).value = true;
new CommonBattleAnim(CommonAnim.PROTECT, target).play(arena.scene);
arena.scene.queueMessage(`${super.getMoveName()} protected ${getPokemonMessage(target, "!")}`);
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
return true;
}
return false;
@ -281,7 +281,7 @@ class MatBlockTag extends ConditionalProtectTag {
onAdd(arena: Arena) {
const source = arena.scene.getPokemonById(this.sourceId);
arena.scene.queueMessage(getPokemonMessage(source, " intends to flip up a mat\nand block incoming attacks!"));
arena.scene.queueMessage(i18next.t("arenaTag:matBlockOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(source) }));
}
}
@ -319,7 +319,7 @@ class WishTag extends ArenaTag {
onAdd(arena: Arena): void {
const user = arena.scene.getPokemonById(this.sourceId);
this.battlerIndex = user.getBattlerIndex();
this.triggerMessage = getPokemonMessage(user, "'s wish\ncame true!");
this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(user) });
this.healHp = Math.max(Math.floor(user.getMaxHp() / 2), 1);
}
@ -373,11 +373,11 @@ class MudSportTag extends WeakenMoveTypeTag {
}
onAdd(arena: Arena): void {
arena.scene.queueMessage("Electricity's power was weakened!");
arena.scene.queueMessage(i18next.t("arenaTag:mudSportOnAdd"));
}
onRemove(arena: Arena): void {
arena.scene.queueMessage("The effects of Mud Sport\nhave faded.");
arena.scene.queueMessage(i18next.t("arenaTag:mudSportOnRemove"));
}
}
@ -391,11 +391,11 @@ class WaterSportTag extends WeakenMoveTypeTag {
}
onAdd(arena: Arena): void {
arena.scene.queueMessage("Fire's power was weakened!");
arena.scene.queueMessage(i18next.t("arenaTag:waterSportOnAdd"));
}
onRemove(arena: Arena): void {
arena.scene.queueMessage("The effects of Water Sport\nhave faded.");
arena.scene.queueMessage(i18next.t("arenaTag:waterSportOnRemove"));
}
}
@ -463,7 +463,7 @@ class SpikesTag extends ArenaTrapTag {
const source = arena.scene.getPokemonById(this.sourceId);
if (!quiet) {
arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${source.getOpponentDescriptor()}'s feet!`);
arena.scene.queueMessage(i18next.t("arenaTag:spikesOnAdd", { moveName: this.getMoveName(), opponentDesc: source.getOpponentDescriptor() }));
}
}
@ -476,7 +476,7 @@ class SpikesTag extends ArenaTrapTag {
const damageHpRatio = 1 / (10 - 2 * this.layers);
const damage = Math.ceil(pokemon.getMaxHp() * damageHpRatio);
pokemon.scene.queueMessage(getPokemonMessage(pokemon, " is hurt\nby the spikes!"));
pokemon.scene.queueMessage(i18next.t("arenaTag:spikesActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
pokemon.damageAndUpdate(damage, HitResult.OTHER);
if (pokemon.turnData) {
pokemon.turnData.damageTaken += damage;
@ -508,7 +508,7 @@ class ToxicSpikesTag extends ArenaTrapTag {
const source = arena.scene.getPokemonById(this.sourceId);
if (!quiet) {
arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${source.getOpponentDescriptor()}'s feet!`);
arena.scene.queueMessage(i18next.t("arenaTag:toxicSpikesOnAdd", { moveName: this.getMoveName(), opponentDesc: source.getOpponentDescriptor() }));
}
}
@ -523,12 +523,12 @@ class ToxicSpikesTag extends ArenaTrapTag {
if (pokemon.isOfType(Type.POISON)) {
this.neutralized = true;
if (pokemon.scene.arena.removeTag(this.tagType)) {
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` absorbed the ${this.getMoveName()}!`));
pokemon.scene.queueMessage(i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: this.getMoveName() }));
return true;
}
} else if (!pokemon.status) {
const toxic = this.layers > 1;
if (pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, `the ${this.getMoveName()}`)) {
if (pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName())) {
return true;
}
}
@ -590,7 +590,7 @@ class StealthRockTag extends ArenaTrapTag {
const source = arena.scene.getPokemonById(this.sourceId);
if (!quiet) {
arena.scene.queueMessage(`Pointed stones float in the air\naround ${source.getOpponentDescriptor()}!`);
arena.scene.queueMessage(i18next.t("arenaTag:stealthRockOnAdd", { opponentDesc: source.getOpponentDescriptor() }));
}
}
@ -635,7 +635,7 @@ class StealthRockTag extends ArenaTrapTag {
if (damageHpRatio) {
const damage = Math.ceil(pokemon.getMaxHp() * damageHpRatio);
pokemon.scene.queueMessage(`Pointed stones dug into\n${getPokemonNameWithAffix(pokemon)}!`);
pokemon.scene.queueMessage(i18next.t("arenaTag:stealthRockActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
pokemon.damageAndUpdate(damage, HitResult.OTHER);
if (pokemon.turnData) {
pokemon.turnData.damageTaken += damage;
@ -663,12 +663,9 @@ class StickyWebTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet: boolean = false): void {
super.onAdd(arena);
// does not seem to be used anywhere
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const source = arena.scene.getPokemonById(this.sourceId);
if (!quiet) {
arena.scene.queueMessage(`A ${this.getMoveName()} has been laid out on the ground around the opposing team!`);
arena.scene.queueMessage(i18next.t("arenaTag:stickyWebOnAdd", { moveName: this.getMoveName(), opponentDesc: source.getOpponentDescriptor() }));
}
}
@ -677,7 +674,7 @@ class StickyWebTag extends ArenaTrapTag {
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.scene.queueMessage(`The opposing ${pokemon.getNameToRender()} was caught in a sticky web!`);
pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() }));
const statLevels = new Utils.NumberHolder(-1);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [BattleStat.SPD], statLevels.value));
}
@ -705,11 +702,11 @@ export class TrickRoomTag extends ArenaTag {
}
onAdd(arena: Arena): void {
arena.scene.queueMessage(getPokemonMessage(arena.scene.getPokemonById(this.sourceId), " twisted\nthe dimensions!"));
arena.scene.queueMessage(i18next.t("arenaTag:trickRoomOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(arena.scene.getPokemonById(this.sourceId)) }));
}
onRemove(arena: Arena): void {
arena.scene.queueMessage("The twisted dimensions\nreturned to normal!");
arena.scene.queueMessage(i18next.t("arenaTag:trickRoomOnRemove"));
}
}
@ -724,7 +721,7 @@ export class GravityTag extends ArenaTag {
}
onAdd(arena: Arena): void {
arena.scene.queueMessage("Gravity intensified!");
arena.scene.queueMessage(i18next.t("arenaTag:gravityOnAdd"));
arena.scene.getField(true).forEach((pokemon) => {
if (pokemon !== null) {
pokemon.removeTag(BattlerTagType.MAGNET_RISEN);
@ -733,7 +730,7 @@ export class GravityTag extends ArenaTag {
}
onRemove(arena: Arena): void {
arena.scene.queueMessage("Gravity returned to normal!");
arena.scene.queueMessage(i18next.t("arenaTag:gravityOnRemove"));
}
}
@ -749,7 +746,7 @@ class TailwindTag extends ArenaTag {
onAdd(arena: Arena, quiet: boolean = false): void {
if (!quiet) {
arena.scene.queueMessage(`The Tailwind blew from behind${this.side === ArenaTagSide.PLAYER ? "\nyour" : this.side === ArenaTagSide.ENEMY ? "\nthe opposing" : ""} team!`);
arena.scene.queueMessage(i18next.t(`arenaTag:tailwindOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
}
const source = arena.scene.getPokemonById(this.sourceId);
@ -771,7 +768,7 @@ class TailwindTag extends ArenaTag {
onRemove(arena: Arena, quiet: boolean = false): void {
if (!quiet) {
arena.scene.queueMessage(`${this.side === ArenaTagSide.PLAYER ? "Your" : this.side === ArenaTagSide.ENEMY ? "The opposing" : ""} team's Tailwind petered out!`);
arena.scene.queueMessage(i18next.t(`arenaTag:tailwindOnRemove${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
}
}
}
@ -786,11 +783,11 @@ class HappyHourTag extends ArenaTag {
}
onAdd(arena: Arena): void {
arena.scene.queueMessage("Everyone is caught up in the happy atmosphere!");
arena.scene.queueMessage(i18next.t("arenaTag:happyHourOnAdd"));
}
onRemove(arena: Arena): void {
arena.scene.queueMessage("The atmosphere returned to normal.");
arena.scene.queueMessage(i18next.t("arenaTag:happyHourOnRemove"));
}
}

View File

@ -1,4 +1,4 @@
import i18next, {ParseKeys} from "i18next";
import i18next, { ParseKeys } from "i18next";
export enum BattleStat {
ATK,
@ -32,7 +32,7 @@ export function getBattleStatName(stat: BattleStat) {
}
}
export function getBattleStatLevelChangeDescription(pokemonNameWithAffix: string, stats: string, levels: integer, up: boolean, count: integer = 1) {
export function getBattleStatLevelChangeDescription(pokemonNameWithAffix: string, stats: string, levels: integer, up: boolean, count: number = 1) {
const stringKey = (() => {
if (up) {
switch (levels) {

View File

@ -31,14 +31,14 @@ export enum BattlerTagLapseType {
export class BattlerTag {
public tagType: BattlerTagType;
public lapseType: BattlerTagLapseType[];
public lapseTypes: BattlerTagLapseType[];
public turnCount: number;
public sourceMove: Moves;
public sourceId?: number;
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType | BattlerTagLapseType[], turnCount: number, sourceMove: Moves, sourceId?: number) {
this.tagType = tagType;
this.lapseType = typeof lapseType === "number" ? [ lapseType ] : lapseType;
this.lapseTypes = Array.isArray(lapseType) ? lapseType : [ lapseType ];
this.turnCount = turnCount;
this.sourceMove = sourceMove;
this.sourceId = sourceId;
@ -497,12 +497,6 @@ export class FrenzyTag extends BattlerTag {
}
}
export class ChargingTag extends BattlerTag {
constructor(sourceMove: Moves, sourceId: number) {
super(BattlerTagType.CHARGING, BattlerTagLapseType.CUSTOM, 1, sourceMove, sourceId);
}
}
export class EncoreTag extends BattlerTag {
public moveId: Moves;
@ -650,8 +644,7 @@ export class OctolockTag extends TrappedTag {
}
canAdd(pokemon: Pokemon): boolean {
const isOctolocked = pokemon.getTag(BattlerTagType.OCTOLOCK);
return !isOctolocked;
return !pokemon.getTag(BattlerTagType.OCTOLOCK);
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -1414,30 +1407,6 @@ export class CritBoostTag extends BattlerTag {
}
}
export class AlwaysCritTag extends BattlerTag {
constructor(sourceMove: Moves) {
super(BattlerTagType.ALWAYS_CRIT, BattlerTagLapseType.TURN_END, 2, sourceMove);
}
}
export class IgnoreAccuracyTag extends BattlerTag {
constructor(sourceMove: Moves) {
super(BattlerTagType.IGNORE_ACCURACY, BattlerTagLapseType.TURN_END, 2, sourceMove);
}
}
export class AlwaysGetHitTag extends BattlerTag {
constructor(sourceMove: Moves) {
super(BattlerTagType.ALWAYS_GET_HIT, BattlerTagLapseType.PRE_MOVE, 1, sourceMove);
}
}
export class ReceiveDoubleDamageTag extends BattlerTag {
constructor(sourceMove: Moves) {
super(BattlerTagType.RECEIVE_DOUBLE_DAMAGE, BattlerTagLapseType.PRE_MOVE, 1, sourceMove);
}
}
export class SaltCuredTag extends BattlerTag {
private sourceIndex: number;
@ -1628,8 +1597,8 @@ export class StockpilingTag extends BattlerTag {
super.loadTag(source);
this.stockpiledCount = source.stockpiledCount || 0;
this.statChangeCounts = {
[BattleStat.DEF]: source.statChangeCounts?.[BattleStat.DEF] || 0,
[BattleStat.SPDEF]: source.statChangeCounts?.[BattleStat.SPDEF] || 0,
[ BattleStat.DEF ]: source.statChangeCounts?.[ BattleStat.DEF ] ?? 0,
[ BattleStat.SPDEF ]: source.statChangeCounts?.[ BattleStat.SPDEF ] ?? 0,
};
}
@ -1697,7 +1666,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.FRENZY:
return new FrenzyTag(turnCount, sourceMove, sourceId);
case BattlerTagType.CHARGING:
return new ChargingTag(sourceMove, sourceId);
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, 1, sourceMove, sourceId);
case BattlerTagType.ENCORE:
return new EncoreTag(sourceId);
case BattlerTagType.HELPING_HAND:
@ -1770,17 +1739,15 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.CRIT_BOOST:
return new CritBoostTag(tagType, sourceMove);
case BattlerTagType.ALWAYS_CRIT:
return new AlwaysCritTag(sourceMove);
case BattlerTagType.IGNORE_ACCURACY:
return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove);
case BattlerTagType.NO_CRIT:
return new BattlerTag(tagType, BattlerTagLapseType.AFTER_MOVE, turnCount, sourceMove);
case BattlerTagType.IGNORE_ACCURACY:
return new IgnoreAccuracyTag(sourceMove);
case BattlerTagType.ALWAYS_GET_HIT:
return new AlwaysGetHitTag(sourceMove);
case BattlerTagType.RECEIVE_DOUBLE_DAMAGE:
return new ReceiveDoubleDamageTag(sourceMove);
return new BattlerTag(tagType, BattlerTagLapseType.PRE_MOVE, 1, sourceMove);
case BattlerTagType.BYPASS_SLEEP:
return new BattlerTag(BattlerTagType.BYPASS_SLEEP, BattlerTagLapseType.TURN_END, turnCount, sourceMove);
return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, turnCount, sourceMove);
case BattlerTagType.IGNORE_FLYING:
return new GroundedTag(tagType, BattlerTagLapseType.CUSTOM, sourceMove);
case BattlerTagType.ROOSTED:

View File

@ -17,8 +17,6 @@ export function getBiomeName(biome: Biome | -1) {
return i18next.t("biome:GRASS");
case Biome.RUINS:
return i18next.t("biome:RUINS");
case Biome.ABYSS:
return i18next.t("biome:ABYSS");
case Biome.END:
return i18next.t("biome:END");
default:

View File

@ -17,6 +17,9 @@ import { Gender } from "./gender";
import { pokemonEvolutions } from "./pokemon-evolutions";
import { pokemonFormChanges } from "./pokemon-forms";
/** A constant for the default max cost of the starting party before a run */
const DEFAULT_PARTY_MAX_COST = 10;
/**
* An enum for all the challenge types. The parameter entries on these describe the
* parameters to use when calling the applyChallenges function.
@ -403,14 +406,7 @@ export class SingleGenerationChallenge extends Challenge {
}
applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder, dexAttr: DexAttrProps, soft: boolean = false, checkEvolutions?: boolean): boolean {
/**
* We have special code below for victini because it is classed as a generation 4 pokemon in the code
* despite being a generation 5 pokemon. This is due to UI constraints, the starter select screen has
* no more room for pokemon so victini is put in the gen 4 section instead. This code just overrides the
* normal generation check to correctly treat victini as gen 5.
*/
const starterGeneration = pokemon.speciesId === Species.VICTINI ? 5 : pokemon.generation;
const generations = [starterGeneration];
const generations = [pokemon.generation];
const checkPokemonEvolutions = checkEvolutions ?? true as boolean;
if (soft) {
const speciesToCheck = [pokemon.speciesId];
@ -689,11 +685,11 @@ export class LowerStarterMaxCostChallenge extends Challenge {
if (overrideValue === undefined) {
overrideValue = this.value;
}
return (10 - overrideValue).toString();
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
}
applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder): boolean {
if (speciesStarters[pokemon.speciesId] > 10 - this.value) {
if (speciesStarters[pokemon.speciesId] > DEFAULT_PARTY_MAX_COST - this.value) {
valid.value = false;
return true;
}
@ -723,7 +719,7 @@ export class LowerStarterPointsChallenge extends Challenge {
if (overrideValue === undefined) {
overrideValue = this.value;
}
return (10 - overrideValue).toString();
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
}
applyStarterPoints(points: Utils.NumberHolder): boolean {

View File

@ -459,6 +459,20 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
]
}
],
[TrainerType.ROCKET_ADMIN]: [
{
encounter: [
"dialogue:rocket_admin.encounter.1",
"dialogue:rocket_admin.encounter.2",
"dialogue:rocket_admin.encounter.3",
],
victory: [
"dialogue:rocket_admin.victory.1",
"dialogue:rocket_admin.victory.2",
"dialogue:rocket_admin.victory.3",
]
}
],
[TrainerType.MAGMA_GRUNT]: [
{
encounter: [
@ -469,6 +483,20 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
]
}
],
[TrainerType.MAGMA_ADMIN]: [
{
encounter: [
"dialogue:magma_admin.encounter.1",
"dialogue:magma_admin.encounter.2",
"dialogue:magma_admin.encounter.3",
],
victory: [
"dialogue:magma_admin.victory.1",
"dialogue:magma_admin.victory.2",
"dialogue:magma_admin.victory.3",
]
}
],
[TrainerType.AQUA_GRUNT]: [
{
encounter: [
@ -479,6 +507,20 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
]
}
],
[TrainerType.AQUA_ADMIN]: [
{
encounter: [
"dialogue:aqua_admin.encounter.1",
"dialogue:aqua_admin.encounter.2",
"dialogue:aqua_admin.encounter.3",
],
victory: [
"dialogue:aqua_admin.victory.1",
"dialogue:aqua_admin.victory.2",
"dialogue:aqua_admin.victory.3",
]
}
],
[TrainerType.GALACTIC_GRUNT]: [
{
encounter: [
@ -489,6 +531,20 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
]
}
],
[TrainerType.GALACTIC_ADMIN]: [
{
encounter: [
"dialogue:galactic_admin.encounter.1",
"dialogue:galactic_admin.encounter.2",
"dialogue:galactic_admin.encounter.3",
],
victory: [
"dialogue:galactic_admin.victory.1",
"dialogue:galactic_admin.victory.2",
"dialogue:galactic_admin.victory.3",
]
}
],
[TrainerType.PLASMA_GRUNT]: [
{
encounter: [
@ -499,6 +555,20 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
]
}
],
[TrainerType.PLASMA_SAGE]: [
{
encounter: [
"dialogue:plasma_sage.encounter.1",
"dialogue:plasma_sage.encounter.2",
"dialogue:plasma_sage.encounter.3",
],
victory: [
"dialogue:plasma_sage.victory.1",
"dialogue:plasma_sage.victory.2",
"dialogue:plasma_sage.victory.3",
]
}
],
[TrainerType.FLARE_GRUNT]: [
{
encounter: [
@ -509,6 +579,20 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
]
}
],
[TrainerType.FLARE_ADMIN]: [
{
encounter: [
"dialogue:flare_admin.encounter.1",
"dialogue:flare_admin.encounter.2",
"dialogue:flare_admin.encounter.3",
],
victory: [
"dialogue:flare_admin.victory.1",
"dialogue:flare_admin.victory.2",
"dialogue:flare_admin.victory.3",
]
}
],
[TrainerType.ROCKET_BOSS_GIOVANNI_1]: [
{
encounter: [

View File

@ -438,7 +438,7 @@ export const speciesEggMoves = {
[Species.CHEWTLE]: [ Moves.FIRE_FANG, Moves.ACCELEROCK, Moves.SHELL_SMASH, Moves.FISHIOUS_REND ],
[Species.YAMPER]: [ Moves.ICE_FANG, Moves.SWORDS_DANCE, Moves.THUNDER_FANG, Moves.ZIPPY_ZAP ],
[Species.ROLYCOLY]: [ Moves.BITTER_BLADE, Moves.BODY_PRESS, Moves.BULK_UP, Moves.DIAMOND_STORM ],
[Species.APPLIN]: [ Moves.DRAGON_CHEER, Moves.DRAGON_HAMMER, Moves.FLOWER_TRICK, Moves.STRENGTH_SAP ],
[Species.APPLIN]: [ Moves.MATCHA_GOTCHA, Moves.DRAGON_HAMMER, Moves.FLOWER_TRICK, Moves.STRENGTH_SAP ],
[Species.SILICOBRA]: [ Moves.SHORE_UP, Moves.SHED_TAIL, Moves.STONE_EDGE, Moves.PRECIPICE_BLADES ],
[Species.CRAMORANT]: [ Moves.APPLE_ACID, Moves.SURF, Moves.SCORCHING_SANDS, Moves.OBLIVION_WING ],
[Species.ARROKUDA]: [ Moves.SUPERCELL_SLAM, Moves.KNOCK_OFF, Moves.ICE_SPINNER, Moves.FILLET_AWAY ],

View File

@ -66,7 +66,7 @@ export interface IEggOptions {
export class Egg {
////
// #region Privat properties
// #region Private properties
////
private _id: number;
@ -182,7 +182,7 @@ export class Egg {
}
////
// #region Public methodes
// #region Public methods
////
public isManaphyEgg(): boolean {
@ -212,7 +212,7 @@ export class Egg {
let abilityIndex = undefined;
if (pokemonSpecies.abilityHidden && (this._overrideHiddenAbility
|| (this._sourceType === EggSourceType.SAME_SPECIES_EGG && !Utils.randSeedInt(SAME_SPECIES_EGG_HA_RATE)))) {
abilityIndex = pokemonSpecies.ability2 ? 2 : 1;
abilityIndex = 2;
}
// This function has way to many optional parameters
@ -281,7 +281,7 @@ export class Egg {
////
////
// #region Private methodes
// #region Private methods
////
private rollEggMoveIndex() {

View File

@ -2,7 +2,7 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./
import { BattleEndPhase, MoveEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases";
import { BattleStat, getBattleStatName } from "./battle-stat";
import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, TypeBoostTag } from "./battler-tags";
import { getPokemonMessage, getPokemonNameWithAffix } from "../messages";
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect";
import { getTypeResistances, Type } from "./type";
@ -13,10 +13,9 @@ import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability";
import { allAbilities } from "./ability";
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
import { BattlerIndex } from "../battle";
import { BattlerIndex, BattleType } from "../battle";
import { Stat } from "./pokemon-stat";
import { TerrainType } from "./terrain";
import { SpeciesFormChangeActiveTrigger } from "./pokemon-forms";
import { ModifierPoolType } from "#app/modifier/modifier-type";
import { Command } from "../ui/command-ui-handler";
import i18next from "i18next";
@ -718,9 +717,13 @@ export default class Move implements Localizable {
* @returns The calculated power of the move.
*/
calculateBattlePower(source: Pokemon, target: Pokemon): number {
const power = new Utils.NumberHolder(this.power);
if (this.category === MoveCategory.STATUS) {
return -1;
}
const power = new Utils.NumberHolder(this.power);
const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1);
applyPreAttackAbAttrs(MoveTypeChangeAttr, source, target, this, typeChangeMovePowerMultiplier);
const sourceTeraType = source.getTeraType();
@ -974,7 +977,9 @@ export class MoveEffectAttr extends MoveAttr {
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): integer {
const moveChance = new Utils.NumberHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, moveChance, move, target, selfEffect, showAbility);
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr,target,user,null,null, moveChance);
if (!selfEffect) {
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, target, user, null, null, moveChance);
}
return moveChance.value;
}
}
@ -1221,7 +1226,7 @@ export class RecoilAttr extends MoveEffectAttr {
}
user.damageAndUpdate(recoilDamage, HitResult.OTHER, false, true, true);
user.scene.queueMessage(getPokemonMessage(user, " is hit\nwith recoil!"));
user.scene.queueMessage(i18next.t("moveTriggers:hitWithRecoil", {pokemonName: getPokemonNameWithAffix(user)}));
user.turnData.damageTaken += recoilDamage;
return true;
@ -1333,7 +1338,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled);
if (!cancelled.value) {
user.damageAndUpdate(Math.ceil(user.getMaxHp()/2), HitResult.OTHER, false, true, true);
user.scene.queueMessage(getPokemonMessage(user, " cut its own HP to power up its move!")); // Queue recoil message
user.scene.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", {pokemonName: getPokemonNameWithAffix(user)})); // Queue recoil message
}
return true;
}
@ -1383,7 +1388,7 @@ export class HealAttr extends MoveEffectAttr {
*/
addHealPhase(target: Pokemon, healRatio: number) {
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
Math.max(Math.floor(target.getMaxHp() * healRatio), 1), getPokemonMessage(target, " \nhad its HP restored."), true, !this.showAnim));
Math.max(Math.floor(target.getMaxHp() * healRatio), 1), i18next.t("moveTriggers:healHp", {pokemonName: getPokemonNameWithAffix(target)}), true, !this.showAnim));
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
@ -1430,6 +1435,39 @@ export class PartyStatusCureAttr extends MoveEffectAttr {
}
}
/**
* Applies damage to the target's ally equal to 1/16 of that ally's max HP.
* @extends MoveEffectAttr
*/
export class FlameBurstAttr extends MoveEffectAttr {
/**
* @param user - n/a
* @param target - The target Pokémon.
* @param move - n/a
* @param args - n/a
* @returns A boolean indicating whether the effect was successfully applied.
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
const targetAlly = target.getAlly();
const cancelled = new Utils.BooleanHolder(false);
if (targetAlly) {
applyAbAttrs(BlockNonDirectDamageAbAttr, targetAlly, cancelled);
}
if (cancelled.value || !targetAlly) {
return false;
}
targetAlly.damageAndUpdate(Math.max(1, Math.floor(1/16 * targetAlly.getMaxHp())), HitResult.OTHER);
return true;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return target.getAlly() ? -5 : 0;
}
}
export class SacrificialFullRestoreAttr extends SacrificialAttr {
constructor() {
super();
@ -1444,7 +1482,7 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
const maxPartyMemberHp = user.scene.getParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0);
user.scene.pushPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(),
maxPartyMemberHp, getPokemonMessage(user, "'s Healing Wish\nwas granted!"), true, false, false, true), true);
maxPartyMemberHp, i18next.t("moveTriggers:sacrificialFullRestore", {pokemonName: getPokemonNameWithAffix(user)}), true, false, false, true), true);
return true;
}
@ -1637,10 +1675,15 @@ export class HitHealAttr extends MoveEffectAttr {
message = i18next.t("battle:regainHealth", {pokemonName: getPokemonNameWithAffix(user)});
}
if (reverseDrain) {
if (user.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
healAmount = 0;
message = null;
} else {
user.turnData.damageTaken += healAmount;
healAmount = healAmount * -1;
message = null;
}
}
user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(), healAmount, message, false, true));
return true;
}
@ -1774,13 +1817,10 @@ export class MultiHitAttr extends MoveAttr {
}
case MultiHitType._2:
return 2;
break;
case MultiHitType._3:
return 3;
break;
case MultiHitType._10:
return 10;
break;
case MultiHitType.BEAT_UP:
const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty();
// No status means the ally pokemon can contribute to Beat Up
@ -1874,7 +1914,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const statusToApply: StatusEffect = user.status?.effect;
const statusToApply: StatusEffect = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
if (target.status) {
return false;
@ -1882,7 +1922,9 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) {
const statusAfflictResult = target.trySetStatus(statusToApply, true, user);
if (statusAfflictResult) {
if (user.status) {
user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user)));
}
user.resetStatus();
user.updateInfo();
}
@ -1923,7 +1965,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
user.scene.tryTransferHeldItemModifier(stolenItem, user, false).then(success => {
if (success) {
user.scene.queueMessage(getPokemonMessage(user, ` stole\n${target.name}'s ${stolenItem.type.name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:stoleItem", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name}));
}
resolve(success);
});
@ -2002,9 +2044,9 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
target.scene.updateModifiers(target.isPlayer());
if (this.berriesOnly) {
user.scene.queueMessage(getPokemonMessage(user, ` incinerated\n${target.name}'s ${removedItem.type.name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name}));
} else {
user.scene.queueMessage(getPokemonMessage(user, ` knocked off\n${target.name}'s ${removedItem.type.name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:knockedOffItem", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name}));
}
}
@ -2150,6 +2192,12 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
return false;
}
// Special edge case for shield dust blocking Sparkling Aria curing burn
const moveTargets = getMoveTargets(user, move.id);
if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) {
return false;
}
const pokemon = this.selfTarget ? user : target;
if (pokemon.status && this.effects.includes(pokemon.status.effect)) {
pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
@ -2325,7 +2373,7 @@ export class ChargeAttr extends OverrideMoveEffectAttr {
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && (this.sameTurn || lastMove.turn !== user.scene.currentBattle.turn))) {
(args[0] as Utils.BooleanHolder).value = true;
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => {
user.scene.queueMessage(getPokemonMessage(user, ` ${this.chargeText.replace("{TARGET}", target.name)}`));
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
if (this.tagType) {
user.addTag(this.tagType, 1, move.id, user.id);
}
@ -2381,7 +2429,7 @@ export class SunlightChargeAttr extends ChargeAttr {
export class ElectroShotChargeAttr extends ChargeAttr {
private statIncreaseApplied: boolean;
constructor() {
super(ChargeAnim.ELECTRO_SHOT_CHARGING, "absorbed electricity!", null, true);
super(ChargeAnim.ELECTRO_SHOT_CHARGING, i18next.t("moveTriggers:absorbedElectricity", {pokemonName: "{USER}"}), null, true);
// Add a flag because ChargeAttr skills use themselves twice instead of once over one-to-two turns
this.statIncreaseApplied = false;
}
@ -2433,14 +2481,14 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
if (args.length < 2 || !args[1]) {
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => {
(args[0] as Utils.BooleanHolder).value = true;
user.scene.queueMessage(getPokemonMessage(user, ` ${this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target))}`));
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
user.scene.arena.addTag(this.tagType, 3, move.id, user.id, ArenaTagSide.BOTH, false, target.getBattlerIndex());
resolve(true);
});
} else {
user.scene.ui.showText(getPokemonMessage(user.scene.getPokemonById(target.id), ` took\nthe ${move.name} attack!`), null, () => resolve(true));
user.scene.ui.showText(i18next.t("moveTriggers:tookMoveAttack", {pokemonName: getPokemonNameWithAffix(user.scene.getPokemonById(target.id)), moveName: move.name}), null, () => resolve(true));
}
});
}
@ -2580,36 +2628,15 @@ export class GrowthStatChangeAttr extends StatChangeAttr {
}
}
export class HalfHpStatMaxAttr extends StatChangeAttr {
constructor(stat: BattleStat) {
super(stat, 12, true, null, false);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise<boolean>(resolve => {
user.damageAndUpdate(Math.floor(user.getMaxHp() / 2), HitResult.OTHER, false, true);
user.updateInfo().then(() => {
const ret = super.apply(user, target, move, args);
user.scene.queueMessage(getPokemonMessage(user, ` cut its own HP\nand maximized its ${getBattleStatName(this.stats[BattleStat.ATK])}!`));
resolve(ret);
});
});
}
getCondition(): MoveConditionFunc {
return (user, target, move) => user.getHpRatio() > 0.5 && user.summonData.battleStats[this.stats[BattleStat.ATK]] < 6;
}
// TODO: Add benefit score that considers HP cut
}
export class CutHpStatBoostAttr extends StatChangeAttr {
private cutRatio: integer;
private messageCallback: ((user: Pokemon) => void) | undefined;
constructor(stat: BattleStat | BattleStat[], levels: integer, cutRatio: integer) {
constructor(stat: BattleStat | BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) {
super(stat, levels, true, null, true);
this.cutRatio = cutRatio;
this.messageCallback = messageCallback;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
@ -2617,13 +2644,16 @@ export class CutHpStatBoostAttr extends StatChangeAttr {
user.damageAndUpdate(Math.floor(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true);
user.updateInfo().then(() => {
const ret = super.apply(user, target, move, args);
if (this.messageCallback) {
this.messageCallback(user);
}
resolve(ret);
});
});
}
getCondition(): MoveConditionFunc {
return (user, target, move) => user.getHpRatio() > 1 / this.cutRatio;
return (user, target, move) => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.summonData.battleStats[s] < 6);
}
}
@ -2643,8 +2673,7 @@ export class CopyStatsAttr extends MoveEffectAttr {
}
target.updateInfo();
user.updateInfo();
target.scene.queueMessage(getPokemonMessage(user, " copied\n") + getPokemonMessage(target, "'s stat changes!"));
target.scene.queueMessage(i18next.t("moveTriggers:copiedStatChanges", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target)}));
return true;
}
@ -2662,7 +2691,7 @@ export class InvertStatsAttr extends MoveEffectAttr {
target.updateInfo();
user.updateInfo();
target.scene.queueMessage(getPokemonMessage(target, "'s stat changes\nwere all reversed!"));
target.scene.queueMessage(i18next.t("moveTriggers:invertStats", {pokemonName: getPokemonNameWithAffix(target)}));
return true;
}
@ -2680,7 +2709,7 @@ export class ResetStatsAttr extends MoveEffectAttr {
target.updateInfo();
user.updateInfo();
target.scene.queueMessage(getPokemonMessage(target, "'s stat changes\nwere eliminated!"));
target.scene.queueMessage(i18next.t("moveTriggers:resetStats", {pokemonName: getPokemonNameWithAffix(target)}));
return true;
}
@ -2710,7 +2739,7 @@ export class SwapStatsAttr extends MoveEffectAttr {
}
target.updateInfo();
user.updateInfo();
target.scene.queueMessage(getPokemonMessage(user, " switched stat changes with the target!"));
target.scene.queueMessage(i18next.t("moveTriggers:switchedStatChanges", {pokemonName: getPokemonNameWithAffix(user)}));
return true;
}
}
@ -2855,7 +2884,12 @@ export class BeatUpAttr extends VariablePowerAttr {
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const power = args[0] as Utils.NumberHolder;
const allyIndex = user.turnData.hitCount - user.turnData.hitsLeft;
const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty();
const allyCount = party.filter(pokemon => {
return pokemon.id === user.id || !pokemon.status?.effect;
}).length;
const allyIndex = (user.turnData.hitCount - user.turnData.hitsLeft) % allyCount;
power.value = beatUpFunc(user, allyIndex);
return true;
}
@ -2866,7 +2900,7 @@ const doublePowerChanceMessageFunc = (user: Pokemon, target: Pokemon, move: Move
user.scene.executeWithSeedOffset(() => {
const rand = Utils.randSeedInt(100);
if (rand < move.chance) {
message = getPokemonMessage(user, " is going all out for this attack!");
message = i18next.t("moveTriggers:goingAllOutForAttack", {pokemonName: getPokemonNameWithAffix(user)});
}
}, user.scene.currentBattle.turn << 6, user.scene.waveSeed);
return message;
@ -3145,7 +3179,7 @@ const magnitudeMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
}
}
message = `Magnitude ${m + 4}!`;
message = i18next.t("moveTriggers:magnitudeMessage", {magnitude: m + 4});
}, user.scene.currentBattle.turn << 6, user.scene.waveSeed);
return message;
};
@ -3295,7 +3329,7 @@ export class PresentPowerAttr extends VariablePowerAttr {
// If this move is multi-hit, disable all other hits
user.stopMultiHit();
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
Math.max(Math.floor(target.getMaxHp() / 4), 1), getPokemonMessage(target, " regained\nhealth!"), true));
Math.max(Math.floor(target.getMaxHp() / 4), 1), i18next.t("moveTriggers:regainedHealth", {pokemonName: getPokemonNameWithAffix(target)}), true));
}
return true;
@ -3405,6 +3439,72 @@ export class MultiHitPowerIncrementAttr extends VariablePowerAttr {
}
}
/**
* Attribute used for moves that double in power if the given move immediately
* preceded the move applying the attribute, namely Fusion Flare and
* Fusion Bolt.
* @extends VariablePowerAttr
* @see {@linkcode apply}
*/
export class LastMoveDoublePowerAttr extends VariablePowerAttr {
/** The move that must precede the current move */
private move: Moves;
constructor(move: Moves) {
super();
this.move = move;
}
/**
* Doubles power of move if the given move is found to precede the current
* move with no other moves being executed in between, only ignoring failed
* moves if any.
* @param user {@linkcode Pokemon} that used the move
* @param target N/A
* @param move N/A
* @param args [0] {@linkcode Utils.NumberHolder} that holds the resulting power of the move
* @returns true if attribute application succeeds, false otherwise
*/
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
const power = args[0] as Utils.NumberHolder;
const enemy = user.getOpponent(0);
const pokemonActed: Pokemon[] = [];
if (enemy.turnData.acted) {
pokemonActed.push(enemy);
}
if (user.scene.currentBattle.double) {
const userAlly = user.getAlly();
const enemyAlly = enemy.getAlly();
if (userAlly && userAlly.turnData.acted) {
pokemonActed.push(userAlly);
}
if (enemyAlly && enemyAlly.turnData.acted) {
pokemonActed.push(enemyAlly);
}
}
pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order);
for (const p of pokemonActed) {
const [ lastMove ] = p.getLastXMoves(1);
if (lastMove.result !== MoveResult.FAIL) {
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
power.value *= 2;
return true;
} else {
break;
}
}
}
return false;
}
}
export class VariableAtkAttr extends MoveAttr {
constructor() {
super();
@ -3716,26 +3816,18 @@ export class IvyCudgelTypeAttr extends VariableMoveTypeAttr {
switch (form) {
case 1: // Wellspring Mask
move.type = Type.WATER;
break;
case 2: // Hearthflame Mask
move.type = Type.FIRE;
break;
case 3: // Cornerstone Mask
move.type = Type.ROCK;
break;
case 4: // Teal Mask Tera
move.type = Type.GRASS;
break;
case 5: // Wellspring Mask Tera
move.type = Type.WATER;
break;
case 2: // Hearthflame Mask
case 6: // Hearthflame Mask Tera
move.type = Type.FIRE;
break;
case 3: // Cornerstone Mask
case 7: // Cornerstone Mask Tera
move.type = Type.ROCK;
break;
case 4: // Teal Mask Tera
default:
move.type = Type.GRASS;
break;
@ -3985,7 +4077,7 @@ const crashDamageFunc = (user: Pokemon, move: Move) => {
}
user.damageAndUpdate(Math.floor(user.getMaxHp() / 2), HitResult.OTHER, false, true);
user.scene.queueMessage(getPokemonMessage(user, " kept going\nand crashed!"));
user.scene.queueMessage(i18next.t("moveTriggers:keptGoingAndCrashed", {pokemonName: getPokemonNameWithAffix(user)}));
user.turnData.damageTaken += Math.floor(user.getMaxHp() / 2);
return true;
@ -4025,7 +4117,7 @@ export class DisableMoveAttr extends MoveEffectAttr {
target.summonData.disabledMove = disabledMove.moveId;
target.summonData.disabledTurns = 4;
user.scene.queueMessage(getPokemonMessage(target, `'s ${disabledMove.getName()}\nwas disabled!`));
user.scene.queueMessage(i18next.t("abilityTriggers:postDefendMoveDisable", { pokemonNameWithAffix: getPokemonNameWithAffix(target), moveName: disabledMove.getName()}));
return true;
}
@ -4306,12 +4398,6 @@ export class ProtectAttr extends AddBattlerTagAttr {
}
}
export class EndureAttr extends ProtectAttr {
constructor() {
super(BattlerTagType.ENDURING);
}
}
export class IgnoreAccuracyAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.IGNORE_ACCURACY, true, false, 2);
@ -4322,35 +4408,7 @@ export class IgnoreAccuracyAttr extends AddBattlerTagAttr {
return false;
}
user.scene.queueMessage(getPokemonMessage(user, ` took aim\nat ${target.name}!`));
return true;
}
}
export class AlwaysGetHitAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.ALWAYS_GET_HIT, true, false, 0, 0, true);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
return true;
}
}
export class ReceiveDoubleDamageAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.RECEIVE_DOUBLE_DAMAGE, true, false, 0, 0, true);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
user.scene.queueMessage(i18next.t("moveTriggers:tookAimAtTarget", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target)}));
return true;
}
@ -4366,7 +4424,7 @@ export class FaintCountdownAttr extends AddBattlerTagAttr {
return false;
}
user.scene.queueMessage(getPokemonMessage(target, `\nwill faint in ${this.turnCountMin - 1} turns.`));
user.scene.queueMessage(i18next.t("moveTriggers:faintCountdown", {pokemonName: getPokemonNameWithAffix(target), turnCount: this.turnCountMin - 1}));
return true;
}
@ -4606,7 +4664,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr {
}
user.scene.queueMessage( `${getPokemonNameWithAffix(user)} swapped the battle effects affecting each side of the field!`);
user.scene.queueMessage( i18next.t("moveTriggers:swapArenaTags", {pokemonName: getPokemonNameWithAffix(user)}));
return true;
}
}
@ -4701,19 +4759,14 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (switchOutTarget.hp > 0) {
applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, switchOutTarget);
// switchOut below sets the UI to select party(this is not a separate Phase), then adds a SwitchSummonPhase with selected 'mon
(switchOutTarget as PlayerPokemon).switchOut(this.batonPass, true).then(() => resolve(true));
(switchOutTarget as PlayerPokemon).switchOut(this.batonPass).then(() => resolve(true));
} else {
resolve(false);
}
return;
} else if (user.scene.currentBattle.battleType) {
// Switch out logic for the battle type
switchOutTarget.resetTurnData();
switchOutTarget.resetSummonData();
switchOutTarget.hideInfo();
switchOutTarget.setVisible(false);
switchOutTarget.scene.field.remove(switchOutTarget);
user.scene.triggerPokemonFormChange(switchOutTarget, SpeciesFormChangeActiveTrigger, true);
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
// Switch out logic for trainer battles
switchOutTarget.leaveField(!this.batonPass);
if (switchOutTarget.hp > 0) {
// for opponent switching out
@ -4726,7 +4779,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (switchOutTarget.hp) {
switchOutTarget.hideInfo().then(() => switchOutTarget.destroy());
switchOutTarget.scene.field.remove(switchOutTarget);
user.scene.queueMessage(getPokemonMessage(switchOutTarget, " fled!"), null, true, 500);
user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500);
}
if (!switchOutTarget.getAlly()?.isActive(true)) {
@ -4750,7 +4803,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
const blockedByAbility = new Utils.BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
return blockedByAbility.value ? getPokemonMessage(target, " can't be switched out!") : null;
return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", {pokemonName: getPokemonNameWithAffix(target)}) : null;
}
getSwitchOutCondition(): MoveConditionFunc {
@ -4872,7 +4925,7 @@ export class CopyTypeAttr extends MoveEffectAttr {
user.summonData.types = target.getTypes(true);
user.updateInfo();
user.scene.queueMessage(getPokemonMessage(user, `'s type\nchanged to match ${getPokemonNameWithAffix(target)}'s!`));
user.scene.queueMessage(i18next.t("moveTriggers:copyType", {pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target)}));
return true;
}
@ -4897,7 +4950,7 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr {
user.summonData.types = [ biomeType ];
user.updateInfo();
user.scene.queueMessage(getPokemonMessage(user, ` transformed\ninto the ${Utils.toReadableString(Type[biomeType])} type!`));
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", {pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[biomeType]}`)}));
return true;
}
@ -4916,7 +4969,7 @@ export class ChangeTypeAttr extends MoveEffectAttr {
target.summonData.types = [this.type];
target.updateInfo();
user.scene.queueMessage(getPokemonMessage(target, ` transformed\ninto the ${Utils.toReadableString(Type[this.type])} type!`));
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", {pokemonName: getPokemonNameWithAffix(target), typeName: i18next.t(`pokemonInfo:Type.${Type[this.type]}`)}));
return true;
}
@ -4943,7 +4996,7 @@ export class AddTypeAttr extends MoveEffectAttr {
target.summonData.types = types;
target.updateInfo();
user.scene.queueMessage(`${Utils.toReadableString(Type[this.type])} was added to\n` + getPokemonMessage(target, "!"));
user.scene.queueMessage(i18next.t("moveTriggers:addType", {typeName: i18next.t(`pokemonInfo:Type.${Type[this.type]}`), pokemonName: getPokemonNameWithAffix(target)}));
return true;
}
@ -4965,7 +5018,7 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
const firstMoveType = target.getMoveset()[0].getMove().type;
user.summonData.types = [ firstMoveType ];
user.scene.queueMessage(i18next.t("battle:transformedIntoType", {pokemonName: getPokemonNameWithAffix(user), type: Utils.toReadableString(Type[firstMoveType])}));
user.scene.queueMessage(i18next.t("battle:transformedIntoType", {pokemonName: getPokemonNameWithAffix(user), type: i18next.t(`pokemonInfo:Type.${Type[firstMoveType]}`)}));
return true;
}
@ -5367,7 +5420,7 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
user.summonData.moveset = user.getMoveset().slice(0);
user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id, 0, 0);
user.scene.queueMessage(getPokemonMessage(user, ` copied\n${copiedMove.name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:copiedMove", {pokemonName: getPokemonNameWithAffix(user), moveName: copiedMove.name}));
return true;
}
@ -5400,7 +5453,7 @@ export class SketchAttr extends MoveEffectAttr {
user.setMove(sketchIndex, sketchedMove.id);
user.scene.queueMessage(getPokemonMessage(user, ` sketched\n${sketchedMove.name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:sketchedMove", {pokemonName: getPokemonNameWithAffix(user), moveName: sketchedMove.name}));
return true;
}
@ -5443,7 +5496,7 @@ export class AbilityChangeAttr extends MoveEffectAttr {
(this.selfTarget ? user : target).summonData.ability = this.ability;
user.scene.queueMessage("The " + getPokemonMessage((this.selfTarget ? user : target), ` acquired\n${allAbilities[this.ability].name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:acquiredAbility", {pokemonName: getPokemonNameWithAffix((this.selfTarget ? user : target)), abilityName: allAbilities[this.ability].name}));
return true;
}
@ -5469,11 +5522,11 @@ export class AbilityCopyAttr extends MoveEffectAttr {
user.summonData.ability = target.getAbility().id;
user.scene.queueMessage(getPokemonMessage(user, " copied the ") + getPokemonMessage(target, `'s\n${allAbilities[target.getAbility().id].name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name}));
if (this.copyToPartner && user.scene.currentBattle?.double && user.getAlly().hp) {
user.getAlly().summonData.ability = target.getAbility().id;
user.getAlly().scene.queueMessage(getPokemonMessage(user.getAlly(), " copied the ") + getPokemonMessage(target, `'s\n${allAbilities[target.getAbility().id].name}!`));
user.getAlly().scene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", {pokemonName: getPokemonNameWithAffix(user.getAlly()), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name}));
}
return true;
@ -5506,7 +5559,7 @@ export class AbilityGiveAttr extends MoveEffectAttr {
target.summonData.ability = user.getAbility().id;
user.scene.queueMessage("The" + getPokemonMessage(target, `\nacquired ${allAbilities[user.getAbility().id].name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:acquiredAbility", {pokemonName: getPokemonNameWithAffix(target), abilityName: allAbilities[user.getAbility().id].name}));
return true;
}
@ -5526,7 +5579,7 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr {
user.summonData.ability = target.getAbility().id;
target.summonData.ability = tempAbilityId;
user.scene.queueMessage(getPokemonMessage(user, " swapped\nabilities with its target!"));
user.scene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", {pokemonName: getPokemonNameWithAffix(user)}));
return true;
}
@ -5553,7 +5606,7 @@ export class SuppressAbilitiesAttr extends MoveEffectAttr {
target.summonData.abilitySuppressed = true;
target.scene.queueMessage(getPokemonMessage(target, "'s ability\nwas suppressed!"));
target.scene.queueMessage(i18next.t("moveTriggers:suppressAbilities", {pokemonName: getPokemonNameWithAffix(target)}));
return true;
}
@ -5610,7 +5663,7 @@ export class TransformAttr extends MoveEffectAttr {
user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m.moveId, m.ppUsed, m.ppUp));
user.summonData.types = target.getTypes();
user.scene.queueMessage(getPokemonMessage(user, ` transformed\ninto ${target.name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", {pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target)}));
user.loadAssets(false).then(() => {
user.playAnim();
@ -5641,7 +5694,7 @@ export class MoneyAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
user.scene.currentBattle.moneyScattered += user.scene.getWaveMoneyAmount(0.2);
user.scene.queueMessage("Coins were scattered everywhere!");
user.scene.queueMessage(i18next.t("moveTriggers:coinsScatteredEverywhere"));
return true;
}
}
@ -5665,7 +5718,7 @@ export class DestinyBondAttr extends MoveEffectAttr {
* @returns true
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
user.scene.queueMessage(`${getPokemonMessage(user, " is trying\nto take its foe down with it!")}`);
user.scene.queueMessage(`${i18next.t("moveTriggers:tryingToTakeFoeDown", {pokemonName: getPokemonNameWithAffix(user)})}`);
user.addTag(BattlerTagType.DESTINY_BOND, undefined, move.id, user.id);
return true;
}
@ -5704,8 +5757,7 @@ export class AttackedByItemAttr extends MoveAttr {
}
const itemName = heldItems[0]?.type?.name ?? "item";
const attackedByItemString = ` is about to be attacked by its ${itemName}!`;
target.scene.queueMessage(getPokemonMessage(target, attackedByItemString));
target.scene.queueMessage(i18next.t("moveTriggers:attackedByItem", {pokemonName: getPokemonNameWithAffix(target), itemName: itemName}));
return true;
};
@ -5739,7 +5791,7 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled));
// Queue a message if an ability prevented usage of the move
if (cancelled.value) {
user.scene.queueMessage(getPokemonMessage(user, ` cannot use ${move.name}!`));
user.scene.queueMessage(i18next.t("moveTriggers:cannotUseMove", {pokemonName: getPokemonNameWithAffix(user), moveName: move.name}));
}
return !cancelled.value;
};
@ -5860,7 +5912,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
getCondition(): MoveConditionFunc {
return (user, target, move) => {
const moveHistory = target.getLastXMoves();
return !!moveHistory.length;
return moveHistory.length !== 0;
};
}
}
@ -5965,7 +6017,7 @@ export function initMoves() {
.attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr),
new AttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.RAZOR_WIND_CHARGING, "whipped\nup a whirlwind!")
.attr(ChargeAttr, ChargeAnim.RAZOR_WIND_CHARGING, i18next.t("moveTriggers:whippedUpAWhirlwind", {pokemonName: "{USER}"}))
.attr(HighCritAttr)
.windMove()
.ignoresVirtual()
@ -5985,7 +6037,7 @@ export function initMoves() {
.hidesTarget()
.windMove(),
new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.FLY_CHARGING, "flew\nup high!", BattlerTagType.FLYING)
.attr(ChargeAttr, ChargeAnim.FLY_CHARGING, i18next.t("moveTriggers:flewUpHigh", {pokemonName: "{USER}"}), BattlerTagType.FLYING)
.condition(failOnGravityCondition)
.ignoresVirtual(),
new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
@ -6133,7 +6185,7 @@ export function initMoves() {
.slicingMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, "took\nin sunlight!")
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, i18next.t("moveTriggers:tookInSunlight", {pokemonName: "{USER}"}))
.attr(AntiSunlightPowerDecreaseAttr)
.ignoresVirtual(),
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
@ -6182,7 +6234,7 @@ export function initMoves() {
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, false)
.makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, "dug a hole!", BattlerTagType.UNDERGROUND)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND)
.ignoresVirtual(),
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.TOXIC)
@ -6278,7 +6330,7 @@ export function initMoves() {
new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, "lowered\nits head!", null, true)
.attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, i18next.t("moveTriggers:loweredItsHead", {pokemonName: "{USER}"}), null, true)
.attr(StatChangeAttr, BattleStat.DEF, 1, true)
.ignoresVirtual(),
new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1)
@ -6317,7 +6369,7 @@ export function initMoves() {
new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP),
new AttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
.attr(ChargeAttr, ChargeAnim.SKY_ATTACK_CHARGING, "is glowing!")
.attr(ChargeAttr, ChargeAnim.SKY_ATTACK_CHARGING, i18next.t("moveTriggers:isGlowing", {pokemonName: "{USER}"}))
.attr(HighCritAttr)
.attr(FlinchAttr)
.makesContact(false)
@ -6442,7 +6494,9 @@ export function initMoves() {
new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2)
.attr(ConfuseAttr),
new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2)
.attr(HalfHpStatMaxAttr, BattleStat.ATK),
.attr(CutHpStatBoostAttr, [BattleStat.ATK], 12, 2, (user) => {
user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", {pokemonName: getPokemonNameWithAffix(user), statName: getBattleStatName(BattleStat.ATK)}));
}),
new AttackMove(Moves.SLUDGE_BOMB, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 30, 0, 2)
.attr(StatusEffectAttr, StatusEffect.POISON)
.ballBombMove(),
@ -6491,7 +6545,7 @@ export function initMoves() {
.attr(HitHealAttr)
.triageMove(),
new SelfStatusMove(Moves.ENDURE, Type.NORMAL, -1, 10, -1, 4, 2)
.attr(EndureAttr),
.attr(ProtectAttr, BattlerTagType.ENDURING),
new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2)
.attr(StatChangeAttr, BattleStat.ATK, -2),
new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2)
@ -6523,7 +6577,7 @@ export function initMoves() {
.target(MoveTarget.ALL_ENEMIES)
.ignoresVirtual(),
new StatusMove(Moves.HEAL_BELL, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(PartyStatusCureAttr, "A bell chimed!", Abilities.SOUNDPROOF)
.attr(PartyStatusCureAttr, i18next.t("moveTriggers:bellChimed"), Abilities.SOUNDPROOF)
.soundBased()
.target(MoveTarget.PARTY),
new AttackMove(Moves.RETURN, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 2)
@ -6626,7 +6680,8 @@ export function initMoves() {
.attr(StatChangeAttr, BattleStat.SPDEF, -1)
.ballBombMove(),
new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
.attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, "foresaw\nan attack!"),
.partial()
.attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", {pokemonName: "{USER}"})),
new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
.attr(StatChangeAttr, BattleStat.DEF, -1),
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
@ -6747,7 +6802,7 @@ export function initMoves() {
.makesContact(false)
.partial(),
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, "hid\nunderwater!", BattlerTagType.UNDERWATER)
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", {pokemonName: "{USER}"}), BattlerTagType.UNDERWATER)
.ignoresVirtual(),
new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3)
.attr(MultiHitAttr),
@ -6803,7 +6858,7 @@ export function initMoves() {
.attr(MovePowerMultiplierAttr, (user, target, move) => [WeatherType.SUNNY, WeatherType.RAIN, WeatherType.SANDSTORM, WeatherType.HAIL, WeatherType.SNOW, WeatherType.FOG, WeatherType.HEAVY_RAIN, WeatherType.HARSH_SUN].includes(user.scene.arena.weather?.weatherType) && !user.scene.arena.weather?.isEffectSuppressed(user.scene) ? 2 : 1)
.ballBombMove(),
new StatusMove(Moves.AROMATHERAPY, Type.GRASS, -1, 5, -1, 0, 3)
.attr(PartyStatusCureAttr, "A soothing aroma wafted through the area!", Abilities.SAP_SIPPER)
.attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER)
.target(MoveTarget.PARTY),
new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3)
.attr(StatChangeAttr, BattleStat.SPDEF, -2),
@ -6878,7 +6933,7 @@ export function initMoves() {
new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3)
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1, true),
new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3)
.attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, "sprang up!", BattlerTagType.FLYING)
.attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", {pokemonName: "{USER}"}), BattlerTagType.FLYING)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.condition(failOnGravityCondition)
.ignoresVirtual(),
@ -6914,7 +6969,8 @@ export function initMoves() {
.attr(ConfuseAttr)
.pulseMove(),
new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3)
.attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, "chose\nDoom Desire as its destiny!"),
.partial()
.attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", {pokemonName: "{USER}"})),
new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
.attr(StatChangeAttr, BattleStat.SPATK, -2, true),
new SelfStatusMove(Moves.ROOST, Type.FLYING, -1, 5, -1, 0, 4)
@ -6977,12 +7033,13 @@ export function initMoves() {
.unimplemented(),
new StatusMove(Moves.PSYCHO_SHIFT, Type.PSYCHIC, 100, 10, -1, 0, 4)
.attr(PsychoShiftEffectAttr)
.condition((user, target, move) => (user.status?.effect === StatusEffect.BURN
|| user.status?.effect === StatusEffect.POISON
|| user.status?.effect === StatusEffect.TOXIC
|| user.status?.effect === StatusEffect.PARALYSIS
|| user.status?.effect === StatusEffect.SLEEP)
&& target.canSetStatus(user.status?.effect, false, false, user)
.condition((user, target, move) => {
let statusToApply = user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined;
if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) {
statusToApply = user.status.effect;
}
return statusToApply && target.canSetStatus(statusToApply, false, false, user);
}
),
new AttackMove(Moves.TRUMP_CARD, Type.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
.makesContact()
@ -7227,7 +7284,7 @@ export function initMoves() {
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
.windMove(),
new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
.attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, "vanished\ninstantly!", BattlerTagType.HIDDEN)
.attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", {pokemonName: "{USER}"}), BattlerTagType.HIDDEN)
.ignoresProtect()
.ignoresVirtual(),
new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5)
@ -7269,7 +7326,7 @@ export function initMoves() {
new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.attr(CritOnlyAttr),
new AttackMove(Moves.FLAME_BURST, Type.FIRE, MoveCategory.SPECIAL, 70, 100, 15, -1, 0, 5)
.partial(),
.attr(FlameBurstAttr),
new AttackMove(Moves.SLUDGE_WAVE, Type.POISON, MoveCategory.SPECIAL, 95, 100, 10, 10, 0, 5)
.attr(StatusEffectAttr, StatusEffect.POISON)
.target(MoveTarget.ALL_NEAR_OTHERS),
@ -7342,7 +7399,7 @@ export function initMoves() {
MovePowerMultiplierAttr,
(user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE)? 2 : 1),
new AttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, "took {TARGET}\ninto the sky!", BattlerTagType.FLYING) // TODO: Add 2nd turn message
.attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", {pokemonName: "{USER}", targetName: "{TARGET}"}), BattlerTagType.FLYING) // TODO: Add 2nd turn message
.condition(failOnGravityCondition)
.ignoresVirtual(),
new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
@ -7462,11 +7519,11 @@ export function initMoves() {
.attr(StatChangeAttr, BattleStat.SPATK, 1, true)
.danceMove(),
new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5)
.attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, "became cloaked\nin a freezing light!")
.attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingLight", {pokemonName: "{USER}"}))
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.makesContact(false),
new AttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5)
.attr(ChargeAttr, ChargeAnim.ICE_BURN_CHARGING, "became cloaked\nin freezing air!")
.attr(ChargeAttr, ChargeAnim.ICE_BURN_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingAir", {pokemonName: "{USER}"}))
.attr(StatusEffectAttr, StatusEffect.BURN)
.ignoresVirtual(),
new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
@ -7480,10 +7537,10 @@ export function initMoves() {
.attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF, BattleStat.SPD ], -1, true),
new AttackMove(Moves.FUSION_FLARE, Type.FIRE, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 5)
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
.partial(),
.attr(LastMoveDoublePowerAttr, Moves.FUSION_BOLT),
new AttackMove(Moves.FUSION_BOLT, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 5)
.makesContact(false)
.partial(),
.attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE)
.makesContact(false),
new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6)
.attr(MinimizeAccuracyAttr)
.attr(FlyingTypeMultiplierAttr)
@ -7508,7 +7565,7 @@ export function initMoves() {
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
.attr(PostVictoryStatChangeAttr, BattleStat.ATK, 3, true ),
new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, "vanished\ninstantly!", BattlerTagType.HIDDEN)
.attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", {pokemonName: "{USER}"}), BattlerTagType.HIDDEN)
.ignoresProtect()
.ignoresVirtual(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
@ -7610,7 +7667,7 @@ export function initMoves() {
.powderMove()
.unimplemented(),
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, "is charging its power!")
.attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", {pokemonName: "{USER}"}))
.attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 2, true)
.ignoresVirtual(),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
@ -7816,7 +7873,7 @@ export function initMoves() {
.condition((user, target, move) => target.summonData.battleStats[BattleStat.ATK] > -6)
.triageMove(),
new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, "is glowing!")
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", {pokemonName: "{USER}"}))
.attr(AntiSunlightPowerDecreaseAttr)
.slicingMove(),
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7)
@ -7856,7 +7913,7 @@ export function initMoves() {
})
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
.attr(RemoveTypeAttr, Type.FIRE, (user) => {
user.scene.queueMessage(getPokemonMessage(user, " burned itself out!"));
user.scene.queueMessage(i18next.t("moveTriggers:burnedItselfOut", {pokemonName: getPokemonNameWithAffix(user)}));
}),
new StatusMove(Moves.SPEED_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 7)
.unimplemented(),
@ -7878,12 +7935,12 @@ export function initMoves() {
new StatusMove(Moves.INSTRUCT, Type.PSYCHIC, -1, 15, -1, 0, 7)
.unimplemented(),
new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, 5, 7)
.attr(ChargeAttr, ChargeAnim.BEAK_BLAST_CHARGING, "started\nheating up its beak!", undefined, false, true, -3)
.attr(ChargeAttr, ChargeAnim.BEAK_BLAST_CHARGING, i18next.t("moveTriggers:startedHeatingUpBeak", {pokemonName: "{USER}"}), undefined, false, true, -3)
.ballBombMove()
.makesContact(false)
.partial(),
new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7)
.attr(StatChangeAttr, BattleStat.DEF, -1, true)
.attr(StatChangeAttr, BattleStat.DEF, -1, true, null, true, false, MoveEffectTrigger.HIT, true)
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7),
@ -8234,7 +8291,7 @@ export function initMoves() {
.makesContact(false)
.partial(),
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, "is overflowing\nwith space power!", null, true)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", {pokemonName: "{USER}"}), null, true)
.attr(StatChangeAttr, BattleStat.SPATK, 1, true)
.ignoresVirtual(),
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
@ -8530,8 +8587,8 @@ export function initMoves() {
new AttackMove(Moves.ICE_SPINNER, Type.ICE, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9)
.attr(ClearTerrainAttr),
new AttackMove(Moves.GLAIVE_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 9)
.attr(AlwaysGetHitAttr)
.attr(ReceiveDoubleDamageAttr),
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_GET_HIT, true, false, 0, 0, true)
.attr(AddBattlerTagAttr, BattlerTagType.RECEIVE_DOUBLE_DAMAGE, true, false, 0, 0, true),
new StatusMove(Moves.REVIVAL_BLESSING, Type.NORMAL, -1, 1, -1, 0, 9)
.triageMove()
.attr(RevivalBlessingAttr)
@ -8633,7 +8690,7 @@ export function initMoves() {
return userTypes.includes(Type.ELECTRIC);
})
.attr(RemoveTypeAttr, Type.ELECTRIC, (user) => {
user.scene.queueMessage(getPokemonMessage(user, " used up all its electricity!"));
user.scene.queueMessage(i18next.t("moveTriggers:usedUpAllElectricity", {pokemonName: getPokemonNameWithAffix(user)}));
}),
new AttackMove(Moves.GIGATON_HAMMER, Type.STEEL, MoveCategory.PHYSICAL, 160, 100, 5, -1, 0, 9)
.makesContact(false)
@ -8729,8 +8786,8 @@ export function initMoves() {
new AttackMove(Moves.MALIGNANT_CHAIN, Type.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9)
.attr(StatusEffectAttr, StatusEffect.TOXIC)
);
allMoves.map(m=>{
if (m.getAttrs(StatChangeAttr).some(a=> a.selfTarget && a.levels < 0)) {
allMoves.map(m => {
if (m.getAttrs(StatChangeAttr).some(a => a.selfTarget && a.levels < 0)) {
selfStatLowerMoves.push(m.id);
}
});

View File

@ -1145,6 +1145,11 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.PAWMOT, 32, null, null)
],
[Species.TANDEMAUS]: [
new SpeciesFormEvolution(Species.MAUSHOLD, "", "three", 25, null, new SpeciesEvolutionCondition(p => {
let ret = false;
p.scene.executeWithSeedOffset(() => ret = !Utils.randSeedInt(4), p.id);
return ret;
})),
new SpeciesEvolution(Species.MAUSHOLD, 25, null, null)
],
[Species.FIDOUGH]: [

Some files were not shown because too many files have changed in this diff Show More