diff --git a/docs/enemy-ai.md b/docs/enemy-ai.md
index f53a8511893..46482f56a90 100644
--- a/docs/enemy-ai.md
+++ b/docs/enemy-ai.md
@@ -191,15 +191,15 @@ Now that the enemy Pokémon with the best matchup score is on the field (assumin
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
+- **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 `StatStageChangeAttr` 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.
+- **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 `StatStageChangeAttr` 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}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$
$\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$
@@ -221,4 +221,4 @@ When implementing a new move attribute, it's important to override `MoveAttr`'s
- 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.
\ No newline at end of file
+- **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.
diff --git a/index.css b/index.css
index abf4f9f708c..1274f2fcead 100644
--- a/index.css
+++ b/index.css
@@ -23,15 +23,6 @@ body {
}
}
-#links {
- width: 90%;
- text-align: center;
- position: fixed;
- bottom: 0;
- display: flex;
- justify-content: space-around;
-}
-
#app {
display: flex;
justify-content: center;
@@ -93,7 +84,7 @@ input:-internal-autofill-selected {
@media (orientation: landscape) {
#touchControls {
- --controls-size: 20vh;
+ --controls-size: 20vh;
--text-shadow-size: 1.3vh;
--small-button-offset: 4vh;
}
diff --git a/index.html b/index.html
index 29b4c0d1a6e..390a29fb365 100644
--- a/index.html
+++ b/index.html
@@ -39,7 +39,6 @@
-
diff --git a/public/images/inputs/keyboard.json b/public/images/inputs/keyboard.json
index b1902df10d6..a8124004c12 100644
--- a/public/images/inputs/keyboard.json
+++ b/public/images/inputs/keyboard.json
@@ -1,596 +1,604 @@
-{"frames": [
-
-{
- "filename": "0.png",
- "frame": {"x":0,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "1.png",
- "frame": {"x":12,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "2.png",
- "frame": {"x":24,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "3.png",
- "frame": {"x":36,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "4.png",
- "frame": {"x":48,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "5.png",
- "frame": {"x":60,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "6.png",
- "frame": {"x":72,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "7.png",
- "frame": {"x":84,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "8.png",
- "frame": {"x":96,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "9.png",
- "frame": {"x":108,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "A.png",
- "frame": {"x":120,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "ALT.png",
- "frame": {"x":132,"y":0,"w":16,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
- "sourceSize": {"w":16,"h":12}
-},
-{
- "filename": "B.png",
- "frame": {"x":148,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "BACK.png",
- "frame": {"x":160,"y":0,"w":24,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":24,"h":12},
- "sourceSize": {"w":24,"h":12}
-},
-{
- "filename": "C.png",
- "frame": {"x":184,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "CTRL.png",
- "frame": {"x":196,"y":0,"w":22,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":22,"h":12},
- "sourceSize": {"w":22,"h":12}
-},
-{
- "filename": "D.png",
- "frame": {"x":218,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "DEL.png",
- "frame": {"x":230,"y":0,"w":17,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":17,"h":12},
- "sourceSize": {"w":17,"h":12}
-},
-{
- "filename": "E.png",
- "frame": {"x":247,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "END.png",
- "frame": {"x":259,"y":0,"w":18,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":18,"h":12},
- "sourceSize": {"w":18,"h":12}
-},
-{
- "filename": "ENTER.png",
- "frame": {"x":277,"y":0,"w":27,"h":11},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":27,"h":11},
- "sourceSize": {"w":27,"h":11}
-},
-{
- "filename": "ESC.png",
- "frame": {"x":304,"y":0,"w":17,"h":11},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":17,"h":11},
- "sourceSize": {"w":17,"h":11}
-},
-{
- "filename": "F.png",
- "frame": {"x":321,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "F1.png",
- "frame": {"x":333,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F2.png",
- "frame": {"x":346,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F3.png",
- "frame": {"x":359,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F4.png",
- "frame": {"x":372,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F5.png",
- "frame": {"x":385,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F6.png",
- "frame": {"x":398,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F7.png",
- "frame": {"x":411,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F8.png",
- "frame": {"x":424,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F9.png",
- "frame": {"x":437,"y":0,"w":13,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
- "sourceSize": {"w":13,"h":12}
-},
-{
- "filename": "F10.png",
- "frame": {"x":450,"y":0,"w":16,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
- "sourceSize": {"w":16,"h":12}
-},
-{
- "filename": "F11.png",
- "frame": {"x":466,"y":0,"w":15,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":15,"h":12},
- "sourceSize": {"w":15,"h":12}
-},
-{
- "filename": "F12.png",
- "frame": {"x":481,"y":0,"w":16,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
- "sourceSize": {"w":16,"h":12}
-},
-{
- "filename": "G.png",
- "frame": {"x":497,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "H.png",
- "frame": {"x":509,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "HOME.png",
- "frame": {"x":521,"y":0,"w":23,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":23,"h":12},
- "sourceSize": {"w":23,"h":12}
-},
-{
- "filename": "I.png",
- "frame": {"x":544,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "INS.png",
- "frame": {"x":556,"y":0,"w":16,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
- "sourceSize": {"w":16,"h":12}
-},
-{
- "filename": "J.png",
- "frame": {"x":572,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "K.png",
- "frame": {"x":584,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "KEY_ARROW_DOWN.png",
- "frame": {"x":596,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "KEY_ARROW_LEFT.png",
- "frame": {"x":608,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "KEY_ARROW_RIGHT.png",
- "frame": {"x":620,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "KEY_ARROW_UP.png",
- "frame": {"x":632,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "L.png",
- "frame": {"x":644,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "LEFT_BRACKET.png",
- "frame": {"x":656,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "M.png",
- "frame": {"x":668,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "MINUS.png",
- "frame": {"x":680,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "N.png",
- "frame": {"x":692,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "O.png",
- "frame": {"x":704,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "P.png",
- "frame": {"x":716,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "PAGE_DOWN.png",
- "frame": {"x":728,"y":0,"w":20,"h":11},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":20,"h":11},
- "sourceSize": {"w":20,"h":11}
-},
-{
- "filename": "PAGE_UP.png",
- "frame": {"x":748,"y":0,"w":20,"h":11},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":20,"h":11},
- "sourceSize": {"w":20,"h":11}
-},
-{
- "filename": "PLUS.png",
- "frame": {"x":768,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "Q.png",
- "frame": {"x":780,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "QUOTE.png",
- "frame": {"x":792,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "R.png",
- "frame": {"x":804,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "RIGHT_BRACKET.png",
- "frame": {"x":816,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "S.png",
- "frame": {"x":828,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "SEMICOLON.png",
- "frame": {"x":840,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "SHIFT.png",
- "frame": {"x":852,"y":0,"w":23,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":23,"h":12},
- "sourceSize": {"w":23,"h":12}
-},
-{
- "filename": "SPACE.png",
- "frame": {"x":875,"y":0,"w":25,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":25,"h":12},
- "sourceSize": {"w":25,"h":12}
-},
-{
- "filename": "T.png",
- "frame": {"x":900,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "TAB.png",
- "frame": {"x":912,"y":0,"w":19,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":19,"h":12},
- "sourceSize": {"w":19,"h":12}
-},
-{
- "filename": "TILDE.png",
- "frame": {"x":931,"y":0,"w":15,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":15,"h":12},
- "sourceSize": {"w":15,"h":12}
-},
-{
- "filename": "U.png",
- "frame": {"x":946,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "V.png",
- "frame": {"x":958,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "W.png",
- "frame": {"x":970,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "X.png",
- "frame": {"x":982,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "Y.png",
- "frame": {"x":994,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-},
-{
- "filename": "Z.png",
- "frame": {"x":1006,"y":0,"w":12,"h":12},
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
- "sourceSize": {"w":12,"h":12}
-}],
-"meta": {
- "app": "https://www.codeandweb.com/texturepacker",
- "version": "1.0",
- "image": "keyboard.png",
- "format": "RGBA8888",
- "size": {"w":1018,"h":12},
- "scale": "1",
- "smartupdate": "$TexturePacker:SmartUpdate:085d4353a5c4d18c90f82f8926710d72:45908b22b446cf7f4904d4e0b658b16a:bad03abb89ad027d879c383c13fd51bc$"
-}
-}
+{"frames": [
+
+{
+ "filename": "0.png",
+ "frame": {"x":0,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "1.png",
+ "frame": {"x":12,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "2.png",
+ "frame": {"x":24,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "3.png",
+ "frame": {"x":36,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "4.png",
+ "frame": {"x":48,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "5.png",
+ "frame": {"x":60,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "6.png",
+ "frame": {"x":72,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "7.png",
+ "frame": {"x":84,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "8.png",
+ "frame": {"x":96,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "9.png",
+ "frame": {"x":108,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "A.png",
+ "frame": {"x":120,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "ALT.png",
+ "frame": {"x":132,"y":0,"w":16,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
+ "sourceSize": {"w":16,"h":12}
+},
+{
+ "filename": "B.png",
+ "frame": {"x":148,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "BACK.png",
+ "frame": {"x":160,"y":0,"w":24,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":24,"h":12},
+ "sourceSize": {"w":24,"h":12}
+},
+{
+ "filename": "C.png",
+ "frame": {"x":184,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "CTRL.png",
+ "frame": {"x":196,"y":0,"w":22,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":22,"h":12},
+ "sourceSize": {"w":22,"h":12}
+},
+{
+ "filename": "D.png",
+ "frame": {"x":218,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "DEL.png",
+ "frame": {"x":230,"y":0,"w":17,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":17,"h":12},
+ "sourceSize": {"w":17,"h":12}
+},
+{
+ "filename": "E.png",
+ "frame": {"x":247,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "END.png",
+ "frame": {"x":259,"y":0,"w":18,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":18,"h":12},
+ "sourceSize": {"w":18,"h":12}
+},
+{
+ "filename": "ENTER.png",
+ "frame": {"x":277,"y":0,"w":27,"h":11},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":27,"h":11},
+ "sourceSize": {"w":27,"h":11}
+},
+{
+ "filename": "ESC.png",
+ "frame": {"x":304,"y":0,"w":17,"h":11},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":17,"h":11},
+ "sourceSize": {"w":17,"h":11}
+},
+{
+ "filename": "F.png",
+ "frame": {"x":321,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "F1.png",
+ "frame": {"x":333,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F2.png",
+ "frame": {"x":346,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F3.png",
+ "frame": {"x":359,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F4.png",
+ "frame": {"x":372,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F5.png",
+ "frame": {"x":385,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F6.png",
+ "frame": {"x":398,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F7.png",
+ "frame": {"x":411,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F8.png",
+ "frame": {"x":424,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F9.png",
+ "frame": {"x":437,"y":0,"w":13,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12},
+ "sourceSize": {"w":13,"h":12}
+},
+{
+ "filename": "F10.png",
+ "frame": {"x":450,"y":0,"w":16,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
+ "sourceSize": {"w":16,"h":12}
+},
+{
+ "filename": "F11.png",
+ "frame": {"x":466,"y":0,"w":15,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":15,"h":12},
+ "sourceSize": {"w":15,"h":12}
+},
+{
+ "filename": "F12.png",
+ "frame": {"x":481,"y":0,"w":16,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
+ "sourceSize": {"w":16,"h":12}
+},
+{
+ "filename": "G.png",
+ "frame": {"x":497,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "H.png",
+ "frame": {"x":509,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "HOME.png",
+ "frame": {"x":521,"y":0,"w":23,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":23,"h":12},
+ "sourceSize": {"w":23,"h":12}
+},
+{
+ "filename": "I.png",
+ "frame": {"x":544,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "INS.png",
+ "frame": {"x":556,"y":0,"w":16,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12},
+ "sourceSize": {"w":16,"h":12}
+},
+{
+ "filename": "J.png",
+ "frame": {"x":572,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "K.png",
+ "frame": {"x":584,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "KEY_ARROW_DOWN.png",
+ "frame": {"x":596,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "KEY_ARROW_LEFT.png",
+ "frame": {"x":608,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "KEY_ARROW_RIGHT.png",
+ "frame": {"x":620,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "KEY_ARROW_UP.png",
+ "frame": {"x":632,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "L.png",
+ "frame": {"x":644,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "LEFT_BRACKET.png",
+ "frame": {"x":656,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "M.png",
+ "frame": {"x":668,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "MINUS.png",
+ "frame": {"x":680,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "N.png",
+ "frame": {"x":692,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "O.png",
+ "frame": {"x":704,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "P.png",
+ "frame": {"x":716,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "PAGE_DOWN.png",
+ "frame": {"x":728,"y":0,"w":20,"h":11},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":20,"h":11},
+ "sourceSize": {"w":20,"h":11}
+},
+{
+ "filename": "PAGE_UP.png",
+ "frame": {"x":748,"y":0,"w":20,"h":11},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":20,"h":11},
+ "sourceSize": {"w":20,"h":11}
+},
+{
+ "filename": "PLUS.png",
+ "frame": {"x":768,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "Q.png",
+ "frame": {"x":780,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "QUOTE.png",
+ "frame": {"x":792,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "R.png",
+ "frame": {"x":804,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "RIGHT_BRACKET.png",
+ "frame": {"x":816,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "S.png",
+ "frame": {"x":828,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "SEMICOLON.png",
+ "frame": {"x":840,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "SHIFT.png",
+ "frame": {"x":852,"y":0,"w":23,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":23,"h":12},
+ "sourceSize": {"w":23,"h":12}
+},
+{
+ "filename": "SPACE.png",
+ "frame": {"x":875,"y":0,"w":25,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":25,"h":12},
+ "sourceSize": {"w":25,"h":12}
+},
+{
+ "filename": "T.png",
+ "frame": {"x":900,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "TAB.png",
+ "frame": {"x":912,"y":0,"w":19,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":19,"h":12},
+ "sourceSize": {"w":19,"h":12}
+},
+{
+ "filename": "TILDE.png",
+ "frame": {"x":931,"y":0,"w":15,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":15,"h":12},
+ "sourceSize": {"w":15,"h":12}
+},
+{
+ "filename": "U.png",
+ "frame": {"x":946,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "V.png",
+ "frame": {"x":958,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "W.png",
+ "frame": {"x":970,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "X.png",
+ "frame": {"x":982,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "Y.png",
+ "frame": {"x":994,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "Z.png",
+ "frame": {"x":1006,"y":0,"w":12,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12},
+ "sourceSize": {"w":12,"h":12}
+},
+{
+ "filename": "ACTION.png",
+ "frame": {"x":1018,"y":0,"w":28,"h":12},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":28,"h":12},
+ "sourceSize": {"w":28,"h":12}
+}],
+"meta": {
+ "app": "https://www.codeandweb.com/texturepacker",
+ "version": "1.0",
+ "image": "keyboard.png",
+ "format": "RGBA8888",
+ "size": {"w":1018,"h":12},
+ "scale": "1",
+ "smartupdate": "$TexturePacker:SmartUpdate:085d4353a5c4d18c90f82f8926710d72:45908b22b446cf7f4904d4e0b658b16a:bad03abb89ad027d879c383c13fd51bc$"
+}
+}
diff --git a/public/images/inputs/keyboard.png b/public/images/inputs/keyboard.png
index 67b26af12de..1fc5adfa31c 100644
Binary files a/public/images/inputs/keyboard.png and b/public/images/inputs/keyboard.png differ
diff --git a/public/images/ui/icon_lock.png b/public/images/ui/icon_lock.png
new file mode 100644
index 00000000000..6a12efa15e8
Binary files /dev/null and b/public/images/ui/icon_lock.png differ
diff --git a/public/images/ui/icon_stop.png b/public/images/ui/icon_stop.png
new file mode 100644
index 00000000000..6d9c201695a
Binary files /dev/null and b/public/images/ui/icon_stop.png differ
diff --git a/public/images/ui/legacy/icon_lock.png b/public/images/ui/legacy/icon_lock.png
new file mode 100644
index 00000000000..6a12efa15e8
Binary files /dev/null and b/public/images/ui/legacy/icon_lock.png differ
diff --git a/public/images/ui/legacy/icon_stop.png b/public/images/ui/legacy/icon_stop.png
new file mode 100644
index 00000000000..6d9c201695a
Binary files /dev/null and b/public/images/ui/legacy/icon_stop.png differ
diff --git a/src/battle-scene.ts b/src/battle-scene.ts
index f2fb2081078..0b2728d150b 100644
--- a/src/battle-scene.ts
+++ b/src/battle-scene.ts
@@ -841,12 +841,13 @@ export default class BattleScene extends SceneBase {
}
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
+ if (Overrides.OPP_LEVEL_OVERRIDE > 0) {
+ level = Overrides.OPP_LEVEL_OVERRIDE;
+ }
if (Overrides.OPP_SPECIES_OVERRIDE) {
species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE);
- }
-
- if (Overrides.OPP_LEVEL_OVERRIDE !== 0) {
- level = Overrides.OPP_LEVEL_OVERRIDE;
+ // The fact that a Pokemon is a boss or not can change based on its Species and level
+ boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
}
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, dataSource);
@@ -973,6 +974,7 @@ export default class BattleScene extends SceneBase {
this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24));
console.log("Seed:", this.seed);
+ this.resetSeed(); // Properly resets RNG after saving and quitting a session
this.disableMenu = false;
@@ -1328,6 +1330,13 @@ export default class BattleScene extends SceneBase {
}
getEncounterBossSegments(waveIndex: integer, level: integer, species?: PokemonSpecies, forceBoss: boolean = false): integer {
+ if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1) {
+ return Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE;
+ } else if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE === 1) {
+ // The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss
+ return 0;
+ }
+
if (this.gameMode.isDaily && this.gameMode.isWaveFinal(waveIndex)) {
return 5;
}
diff --git a/src/data/ability.ts b/src/data/ability.ts
index d75c62df6be..ac26491a295 100644
--- a/src/data/ability.ts
+++ b/src/data/ability.ts
@@ -2,7 +2,6 @@ import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon
import { Type } from "./type";
import { Constructor } from "#app/utils";
import * as Utils from "../utils";
-import { BattleStat, getBattleStatName } from "./battle-stat";
import { getPokemonNameWithAffix } from "../messages";
import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
@@ -10,12 +9,11 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g
import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
-import { Stat, getStatName } from "./pokemon-stat";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
import i18next from "i18next";
-import { Localizable } from "#app/interfaces/locales.js";
+import { Localizable } from "#app/interfaces/locales";
import { Command } from "../ui/command-ui-handler";
import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "./pokeball";
@@ -25,10 +23,11 @@ import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
+import { Stat, type BattleStat, type EffectiveStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat";
import { MovePhase } from "#app/phases/move-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
-import { StatChangePhase } from "#app/phases/stat-change-phase";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import BattleScene from "#app/battle-scene";
export class Ability implements Localizable {
@@ -126,7 +125,7 @@ type AbAttrCondition = (pokemon: Pokemon) => boolean;
type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean;
type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean;
-type PokemonStatChangeCondition = (target: Pokemon, statsChanged: BattleStat[], levels: integer) => boolean;
+type PokemonStatStageChangeCondition = (target: Pokemon, statsChanged: BattleStat[], stages: number) => boolean;
export abstract class AbAttr {
public showAbility: boolean;
@@ -203,38 +202,36 @@ export class PostBattleInitFormChangeAbAttr extends PostBattleInitAbAttr {
}
}
-export class PostBattleInitStatChangeAbAttr extends PostBattleInitAbAttr {
+export class PostBattleInitStatStageChangeAbAttr extends PostBattleInitAbAttr {
private stats: BattleStat[];
- private levels: integer;
+ private stages: number;
private selfTarget: boolean;
- constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean) {
+ constructor(stats: BattleStat[], stages: number, selfTarget?: boolean) {
super();
- this.stats = typeof(stats) === "number"
- ? [ stats as BattleStat ]
- : stats as BattleStat[];
- this.levels = levels;
+ this.stats = stats;
+ this.stages = stages;
this.selfTarget = !!selfTarget;
}
applyPostBattleInit(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
- const statChangePhases: StatChangePhase[] = [];
+ const statStageChangePhases: StatStageChangePhase[] = [];
if (!simulated) {
if (this.selfTarget) {
- statChangePhases.push(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels));
+ statStageChangePhases.push(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages));
} else {
for (const opponent of pokemon.getOpponents()) {
- statChangePhases.push(new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels));
+ statStageChangePhases.push(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages));
}
}
- for (const statChangePhase of statChangePhases) {
- if (!this.selfTarget && !statChangePhase.getPokemon()?.summonData) {
- pokemon.scene.pushPhase(statChangePhase);
+ for (const statStageChangePhase of statStageChangePhases) {
+ if (!this.selfTarget && !statStageChangePhase.getPokemon()?.summonData) {
+ pokemon.scene.pushPhase(statStageChangePhase);
} else { // TODO: This causes the ability bar to be shown at the wrong time
- pokemon.scene.unshiftPhase(statChangePhase);
+ pokemon.scene.unshiftPhase(statStageChangePhase);
}
}
}
@@ -402,15 +399,15 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr {
}
}
-class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr {
+class TypeImmunityStatStageChangeAbAttr extends TypeImmunityAbAttr {
private stat: BattleStat;
- private levels: integer;
+ private stages: number;
- constructor(immuneType: Type, stat: BattleStat, levels: integer, condition?: AbAttrCondition) {
+ constructor(immuneType: Type, stat: BattleStat, stages: number, condition?: AbAttrCondition) {
super(immuneType, condition);
this.stat = stat;
- this.levels = levels;
+ this.stages = stages;
}
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
@@ -419,7 +416,7 @@ class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr {
if (ret) {
cancelled.value = true; // Suppresses "No Effect" message
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages));
}
}
@@ -559,7 +556,7 @@ export class PostDefendGulpMissileAbAttr extends PostDefendAbAttr {
}
if (battlerTag.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ BattleStat.DEF ], -1));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1));
} else {
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
}
@@ -588,8 +585,8 @@ export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
}
}
-export class PostStatChangeAbAttr extends AbAttr {
- applyPostStatChange(pokemon: Pokemon, simulated: boolean, statsChanged: BattleStat[], levelChanged: integer, selfTarget: boolean, args: any[]): boolean | Promise {
+export class PostStatStageChangeAbAttr extends AbAttr {
+ applyPostStatStageChange(pokemon: Pokemon, simulated: boolean, statsChanged: BattleStat[], stagesChanged: integer, selfTarget: boolean, args: any[]): boolean | Promise {
return false;
}
}
@@ -635,20 +632,20 @@ export class WonderSkinAbAttr extends PreDefendAbAttr {
}
}
-export class MoveImmunityStatChangeAbAttr extends MoveImmunityAbAttr {
+export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr {
private stat: BattleStat;
- private levels: integer;
+ private stages: number;
- constructor(immuneCondition: PreDefendAbAttrCondition, stat: BattleStat, levels: integer) {
+ constructor(immuneCondition: PreDefendAbAttrCondition, stat: BattleStat, stages: number) {
super(immuneCondition);
this.stat = stat;
- this.levels = levels;
+ this.stages = stages;
}
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const ret = super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args);
if (ret && !simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages));
}
return ret;
@@ -683,19 +680,19 @@ export class ReverseDrainAbAttr extends PostDefendAbAttr {
}
}
-export class PostDefendStatChangeAbAttr extends PostDefendAbAttr {
+export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
private condition: PokemonDefendCondition;
private stat: BattleStat;
- private levels: integer;
+ private stages: number;
private selfTarget: boolean;
private allOthers: boolean;
- constructor(condition: PokemonDefendCondition, stat: BattleStat, levels: integer, selfTarget: boolean = true, allOthers: boolean = false) {
+ constructor(condition: PokemonDefendCondition, stat: BattleStat, stages: number, selfTarget: boolean = true, allOthers: boolean = false) {
super(true);
this.condition = condition;
this.stat = stat;
- this.levels = levels;
+ this.stages = stages;
this.selfTarget = selfTarget;
this.allOthers = allOthers;
}
@@ -709,11 +706,11 @@ export class PostDefendStatChangeAbAttr extends PostDefendAbAttr {
if (this.allOthers) {
const otherPokemon = pokemon.getAlly() ? pokemon.getOpponents().concat([ pokemon.getAlly() ]) : pokemon.getOpponents();
for (const other of otherPokemon) {
- other.scene.unshiftPhase(new StatChangePhase(other.scene, (other).getBattlerIndex(), false, [ this.stat ], this.levels));
+ other.scene.unshiftPhase(new StatStageChangePhase(other.scene, (other).getBattlerIndex(), false, [ this.stat ], this.stages));
}
return true;
}
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), this.selfTarget, [ this.stat ], this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), this.selfTarget, [ this.stat ], this.stages));
return true;
}
@@ -721,20 +718,20 @@ export class PostDefendStatChangeAbAttr extends PostDefendAbAttr {
}
}
-export class PostDefendHpGatedStatChangeAbAttr extends PostDefendAbAttr {
+export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr {
private condition: PokemonDefendCondition;
private hpGate: number;
private stats: BattleStat[];
- private levels: integer;
+ private stages: number;
private selfTarget: boolean;
- constructor(condition: PokemonDefendCondition, hpGate: number, stats: BattleStat[], levels: integer, selfTarget: boolean = true) {
+ constructor(condition: PokemonDefendCondition, hpGate: number, stats: BattleStat[], stages: number, selfTarget: boolean = true) {
super(true);
this.condition = condition;
this.hpGate = hpGate;
this.stats = stats;
- this.levels = levels;
+ this.stages = stages;
this.selfTarget = selfTarget;
}
@@ -744,8 +741,8 @@ export class PostDefendHpGatedStatChangeAbAttr extends PostDefendAbAttr {
const damageReceived = lastAttackReceived?.damage || 0;
if (this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat)) {
- if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.levels));
+ if (!simulated ) {
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.stages));
}
return true;
}
@@ -913,20 +910,20 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
}
}
-export class PostDefendCritStatChangeAbAttr extends PostDefendAbAttr {
+export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr {
private stat: BattleStat;
- private levels: integer;
+ private stages: number;
- constructor(stat: BattleStat, levels: integer) {
+ constructor(stat: BattleStat, stages: number) {
super();
this.stat = stat;
- this.levels = levels;
+ this.stages = stages;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages));
}
return true;
@@ -1113,23 +1110,23 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
}
}
-export class PostStatChangeStatChangeAbAttr extends PostStatChangeAbAttr {
- private condition: PokemonStatChangeCondition;
+export class PostStatStageChangeStatStageChangeAbAttr extends PostStatStageChangeAbAttr {
+ private condition: PokemonStatStageChangeCondition;
private statsToChange: BattleStat[];
- private levels: integer;
+ private stages: number;
- constructor(condition: PokemonStatChangeCondition, statsToChange: BattleStat[], levels: integer) {
+ constructor(condition: PokemonStatStageChangeCondition, statsToChange: BattleStat[], stages: number) {
super(true);
this.condition = condition;
this.statsToChange = statsToChange;
- this.levels = levels;
+ this.stages = stages;
}
- applyPostStatChange(pokemon: Pokemon, simulated: boolean, statsChanged: BattleStat[], levelsChanged: integer, selfTarget: boolean, args: any[]): boolean {
- if (this.condition(pokemon, statsChanged, levelsChanged) && !selfTarget) {
+ applyPostStatStageChange(pokemon: Pokemon, simulated: boolean, statStagesChanged: BattleStat[], stagesChanged: number, selfTarget: boolean, args: any[]): boolean {
+ if (this.condition(pokemon, statStagesChanged, stagesChanged) && !selfTarget) {
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (pokemon).getBattlerIndex(), true, this.statsToChange, this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (pokemon).getBattlerIndex(), true, this.statsToChange, this.stages));
}
return true;
}
@@ -1210,13 +1207,13 @@ export class FieldPreventExplosiveMovesAbAttr extends AbAttr {
}
/**
- * Multiplies a BattleStat if the checked Pokemon lacks this ability.
+ * Multiplies a Stat if the checked Pokemon lacks this ability.
* If this ability cannot stack, a BooleanHolder can be used to prevent this from stacking.
- * @see {@link applyFieldBattleStatMultiplierAbAttrs}
- * @see {@link applyFieldBattleStat}
+ * @see {@link applyFieldStatMultiplierAbAttrs}
+ * @see {@link applyFieldStat}
* @see {@link Utils.BooleanHolder}
*/
-export class FieldMultiplyBattleStatAbAttr extends AbAttr {
+export class FieldMultiplyStatAbAttr extends AbAttr {
private stat: Stat;
private multiplier: number;
private canStack: boolean;
@@ -1230,7 +1227,7 @@ export class FieldMultiplyBattleStatAbAttr extends AbAttr {
}
/**
- * applyFieldBattleStat: Tries to multiply a Pokemon's BattleStat
+ * applyFieldStat: Tries to multiply a Pokemon's Stat
* @param pokemon {@linkcode Pokemon} the Pokemon using this ability
* @param passive {@linkcode boolean} unused
* @param stat {@linkcode Stat} the type of the checked stat
@@ -1240,12 +1237,12 @@ export class FieldMultiplyBattleStatAbAttr extends AbAttr {
* @param args {any[]} unused
* @returns true if this changed the checked stat, false otherwise.
*/
- applyFieldBattleStat(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, args: any[]): boolean {
+ applyFieldStat(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, args: any[]): boolean {
if (!this.canStack && hasApplied.value) {
return false;
}
- if (this.stat === stat && checkedPokemon.getAbilityAttrs(FieldMultiplyBattleStatAbAttr).every(attr => (attr as FieldMultiplyBattleStatAbAttr).stat !== stat)) {
+ if (this.stat === stat && checkedPokemon.getAbilityAttrs(FieldMultiplyStatAbAttr).every(attr => (attr as FieldMultiplyStatAbAttr).stat !== stat)) {
statValue.value *= this.multiplier;
hasApplied.value = true;
return true;
@@ -1579,22 +1576,22 @@ export class AllyMoveCategoryPowerBoostAbAttr extends FieldMovePowerBoostAbAttr
}
}
-export class BattleStatMultiplierAbAttr extends AbAttr {
- private battleStat: BattleStat;
+export class StatMultiplierAbAttr extends AbAttr {
+ private stat: BattleStat;
private multiplier: number;
private condition: PokemonAttackCondition | null;
- constructor(battleStat: BattleStat, multiplier: number, condition?: PokemonAttackCondition) {
+ constructor(stat: BattleStat, multiplier: number, condition?: PokemonAttackCondition) {
super(false);
- this.battleStat = battleStat;
+ this.stat = stat;
this.multiplier = multiplier;
this.condition = condition ?? null;
}
- applyBattleStat(pokemon: Pokemon, passive: boolean, simulated: boolean, battleStat: BattleStat, statValue: Utils.NumberHolder, args: any[]): boolean | Promise {
+ applyStatStage(pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, statValue: Utils.NumberHolder, args: any[]): boolean | Promise {
const move = (args[0] as Move);
- if (battleStat === this.battleStat && (!this.condition || this.condition(pokemon, null, move))) {
+ if (stat === this.stat && (!this.condition || this.condition(pokemon, null, move))) {
statValue.value *= this.multiplier;
return true;
}
@@ -1765,15 +1762,15 @@ export class PostVictoryAbAttr extends AbAttr {
}
}
-class PostVictoryStatChangeAbAttr extends PostVictoryAbAttr {
+class PostVictoryStatStageChangeAbAttr extends PostVictoryAbAttr {
private stat: BattleStat | ((p: Pokemon) => BattleStat);
- private levels: integer;
+ private stages: number;
- constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), levels: integer) {
+ constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), stages: number) {
super();
this.stat = stat;
- this.levels = levels;
+ this.stages = stages;
}
applyPostVictory(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise {
@@ -1781,7 +1778,7 @@ class PostVictoryStatChangeAbAttr extends PostVictoryAbAttr {
? this.stat(pokemon)
: this.stat;
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.stages));
}
return true;
}
@@ -1815,15 +1812,15 @@ export class PostKnockOutAbAttr extends AbAttr {
}
}
-export class PostKnockOutStatChangeAbAttr extends PostKnockOutAbAttr {
+export class PostKnockOutStatStageChangeAbAttr extends PostKnockOutAbAttr {
private stat: BattleStat | ((p: Pokemon) => BattleStat);
- private levels: integer;
+ private stages: number;
- constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), levels: integer) {
+ constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), stages: number) {
super();
this.stat = stat;
- this.levels = levels;
+ this.stages = stages;
}
applyPostKnockOut(pokemon: Pokemon, passive: boolean, simulated: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise {
@@ -1831,7 +1828,7 @@ export class PostKnockOutStatChangeAbAttr extends PostKnockOutAbAttr {
? this.stat(pokemon)
: this.stat;
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.stages));
}
return true;
}
@@ -1855,37 +1852,21 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr {
}
}
-export class IgnoreOpponentStatChangesAbAttr extends AbAttr {
- constructor() {
+export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
+ private stats: readonly BattleStat[];
+
+ constructor(stats?: BattleStat[]) {
super(false);
+
+ this.stats = stats ?? BATTLE_STATS;
}
- apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]) {
- (args[0] as Utils.IntegerHolder).value = 0;
-
- return true;
- }
-}
-/**
- * Ignores opponent's evasion stat changes when determining if a move hits or not
- * @extends AbAttr
- * @see {@linkcode apply}
- */
-export class IgnoreOpponentEvasionAbAttr extends AbAttr {
- constructor() {
- super(false);
- }
- /**
- * Checks if enemy Pokemon is trapped by an Arena Trap-esque ability
- * @param pokemon N/A
- * @param passive N/A
- * @param cancelled N/A
- * @param args [0] {@linkcode Utils.IntegerHolder} of BattleStat.EVA
- * @returns if evasion level was successfully considered as 0
- */
- apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]) {
- (args[0] as Utils.IntegerHolder).value = 0;
- return true;
+ apply(_pokemon: Pokemon, _passive: boolean, simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]) {
+ if (this.stats.includes(args[0])) {
+ (args[1] as Utils.BooleanHolder).value = true;
+ return true;
+ }
+ return false;
}
}
@@ -1907,21 +1888,21 @@ export class IntimidateImmunityAbAttr extends AbAttr {
}
}
-export class PostIntimidateStatChangeAbAttr extends AbAttr {
+export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
private stats: BattleStat[];
- private levels: integer;
+ private stages: number;
private overwrites: boolean;
- constructor(stats: BattleStat[], levels: integer, overwrites?: boolean) {
+ constructor(stats: BattleStat[], stages: number, overwrites?: boolean) {
super(true);
this.stats = stats;
- this.levels = levels;
+ this.stages = stages;
this.overwrites = !!overwrites;
}
- apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
+ apply(pokemon: Pokemon, passive: boolean, simulated:boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!simulated) {
- pokemon.scene.pushPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, this.levels));
+ pokemon.scene.pushPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, this.stages));
}
cancelled.value = this.overwrites;
return true;
@@ -2026,19 +2007,17 @@ export class PostSummonAddBattlerTagAbAttr extends PostSummonAbAttr {
}
}
-export class PostSummonStatChangeAbAttr extends PostSummonAbAttr {
+export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
private stats: BattleStat[];
- private levels: integer;
+ private stages: number;
private selfTarget: boolean;
private intimidate: boolean;
- constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, intimidate?: boolean) {
+ constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, intimidate?: boolean) {
super(false);
- this.stats = typeof(stats) === "number"
- ? [ stats as BattleStat ]
- : stats as BattleStat[];
- this.levels = levels;
+ this.stats = stats;
+ this.stages = stages;
this.selfTarget = !!selfTarget;
this.intimidate = !!intimidate;
}
@@ -2050,20 +2029,19 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr {
queueShowAbility(pokemon, passive); // TODO: Better solution than manually showing the ability here
if (this.selfTarget) {
- // we unshift the StatChangePhase to put it right after the showAbility and not at the end of the
+ // we unshift the StatStageChangePhase to put it right after the showAbility and not at the end of the
// phase list (which could be after CommandPhase for example)
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages));
return true;
}
for (const opponent of pokemon.getOpponents()) {
const cancelled = new Utils.BooleanHolder(false);
if (this.intimidate) {
applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated);
- applyAbAttrs(PostIntimidateStatChangeAbAttr, opponent, cancelled, simulated);
+ applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated);
}
if (!cancelled.value) {
- const statChangePhase = new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels);
- pokemon.scene.unshiftPhase(statChangePhase);
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages));
}
}
return true;
@@ -2104,7 +2082,7 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr {
* @param args N/A
* @returns if the move was successful
*/
-export class PostSummonClearAllyStatsAbAttr extends PostSummonAbAttr {
+export class PostSummonClearAllyStatStagesAbAttr extends PostSummonAbAttr {
constructor() {
super();
}
@@ -2113,8 +2091,8 @@ export class PostSummonClearAllyStatsAbAttr extends PostSummonAbAttr {
const target = pokemon.getAlly();
if (target?.isActive(true)) {
if (!simulated) {
- for (let s = 0; s < target.summonData.battleStats.length; s++) {
- target.summonData.battleStats[s] = 0;
+ for (const s of BATTLE_STATS) {
+ target.setStatStage(s, 0);
}
target.scene.queueMessage(i18next.t("abilityTriggers:postSummonClearAllyStats", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
@@ -2143,7 +2121,7 @@ export class DownloadAbAttr extends PostSummonAbAttr {
// TODO: Implement the Substitute feature(s) once move is implemented.
/**
* Checks to see if it is the opening turn (starting a new game), if so, Download won't work. This is because Download takes into account
- * vitamins and items, so it needs to use the BattleStat and the stat alone.
+ * vitamins and items, so it needs to use the Stat and the stat alone.
* @param {Pokemon} pokemon Pokemon that is using the move, as well as seeing the opposing pokemon.
* @param {boolean} passive N/A
* @param {any[]} args N/A
@@ -2156,21 +2134,21 @@ export class DownloadAbAttr extends PostSummonAbAttr {
for (const opponent of pokemon.getOpponents()) {
this.enemyCountTally++;
- this.enemyDef += opponent.getBattleStat(Stat.DEF);
- this.enemySpDef += opponent.getBattleStat(Stat.SPDEF);
+ this.enemyDef += opponent.getEffectiveStat(Stat.DEF);
+ this.enemySpDef += opponent.getEffectiveStat(Stat.SPDEF);
}
this.enemyDef = Math.round(this.enemyDef / this.enemyCountTally);
this.enemySpDef = Math.round(this.enemySpDef / this.enemyCountTally);
if (this.enemyDef < this.enemySpDef) {
- this.stats = [BattleStat.ATK];
+ this.stats = [ Stat.ATK ];
} else {
- this.stats = [BattleStat.SPATK];
+ this.stats = [ Stat.SPATK ];
}
if (this.enemyDef > 0 && this.enemySpDef > 0) { // only activate if there's actually an enemy to download from
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, 1));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, 1));
}
return true;
}
@@ -2339,12 +2317,14 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
}
const ally = pokemon.getAlly();
- if (!ally || ally.summonData.battleStats.every((change) => change === 0)) {
+ if (!ally || ally.getStatStages().every(s => s === 0)) {
return false;
}
if (!simulated) {
- pokemon.summonData.battleStats = ally.summonData.battleStats;
+ for (const s of BATTLE_STATS) {
+ pokemon.setStatStage(s, ally.getStatStage(s));
+ }
pokemon.updateInfo();
}
@@ -2383,14 +2363,27 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
pokemon.summonData.ability = target.getAbility().id;
pokemon.summonData.gender = target.getGender();
pokemon.summonData.fusionGender = target.getFusionGender();
- pokemon.summonData.stats = [ pokemon.stats[Stat.HP] ].concat(target.stats.slice(1));
- pokemon.summonData.battleStats = target.summonData.battleStats.slice(0);
+
+ // Copy all stats (except HP)
+ for (const s of EFFECTIVE_STATS) {
+ pokemon.setStat(s, target.getStat(s, false), false);
+ }
+
+ // Copy all stat stages
+ for (const s of BATTLE_STATS) {
+ pokemon.setStatStage(s, target.getStatStage(s));
+ }
+
pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m!.moveId, m!.ppUsed, m!.ppUp)); // TODO: are those bangs correct?
pokemon.summonData.types = target.getTypes();
+
pokemon.scene.playSound("battle_anims/PRSFX- Transform");
- pokemon.loadAssets(false).then(() => pokemon.playAnim());
+ pokemon.loadAssets(false).then(() => {
+ pokemon.playAnim();
+ pokemon.updateInfo();
+ });
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, }));
@@ -2594,13 +2587,13 @@ export class PreSwitchOutFormChangeAbAttr extends PreSwitchOutAbAttr {
}
-export class PreStatChangeAbAttr extends AbAttr {
- applyPreStatChange(pokemon: Pokemon | null, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise {
+export class PreStatStageChangeAbAttr extends AbAttr {
+ applyPreStatStageChange(pokemon: Pokemon | null, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise {
return false;
}
}
-export class ProtectStatAbAttr extends PreStatChangeAbAttr {
+export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
private protectedStat?: BattleStat;
constructor(protectedStat?: BattleStat) {
@@ -2609,7 +2602,7 @@ export class ProtectStatAbAttr extends PreStatChangeAbAttr {
this.protectedStat = protectedStat;
}
- applyPreStatChange(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean {
+ applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean {
if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) {
cancelled.value = true;
return true;
@@ -2618,11 +2611,11 @@ export class ProtectStatAbAttr extends PreStatChangeAbAttr {
return false;
}
- getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
+ getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:protectStat", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName,
- statName: this.protectedStat ? getBattleStatName(this.protectedStat) : i18next.t("battle:stats")
+ statName: this.protectedStat ? i18next.t(getStatKey(this.protectedStat)) : i18next.t("battle:stats")
});
}
}
@@ -3465,51 +3458,53 @@ export class MoodyAbAttr extends PostTurnAbAttr {
super(true);
}
/**
- * Randomly increases one BattleStat by 2 stages and decreases a different BattleStat by 1 stage
+ * Randomly increases one stat stage by 2 and decreases a different stat stage by 1
* @param {Pokemon} pokemon Pokemon that has this ability
* @param passive N/A
* @param simulated true if applying in a simulated call.
* @param args N/A
* @returns true
*
- * Any BattleStats at +6 or -6 are excluded from being increased or decreased, respectively
- * If the pokemon already has all BattleStats raised to stage 6, it will only decrease one BattleStat by 1 stage
- * If the pokemon already has all BattleStats lowered to stage -6, it will only increase one BattleStat by 2 stages
+ * Any stat stages at +6 or -6 are excluded from being increased or decreased, respectively
+ * If the pokemon already has all stat stages raised to 6, it will only decrease one stat stage by 1
+ * If the pokemon already has all stat stages lowered to -6, it will only increase one stat stage by 2
*/
applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
- const selectableStats = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD];
- const increaseStatArray = selectableStats.filter(s => pokemon.summonData.battleStats[s] < 6);
- let decreaseStatArray = selectableStats.filter(s => pokemon.summonData.battleStats[s] > -6);
+ const canRaise = EFFECTIVE_STATS.filter(s => pokemon.getStatStage(s) < 6);
+ let canLower = EFFECTIVE_STATS.filter(s => pokemon.getStatStage(s) > -6);
- if (!simulated && increaseStatArray.length > 0) {
- const increaseStat = increaseStatArray[Utils.randInt(increaseStatArray.length)];
- decreaseStatArray = decreaseStatArray.filter(s => s !== increaseStat);
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [increaseStat], 2));
- }
- if (!simulated && decreaseStatArray.length > 0) {
- const decreaseStat = decreaseStatArray[Utils.randInt(decreaseStatArray.length)];
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [decreaseStat], -1));
+ if (!simulated) {
+ if (canRaise.length > 0) {
+ const raisedStat = Utils.randSeedItem(canRaise);
+ canLower = canRaise.filter(s => s !== raisedStat);
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ raisedStat ], 2));
+ }
+ if (canLower.length > 0) {
+ const loweredStat = Utils.randSeedItem(canLower);
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ loweredStat ], -1));
+ }
}
+
return true;
}
}
-export class PostTurnStatChangeAbAttr extends PostTurnAbAttr {
+export class PostTurnStatStageChangeAbAttr extends PostTurnAbAttr {
private stats: BattleStat[];
- private levels: integer;
+ private stages: number;
- constructor(stats: BattleStat | BattleStat[], levels: integer) {
+ constructor(stats: BattleStat[], stages: number) {
super(true);
this.stats = Array.isArray(stats)
? stats
: [ stats ];
- this.levels = levels;
+ this.stages = stages;
}
applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages));
}
return true;
}
@@ -3721,7 +3716,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
}
}
-export class StatChangeMultiplierAbAttr extends AbAttr {
+export class StatStageChangeMultiplierAbAttr extends AbAttr {
private multiplier: integer;
constructor(multiplier: integer) {
@@ -3737,10 +3732,10 @@ export class StatChangeMultiplierAbAttr extends AbAttr {
}
}
-export class StatChangeCopyAbAttr extends AbAttr {
+export class StatStageChangeCopyAbAttr extends AbAttr {
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise {
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, (args[0] as BattleStat[]), (args[1] as integer), true, false, false));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, (args[0] as BattleStat[]), (args[1] as number), true, false, false));
}
return true;
}
@@ -4140,22 +4135,22 @@ export class FlinchEffectAbAttr extends AbAttr {
}
}
-export class FlinchStatChangeAbAttr extends FlinchEffectAbAttr {
+export class FlinchStatStageChangeAbAttr extends FlinchEffectAbAttr {
private stats: BattleStat[];
- private levels: integer;
+ private stages: number;
- constructor(stats: BattleStat | BattleStat[], levels: integer) {
+ constructor(stats: BattleStat[], stages: number) {
super();
this.stats = Array.isArray(stats)
? stats
: [ stats ];
- this.levels = levels;
+ this.stages = stages;
}
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!simulated) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages));
}
return true;
}
@@ -4355,9 +4350,9 @@ export class MoneyAbAttr extends PostBattleAbAttr {
* Applies a stat change after a Pokémon is summoned,
* conditioned on the presence of a specific arena tag.
*
- * @extends {PostSummonStatChangeAbAttr}
+ * @extends {PostSummonStatStageChangeAbAttr}
*/
-export class PostSummonStatChangeOnArenaAbAttr extends PostSummonStatChangeAbAttr {
+export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageChangeAbAttr {
/**
* The type of arena tag that conditions the stat change.
* @private
@@ -4366,13 +4361,13 @@ export class PostSummonStatChangeOnArenaAbAttr extends PostSummonStatChangeAbAtt
private tagType: ArenaTagType;
/**
- * Creates an instance of PostSummonStatChangeOnArenaAbAttr.
+ * Creates an instance of PostSummonStatStageChangeOnArenaAbAttr.
* Initializes the stat change to increase Attack by 1 stage if the specified arena tag is present.
*
* @param {ArenaTagType} tagType - The type of arena tag to check for.
*/
constructor(tagType: ArenaTagType) {
- super([BattleStat.ATK], 1, true, false);
+ super([ Stat.ATK ], 1, true, false);
this.tagType = tagType;
}
@@ -4619,14 +4614,14 @@ export function applyPostMoveUsedAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostMoveUsed(pokemon, move, source, targets, simulated, args), args, false, simulated);
}
-export function applyBattleStatMultiplierAbAttrs(attrType: Constructor,
- pokemon: Pokemon, battleStat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, ...args: any[]): Promise {
- return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyBattleStat(pokemon, passive, simulated, battleStat, statValue, args), args, false, simulated);
+export function applyStatMultiplierAbAttrs(attrType: Constructor,
+ pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, ...args: any[]): Promise {
+ return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args);
}
/**
- * Applies a field Battle Stat multiplier attribute
- * @param attrType {@linkcode FieldMultiplyBattleStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
+ * Applies a field Stat multiplier attribute
+ * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
* @param pokemon {@linkcode Pokemon} the Pokemon applying this ability
* @param stat {@linkcode Stat} the type of the checked stat
* @param statValue {@linkcode Utils.NumberHolder} the value of the checked stat
@@ -4634,9 +4629,9 @@ export function applyBattleStatMultiplierAbAttrs(attrType: Constructor,
+export function applyFieldStatMultiplierAbAttrs(attrType: Constructor,
pokemon: Pokemon, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, simulated: boolean = false, ...args: any[]): Promise {
- return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyFieldBattleStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, hasApplied, args), args, false, simulated);
+ return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyFieldStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, hasApplied, args), args);
}
export function applyPreAttackAbAttrs(attrType: Constructor,
@@ -4669,14 +4664,14 @@ export function applyPreSwitchOutAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, args), args, true, simulated);
}
-export function applyPreStatChangeAbAttrs(attrType: Constructor,
+export function applyPreStatStageChangeAbAttrs(attrType: Constructor,
pokemon: Pokemon | null, stat: BattleStat, cancelled: Utils.BooleanHolder, simulated: boolean = false, ...args: any[]): Promise {
- return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreStatChange(pokemon, passive, simulated, stat, cancelled, args), args, false, simulated);
+ return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), args, false, simulated);
}
-export function applyPostStatChangeAbAttrs(attrType: Constructor,
- pokemon: Pokemon, stats: BattleStat[], levels: integer, selfTarget: boolean, simulated: boolean = false, ...args: any[]): Promise {
- return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostStatChange(pokemon, simulated, stats, levels, selfTarget, args), args, false, simulated);
+export function applyPostStatStageChangeAbAttrs(attrType: Constructor,
+ pokemon: Pokemon, stats: BattleStat[], stages: integer, selfTarget: boolean, simulated: boolean = false, ...args: any[]): Promise {
+ return applyAbAttrsInternal(attrType, pokemon, (attr, _passive) => attr.applyPostStatStageChange(pokemon, simulated, stats, stages, selfTarget, args), args, false, simulated);
}
export function applyPreSetStatusAbAttrs(attrType: Constructor,
@@ -4766,7 +4761,7 @@ export function initAbilities() {
.attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN),
new Ability(Abilities.SPEED_BOOST, 3)
- .attr(PostTurnStatChangeAbAttr, BattleStat.SPD, 1),
+ .attr(PostTurnStatStageChangeAbAttr, [ Stat.SPD ], 1),
new Ability(Abilities.BATTLE_ARMOR, 3)
.attr(BlockCritAbAttr)
.ignorable(),
@@ -4781,7 +4776,7 @@ export function initAbilities() {
.attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS)
.ignorable(),
new Ability(Abilities.SAND_VEIL, 3)
- .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2)
+ .attr(StatMultiplierAbAttr, Stat.EVA, 1.2)
.attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM)
.condition(getWeatherCondition(WeatherType.SANDSTORM))
.ignorable(),
@@ -4807,7 +4802,7 @@ export function initAbilities() {
.attr(PostFaintUnsuppressedWeatherFormChangeAbAttr)
.bypassFaint(),
new Ability(Abilities.COMPOUND_EYES, 3)
- .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.3),
+ .attr(StatMultiplierAbAttr, Stat.ACC, 1.3),
new Ability(Abilities.INSOMNIA, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
@@ -4832,7 +4827,7 @@ export function initAbilities() {
.attr(ForceSwitchOutImmunityAbAttr)
.ignorable(),
new Ability(Abilities.INTIMIDATE, 3)
- .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, -1, false, true),
+ .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], -1, false, true),
new Ability(Abilities.SHADOW_TAG, 3)
.attr(ArenaTrapAbAttr, (user, target) => {
if (target.hasAbility(Abilities.SHADOW_TAG)) {
@@ -4863,26 +4858,26 @@ export function initAbilities() {
.attr(PreSwitchOutResetStatusAbAttr),
new Ability(Abilities.LIGHTNING_ROD, 3)
.attr(RedirectTypeMoveAbAttr, Type.ELECTRIC)
- .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPATK, 1)
+ .attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1)
.ignorable(),
new Ability(Abilities.SERENE_GRACE, 3)
.attr(MoveEffectChanceMultiplierAbAttr, 2)
.partial(),
new Ability(Abilities.SWIFT_SWIM, 3)
- .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
+ .attr(StatMultiplierAbAttr, Stat.SPD, 2)
.condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)),
new Ability(Abilities.CHLOROPHYLL, 3)
- .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
+ .attr(StatMultiplierAbAttr, Stat.SPD, 2)
.condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)),
new Ability(Abilities.ILLUMINATE, 3)
- .attr(ProtectStatAbAttr, BattleStat.ACC)
+ .attr(ProtectStatAbAttr, Stat.ACC)
.attr(DoubleBattleChanceAbAttr)
.ignorable(),
new Ability(Abilities.TRACE, 3)
.attr(PostSummonCopyAbilityAbAttr)
.attr(UncopiableAbilityAbAttr),
new Ability(Abilities.HUGE_POWER, 3)
- .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2),
+ .attr(StatMultiplierAbAttr, Stat.ATK, 2),
new Ability(Abilities.POISON_POINT, 3)
.attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON)
.bypassFaint(),
@@ -4927,25 +4922,25 @@ export function initAbilities() {
new Ability(Abilities.RUN_AWAY, 3)
.attr(RunSuccessAbAttr),
new Ability(Abilities.KEEN_EYE, 3)
- .attr(ProtectStatAbAttr, BattleStat.ACC)
+ .attr(ProtectStatAbAttr, Stat.ACC)
.ignorable(),
new Ability(Abilities.HYPER_CUTTER, 3)
- .attr(ProtectStatAbAttr, BattleStat.ATK)
+ .attr(ProtectStatAbAttr, Stat.ATK)
.ignorable(),
new Ability(Abilities.PICKUP, 3)
.attr(PostBattleLootAbAttr),
new Ability(Abilities.TRUANT, 3)
.attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false),
new Ability(Abilities.HUSTLE, 3)
- .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 1.5)
- .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 0.8, (user, target, move) => move.category === MoveCategory.PHYSICAL),
+ .attr(StatMultiplierAbAttr, Stat.ATK, 1.5)
+ .attr(StatMultiplierAbAttr, Stat.ACC, 0.8, (_user, _target, move) => move.category === MoveCategory.PHYSICAL),
new Ability(Abilities.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
new Ability(Abilities.PLUS, 3)
- .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), BattleStatMultiplierAbAttr, BattleStat.SPATK, 1.5)
+ .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5)
.ignorable(),
new Ability(Abilities.MINUS, 3)
- .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), BattleStatMultiplierAbAttr, BattleStat.SPATK, 1.5)
+ .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5)
.ignorable(),
new Ability(Abilities.FORECAST, 3)
.attr(UncopiableAbilityAbAttr)
@@ -4960,9 +4955,9 @@ export function initAbilities() {
.conditionalAttr(pokemon => !Utils.randSeedInt(3), PostTurnResetStatusAbAttr),
new Ability(Abilities.GUTS, 3)
.attr(BypassBurnDamageReductionAbAttr)
- .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.ATK, 1.5),
+ .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatMultiplierAbAttr, Stat.ATK, 1.5),
new Ability(Abilities.MARVEL_SCALE, 3)
- .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.DEF, 1.5)
+ .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatMultiplierAbAttr, Stat.DEF, 1.5)
.ignorable(),
new Ability(Abilities.LIQUID_OOZE, 3)
.attr(ReverseDrainAbAttr),
@@ -4995,7 +4990,7 @@ export function initAbilities() {
.attr(ProtectStatAbAttr)
.ignorable(),
new Ability(Abilities.PURE_POWER, 3)
- .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2),
+ .attr(StatMultiplierAbAttr, Stat.ATK, 2),
new Ability(Abilities.SHELL_ARMOR, 3)
.attr(BlockCritAbAttr)
.ignorable(),
@@ -5006,25 +5001,25 @@ export function initAbilities() {
.attr(PostFaintUnsuppressedWeatherFormChangeAbAttr)
.bypassFaint(),
new Ability(Abilities.TANGLED_FEET, 4)
- .conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), BattleStatMultiplierAbAttr, BattleStat.EVA, 2)
+ .conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), StatMultiplierAbAttr, Stat.EVA, 2)
.ignorable(),
new Ability(Abilities.MOTOR_DRIVE, 4)
- .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPD, 1)
+ .attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPD, 1)
.ignorable(),
new Ability(Abilities.RIVALRY, 4)
.attr(MovePowerBoostAbAttr, (user, target, move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25, true)
.attr(MovePowerBoostAbAttr, (user, target, move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75),
new Ability(Abilities.STEADFAST, 4)
- .attr(FlinchStatChangeAbAttr, BattleStat.SPD, 1),
+ .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1),
new Ability(Abilities.SNOW_CLOAK, 4)
- .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2)
+ .attr(StatMultiplierAbAttr, Stat.EVA, 1.2)
.attr(BlockWeatherDamageAttr, WeatherType.HAIL)
.condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW))
.ignorable(),
new Ability(Abilities.GLUTTONY, 4)
.attr(ReduceBerryUseThresholdAbAttr),
new Ability(Abilities.ANGER_POINT, 4)
- .attr(PostDefendCritStatChangeAbAttr, BattleStat.ATK, 6),
+ .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6),
new Ability(Abilities.UNBURDEN, 4)
.unimplemented(),
new Ability(Abilities.HEATPROOF, 4)
@@ -5032,7 +5027,7 @@ export function initAbilities() {
.attr(ReduceBurnDamageAbAttr, 0.5)
.ignorable(),
new Ability(Abilities.SIMPLE, 4)
- .attr(StatChangeMultiplierAbAttr, 2)
+ .attr(StatStageChangeMultiplierAbAttr, 2)
.ignorable(),
new Ability(Abilities.DRY_SKIN, 4)
.attr(PostWeatherLapseDamageAbAttr, 2, WeatherType.SUNNY, WeatherType.HARSH_SUN)
@@ -5057,11 +5052,11 @@ export function initAbilities() {
.condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)),
new Ability(Abilities.SOLAR_POWER, 4)
.attr(PostWeatherLapseDamageAbAttr, 2, WeatherType.SUNNY, WeatherType.HARSH_SUN)
- .attr(BattleStatMultiplierAbAttr, BattleStat.SPATK, 1.5)
+ .attr(StatMultiplierAbAttr, Stat.SPATK, 1.5)
.condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)),
new Ability(Abilities.QUICK_FEET, 4)
- .conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
- .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.SPD, 1.5),
+ .conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, StatMultiplierAbAttr, Stat.SPD, 2)
+ .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatMultiplierAbAttr, Stat.SPD, 1.5),
new Ability(Abilities.NORMALIZE, 4)
.attr(MoveTypeChangeAbAttr, Type.NORMAL, 1.2, (user, target, move) => {
return ![Moves.HIDDEN_POWER, Moves.WEATHER_BALL, Moves.NATURAL_GIFT, Moves.JUDGMENT, Moves.TECHNO_BLAST].includes(move.id);
@@ -5101,7 +5096,7 @@ export function initAbilities() {
new Ability(Abilities.FOREWARN, 4)
.attr(ForewarnAbAttr),
new Ability(Abilities.UNAWARE, 4)
- .attr(IgnoreOpponentStatChangesAbAttr)
+ .attr(IgnoreOpponentStatStagesAbAttr)
.ignorable(),
new Ability(Abilities.TINTED_LENS, 4)
//@ts-ignore
@@ -5116,7 +5111,7 @@ export function initAbilities() {
.attr(IntimidateImmunityAbAttr),
new Ability(Abilities.STORM_DRAIN, 4)
.attr(RedirectTypeMoveAbAttr, Type.WATER)
- .attr(TypeImmunityStatChangeAbAttr, Type.WATER, BattleStat.SPATK, 1)
+ .attr(TypeImmunityStatStageChangeAbAttr, Type.WATER, Stat.SPATK, 1)
.ignorable(),
new Ability(Abilities.ICE_BODY, 4)
.attr(BlockWeatherDamageAttr, WeatherType.HAIL)
@@ -5140,8 +5135,8 @@ export function initAbilities() {
.attr(UnsuppressableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr),
new Ability(Abilities.FLOWER_GIFT, 4)
- .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.ATK, 1.5)
- .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.SPDEF, 1.5)
+ .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 1.5)
+ .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.SPDEF, 1.5)
.attr(UncopiableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr)
.attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FLOWER_GIFT)
@@ -5158,15 +5153,15 @@ export function initAbilities() {
.attr(MoveEffectChanceMultiplierAbAttr, 0)
.partial(),
new Ability(Abilities.CONTRARY, 5)
- .attr(StatChangeMultiplierAbAttr, -1)
+ .attr(StatStageChangeMultiplierAbAttr, -1)
.ignorable(),
new Ability(Abilities.UNNERVE, 5)
.attr(PreventBerryUseAbAttr),
new Ability(Abilities.DEFIANT, 5)
- .attr(PostStatChangeStatChangeAbAttr, (target, statsChanged, levels) => levels < 0, [BattleStat.ATK], 2),
+ .attr(PostStatStageChangeStatStageChangeAbAttr, (target, statsChanged, stages) => stages < 0, [Stat.ATK], 2),
new Ability(Abilities.DEFEATIST, 5)
- .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 0.5)
- .attr(BattleStatMultiplierAbAttr, BattleStat.SPATK, 0.5)
+ .attr(StatMultiplierAbAttr, Stat.ATK, 0.5)
+ .attr(StatMultiplierAbAttr, Stat.SPATK, 0.5)
.condition((pokemon) => pokemon.getHpRatio() <= 0.5),
new Ability(Abilities.CURSED_BODY, 5)
.attr(PostDefendMoveDisableAbAttr, 30)
@@ -5177,8 +5172,8 @@ export function initAbilities() {
.ignorable()
.unimplemented(),
new Ability(Abilities.WEAK_ARMOR, 5)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, BattleStat.DEF, -1)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, BattleStat.SPD, 2),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1)
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2),
new Ability(Abilities.HEAVY_METAL, 5)
.attr(WeightMultiplierAbAttr, 2)
.ignorable(),
@@ -5214,10 +5209,10 @@ export function initAbilities() {
new Ability(Abilities.REGENERATOR, 5)
.attr(PreSwitchOutHealAbAttr),
new Ability(Abilities.BIG_PECKS, 5)
- .attr(ProtectStatAbAttr, BattleStat.DEF)
+ .attr(ProtectStatAbAttr, Stat.DEF)
.ignorable(),
new Ability(Abilities.SAND_RUSH, 5)
- .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
+ .attr(StatMultiplierAbAttr, Stat.SPD, 2)
.attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM)
.condition(getWeatherCondition(WeatherType.SANDSTORM)),
new Ability(Abilities.WONDER_SKIN, 5)
@@ -5239,18 +5234,18 @@ export function initAbilities() {
.attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY)
.bypassFaint(),
new Ability(Abilities.MOXIE, 5)
- .attr(PostVictoryStatChangeAbAttr, BattleStat.ATK, 1),
+ .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1),
new Ability(Abilities.JUSTIFIED, 5)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.type === Type.DARK && move.category !== MoveCategory.STATUS, BattleStat.ATK, 1),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.DARK && move.category !== MoveCategory.STATUS, Stat.ATK, 1),
new Ability(Abilities.RATTLED, 5)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS && (move.type === Type.DARK || move.type === Type.BUG ||
- move.type === Type.GHOST), BattleStat.SPD, 1)
- .attr(PostIntimidateStatChangeAbAttr, [BattleStat.SPD], 1),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS && (move.type === Type.DARK || move.type === Type.BUG ||
+ move.type === Type.GHOST), Stat.SPD, 1)
+ .attr(PostIntimidateStatStageChangeAbAttr, [Stat.SPD], 1),
new Ability(Abilities.MAGIC_BOUNCE, 5)
.ignorable()
.unimplemented(),
new Ability(Abilities.SAP_SIPPER, 5)
- .attr(TypeImmunityStatChangeAbAttr, Type.GRASS, BattleStat.ATK, 1)
+ .attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1)
.ignorable(),
new Ability(Abilities.PRANKSTER, 5)
.attr(ChangeMovePriorityAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS, 1),
@@ -5273,7 +5268,7 @@ export function initAbilities() {
.attr(NoFusionAbilityAbAttr)
.bypassFaint(),
new Ability(Abilities.VICTORY_STAR, 5)
- .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.1)
+ .attr(StatMultiplierAbAttr, Stat.ACC, 1.1)
.partial(),
new Ability(Abilities.TURBOBLAZE, 5)
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTurboblaze", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
@@ -5302,7 +5297,7 @@ export function initAbilities() {
.attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.BALLBOMB_MOVE))
.ignorable(),
new Ability(Abilities.COMPETITIVE, 6)
- .attr(PostStatChangeStatChangeAbAttr, (target, statsChanged, levels) => levels < 0, [BattleStat.SPATK], 2),
+ .attr(PostStatStageChangeStatStageChangeAbAttr, (target, statsChanged, stages) => stages < 0, [Stat.SPATK], 2),
new Ability(Abilities.STRONG_JAW, 6)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5),
new Ability(Abilities.REFRIGERATE, 6)
@@ -5322,7 +5317,7 @@ export function initAbilities() {
new Ability(Abilities.MEGA_LAUNCHER, 6)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5),
new Ability(Abilities.GRASS_PELT, 6)
- .conditionalAttr(getTerrainCondition(TerrainType.GRASSY), BattleStatMultiplierAbAttr, BattleStat.DEF, 1.5)
+ .conditionalAttr(getTerrainCondition(TerrainType.GRASSY), StatMultiplierAbAttr, Stat.DEF, 1.5)
.ignorable(),
new Ability(Abilities.SYMBIOSIS, 6)
.unimplemented(),
@@ -5331,7 +5326,7 @@ export function initAbilities() {
new Ability(Abilities.PIXILATE, 6)
.attr(MoveTypeChangeAbAttr, Type.FAIRY, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
new Ability(Abilities.GOOEY, 6)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false),
new Ability(Abilities.AERILATE, 6)
.attr(MoveTypeChangeAbAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
new Ability(Abilities.PARENTAL_BOND, 6)
@@ -5365,7 +5360,7 @@ export function initAbilities() {
.attr(PostFaintClearWeatherAbAttr)
.bypassFaint(),
new Ability(Abilities.STAMINA, 7)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattleStat.DEF, 1),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7)
.condition(getSheerForceHitDisableAbCondition())
.unimplemented(),
@@ -5373,7 +5368,7 @@ export function initAbilities() {
.condition(getSheerForceHitDisableAbCondition())
.unimplemented(),
new Ability(Abilities.WATER_COMPACTION, 7)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.type === Type.WATER && move.category !== MoveCategory.STATUS, BattleStat.DEF, 2),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7)
.attr(ConditionalCritAbAttr, (user, target, move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON),
new Ability(Abilities.SHIELDS_DOWN, 7)
@@ -5397,10 +5392,10 @@ export function initAbilities() {
new Ability(Abilities.STEELWORKER, 7)
.attr(MoveTypePowerBoostAbAttr, Type.STEEL),
new Ability(Abilities.BERSERK, 7)
- .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [BattleStat.SPATK], 1)
+ .attr(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [Stat.SPATK], 1)
.condition(getSheerForceHitDisableAbCondition()),
new Ability(Abilities.SLUSH_RUSH, 7)
- .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
+ .attr(StatMultiplierAbAttr, Stat.SPD, 2)
.condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)),
new Ability(Abilities.LONG_REACH, 7)
.attr(IgnoreContactAbAttr),
@@ -5411,7 +5406,7 @@ export function initAbilities() {
new Ability(Abilities.GALVANIZE, 7)
.attr(MoveTypeChangeAbAttr, Type.ELECTRIC, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
new Ability(Abilities.SURGE_SURFER, 7)
- .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPD, 2),
+ .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2),
new Ability(Abilities.SCHOOLING, 7)
.attr(PostBattleInitFormChangeAbAttr, () => 0)
.attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1)
@@ -5480,9 +5475,9 @@ export function initAbilities() {
.attr(FieldPriorityMoveImmunityAbAttr)
.ignorable(),
new Ability(Abilities.SOUL_HEART, 7)
- .attr(PostKnockOutStatChangeAbAttr, BattleStat.SPATK, 1),
+ .attr(PostKnockOutStatStageChangeAbAttr, Stat.SPATK, 1),
new Ability(Abilities.TANGLING_HAIR, 7)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false),
new Ability(Abilities.RECEIVER, 7)
.attr(CopyFaintedAllyAbilityAbAttr)
.attr(UncopiableAbilityAbAttr),
@@ -5490,18 +5485,17 @@ export function initAbilities() {
.attr(CopyFaintedAllyAbilityAbAttr)
.attr(UncopiableAbilityAbAttr),
new Ability(Abilities.BEAST_BOOST, 7)
- .attr(PostVictoryStatChangeAbAttr, p => {
- const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3).map(s => s as BattleStat);
- let highestBattleStat = 0;
- let highestBattleStatIndex = 0;
- battleStats.map((bs: BattleStat, i: integer) => {
- const stat = p.getStat(bs + 1);
- if (stat > highestBattleStat) {
- highestBattleStatIndex = i;
- highestBattleStat = stat;
+ .attr(PostVictoryStatStageChangeAbAttr, p => {
+ let highestStat: EffectiveStat;
+ let highestValue = 0;
+ for (const s of EFFECTIVE_STATS) {
+ const value = p.getStat(s, false);
+ if (value > highestValue) {
+ highestStat = s;
+ highestValue = value;
}
- });
- return highestBattleStatIndex;
+ }
+ return highestStat!;
}, 1),
new Ability(Abilities.RKS_SYSTEM, 7)
.attr(UncopiableAbilityAbAttr)
@@ -5530,10 +5524,10 @@ export function initAbilities() {
//@ts-ignore
.attr(MovePowerBoostAbAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 1.25), // TODO: fix TS issues
new Ability(Abilities.INTREPID_SWORD, 8)
- .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, 1, true)
+ .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true)
.condition(getOncePerBattleCondition(Abilities.INTREPID_SWORD)),
new Ability(Abilities.DAUNTLESS_SHIELD, 8)
- .attr(PostSummonStatChangeAbAttr, BattleStat.DEF, 1, true)
+ .attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true)
.condition(getOncePerBattleCondition(Abilities.DAUNTLESS_SHIELD)),
new Ability(Abilities.LIBERO, 8)
.attr(PokemonTypeChangeAbAttr),
@@ -5542,7 +5536,7 @@ export function initAbilities() {
.attr(FetchBallAbAttr)
.condition(getOncePerBattleCondition(Abilities.BALL_FETCH)),
new Ability(Abilities.COTTON_DOWN, 8)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattleStat.SPD, -1, false, true)
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.SPD, -1, false, true)
.bypassFaint(),
new Ability(Abilities.PROPELLER_TAIL, 8)
.attr(BlockRedirectAbAttr),
@@ -5559,7 +5553,7 @@ export function initAbilities() {
new Ability(Abilities.STALWART, 8)
.attr(BlockRedirectAbAttr),
new Ability(Abilities.STEAM_ENGINE, 8)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => (move.type === Type.FIRE || move.type === Type.WATER) && move.category !== MoveCategory.STATUS, BattleStat.SPD, 6),
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => (move.type === Type.FIRE || move.type === Type.WATER) && move.category !== MoveCategory.STATUS, Stat.SPD, 6),
new Ability(Abilities.PUNK_ROCK, 8)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SOUND_BASED), 1.3)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.hasFlag(MoveFlags.SOUND_BASED), 0.5)
@@ -5630,26 +5624,26 @@ export function initAbilities() {
new Ability(Abilities.UNSEEN_FIST, 8)
.attr(IgnoreProtectOnContactAbAttr),
new Ability(Abilities.CURIOUS_MEDICINE, 8)
- .attr(PostSummonClearAllyStatsAbAttr),
+ .attr(PostSummonClearAllyStatStagesAbAttr),
new Ability(Abilities.TRANSISTOR, 8)
.attr(MoveTypePowerBoostAbAttr, Type.ELECTRIC),
new Ability(Abilities.DRAGONS_MAW, 8)
.attr(MoveTypePowerBoostAbAttr, Type.DRAGON),
new Ability(Abilities.CHILLING_NEIGH, 8)
- .attr(PostVictoryStatChangeAbAttr, BattleStat.ATK, 1),
+ .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1),
new Ability(Abilities.GRIM_NEIGH, 8)
- .attr(PostVictoryStatChangeAbAttr, BattleStat.SPATK, 1),
+ .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1),
new Ability(Abilities.AS_ONE_GLASTRIER, 8)
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(PreventBerryUseAbAttr)
- .attr(PostVictoryStatChangeAbAttr, BattleStat.ATK, 1)
+ .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(UnsuppressableAbilityAbAttr),
new Ability(Abilities.AS_ONE_SPECTRIER, 8)
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(PreventBerryUseAbAttr)
- .attr(PostVictoryStatChangeAbAttr, BattleStat.SPATK, 1)
+ .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(UnsuppressableAbilityAbAttr),
@@ -5659,26 +5653,26 @@ export function initAbilities() {
new Ability(Abilities.SEED_SOWER, 9)
.attr(PostDefendTerrainChangeAbAttr, TerrainType.GRASSY),
new Ability(Abilities.THERMAL_EXCHANGE, 9)
- .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.type === Type.FIRE && move.category !== MoveCategory.STATUS, BattleStat.ATK, 1)
+ .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(StatusEffectImmunityAbAttr, StatusEffect.BURN)
.ignorable(),
new Ability(Abilities.ANGER_SHELL, 9)
- .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], 1)
- .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ BattleStat.DEF, BattleStat.SPDEF ], -1)
+ .attr(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 1)
+ .attr(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.DEF, Stat.SPDEF ], -1)
.condition(getSheerForceHitDisableAbCondition()),
new Ability(Abilities.PURIFYING_SALT, 9)
.attr(StatusEffectImmunityAbAttr)
.attr(ReceivedTypeDamageMultiplierAbAttr, Type.GHOST, 0.5)
.ignorable(),
new Ability(Abilities.WELL_BAKED_BODY, 9)
- .attr(TypeImmunityStatChangeAbAttr, Type.FIRE, BattleStat.DEF, 2)
+ .attr(TypeImmunityStatStageChangeAbAttr, Type.FIRE, Stat.DEF, 2)
.ignorable(),
new Ability(Abilities.WIND_RIDER, 9)
- .attr(MoveImmunityStatChangeAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.WIND_MOVE) && move.category !== MoveCategory.STATUS, BattleStat.ATK, 1)
- .attr(PostSummonStatChangeOnArenaAbAttr, ArenaTagType.TAILWIND)
+ .attr(MoveImmunityStatStageChangeAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.WIND_MOVE) && move.category !== MoveCategory.STATUS, Stat.ATK, 1)
+ .attr(PostSummonStatStageChangeOnArenaAbAttr, ArenaTagType.TAILWIND)
.ignorable(),
new Ability(Abilities.GUARD_DOG, 9)
- .attr(PostIntimidateStatChangeAbAttr, [BattleStat.ATK], 1, true)
+ .attr(PostIntimidateStatStageChangeAbAttr, [Stat.ATK], 1, true)
.attr(ForceSwitchOutImmunityAbAttr)
.ignorable(),
new Ability(Abilities.ROCKY_PAYLOAD, 9)
@@ -5719,31 +5713,31 @@ export function initAbilities() {
.ignorable()
.partial(),
new Ability(Abilities.VESSEL_OF_RUIN, 9)
- .attr(FieldMultiplyBattleStatAbAttr, Stat.SPATK, 0.75)
- .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonVesselOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.SPATK) }))
+ .attr(FieldMultiplyStatAbAttr, Stat.SPATK, 0.75)
+ .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonVesselOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPATK)) }))
.ignorable(),
new Ability(Abilities.SWORD_OF_RUIN, 9)
- .attr(FieldMultiplyBattleStatAbAttr, Stat.DEF, 0.75)
- .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.DEF) }))
+ .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
+ .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) }))
.ignorable(),
new Ability(Abilities.TABLETS_OF_RUIN, 9)
- .attr(FieldMultiplyBattleStatAbAttr, Stat.ATK, 0.75)
- .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.ATK) }))
+ .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
+ .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
.ignorable(),
new Ability(Abilities.BEADS_OF_RUIN, 9)
- .attr(FieldMultiplyBattleStatAbAttr, Stat.SPDEF, 0.75)
- .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.SPDEF) }))
+ .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
+ .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) }))
.ignorable(),
new Ability(Abilities.ORICHALCUM_PULSE, 9)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)
- .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.ATK, 4 / 3),
+ .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 4 / 3),
new Ability(Abilities.HADRON_ENGINE, 9)
.attr(PostSummonTerrainChangeAbAttr, TerrainType.ELECTRIC)
.attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.ELECTRIC)
- .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPATK, 4 / 3),
+ .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPATK, 4 / 3),
new Ability(Abilities.OPPORTUNIST, 9)
- .attr(StatChangeCopyAbAttr),
+ .attr(StatStageChangeCopyAbAttr),
new Ability(Abilities.CUD_CHEW, 9)
.unimplemented(),
new Ability(Abilities.SHARPNESS, 9)
@@ -5769,11 +5763,11 @@ export function initAbilities() {
.attr(MoveAbilityBypassAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS),
new Ability(Abilities.MINDS_EYE, 9)
.attr(IgnoreTypeImmunityAbAttr, Type.GHOST, [Type.NORMAL, Type.FIGHTING])
- .attr(ProtectStatAbAttr, BattleStat.ACC)
- .attr(IgnoreOpponentEvasionAbAttr)
+ .attr(ProtectStatAbAttr, Stat.ACC)
+ .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ])
.ignorable(),
new Ability(Abilities.SUPERSWEET_SYRUP, 9)
- .attr(PostSummonStatChangeAbAttr, BattleStat.EVA, -1)
+ .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1)
.condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)),
new Ability(Abilities.HOSPITALITY, 9)
.attr(PostSummonAllyHealAbAttr, 4, true)
@@ -5781,25 +5775,25 @@ export function initAbilities() {
new Ability(Abilities.TOXIC_CHAIN, 9)
.attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC),
new Ability(Abilities.EMBODY_ASPECT_TEAL, 9)
- .attr(PostBattleInitStatChangeAbAttr, BattleStat.SPD, 1, true)
+ .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.SPD ], 1, true)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.partial(),
new Ability(Abilities.EMBODY_ASPECT_WELLSPRING, 9)
- .attr(PostBattleInitStatChangeAbAttr, BattleStat.SPDEF, 1, true)
+ .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.SPDEF ], 1, true)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.partial(),
new Ability(Abilities.EMBODY_ASPECT_HEARTHFLAME, 9)
- .attr(PostBattleInitStatChangeAbAttr, BattleStat.ATK, 1, true)
+ .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.ATK ], 1, true)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.partial(),
new Ability(Abilities.EMBODY_ASPECT_CORNERSTONE, 9)
- .attr(PostBattleInitStatChangeAbAttr, BattleStat.DEF, 1, true)
+ .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.DEF ], 1, true)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts
index 09cc7a5b97c..fdc32b75c19 100644
--- a/src/data/arena-tag.ts
+++ b/src/data/arena-tag.ts
@@ -7,17 +7,17 @@ import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
import { StatusEffect } from "./status-effect";
import { BattlerIndex } from "../battle";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
-import { BattleStat } from "./battle-stat";
+import { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "./battle-anims";
import i18next from "i18next";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
-import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
-import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
-import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
-import { StatChangePhase } from "#app/phases/stat-change-phase.js";
+import { MoveEffectPhase } from "#app/phases/move-effect-phase";
+import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
+import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
export enum ArenaTagSide {
BOTH,
@@ -786,8 +786,8 @@ class StickyWebTag extends ArenaTrapTag {
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) {
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));
+ const stages = new Utils.NumberHolder(-1);
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value));
}
}
@@ -875,7 +875,7 @@ class TailwindTag extends ArenaTag {
// Raise attack by one stage if party member has WIND_RIDER ability
if (pokemon.hasAbility(Abilities.WIND_RIDER)) {
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex()));
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK], 1, true));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK ], 1, true));
}
}
}
diff --git a/src/data/battle-stat.ts b/src/data/battle-stat.ts
deleted file mode 100644
index a0cb7ca88e1..00000000000
--- a/src/data/battle-stat.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import i18next, { ParseKeys } from "i18next";
-
-export enum BattleStat {
- ATK,
- DEF,
- SPATK,
- SPDEF,
- SPD,
- ACC,
- EVA,
- RAND,
- HP
-}
-
-export function getBattleStatName(stat: BattleStat) {
- switch (stat) {
- case BattleStat.ATK:
- return i18next.t("pokemonInfo:Stat.ATK");
- case BattleStat.DEF:
- return i18next.t("pokemonInfo:Stat.DEF");
- case BattleStat.SPATK:
- return i18next.t("pokemonInfo:Stat.SPATK");
- case BattleStat.SPDEF:
- return i18next.t("pokemonInfo:Stat.SPDEF");
- case BattleStat.SPD:
- return i18next.t("pokemonInfo:Stat.SPD");
- case BattleStat.ACC:
- return i18next.t("pokemonInfo:Stat.ACC");
- case BattleStat.EVA:
- return i18next.t("pokemonInfo:Stat.EVA");
- case BattleStat.HP:
- return i18next.t("pokemonInfo:Stat.HPStat");
- default:
- return "???";
- }
-}
-
-export function getBattleStatLevelChangeDescription(pokemonNameWithAffix: string, stats: string, levels: integer, up: boolean, count: number = 1) {
- const stringKey = (() => {
- if (up) {
- switch (levels) {
- case 1:
- return "battle:statRose";
- case 2:
- return "battle:statSharplyRose";
- case 3:
- case 4:
- case 5:
- case 6:
- return "battle:statRoseDrastically";
- default:
- return "battle:statWontGoAnyHigher";
- }
- } else {
- switch (levels) {
- case 1:
- return "battle:statFell";
- case 2:
- return "battle:statHarshlyFell";
- case 3:
- case 4:
- case 5:
- case 6:
- return "battle:statSeverelyFell";
- default:
- return "battle:statWontGoAnyLower";
- }
- }
- })();
- return i18next.t(stringKey as ParseKeys, { pokemonNameWithAffix, stats, count });
-}
diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts
index 5eb20239cc4..e35ab3aadb1 100644
--- a/src/data/battler-tags.ts
+++ b/src/data/battler-tags.ts
@@ -1,7 +1,6 @@
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims";
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
-import { Stat, getStatName } from "./pokemon-stat";
import { StatusEffect } from "./status-effect";
import * as Utils from "../utils";
import { ChargeAttr, MoveFlags, allMoves } from "./move";
@@ -9,20 +8,20 @@ import { Type } from "./type";
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability";
import { TerrainType } from "./terrain";
import { WeatherType } from "./weather";
-import { BattleStat } from "./battle-stat";
import { allAbilities } from "./ability";
import { SpeciesFormChangeManualTrigger } from "./pokemon-forms";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import i18next from "#app/plugins/i18n.js";
-import { CommonAnimPhase } from "#app/phases/common-anim-phase.js";
-import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
-import { MovePhase } from "#app/phases/move-phase.js";
-import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
-import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
-import { StatChangePhase, StatChangeCallback } from "#app/phases/stat-change-phase.js";
+import i18next from "#app/plugins/i18n";
+import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat";
+import { CommonAnimPhase } from "#app/phases/common-anim-phase";
+import { MoveEffectPhase } from "#app/phases/move-effect-phase";
+import { MovePhase } from "#app/phases/move-phase";
+import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
+import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
+import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
export enum BattlerTagLapseType {
FAINT,
@@ -362,8 +361,8 @@ export class ConfusedTag extends BattlerTag {
// 1/3 chance of hitting self with a 40 base power move
if (pokemon.randSeedInt(3) === 0) {
- const atk = pokemon.getBattleStat(Stat.ATK);
- const def = pokemon.getBattleStat(Stat.DEF);
+ const atk = pokemon.getEffectiveStat(Stat.ATK);
+ const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100));
pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage);
@@ -767,7 +766,7 @@ export class OctolockTag extends TrappedTag {
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (shouldLapse) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [BattleStat.DEF, BattleStat.SPDEF], -1));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.DEF, Stat.SPDEF ], -1));
return true;
}
@@ -1093,7 +1092,7 @@ export class ContactDamageProtectedTag extends ProtectedTag {
}
}
-export class ContactStatChangeProtectedTag extends ProtectedTag {
+export class ContactStatStageChangeProtectedTag extends ProtectedTag {
private stat: BattleStat;
private levels: number;
@@ -1110,7 +1109,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag {
*/
loadTag(source: BattlerTag | any): void {
super.loadTag(source);
- this.stat = source.stat as BattleStat;
+ this.stat = source.stat;
this.levels = source.levels;
}
@@ -1121,7 +1120,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag {
const effectPhase = pokemon.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon();
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels));
}
}
@@ -1348,11 +1347,10 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
- const stats = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ];
- let highestStat: Stat;
- stats.map(s => pokemon.getBattleStat(s)).reduce((highestValue: number, value: number, i: number) => {
+ let highestStat: EffectiveStat;
+ EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => {
if (value > highestValue) {
- highestStat = stats[i];
+ highestStat = EFFECTIVE_STATS[i];
return value;
}
return highestValue;
@@ -1370,7 +1368,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
break;
}
- pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: getStatName(highestStat) }), null, false, null, true);
+ pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true);
}
onRemove(pokemon: Pokemon): void {
@@ -1714,25 +1712,25 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag {
*/
export class StockpilingTag extends BattlerTag {
public stockpiledCount: number = 0;
- public statChangeCounts: { [BattleStat.DEF]: number; [BattleStat.SPDEF]: number } = {
- [BattleStat.DEF]: 0,
- [BattleStat.SPDEF]: 0
+ public statChangeCounts: { [Stat.DEF]: number; [Stat.SPDEF]: number } = {
+ [Stat.DEF]: 0,
+ [Stat.SPDEF]: 0
};
constructor(sourceMove: Moves = Moves.NONE) {
super(BattlerTagType.STOCKPILING, BattlerTagLapseType.CUSTOM, 1, sourceMove);
}
- private onStatsChanged: StatChangeCallback = (_, statsChanged, statChanges) => {
- const defChange = statChanges[statsChanged.indexOf(BattleStat.DEF)] ?? 0;
- const spDefChange = statChanges[statsChanged.indexOf(BattleStat.SPDEF)] ?? 0;
+ private onStatStagesChanged: StatStageChangeCallback = (_, statsChanged, statChanges) => {
+ const defChange = statChanges[statsChanged.indexOf(Stat.DEF)] ?? 0;
+ const spDefChange = statChanges[statsChanged.indexOf(Stat.SPDEF)] ?? 0;
if (defChange) {
- this.statChangeCounts[BattleStat.DEF]++;
+ this.statChangeCounts[Stat.DEF]++;
}
if (spDefChange) {
- this.statChangeCounts[BattleStat.SPDEF]++;
+ this.statChangeCounts[Stat.SPDEF]++;
}
};
@@ -1740,8 +1738,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,
+ [ Stat.DEF ]: source.statChangeCounts?.[ Stat.DEF ] ?? 0,
+ [ Stat.SPDEF ]: source.statChangeCounts?.[ Stat.SPDEF ] ?? 0,
};
}
@@ -1761,9 +1759,9 @@ export class StockpilingTag extends BattlerTag {
}));
// Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes.
- pokemon.scene.unshiftPhase(new StatChangePhase(
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(
pokemon.scene, pokemon.getBattlerIndex(), true,
- [BattleStat.SPDEF, BattleStat.DEF], 1, true, false, true, this.onStatsChanged
+ [Stat.SPDEF, Stat.DEF], 1, true, false, true, this.onStatStagesChanged
));
}
}
@@ -1777,15 +1775,15 @@ export class StockpilingTag extends BattlerTag {
* one stage for each stack which had successfully changed that particular stat during onAdd.
*/
onRemove(pokemon: Pokemon): void {
- const defChange = this.statChangeCounts[BattleStat.DEF];
- const spDefChange = this.statChangeCounts[BattleStat.SPDEF];
+ const defChange = this.statChangeCounts[Stat.DEF];
+ const spDefChange = this.statChangeCounts[Stat.SPDEF];
if (defChange) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF], -defChange, true, false, true));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.DEF ], -defChange, true, false, true));
}
if (spDefChange) {
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.SPDEF], -spDefChange, true, false, true));
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF ], -spDefChange, true, false, true));
}
}
}
@@ -1927,11 +1925,11 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.SPIKY_SHIELD:
return new ContactDamageProtectedTag(sourceMove, 8);
case BattlerTagType.KINGS_SHIELD:
- return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.ATK, -1);
+ return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.ATK, -1);
case BattlerTagType.OBSTRUCT:
- return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.DEF, -2);
+ return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.DEF, -2);
case BattlerTagType.SILK_TRAP:
- return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.SPD, -1);
+ return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.SPD, -1);
case BattlerTagType.BANEFUL_BUNKER:
return new ContactPoisonProtectedTag(sourceMove);
case BattlerTagType.BURNING_BULWARK:
diff --git a/src/data/berry.ts b/src/data/berry.ts
index d0c9c311e16..01325ee39dd 100644
--- a/src/data/berry.ts
+++ b/src/data/berry.ts
@@ -1,14 +1,14 @@
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult } from "../field/pokemon";
-import { BattleStat } from "./battle-stat";
import { getStatusEffectHealText } from "./status-effect";
import * as Utils from "../utils";
import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability";
import i18next from "i18next";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
-import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
-import { StatChangePhase } from "#app/phases/stat-change-phase.js";
+import { Stat, type BattleStat } from "#app/enums/stat";
+import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
export function getBerryName(berryType: BerryType): string {
return i18next.t(`berry:${BerryType[berryType]}.name`);
@@ -35,9 +35,10 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
case BerryType.SALAC:
return (pokemon: Pokemon) => {
const threshold = new Utils.NumberHolder(0.25);
- const battleStat = (berryType - BerryType.LIECHI) as BattleStat;
+ // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
+ const stat: BattleStat = berryType - BerryType.ENIGMA;
applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold);
- return pokemon.getHpRatio() < threshold.value && pokemon.summonData.battleStats[battleStat] < 6;
+ return pokemon.getHpRatio() < threshold.value && pokemon.getStatStage(stat) < 6;
};
case BerryType.LANSAT:
return (pokemon: Pokemon) => {
@@ -95,10 +96,11 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
- const battleStat = (berryType - BerryType.LIECHI) as BattleStat;
- const statLevels = new Utils.NumberHolder(1);
- applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels);
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ battleStat ], statLevels.value));
+ // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
+ const stat: BattleStat = berryType - BerryType.ENIGMA;
+ const statStages = new Utils.NumberHolder(1);
+ applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages);
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value));
};
case BerryType.LANSAT:
return (pokemon: Pokemon) => {
@@ -112,9 +114,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType);
}
- const statLevels = new Utils.NumberHolder(2);
- applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels);
- pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.RAND ], statLevels.value));
+ const randStat = Utils.randSeedInt(Stat.SPD, Stat.ATK);
+ const stages = new Utils.NumberHolder(2);
+ applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages);
+ pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value));
};
case BerryType.LEPPA:
return (pokemon: Pokemon) => {
diff --git a/src/data/challenge.ts b/src/data/challenge.ts
index 36f696e63c1..62751b92f9c 100644
--- a/src/data/challenge.ts
+++ b/src/data/challenge.ts
@@ -13,7 +13,6 @@ import { TrainerType } from "#enums/trainer-type";
import { Nature } from "./nature";
import { Moves } from "#app/enums/moves.js";
import { TypeColor, TypeShadow } from "#app/enums/color.js";
-import { Gender } from "./gender";
import { pokemonEvolutions } from "./pokemon-evolutions";
import { pokemonFormChanges } from "./pokemon-forms";
@@ -659,7 +658,6 @@ export class FreshStartChallenge extends Challenge {
pokemon.luck = 0; // No luck
pokemon.shiny = false; // Not shiny
pokemon.variant = 0; // Not shiny
- pokemon.gender = Gender.MALE; // Starters default to male
pokemon.formIndex = 0; // Froakie should be base form
pokemon.ivs = [10, 10, 10, 10, 10, 10]; // Default IVs of 10 for all stats
return true;
diff --git a/src/data/egg.ts b/src/data/egg.ts
index 3e872d364f3..9beb944de69 100644
--- a/src/data/egg.ts
+++ b/src/data/egg.ts
@@ -139,46 +139,57 @@ export class Egg {
////
constructor(eggOptions?: IEggOptions) {
- //if (eggOptions.tier && eggOptions.species) throw Error("Error egg can't have species and tier as option. only choose one of them.")
+ const generateEggProperties = (eggOptions?: IEggOptions) => {
+ //if (eggOptions.tier && eggOptions.species) throw Error("Error egg can't have species and tier as option. only choose one of them.")
- this._sourceType = eggOptions?.sourceType!; // TODO: is this bang correct?
- // Ensure _sourceType is defined before invoking rollEggTier(), as it is referenced
- this._tier = eggOptions?.tier ?? (Overrides.EGG_TIER_OVERRIDE ?? this.rollEggTier());
- // If egg was pulled, check if egg pity needs to override the egg tier
- if (eggOptions?.pulled) {
- // Needs this._tier and this._sourceType to work
- this.checkForPityTierOverrides(eggOptions.scene!); // TODO: is this bang correct?
- }
+ this._sourceType = eggOptions?.sourceType!; // TODO: is this bang correct?
+ // Ensure _sourceType is defined before invoking rollEggTier(), as it is referenced
+ this._tier = eggOptions?.tier ?? (Overrides.EGG_TIER_OVERRIDE ?? this.rollEggTier());
+ // If egg was pulled, check if egg pity needs to override the egg tier
+ if (eggOptions?.pulled) {
+ // Needs this._tier and this._sourceType to work
+ this.checkForPityTierOverrides(eggOptions.scene!); // TODO: is this bang correct?
+ }
- this._id = eggOptions?.id ?? Utils.randInt(EGG_SEED, EGG_SEED * this._tier);
+ this._id = eggOptions?.id ?? Utils.randInt(EGG_SEED, EGG_SEED * this._tier);
- this._sourceType = eggOptions?.sourceType ?? undefined;
- this._hatchWaves = eggOptions?.hatchWaves ?? this.getEggTierDefaultHatchWaves();
- this._timestamp = eggOptions?.timestamp ?? new Date().getTime();
+ this._sourceType = eggOptions?.sourceType ?? undefined;
+ this._hatchWaves = eggOptions?.hatchWaves ?? this.getEggTierDefaultHatchWaves();
+ this._timestamp = eggOptions?.timestamp ?? new Date().getTime();
- // First roll shiny and variant so we can filter if species with an variant exist
- this._isShiny = eggOptions?.isShiny ?? (Overrides.EGG_SHINY_OVERRIDE || this.rollShiny());
- this._variantTier = eggOptions?.variantTier ?? (Overrides.EGG_VARIANT_OVERRIDE ?? this.rollVariant());
- this._species = eggOptions?.species ?? this.rollSpecies(eggOptions!.scene!)!; // TODO: Are those bangs correct?
+ // First roll shiny and variant so we can filter if species with an variant exist
+ this._isShiny = eggOptions?.isShiny ?? (Overrides.EGG_SHINY_OVERRIDE || this.rollShiny());
+ this._variantTier = eggOptions?.variantTier ?? (Overrides.EGG_VARIANT_OVERRIDE ?? this.rollVariant());
+ this._species = eggOptions?.species ?? this.rollSpecies(eggOptions!.scene!)!; // TODO: Are those bangs correct?
- this._overrideHiddenAbility = eggOptions?.overrideHiddenAbility ?? false;
+ this._overrideHiddenAbility = eggOptions?.overrideHiddenAbility ?? false;
- // Override egg tier and hatchwaves if species was given
- if (eggOptions?.species) {
- this._tier = this.getEggTierFromSpeciesStarterValue();
- this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
- }
- // If species has no variant, set variantTier to common. This needs to
- // be done because species with no variants get filtered at rollSpecies but if the
- // species is set via options or the legendary gacha pokemon gets choosen the check never happens
- if (this._species && !getPokemonSpecies(this._species).hasVariants()) {
- this._variantTier = VariantTier.COMMON;
- }
- // Needs this._tier so it needs to be generated afer the tier override if bought from same species
- this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex();
- if (eggOptions?.pulled) {
- this.increasePullStatistic(eggOptions.scene!); // TODO: is this bang correct?
- this.addEggToGameData(eggOptions.scene!); // TODO: is this bang correct?
+ // Override egg tier and hatchwaves if species was given
+ if (eggOptions?.species) {
+ this._tier = this.getEggTierFromSpeciesStarterValue();
+ this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
+ }
+ // If species has no variant, set variantTier to common. This needs to
+ // be done because species with no variants get filtered at rollSpecies but if the
+ // species is set via options or the legendary gacha pokemon gets choosen the check never happens
+ if (this._species && !getPokemonSpecies(this._species).hasVariants()) {
+ this._variantTier = VariantTier.COMMON;
+ }
+ // Needs this._tier so it needs to be generated afer the tier override if bought from same species
+ this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex();
+ if (eggOptions?.pulled) {
+ this.increasePullStatistic(eggOptions.scene!); // TODO: is this bang correct?
+ this.addEggToGameData(eggOptions.scene!); // TODO: is this bang correct?
+ }
+ };
+
+ if (eggOptions?.scene) {
+ const seedOverride = Utils.randomString(24);
+ eggOptions?.scene.executeWithSeedOffset(() => {
+ generateEggProperties(eggOptions);
+ }, 0, seedOverride);
+ } else { // For legacy eggs without scene
+ generateEggProperties(eggOptions);
}
}
@@ -200,37 +211,46 @@ export class Egg {
// Generates a PlayerPokemon from an egg
public generatePlayerPokemon(scene: BattleScene): PlayerPokemon {
- // Legacy egg wants to hatch. Generate missing properties
- if (!this._species) {
- this._isShiny = this.rollShiny();
- this._species = this.rollSpecies(scene!)!; // TODO: are these bangs correct?
- }
+ let ret: PlayerPokemon;
- let pokemonSpecies = getPokemonSpecies(this._species);
- // Special condition to have Phione eggs also have a chance of generating Manaphy
- if (this._species === Species.PHIONE) {
- pokemonSpecies = getPokemonSpecies(Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE) ? Species.PHIONE : Species.MANAPHY);
- }
+ const generatePlayerPokemonHelper = (scene: BattleScene) => {
+ // Legacy egg wants to hatch. Generate missing properties
+ if (!this._species) {
+ this._isShiny = this.rollShiny();
+ this._species = this.rollSpecies(scene!)!; // TODO: are these bangs correct?
+ }
- // Sets the hidden ability if a hidden ability exists and
- // the override is set or the egg hits the chance
- let abilityIndex: number | undefined = undefined;
- const sameSpeciesEggHACheck = (this._sourceType === EggSourceType.SAME_SPECIES_EGG && !Utils.randSeedInt(SAME_SPECIES_EGG_HA_RATE));
- const gachaEggHACheck = (!(this._sourceType === EggSourceType.SAME_SPECIES_EGG) && !Utils.randSeedInt(GACHA_EGG_HA_RATE));
- if (pokemonSpecies.abilityHidden && (this._overrideHiddenAbility || sameSpeciesEggHACheck || gachaEggHACheck)) {
- abilityIndex = 2;
- }
+ let pokemonSpecies = getPokemonSpecies(this._species);
+ // Special condition to have Phione eggs also have a chance of generating Manaphy
+ if (this._species === Species.PHIONE) {
+ pokemonSpecies = getPokemonSpecies(Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE) ? Species.PHIONE : Species.MANAPHY);
+ }
- // This function has way to many optional parameters
- const ret: PlayerPokemon = scene.addPlayerPokemon(pokemonSpecies, 1, abilityIndex, undefined, undefined, false);
- ret.shiny = this._isShiny;
- ret.variant = this._variantTier;
+ // Sets the hidden ability if a hidden ability exists and
+ // the override is set or the egg hits the chance
+ let abilityIndex: number | undefined = undefined;
+ const sameSpeciesEggHACheck = (this._sourceType === EggSourceType.SAME_SPECIES_EGG && !Utils.randSeedInt(SAME_SPECIES_EGG_HA_RATE));
+ const gachaEggHACheck = (!(this._sourceType === EggSourceType.SAME_SPECIES_EGG) && !Utils.randSeedInt(GACHA_EGG_HA_RATE));
+ if (pokemonSpecies.abilityHidden && (this._overrideHiddenAbility || sameSpeciesEggHACheck || gachaEggHACheck)) {
+ abilityIndex = 2;
+ }
- const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
+ // This function has way to many optional parameters
+ ret = scene.addPlayerPokemon(pokemonSpecies, 1, abilityIndex, undefined, undefined, false);
+ ret.shiny = this._isShiny;
+ ret.variant = this._variantTier;
- for (let s = 0; s < ret.ivs.length; s++) {
- ret.ivs[s] = Math.max(ret.ivs[s], secondaryIvs[s]);
- }
+ const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
+
+ for (let s = 0; s < ret.ivs.length; s++) {
+ ret.ivs[s] = Math.max(ret.ivs[s], secondaryIvs[s]);
+ }
+ };
+
+ ret = ret!; // Tell TS compiler it's defined now
+ scene.executeWithSeedOffset(() => {
+ generatePlayerPokemonHelper(scene);
+ }, this._id, EGG_SEED.toString());
return ret;
}
diff --git a/src/data/move.ts b/src/data/move.ts
index cc431991840..8e0390f5e16 100644
--- a/src/data/move.ts
+++ b/src/data/move.ts
@@ -1,5 +1,4 @@
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
-import { BattleStat, getBattleStatName } from "./battle-stat";
import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags";
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
@@ -13,7 +12,6 @@ import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilit
import { allAbilities } from "./ability";
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle";
-import { Stat } from "./pokemon-stat";
import { TerrainType } from "./terrain";
import { ModifierPoolType } from "#app/modifier/modifier-type";
import { Command } from "../ui/command-ui-handler";
@@ -27,13 +25,14 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { MoveUsedEvent } from "#app/events/battle-scene";
+import { Stat, type BattleStat, type EffectiveStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat";
import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { MovePhase } from "#app/phases/move-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
-import { StatChangePhase } from "#app/phases/stat-change-phase";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms";
@@ -819,10 +818,10 @@ export class AttackMove extends Move {
attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2;
if (attackScore) {
if (this.category === MoveCategory.PHYSICAL) {
- const atk = new Utils.IntegerHolder(user.getBattleStat(Stat.ATK, target));
+ const atk = new Utils.IntegerHolder(user.getEffectiveStat(Stat.ATK, target));
applyMoveAttrs(VariableAtkAttr, user, target, move, atk);
- if (atk.value > user.getBattleStat(Stat.SPATK, target)) {
- const statRatio = user.getBattleStat(Stat.SPATK, target) / atk.value;
+ if (atk.value > user.getEffectiveStat(Stat.SPATK, target)) {
+ const statRatio = user.getEffectiveStat(Stat.SPATK, target) / atk.value;
if (statRatio <= 0.75) {
attackScore *= 2;
} else if (statRatio <= 0.875) {
@@ -830,10 +829,10 @@ export class AttackMove extends Move {
}
}
} else {
- const spAtk = new Utils.IntegerHolder(user.getBattleStat(Stat.SPATK, target));
+ const spAtk = new Utils.IntegerHolder(user.getEffectiveStat(Stat.SPATK, target));
applyMoveAttrs(VariableAtkAttr, user, target, move, spAtk);
- if (spAtk.value > user.getBattleStat(Stat.ATK, target)) {
- const statRatio = user.getBattleStat(Stat.ATK, target) / spAtk.value;
+ if (spAtk.value > user.getEffectiveStat(Stat.ATK, target)) {
+ const statRatio = user.getEffectiveStat(Stat.ATK, target) / spAtk.value;
if (statRatio <= 0.75) {
attackScore *= 2;
} else if (statRatio <= 0.875) {
@@ -1100,9 +1099,9 @@ export class PreMoveMessageAttr extends MoveAttr {
*/
export class RespectAttackTypeImmunityAttr extends MoveAttr { }
-export class IgnoreOpponentStatChangesAttr extends MoveAttr {
+export class IgnoreOpponentStatStagesAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- (args[0] as Utils.IntegerHolder).value = 0;
+ (args[0] as Utils.BooleanHolder).value = true;
return true;
}
@@ -1730,10 +1729,9 @@ export class HealOnAllyAttr extends HealAttr {
*/
export class HitHealAttr extends MoveEffectAttr {
private healRatio: number;
- private message: string;
- private healStat: Stat | null;
+ private healStat: EffectiveStat | null;
- constructor(healRatio?: number | null, healStat?: Stat) {
+ constructor(healRatio?: number | null, healStat?: EffectiveStat) {
super(true, MoveEffectTrigger.HIT);
this.healRatio = healRatio ?? 0.5;
@@ -1755,7 +1753,7 @@ export class HitHealAttr extends MoveEffectAttr {
const reverseDrain = target.hasAbilityWithAttr(ReverseDrainAbAttr, false);
if (this.healStat !== null) {
// Strength Sap formula
- healAmount = target.getBattleStat(this.healStat);
+ healAmount = target.getEffectiveStat(this.healStat);
message = i18next.t("battle:drainMessage", {pokemonName: getPokemonNameWithAffix(target)});
} else {
// Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc.
@@ -1785,7 +1783,7 @@ export class HitHealAttr extends MoveEffectAttr {
*/
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
if (this.healStat) {
- const healAmount = target.getBattleStat(this.healStat);
+ const healAmount = target.getEffectiveStat(this.healStat);
return Math.floor(Math.max(0, (Math.min(1, (healAmount+user.hp)/user.getMaxHp() - 0.33))) / user.getHpRatio());
}
return Math.floor(Math.max((1 - user.getHpRatio()) - 0.33, 0) * (move.power / 4));
@@ -2516,14 +2514,14 @@ export class ElectroShotChargeAttr extends ChargeAttr {
const weatherType = user.scene.arena.weather?.weatherType;
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.RAIN || weatherType === WeatherType.HEAVY_RAIN)) {
// Apply the SPATK increase every call when used in the rain
- const statChangeAttr = new StatChangeAttr(BattleStat.SPATK, 1, true);
+ const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
statChangeAttr.apply(user, target, move, args);
// After the SPATK is raised, execute the move resolution e.g. deal damage
resolve(false);
} else {
if (!this.statIncreaseApplied) {
// Apply the SPATK increase only if it hasn't been applied before e.g. on the first turn charge up animation
- const statChangeAttr = new StatChangeAttr(BattleStat.SPATK, 1, true);
+ const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
statChangeAttr.apply(user, target, move, args);
// Set the flag to true so that on the following turn it doesn't raise SPATK a second time
this.statIncreaseApplied = true;
@@ -2571,18 +2569,16 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
}
}
-export class StatChangeAttr extends MoveEffectAttr {
+export class StatStageChangeAttr extends MoveEffectAttr {
public stats: BattleStat[];
- public levels: integer;
+ public stages: integer;
private condition: MoveConditionFunc | null;
private showMessage: boolean;
- constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false) {
+ constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false) {
super(selfTarget, moveEffectTrigger, firstHitOnly, false, firstTargetOnly);
- this.stats = typeof(stats) === "number"
- ? [ stats as BattleStat ]
- : stats as BattleStat[];
- this.levels = levels;
+ this.stats = stats;
+ this.stages = stages;
this.condition = condition!; // TODO: is this bang correct?
this.showMessage = showMessage;
}
@@ -2594,8 +2590,8 @@ export class StatChangeAttr extends MoveEffectAttr {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
- const levels = this.getLevels(user);
- user.scene.unshiftPhase(new StatChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, levels, this.showMessage));
+ const stages = this.getLevels(user);
+ user.scene.unshiftPhase(new StatStageChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, stages, this.showMessage));
return true;
}
@@ -2603,7 +2599,7 @@ export class StatChangeAttr extends MoveEffectAttr {
}
getLevels(_user: Pokemon): integer {
- return this.levels;
+ return this.stages;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
@@ -2611,29 +2607,30 @@ export class StatChangeAttr extends MoveEffectAttr {
const moveLevels = this.getLevels(user);
for (const stat of this.stats) {
let levels = moveLevels;
+ const statStage = target.getStatStage(stat);
if (levels > 0) {
- levels = Math.min(target.summonData.battleStats[stat] + levels, 6) - target.summonData.battleStats[stat];
+ levels = Math.min(statStage + levels, 6) - statStage;
} else {
- levels = Math.max(target.summonData.battleStats[stat] + levels, -6) - target.summonData.battleStats[stat];
+ levels = Math.max(statStage + levels, -6) - statStage;
}
let noEffect = false;
switch (stat) {
- case BattleStat.ATK:
+ case Stat.ATK:
if (this.selfTarget) {
noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.PHYSICAL);
}
break;
- case BattleStat.DEF:
+ case Stat.DEF:
if (!this.selfTarget) {
noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.PHYSICAL);
}
break;
- case BattleStat.SPATK:
+ case Stat.SPATK:
if (this.selfTarget) {
noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.SPECIAL);
}
break;
- case BattleStat.SPDEF:
+ case Stat.SPDEF:
if (!this.selfTarget) {
noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.SPECIAL);
}
@@ -2648,18 +2645,16 @@ export class StatChangeAttr extends MoveEffectAttr {
}
}
-export class PostVictoryStatChangeAttr extends MoveAttr {
+export class PostVictoryStatStageChangeAttr extends MoveAttr {
private stats: BattleStat[];
- private levels: integer;
+ private stages: number;
private condition: MoveConditionFunc | null;
private showMessage: boolean;
- constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false) {
+ constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false) {
super();
- this.stats = typeof(stats) === "number"
- ? [ stats as BattleStat ]
- : stats as BattleStat[];
- this.levels = levels;
+ this.stats = stats;
+ this.stages = stages;
this.condition = condition!; // TODO: is this bang correct?
this.showMessage = showMessage;
}
@@ -2667,49 +2662,48 @@ export class PostVictoryStatChangeAttr extends MoveAttr {
if (this.condition && !this.condition(user, target, move)) {
return;
}
- const statChangeAttr = new StatChangeAttr(this.stats, this.levels, this.showMessage);
+ const statChangeAttr = new StatStageChangeAttr(this.stats, this.stages, this.showMessage);
statChangeAttr.apply(user, target, move);
}
}
-export class AcupressureStatChangeAttr extends MoveEffectAttr {
+export class AcupressureStatStageChangeAttr extends MoveEffectAttr {
constructor() {
super();
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise {
- let randStats = [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD, BattleStat.ACC, BattleStat.EVA ];
- randStats = randStats.filter(s => target.summonData.battleStats[s] < 6);
+ const randStats = BATTLE_STATS.filter(s => target.getStatStage(s) < 6);
if (randStats.length > 0) {
const boostStat = [randStats[Utils.randInt(randStats.length)]];
- user.scene.unshiftPhase(new StatChangePhase(user.scene, target.getBattlerIndex(), this.selfTarget, boostStat, 2));
+ user.scene.unshiftPhase(new StatStageChangePhase(user.scene, target.getBattlerIndex(), this.selfTarget, boostStat, 2));
return true;
}
return false;
}
}
-export class GrowthStatChangeAttr extends StatChangeAttr {
+export class GrowthStatStageChangeAttr extends StatStageChangeAttr {
constructor() {
- super([ BattleStat.ATK, BattleStat.SPATK ], 1, true);
+ super([ Stat.ATK, Stat.SPATK ], 1, true);
}
getLevels(user: Pokemon): number {
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene)) {
const weatherType = user.scene.arena.weather?.weatherType;
if (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN) {
- return this.levels + 1;
+ return this.stages + 1;
}
}
- return this.levels;
+ return this.stages;
}
}
-export class CutHpStatBoostAttr extends StatChangeAttr {
+export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
private cutRatio: integer;
private messageCallback: ((user: Pokemon) => void) | undefined;
- constructor(stat: BattleStat | BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) {
+ constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) {
super(stat, levels, true, null, true);
this.cutRatio = cutRatio;
@@ -2730,7 +2724,7 @@ export class CutHpStatBoostAttr extends StatChangeAttr {
}
getCondition(): MoveConditionFunc {
- return (user, target, move) => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.summonData.battleStats[s] < 6);
+ return (user, _target, _move) => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.getStatStage(s) < 6);
}
}
@@ -2740,9 +2734,11 @@ export class CopyStatsAttr extends MoveEffectAttr {
return false;
}
- for (let s = 0; s < target.summonData.battleStats.length; s++) {
- user.summonData.battleStats[s] = target.summonData.battleStats[s];
+ // Copy all stat stages
+ for (const s of BATTLE_STATS) {
+ user.setStatStage(s, target.getStatStage(s));
}
+
if (target.getTag(BattlerTagType.CRIT_BOOST)) {
user.addTag(BattlerTagType.CRIT_BOOST, 0, move.id);
} else {
@@ -2762,9 +2758,10 @@ export class InvertStatsAttr extends MoveEffectAttr {
return false;
}
- for (let s = 0; s < target.summonData.battleStats.length; s++) {
- target.summonData.battleStats[s] *= -1;
+ for (const s of BATTLE_STATS) {
+ target.setStatStage(s, -target.getStatStage(s));
}
+
target.updateInfo();
user.updateInfo();
@@ -2798,39 +2795,61 @@ export class ResetStatsAttr extends MoveEffectAttr {
}
resetStats(pokemon: Pokemon) {
- for (let s = 0; s < pokemon.summonData.battleStats.length; s++) {
- pokemon.summonData.battleStats[s] = 0;
+ for (const s of BATTLE_STATS) {
+ pokemon.setStatStage(s, 0);
}
pokemon.updateInfo();
}
}
/**
- * Attribute used for moves which swap the user and the target's stat changes.
+ * Attribute used for status moves, specifically Heart, Guard, and Power Swap,
+ * that swaps the user's and target's corresponding stat stages.
+ * @extends MoveEffectAttr
+ * @see {@linkcode apply}
*/
-export class SwapStatsAttr extends MoveEffectAttr {
+export class SwapStatStagesAttr extends MoveEffectAttr {
+ /** The stat stages to be swapped between the user and the target */
+ private stats: readonly BattleStat[];
+
+ constructor(stats: readonly BattleStat[]) {
+ super();
+
+ this.stats = stats;
+ }
+
/**
- * Swaps the user and the target's stat changes.
- * @param user Pokemon that used the move
- * @param target The target of the move
- * @param move Move with this attribute
+ * For all {@linkcode stats}, swaps the user's and target's corresponding stat
+ * stage.
+ * @param user the {@linkcode Pokemon} that used the move
+ * @param target the {@linkcode Pokemon} that the move was used on
+ * @param move N/A
* @param args N/A
- * @returns true if the function succeeds
+ * @returns true if attribute application succeeds
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any []): boolean {
- if (!super.apply(user, target, move, args)) {
- return false;
- } //Exits if the move can't apply
- let priorBoost : integer; //For storing a stat boost
- for (let s = 0; s < target.summonData.battleStats.length; s++) {
- priorBoost = user.summonData.battleStats[s]; //Store user stat boost
- user.summonData.battleStats[s] = target.summonData.battleStats[s]; //Applies target boost to self
- target.summonData.battleStats[s] = priorBoost; //Applies stored boost to target
+ if (super.apply(user, target, move, args)) {
+ for (const s of BATTLE_STATS) {
+ const temp = user.getStatStage(s);
+ user.setStatStage(s, target.getStatStage(s));
+ target.setStatStage(s, temp);
+ }
+
+ target.updateInfo();
+ user.updateInfo();
+
+ if (this.stats.length === 7) {
+ user.scene.queueMessage(i18next.t("moveTriggers:switchedStatChanges", { pokemonName: getPokemonNameWithAffix(user) }));
+ } else if (this.stats.length === 2) {
+ user.scene.queueMessage(i18next.t("moveTriggers:switchedTwoStatChanges", {
+ pokemonName: getPokemonNameWithAffix(user),
+ firstStat: i18next.t(getStatKey(this.stats[0])),
+ secondStat: i18next.t(getStatKey(this.stats[1]))
+ }));
+ }
+ return true;
}
- target.updateInfo();
- user.updateInfo();
- target.scene.queueMessage(i18next.t("moveTriggers:switchedStatChanges", {pokemonName: getPokemonNameWithAffix(user)}));
- return true;
+ return false;
}
}
@@ -3075,7 +3094,7 @@ export class WeightPowerAttr extends VariablePowerAttr {
**/
export class ElectroBallPowerAttr extends VariablePowerAttr {
/**
- * Move that deals more damage the faster {@linkcode BattleStat.SPD}
+ * Move that deals more damage the faster {@linkcode Stat.SPD}
* the user is compared to the target.
* @param user Pokemon that used the move
* @param target The target of the move
@@ -3086,7 +3105,7 @@ export class ElectroBallPowerAttr extends VariablePowerAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const power = args[0] as Utils.NumberHolder;
- const statRatio = target.getBattleStat(Stat.SPD) / user.getBattleStat(Stat.SPD);
+ const statRatio = target.getEffectiveStat(Stat.SPD) / user.getEffectiveStat(Stat.SPD);
const statThresholds = [ 0.25, 1 / 3, 0.5, 1, -1 ];
const statThresholdPowers = [ 150, 120, 80, 60, 40 ];
@@ -3110,7 +3129,7 @@ export class ElectroBallPowerAttr extends VariablePowerAttr {
**/
export class GyroBallPowerAttr extends VariablePowerAttr {
/**
- * Move that deals more damage the slower {@linkcode BattleStat.SPD}
+ * Move that deals more damage the slower {@linkcode Stat.SPD}
* the user is compared to the target.
* @param user Pokemon that used the move
* @param target The target of the move
@@ -3120,14 +3139,14 @@ export class GyroBallPowerAttr extends VariablePowerAttr {
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const power = args[0] as Utils.NumberHolder;
- const userSpeed = user.getBattleStat(Stat.SPD);
+ const userSpeed = user.getEffectiveStat(Stat.SPD);
if (userSpeed < 1) {
// Gen 6+ always have 1 base power
power.value = 1;
return true;
}
- power.value = Math.floor(Math.min(150, 25 * target.getBattleStat(Stat.SPD) / userSpeed + 1));
+ power.value = Math.floor(Math.min(150, 25 * target.getEffectiveStat(Stat.SPD) / userSpeed + 1));
return true;
}
}
@@ -3347,18 +3366,18 @@ export class HitCountPowerAttr extends VariablePowerAttr {
}
/**
- * Turning a once was (StatChangeCountPowerAttr) statement and making it available to call for any attribute.
- * @param {Pokemon} pokemon The pokemon that is being used to calculate the count of positive stats
- * @returns {number} Returns the amount of positive stats
+ * Tallies the number of positive stages for a given {@linkcode Pokemon}.
+ * @param pokemon The {@linkcode Pokemon} that is being used to calculate the count of positive stats
+ * @returns the amount of positive stats
*/
-const countPositiveStats = (pokemon: Pokemon): number => {
- return pokemon.summonData.battleStats.reduce((total, stat) => (stat && stat > 0) ? total + stat : total, 0);
+const countPositiveStatStages = (pokemon: Pokemon): number => {
+ return pokemon.getStatStages().reduce((total, stat) => (stat && stat > 0) ? total + stat : total, 0);
};
/**
- * Attribute that increases power based on the amount of positive stat increases.
+ * Attribute that increases power based on the amount of positive stat stage increases.
*/
-export class StatChangeCountPowerAttr extends VariablePowerAttr {
+export class PositiveStatStagePowerAttr extends VariablePowerAttr {
/**
* @param {Pokemon} user The pokemon that is being used to calculate the amount of positive stats
@@ -3368,9 +3387,9 @@ export class StatChangeCountPowerAttr extends VariablePowerAttr {
* @returns {boolean} Returns true if attribute is applied
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- const positiveStats: number = countPositiveStats(user);
+ const positiveStatStages: number = countPositiveStatStages(user);
- (args[0] as Utils.NumberHolder).value += positiveStats * 20;
+ (args[0] as Utils.NumberHolder).value += positiveStatStages * 20;
return true;
}
}
@@ -3392,10 +3411,10 @@ export class PunishmentPowerAttr extends VariablePowerAttr {
* @returns Returns true if attribute is applied
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- const positiveStats: number = countPositiveStats(target);
+ const positiveStatStages: number = countPositiveStatStages(target);
(args[0] as Utils.NumberHolder).value = Math.min(
this.PUNISHMENT_MAX_BASE_POWER,
- this.PUNISHMENT_MIN_BASE_POWER + positiveStats * 20
+ this.PUNISHMENT_MIN_BASE_POWER + positiveStatStages * 20
);
return true;
}
@@ -3615,7 +3634,7 @@ export class TargetAtkUserAtkAttr extends VariableAtkAttr {
super();
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- (args[0] as Utils.IntegerHolder).value = target.getBattleStat(Stat.ATK, target);
+ (args[0] as Utils.IntegerHolder).value = target.getEffectiveStat(Stat.ATK, target);
return true;
}
}
@@ -3626,7 +3645,7 @@ export class DefAtkAttr extends VariableAtkAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- (args[0] as Utils.IntegerHolder).value = user.getBattleStat(Stat.DEF, target);
+ (args[0] as Utils.IntegerHolder).value = user.getEffectiveStat(Stat.DEF, target);
return true;
}
}
@@ -3648,7 +3667,7 @@ export class DefDefAttr extends VariableDefAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- (args[0] as Utils.IntegerHolder).value = target.getBattleStat(Stat.DEF, user);
+ (args[0] as Utils.IntegerHolder).value = target.getEffectiveStat(Stat.DEF, user);
return true;
}
}
@@ -3770,7 +3789,7 @@ export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder);
- if (user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) {
+ if (user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) {
category.value = MoveCategory.PHYSICAL;
return true;
}
@@ -3783,7 +3802,7 @@ export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder);
- if (user.isTerastallized() && user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) {
+ if (user.isTerastallized() && user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) {
category.value = MoveCategory.PHYSICAL;
return true;
}
@@ -3847,8 +3866,8 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr {
export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.IntegerHolder);
- const atkRatio = user.getBattleStat(Stat.ATK, target, move) / target.getBattleStat(Stat.DEF, user, move);
- const specialRatio = user.getBattleStat(Stat.SPATK, target, move) / target.getBattleStat(Stat.SPDEF, user, move);
+ const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move);
+ const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move);
// Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games
if (atkRatio > specialRatio) {
@@ -4605,8 +4624,8 @@ export class CurseAttr extends MoveEffectAttr {
target.addTag(BattlerTagType.CURSED, 0, move.id, user.id);
return true;
} else {
- user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF], 1));
- user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), true, [BattleStat.SPD], -1));
+ user.scene.unshiftPhase(new StatStageChangePhase(user.scene, user.getBattlerIndex(), true, [ Stat.ATK, Stat.DEF], 1));
+ user.scene.unshiftPhase(new StatStageChangePhase(user.scene, user.getBattlerIndex(), true, [ Stat.SPD ], -1));
return true;
}
}
@@ -5163,8 +5182,8 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
}
let ret = this.user ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move);
if (this.user && this.batonPass) {
- const battleStatTotal = user.summonData.battleStats.reduce((bs: integer, total: integer) => total += bs, 0);
- ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(battleStatTotal), 10) / 10) * (battleStatTotal >= 0 ? 10 : -10));
+ const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0);
+ ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10));
}
return ret;
}
@@ -5989,8 +6008,17 @@ export class TransformAttr extends MoveEffectAttr {
user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender();
user.summonData.fusionGender = target.getFusionGender();
- user.summonData.stats = [ user.stats[Stat.HP] ].concat(target.stats.slice(1));
- user.summonData.battleStats = target.summonData.battleStats.slice(0);
+
+ // Copy all stats (except HP)
+ for (const s of EFFECTIVE_STATS) {
+ user.setStat(s, target.getStat(s, false), false);
+ }
+
+ // Copy all stat stages
+ for (const s of BATTLE_STATS) {
+ user.setStatStage(s, target.getStatStage(s));
+ }
+
user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId!, m?.ppUsed, m?.ppUp)); // TODO: is this bang correct?
user.summonData.types = target.getTypes();
@@ -5998,12 +6026,102 @@ export class TransformAttr extends MoveEffectAttr {
user.loadAssets(false).then(() => {
user.playAnim();
+ user.updateInfo();
resolve(true);
});
});
}
}
+/**
+ * Attribute used for status moves, namely Speed Swap,
+ * that swaps the user's and target's corresponding stats.
+ * @extends MoveEffectAttr
+ * @see {@linkcode apply}
+ */
+export class SwapStatAttr extends MoveEffectAttr {
+ /** The stat to be swapped between the user and the target */
+ private stat: EffectiveStat;
+
+ constructor(stat: EffectiveStat) {
+ super();
+
+ this.stat = stat;
+ }
+
+ /**
+ * Takes the average of the user's and target's corresponding current
+ * {@linkcode stat} values and sets that stat to the average for both
+ * temporarily.
+ * @param user the {@linkcode Pokemon} that used the move
+ * @param target the {@linkcode Pokemon} that the move was used on
+ * @param move N/A
+ * @param args N/A
+ * @returns true if attribute application succeeds
+ */
+ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
+ if (super.apply(user, target, move, args)) {
+ const temp = user.getStat(this.stat, false);
+ user.setStat(this.stat, target.getStat(this.stat, false), false);
+ target.setStat(this.stat, temp, false);
+
+ user.scene.queueMessage(i18next.t("moveTriggers:switchedStat", {
+ pokemonName: getPokemonNameWithAffix(user),
+ stat: i18next.t(getStatKey(this.stat)),
+ }));
+
+ return true;
+ }
+ return false;
+ }
+}
+
+/**
+ * Attribute used for status moves, namely Power Split and Guard Split,
+ * that take the average of a user's and target's corresponding
+ * stats and assign that average back to each corresponding stat.
+ * @extends MoveEffectAttr
+ * @see {@linkcode apply}
+ */
+export class AverageStatsAttr extends MoveEffectAttr {
+ /** The stats to be averaged individually between the user and the target */
+ private stats: readonly EffectiveStat[];
+ private msgKey: string;
+
+ constructor(stats: readonly EffectiveStat[], msgKey: string) {
+ super();
+
+ this.stats = stats;
+ this.msgKey = msgKey;
+ }
+
+ /**
+ * Takes the average of the user's and target's corresponding {@linkcode stat}
+ * values and sets those stats to the corresponding average for both
+ * temporarily.
+ * @param user the {@linkcode Pokemon} that used the move
+ * @param target the {@linkcode Pokemon} that the move was used on
+ * @param move N/A
+ * @param args N/A
+ * @returns true if attribute application succeeds
+ */
+ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
+ if (super.apply(user, target, move, args)) {
+ for (const s of this.stats) {
+ const avg = Math.floor((user.getStat(s, false) + target.getStat(s, false)) / 2);
+
+ user.setStat(s, avg, false);
+ target.setStat(s, avg, false);
+ }
+
+ user.scene.queueMessage(i18next.t(this.msgKey, { pokemonName: getPokemonNameWithAffix(user) }));
+
+ return true;
+ }
+ return false;
+ }
+}
+
export class DiscourageFrequentUseAttr extends MoveAttr {
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
const lastMoves = user.getLastXMoves(4);
@@ -6072,7 +6190,7 @@ export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr {
* @returns true
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- if (target.turnData.battleStatsIncreased) {
+ if (target.turnData.statStagesIncreased) {
super.apply(user, target, move, args);
}
return true;
@@ -6099,7 +6217,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
* @returns true
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
- if (target.turnData.battleStatsIncreased) {
+ if (target.turnData.statStagesIncreased) {
target.trySetStatus(this.effect, true, user);
}
return true;
@@ -6457,7 +6575,7 @@ export function initMoves() {
.ignoresVirtual()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(Moves.SWORDS_DANCE, Type.NORMAL, -1, 20, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ATK, 2, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 2, true)
.danceMove(),
new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1)
.slicingMove(),
@@ -6493,7 +6611,7 @@ export function initMoves() {
new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1)
.attr(FlinchAttr),
new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1)
.attr(FlinchAttr),
new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1),
@@ -6521,7 +6639,7 @@ export function initMoves() {
.attr(RecoilAttr, false, 0.33)
.recklessMove(),
new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON)
@@ -6534,13 +6652,13 @@ export function initMoves() {
.attr(MultiHitAttr)
.makesContact(false),
new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1)
.attr(FlinchAttr)
.bitingMove(),
new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ATK, -1)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1)
@@ -6559,7 +6677,7 @@ export function initMoves() {
.attr(DisableMoveAttr)
.condition(failOnMaxCondition),
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.EMBER, Type.FIRE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 1)
.attr(StatusEffectAttr, StatusEffect.BURN),
@@ -6584,9 +6702,9 @@ export function initMoves() {
new AttackMove(Moves.PSYBEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 65, 100, 20, 10, 0, 1)
.attr(ConfuseAttr),
new AttackMove(Moves.BUBBLE_BEAM, Type.WATER, MoveCategory.SPECIAL, 65, 100, 20, 10, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPD, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1),
new AttackMove(Moves.AURORA_BEAM, Type.ICE, MoveCategory.SPECIAL, 65, 100, 20, 10, 0, 1)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new AttackMove(Moves.HYPER_BEAM, Type.NORMAL, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 1)
.attr(RechargeAttr),
new AttackMove(Moves.PECK, Type.FLYING, MoveCategory.PHYSICAL, 35, 100, 35, -1, 0, 1),
@@ -6613,7 +6731,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.SEEDED)
.condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)),
new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1)
- .attr(GrowthStatChangeAttr),
+ .attr(GrowthStatStageChangeAttr),
new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1)
.attr(HighCritAttr)
.makesContact(false)
@@ -6640,7 +6758,7 @@ export function initMoves() {
.danceMove()
.target(MoveTarget.RANDOM_NEAR_ENEMY),
new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPD, -2)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1)
.attr(FixedDamageAttr, 40),
@@ -6677,13 +6795,13 @@ export function initMoves() {
new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1)
.attr(ConfuseAttr),
new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1),
new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP),
new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ATK, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true),
new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPD, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
new AttackMove(Moves.QUICK_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 1),
new AttackMove(Moves.RAGE, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 20, -1, 0, 1)
.partial(),
@@ -6696,28 +6814,28 @@ export function initMoves() {
.attr(MovesetCopyMoveAttr)
.ignoresVirtual(),
new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, -2)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -2)
.soundBased(),
new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.EVA, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.EVA ], 1, true),
new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1)
.attr(HealAttr, 0.5)
.triageMove(),
new SelfStatusMove(Moves.HARDEN, Type.NORMAL, -1, 30, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new SelfStatusMove(Moves.MINIMIZE, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false)
- .attr(StatChangeAttr, BattleStat.EVA, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.EVA ], 2, true),
new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1)
.attr(ConfuseAttr),
new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new SelfStatusMove(Moves.BARRIER, Type.PSYCHIC, -1, 20, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new StatusMove(Moves.LIGHT_SCREEN, Type.PSYCHIC, -1, 30, -1, 0, 1)
.attr(AddArenaTagAttr, ArenaTagType.LIGHT_SCREEN, 5, true)
.target(MoveTarget.USER_SIDE),
@@ -6765,17 +6883,17 @@ export function initMoves() {
.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, i18next.t("moveTriggers:loweredItsHead", {pokemonName: "{USER}"}), null, true)
- .attr(StatChangeAttr, BattleStat.DEF, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true)
.ignoresVirtual(),
new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1)
.attr(MultiHitAttr)
.makesContact(false),
new AttackMove(Moves.CONSTRICT, Type.NORMAL, MoveCategory.PHYSICAL, 10, 100, 35, 10, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPD, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1),
new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPDEF, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true),
new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1)
.attr(HealAttr, 0.5)
.triageMove(),
@@ -6812,7 +6930,7 @@ export function initMoves() {
.attr(TransformAttr)
.ignoresProtect(),
new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.DIZZY_PUNCH, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, 20, 0, 1)
.attr(ConfuseAttr)
@@ -6821,13 +6939,13 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.SLEEP)
.powderMove(),
new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
.attr(RandomLevelDamageAttr),
new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1)
.condition(failOnGravityCondition),
new SelfStatusMove(Moves.ACID_ARMOR, Type.POISON, -1, 20, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.DEF, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new AttackMove(Moves.CRABHAMMER, Type.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1)
.attr(HighCritAttr),
new AttackMove(Moves.EXPLOSION, Type.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1)
@@ -6853,7 +6971,7 @@ export function initMoves() {
.attr(FlinchAttr)
.bitingMove(),
new SelfStatusMove(Moves.SHARPEN, Type.NORMAL, -1, 30, -1, 0, 1)
- .attr(StatChangeAttr, BattleStat.ATK, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true),
new SelfStatusMove(Moves.CONVERSION, Type.NORMAL, -1, 30, -1, 0, 1)
.attr(FirstMoveTypeAttr),
new AttackMove(Moves.TRI_ATTACK, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, 20, 0, 1)
@@ -6908,7 +7026,7 @@ export function initMoves() {
.windMove()
.attr(HighCritAttr),
new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2)
- .attr(StatChangeAttr, BattleStat.SPD, -2)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.powderMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2)
@@ -6923,21 +7041,21 @@ export function initMoves() {
new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2)
.punchingMove(),
new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2)
- .attr(StatChangeAttr, BattleStat.SPD, -2),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -2),
new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2),
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(CutHpStatBoostAttr, [BattleStat.ATK], 12, 2, (user) => {
- user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", {pokemonName: getPokemonNameWithAffix(user), statName: getBattleStatName(BattleStat.ATK)}));
+ .attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => {
+ user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }));
}),
new AttackMove(Moves.SLUDGE_BOMB, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 30, 0, 2)
.attr(StatusEffectAttr, StatusEffect.POISON)
.ballBombMove(),
new AttackMove(Moves.MUD_SLAP, Type.GROUND, MoveCategory.SPECIAL, 20, 100, 10, 100, 0, 2)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.OCTAZOOKA, Type.WATER, MoveCategory.SPECIAL, 65, 85, 10, 50, 0, 2)
- .attr(StatChangeAttr, BattleStat.ACC, -1)
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.ballBombMove(),
new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2)
.attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES)
@@ -6966,7 +7084,7 @@ export function initMoves() {
.condition(failOnBossCondition)
.target(MoveTarget.ALL),
new AttackMove(Moves.ICY_WIND, Type.ICE, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 2)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(Moves.DETECT, Type.FIGHTING, -1, 5, -1, 4, 2)
@@ -6990,13 +7108,13 @@ export function initMoves() {
new SelfStatusMove(Moves.ENDURE, Type.NORMAL, -1, 10, -1, 4, 2)
.attr(ProtectAttr, BattlerTagType.ENDURING),
new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2)
- .attr(StatChangeAttr, BattleStat.ATK, -2),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -2),
new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2)
.attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL),
new AttackMove(Moves.FALSE_SWIPE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 2)
.attr(SurviveDamageAttr),
new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2)
- .attr(StatChangeAttr, BattleStat.ATK, 2)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 2)
.attr(ConfuseAttr),
new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(HealAttr, 0.5)
@@ -7007,7 +7125,7 @@ export function initMoves() {
.attr(ConsecutiveUseDoublePowerAttr, 3, true)
.slicingMove(),
new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2)
- .attr(StatChangeAttr, BattleStat.DEF, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
@@ -7061,7 +7179,7 @@ export function initMoves() {
new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
.partial(),
new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2)
- .attr(StatChangeAttr, BattleStat.SPD, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true)
.attr(RemoveBattlerTagAttr, [
BattlerTagType.BIND,
BattlerTagType.WRAP,
@@ -7077,12 +7195,12 @@ export function initMoves() {
], true)
.attr(RemoveArenaTrapAttr),
new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2)
- .attr(StatChangeAttr, BattleStat.EVA, -2)
+ .attr(StatStageChangeAttr, [ Stat.EVA ], -2)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2)
- .attr(StatChangeAttr, BattleStat.DEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2)
- .attr(StatChangeAttr, BattleStat.ATK, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true),
new AttackMove(Moves.VITAL_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 70, -1, 10, -1, -1, 2),
new SelfStatusMove(Moves.MORNING_SUN, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(PlantHealAttr)
@@ -7109,7 +7227,7 @@ export function initMoves() {
.attr(WeatherChangeAttr, WeatherType.SUNNY)
.target(MoveTarget.BOTH_SIDES),
new AttackMove(Moves.CRUNCH, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 20, 0, 2)
- .attr(StatChangeAttr, BattleStat.DEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.bitingMove(),
new AttackMove(Moves.MIRROR_COAT, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 20, -1, -5, 2)
.attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.SPECIAL, 2)
@@ -7118,15 +7236,15 @@ export function initMoves() {
.attr(CopyStatsAttr),
new AttackMove(Moves.EXTREME_SPEED, Type.NORMAL, MoveCategory.PHYSICAL, 80, 100, 5, -1, 2, 2),
new AttackMove(Moves.ANCIENT_POWER, Type.ROCK, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 2)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true),
new AttackMove(Moves.SHADOW_BALL, Type.GHOST, MoveCategory.SPECIAL, 80, 100, 15, 20, 0, 2)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.ballBombMove(),
new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
.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),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
.attr(TrapAttr, BattlerTagType.WHIRLPOOL)
.attr(HitsTagAttr, BattlerTagType.UNDERWATER, true),
@@ -7165,13 +7283,13 @@ export function initMoves() {
new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3)
.unimplemented(),
new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPATK, 1)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], 1)
.attr(ConfuseAttr),
new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3)
.attr(StatusEffectAttr, StatusEffect.BURN),
new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3)
.attr(SacrificialAttrOnHit)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -2),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2),
new AttackMove(Moves.FACADE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.status
&& (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1)
@@ -7190,7 +7308,7 @@ export function initMoves() {
.attr(NaturePowerAttr)
.ignoresVirtual(),
new SelfStatusMove(Moves.CHARGE, Type.ELECTRIC, -1, 20, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPDEF, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false),
new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3)
.unimplemented(),
@@ -7210,7 +7328,7 @@ export function initMoves() {
new SelfStatusMove(Moves.INGRAIN, Type.GRASS, -1, 20, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true),
new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], -1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true),
new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3)
.unimplemented(),
new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3)
@@ -7254,14 +7372,14 @@ export function initMoves() {
new SelfStatusMove(Moves.CAMOUFLAGE, Type.NORMAL, -1, 20, -1, 0, 3)
.attr(CopyBiomeTypeAttr),
new SelfStatusMove(Moves.TAIL_GLOW, Type.BUG, -1, 20, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPATK, 3, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], 3, true),
new AttackMove(Moves.LUSTER_PURGE, Type.PSYCHIC, MoveCategory.SPECIAL, 95, 100, 5, 50, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1),
new AttackMove(Moves.MIST_BALL, Type.PSYCHIC, MoveCategory.SPECIAL, 95, 100, 5, 50, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPATK, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.ballBombMove(),
new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.ATK, -2)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -2)
.danceMove(),
new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3)
.attr(ConfuseAttr)
@@ -7288,13 +7406,13 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.TOXIC)
.bitingMove(),
new AttackMove(Moves.CRUSH_CLAW, Type.NORMAL, MoveCategory.PHYSICAL, 75, 95, 10, 50, 0, 3)
- .attr(StatChangeAttr, BattleStat.DEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.BLAST_BURN, Type.FIRE, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 3)
.attr(RechargeAttr),
new AttackMove(Moves.HYDRO_CANNON, Type.WATER, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 3)
.attr(RechargeAttr),
new AttackMove(Moves.METEOR_MASH, Type.STEEL, MoveCategory.PHYSICAL, 90, 90, 10, 20, 0, 3)
- .attr(StatChangeAttr, BattleStat.ATK, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.punchingMove(),
new AttackMove(Moves.ASTONISH, Type.GHOST, MoveCategory.PHYSICAL, 30, 100, 15, 30, 0, 3)
.attr(FlinchAttr),
@@ -7306,33 +7424,33 @@ export function initMoves() {
.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),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3)
.attr(HighCritAttr)
.slicingMove()
.windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.OVERHEAT, Type.FIRE, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPATK, -2, true)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true)
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST),
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.makesContact(false),
new AttackMove(Moves.SILVER_WIND, Type.BUG, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 3)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.windMove(),
new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPDEF, -2)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2)
.soundBased(),
new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3)
.attr(StatusEffectAttr, StatusEffect.SLEEP)
.soundBased(),
new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1),
new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true),
new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3)
.attr(HpPowerAttr)
.target(MoveTarget.ALL_NEAR_ENEMIES),
@@ -7353,7 +7471,7 @@ export function initMoves() {
.attr(OneHitKOAttr)
.attr(SheerColdAccuracyAttr),
new AttackMove(Moves.MUDDY_WATER, Type.WATER, MoveCategory.SPECIAL, 90, 85, 10, 30, 0, 3)
- .attr(StatChangeAttr, BattleStat.ACC, -1)
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.BULLET_SEED, Type.GRASS, MoveCategory.PHYSICAL, 25, 100, 30, -1, 0, 3)
.attr(MultiHitAttr)
@@ -7365,25 +7483,25 @@ export function initMoves() {
.attr(MultiHitAttr)
.makesContact(false),
new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.DEF, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3)
- .attr(StatChangeAttr, BattleStat.ATK, 1)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 1)
.soundBased()
.target(MoveTarget.USER_AND_ALLIES),
new AttackMove(Moves.DRAGON_CLAW, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 3),
new AttackMove(Moves.FRENZY_PLANT, Type.GRASS, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 3)
.attr(RechargeAttr),
new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true),
new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3)
.attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", {pokemonName: "{USER}"}), BattlerTagType.FLYING)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.condition(failOnGravityCondition)
.ignoresVirtual(),
new AttackMove(Moves.MUD_SHOT, Type.GROUND, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 3)
- .attr(StatChangeAttr, BattleStat.SPD, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1),
new AttackMove(Moves.POISON_TAIL, Type.POISON, MoveCategory.PHYSICAL, 50, 100, 25, 10, 0, 3)
.attr(HighCritAttr)
.attr(StatusEffectAttr, StatusEffect.POISON),
@@ -7398,12 +7516,12 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.WATER_SPORT, 5)
.target(MoveTarget.BOTH_SIDES),
new SelfStatusMove(Moves.CALM_MIND, Type.PSYCHIC, -1, 20, -1, 0, 3)
- .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true),
new AttackMove(Moves.LEAF_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 3)
.attr(HighCritAttr)
.slicingMove(),
new SelfStatusMove(Moves.DRAGON_DANCE, Type.DRAGON, -1, 20, -1, 0, 3)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPD ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)
.danceMove(),
new AttackMove(Moves.ROCK_BLAST, Type.ROCK, MoveCategory.PHYSICAL, 25, 90, 10, -1, 0, 3)
.attr(MultiHitAttr)
@@ -7417,7 +7535,7 @@ export function initMoves() {
.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),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
new SelfStatusMove(Moves.ROOST, Type.FLYING, -1, 5, -1, 0, 4)
.attr(HealAttr, 0.5)
.attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false)
@@ -7431,7 +7549,7 @@ export function initMoves() {
.attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1)
.attr(HealStatusEffectAttr, false, StatusEffect.SLEEP),
new AttackMove(Moves.HAMMER_ARM, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPD, -1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1, true)
.punchingMove(),
new AttackMove(Moves.GYRO_BALL, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4)
.attr(GyroBallPowerAttr)
@@ -7456,7 +7574,7 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.TAILWIND, 4, true)
.target(MoveTarget.USER_SIDE),
new StatusMove(Moves.ACUPRESSURE, Type.NORMAL, -1, 30, -1, 0, 4)
- .attr(AcupressureStatChangeAttr)
+ .attr(AcupressureStatStageChangeAttr)
.target(MoveTarget.USER_OR_NEAR_ALLY),
new AttackMove(Moves.METAL_BURST, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4)
.attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5)
@@ -7466,7 +7584,7 @@ export function initMoves() {
new AttackMove(Moves.U_TURN, Type.BUG, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4)
.attr(ForceSwitchOutAttr, true, false),
new AttackMove(Moves.CLOSE_COMBAT, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true),
new AttackMove(Moves.PAYBACK, Type.DARK, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 4)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getLastXMoves(1).find(m => m.turn === target.scene.currentBattle.turn) || user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.BALL ? 2 : 1),
new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4)
@@ -7510,9 +7628,9 @@ export function initMoves() {
.attr(CopyMoveAttr)
.ignoresVirtual(),
new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
- .unimplemented(),
+ .attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]),
new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
- .unimplemented(),
+ .attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ]),
new AttackMove(Moves.PUNISHMENT, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4)
.makesContact(true)
.attr(PunishmentPowerAttr),
@@ -7526,7 +7644,7 @@ export function initMoves() {
.attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES)
.target(MoveTarget.ENEMY_SIDE),
new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4)
- .attr(SwapStatsAttr),
+ .attr(SwapStatStagesAttr, BATTLE_STATS),
new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4)
@@ -7543,7 +7661,7 @@ export function initMoves() {
.pulseMove()
.ballBombMove(),
new SelfStatusMove(Moves.ROCK_POLISH, Type.ROCK, -1, 20, -1, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPD, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
new AttackMove(Moves.POISON_JAB, Type.POISON, MoveCategory.PHYSICAL, 80, 100, 20, 30, 0, 4)
.attr(StatusEffectAttr, StatusEffect.POISON),
new AttackMove(Moves.DARK_PULSE, Type.DARK, MoveCategory.SPECIAL, 80, 100, 15, 20, 0, 4)
@@ -7562,7 +7680,7 @@ export function initMoves() {
new AttackMove(Moves.X_SCISSOR, Type.BUG, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 4)
.slicingMove(),
new AttackMove(Moves.BUG_BUZZ, Type.BUG, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.soundBased(),
new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4)
.pulseMove(),
@@ -7577,22 +7695,22 @@ export function initMoves() {
.triageMove(),
new AttackMove(Moves.VACUUM_WAVE, Type.FIGHTING, MoveCategory.SPECIAL, 40, 100, 30, -1, 1, 4),
new AttackMove(Moves.FOCUS_BLAST, Type.FIGHTING, MoveCategory.SPECIAL, 120, 70, 5, 10, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.ballBombMove(),
new AttackMove(Moves.ENERGY_BALL, Type.GRASS, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.ballBombMove(),
new AttackMove(Moves.BRAVE_BIRD, Type.FLYING, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 4)
.attr(RecoilAttr, false, 0.33)
.recklessMove(),
new AttackMove(Moves.EARTH_POWER, Type.GROUND, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1),
new StatusMove(Moves.SWITCHEROO, Type.DARK, 100, 10, -1, 0, 4)
.unimplemented(),
new AttackMove(Moves.GIGA_IMPACT, Type.NORMAL, MoveCategory.PHYSICAL, 150, 90, 5, -1, 0, 4)
.attr(RechargeAttr),
new SelfStatusMove(Moves.NASTY_PLOT, Type.DARK, -1, 20, -1, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPATK, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], 2, true),
new AttackMove(Moves.BULLET_PUNCH, Type.STEEL, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 4)
.punchingMove(),
new AttackMove(Moves.AVALANCHE, Type.ICE, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 4)
@@ -7615,7 +7733,7 @@ export function initMoves() {
.bitingMove(),
new AttackMove(Moves.SHADOW_SNEAK, Type.GHOST, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 4),
new AttackMove(Moves.MUD_BOMB, Type.GROUND, MoveCategory.SPECIAL, 65, 85, 10, 30, 0, 4)
- .attr(StatChangeAttr, BattleStat.ACC, -1)
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.ballBombMove(),
new AttackMove(Moves.PSYCHO_CUT, Type.PSYCHIC, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4)
.attr(HighCritAttr)
@@ -7624,13 +7742,13 @@ export function initMoves() {
new AttackMove(Moves.ZEN_HEADBUTT, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 90, 15, 20, 0, 4)
.attr(FlinchAttr),
new AttackMove(Moves.MIRROR_SHOT, Type.STEEL, MoveCategory.SPECIAL, 65, 85, 10, 30, 0, 4)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.FLASH_CANNON, Type.STEEL, MoveCategory.SPECIAL, 80, 100, 10, 10, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1),
new AttackMove(Moves.ROCK_CLIMB, Type.NORMAL, MoveCategory.PHYSICAL, 90, 85, 20, 20, 0, 4)
.attr(ConfuseAttr),
new StatusMove(Moves.DEFOG, Type.FLYING, -1, 15, -1, 0, 4)
- .attr(StatChangeAttr, BattleStat.EVA, -1)
+ .attr(StatStageChangeAttr, [ Stat.EVA ], -1)
.attr(ClearWeatherAttr, WeatherType.FOG)
.attr(ClearTerrainAttr)
.attr(RemoveScreensAttr, false)
@@ -7640,7 +7758,7 @@ export function initMoves() {
.ignoresProtect()
.target(MoveTarget.BOTH_SIDES),
new AttackMove(Moves.DRACO_METEOR, Type.DRAGON, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPATK, -2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
new AttackMove(Moves.DISCHARGE, Type.ELECTRIC, MoveCategory.SPECIAL, 80, 100, 15, 30, 0, 4)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.target(MoveTarget.ALL_NEAR_OTHERS),
@@ -7648,7 +7766,7 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.BURN)
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.LEAF_STORM, Type.GRASS, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPATK, -2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
new AttackMove(Moves.POWER_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 120, 85, 10, -1, 0, 4),
new AttackMove(Moves.ROCK_WRECKER, Type.ROCK, MoveCategory.PHYSICAL, 150, 90, 5, -1, 0, 4)
.attr(RechargeAttr)
@@ -7670,7 +7788,7 @@ export function initMoves() {
.attr(HighCritAttr)
.makesContact(false),
new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPATK, -2)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -2)
.condition((user, target, move) => target.isOppositeGender(user))
.target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4)
@@ -7688,7 +7806,7 @@ export function initMoves() {
new AttackMove(Moves.BUG_BITE, Type.BUG, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 4)
.attr(StealEatBerryAttr),
new AttackMove(Moves.CHARGE_BEAM, Type.ELECTRIC, MoveCategory.SPECIAL, 50, 90, 10, 70, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPATK, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true),
new AttackMove(Moves.WOOD_HAMMER, Type.GRASS, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 4)
.attr(RecoilAttr, false, 0.33)
.recklessMove(),
@@ -7697,7 +7815,7 @@ export function initMoves() {
.attr(HighCritAttr)
.makesContact(false),
new SelfStatusMove(Moves.DEFEND_ORDER, Type.BUG, -1, 10, -1, 0, 4)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true),
new SelfStatusMove(Moves.HEAL_ORDER, Type.BUG, -1, 10, -1, 0, 4)
.attr(HealAttr, 0.5)
.triageMove(),
@@ -7723,23 +7841,23 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.SLEEP)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4)
- .attr(StatChangeAttr, BattleStat.SPDEF, -2),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.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, i18next.t("moveTriggers:vanishedInstantly", {pokemonName: "{USER}"}), BattlerTagType.HIDDEN)
.ignoresProtect()
.ignoresVirtual(),
new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.ACC ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true),
new StatusMove(Moves.WIDE_GUARD, Type.ROCK, -1, 10, -1, 3, 5)
.target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true),
new StatusMove(Moves.GUARD_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5)
- .unimplemented(),
+ .attr(AverageStatsAttr, [ Stat.DEF, Stat.SPDEF ], "moveTriggers:sharedGuard"),
new StatusMove(Moves.POWER_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5)
- .unimplemented(),
+ .attr(AverageStatsAttr, [ Stat.ATK, Stat.SPATK ], "moveTriggers:sharedPower"),
new StatusMove(Moves.WONDER_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5)
.ignoresProtect()
.target(MoveTarget.BOTH_SIDES)
@@ -7749,7 +7867,7 @@ export function initMoves() {
new AttackMove(Moves.VENOSHOCK, Type.POISON, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status && (target.status.effect === StatusEffect.POISON || target.status.effect === StatusEffect.TOXIC) ? 2 : 1),
new SelfStatusMove(Moves.AUTOTOMIZE, Type.STEEL, -1, 15, -1, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPD, 2, true)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true)
.partial(),
new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5)
.powderMove()
@@ -7775,7 +7893,7 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.POISON)
.target(MoveTarget.ALL_NEAR_OTHERS),
new SelfStatusMove(Moves.QUIVER_DANCE, Type.BUG, -1, 20, -1, 0, 5)
- .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.danceMove(),
new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(MinimizeAccuracyAttr)
@@ -7792,13 +7910,13 @@ export function initMoves() {
new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5)
.attr(ChangeTypeAttr, Type.WATER),
new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPD, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true),
new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.ACC ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.ACC ], 1, true),
new AttackMove(Moves.LOW_SWEEP, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 20, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPD, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1),
new AttackMove(Moves.ACID_SPRAY, Type.POISON, MoveCategory.SPECIAL, 40, 100, 20, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPDEF, -2)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2)
.ballBombMove(),
new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5)
.attr(TargetAtkUserAtkAttr),
@@ -7816,11 +7934,11 @@ export function initMoves() {
.attr(ConsecutiveUseMultiBasePowerAttr, 5, false)
.soundBased(),
new AttackMove(Moves.CHIP_AWAY, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 5)
- .attr(IgnoreOpponentStatChangesAttr),
+ .attr(IgnoreOpponentStatStagesAttr),
new AttackMove(Moves.CLEAR_SMOG, Type.POISON, MoveCategory.SPECIAL, 50, -1, 15, -1, 0, 5)
.attr(ResetStatsAttr, false),
new AttackMove(Moves.STORED_POWER, Type.PSYCHIC, MoveCategory.SPECIAL, 20, 100, 10, -1, 0, 5)
- .attr(StatChangeCountPowerAttr),
+ .attr(PositiveStatStagePowerAttr),
new StatusMove(Moves.QUICK_GUARD, Type.FIGHTING, -1, 15, -1, 3, 5)
.target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true),
@@ -7832,8 +7950,8 @@ export function initMoves() {
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
.attr(StatusEffectAttr, StatusEffect.BURN),
new SelfStatusMove(Moves.SHELL_SMASH, Type.NORMAL, -1, 15, -1, 0, 5)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], 2, true)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 2, true)
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true),
new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5)
.attr(HealAttr, 0.5, false, false)
.pulseMove()
@@ -7847,8 +7965,8 @@ export function initMoves() {
.condition(failOnGravityCondition)
.ignoresVirtual(),
new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
- .attr(StatChangeAttr, BattleStat.ATK, 1, true)
- .attr(StatChangeAttr, BattleStat.SPD, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
new AttackMove(Moves.CIRCLE_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
.attr(ForceSwitchOutAttr),
new AttackMove(Moves.INCINERATE, Type.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
@@ -7879,10 +7997,10 @@ export function initMoves() {
new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5)
.attr(ForceSwitchOutAttr, true, false),
new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPATK, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.BULLDOZE, Type.GROUND, MoveCategory.PHYSICAL, 60, 100, 20, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5)
@@ -7891,9 +8009,9 @@ export function initMoves() {
.attr(ForceSwitchOutAttr)
.hidesTarget(),
new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, true),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, true),
new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.WILD_CHARGE, Type.ELECTRIC, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 5)
.attr(RecoilAttr)
@@ -7908,10 +8026,10 @@ export function initMoves() {
.attr(HitHealAttr)
.triageMove(),
new AttackMove(Moves.SACRED_SWORD, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 5)
- .attr(IgnoreOpponentStatChangesAttr)
+ .attr(IgnoreOpponentStatStagesAttr)
.slicingMove(),
new AttackMove(Moves.RAZOR_SHELL, Type.WATER, MoveCategory.PHYSICAL, 75, 95, 10, 50, 0, 5)
- .attr(StatChangeAttr, BattleStat.DEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.slicingMove(),
new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(MinimizeAccuracyAttr)
@@ -7919,13 +8037,13 @@ export function initMoves() {
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
.condition(failOnMaxCondition),
new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5)
.attr(FlinchAttr),
new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5)
- .attr(StatChangeAttr, BattleStat.DEF, 3, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true),
new AttackMove(Moves.NIGHT_DAZE, Type.DARK, MoveCategory.SPECIAL, 85, 95, 10, 40, 0, 5)
- .attr(StatChangeAttr, BattleStat.ACC, -1),
+ .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.PSYSTRIKE, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 10, -1, 0, 5)
.attr(DefDefAttr),
new AttackMove(Moves.TAIL_SLAP, Type.NORMAL, MoveCategory.PHYSICAL, 25, 85, 10, -1, 0, 5)
@@ -7954,14 +8072,14 @@ export function initMoves() {
.attr(DefDefAttr)
.slicingMove(),
new AttackMove(Moves.GLACIATE, Type.ICE, MoveCategory.SPECIAL, 65, 95, 10, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.BOLT_STRIKE, Type.ELECTRIC, MoveCategory.PHYSICAL, 130, 85, 5, 20, 0, 5)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.BLUE_FLARE, Type.FIRE, MoveCategory.SPECIAL, 130, 85, 5, 20, 0, 5)
.attr(StatusEffectAttr, StatusEffect.BURN),
new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPATK, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.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, i18next.t("moveTriggers:becameCloakedInFreezingLight", {pokemonName: "{USER}"}))
@@ -7972,14 +8090,14 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.BURN)
.ignoresVirtual(),
new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
- .attr(StatChangeAttr, BattleStat.SPATK, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.ICICLE_CRASH, Type.ICE, MoveCategory.PHYSICAL, 85, 90, 10, 30, 0, 5)
.attr(FlinchAttr)
.makesContact(false),
new AttackMove(Moves.V_CREATE, Type.FIRE, MoveCategory.PHYSICAL, 180, 95, 5, -1, 0, 5)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF, BattleStat.SPD ], -1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF, Stat.SPD ], -1, true),
new AttackMove(Moves.FUSION_FLARE, Type.FIRE, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 5)
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
.attr(LastMoveDoublePowerAttr, Moves.FUSION_BOLT),
@@ -8003,12 +8121,12 @@ export function initMoves() {
// If any fielded pokémon is grass-type and grounded.
return [...user.scene.getEnemyParty(), ...user.scene.getParty()].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded());
})
- .attr(StatChangeAttr, [BattleStat.ATK, BattleStat.SPATK], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()),
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
.attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB)
.target(MoveTarget.ENEMY_SIDE),
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
- .attr(PostVictoryStatChangeAttr, BattleStat.ATK, 3, true ),
+ .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", {pokemonName: "{USER}"}), BattlerTagType.HIDDEN)
.ignoresProtect()
@@ -8017,7 +8135,7 @@ export function initMoves() {
.attr(AddTypeAttr, Type.GHOST)
.partial(),
new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
.soundBased(),
new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6)
.target(MoveTarget.BOTH_SIDES)
@@ -8041,7 +8159,7 @@ export function initMoves() {
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY)
.attr(ForceSwitchOutAttr, true, false)
.soundBased(),
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
@@ -8055,7 +8173,7 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true),
new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6)
.target(MoveTarget.ALL)
- .attr(StatChangeAttr, BattleStat.DEF, 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)),
new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6)
.attr(TerrainChangeAttr, TerrainType.GRASSY)
.target(MoveTarget.BOTH_SIDES),
@@ -8065,11 +8183,11 @@ export function initMoves() {
new StatusMove(Moves.ELECTRIFY, Type.ELECTRIC, -1, 20, -1, 0, 6)
.unimplemented(),
new AttackMove(Moves.PLAY_ROUGH, Type.FAIRY, MoveCategory.PHYSICAL, 90, 90, 10, 10, 0, 6)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new AttackMove(Moves.FAIRY_WIND, Type.FAIRY, MoveCategory.SPECIAL, 40, 100, 30, -1, 0, 6)
.windMove(),
new AttackMove(Moves.MOONBLAST, Type.FAIRY, MoveCategory.SPECIAL, 95, 100, 15, 30, 0, 6)
- .attr(StatChangeAttr, BattleStat.SPATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1),
new AttackMove(Moves.BOOMBURST, Type.NORMAL, MoveCategory.SPECIAL, 140, 100, 10, -1, 0, 6)
.soundBased()
.target(MoveTarget.ALL_NEAR_OTHERS),
@@ -8079,12 +8197,12 @@ export function initMoves() {
new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.KINGS_SHIELD),
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
- .attr(StatChangeAttr, BattleStat.SPATK, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.soundBased(),
new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6)
- .attr(StatChangeAttr, BattleStat.DEF, 2, true)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.makesContact(false)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6)
@@ -8098,26 +8216,26 @@ export function initMoves() {
.attr(WaterShurikenPowerAttr)
.attr(WaterShurikenMultiHitTypeAttr),
new AttackMove(Moves.MYSTICAL_FIRE, Type.FIRE, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 6)
- .attr(StatChangeAttr, BattleStat.SPATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1),
new SelfStatusMove(Moves.SPIKY_SHIELD, Type.GRASS, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD),
new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6)
- .attr(StatChangeAttr, BattleStat.SPDEF, 1)
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1)
.target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
- .attr(StatChangeAttr, BattleStat.SPATK, -2),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -2),
new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
.powderMove()
.unimplemented(),
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", {pokemonName: "{USER}"}))
- .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 2, true)
+ .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true)
.ignoresVirtual(),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
.target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))),
new StatusMove(Moves.HAPPY_HOUR, Type.NORMAL, -1, 30, -1, 0, 6) // No animation
@@ -8132,7 +8250,7 @@ export function initMoves() {
new StatusMove(Moves.HOLD_HANDS, Type.NORMAL, -1, 40, -1, 0, 6)
.target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6)
@@ -8141,7 +8259,7 @@ export function initMoves() {
.makesContact()
.attr(TrapAttr, BattlerTagType.INFESTATION),
new AttackMove(Moves.POWER_UP_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 20, 100, 0, 6)
- .attr(StatChangeAttr, BattleStat.ATK, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.punchingMove(),
new AttackMove(Moves.OBLIVION_WING, Type.FLYING, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6)
.attr(HitHealAttr, 0.75)
@@ -8172,9 +8290,9 @@ export function initMoves() {
.makesContact(false)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.DRAGON_ASCENT, Type.FLYING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 6)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true),
new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, -1, 0, 6)
- .attr(StatChangeAttr, BattleStat.DEF, -1, true)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true)
.makesContact(false)
.ignoresProtect(),
/* Unused */
@@ -8301,13 +8419,13 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
.makesContact(false),
new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
- .attr(IgnoreOpponentStatChangesAttr),
+ .attr(IgnoreOpponentStatStagesAttr),
new AttackMove(Moves.SPARKLING_ARIA, Type.WATER, MoveCategory.SPECIAL, 90, 100, 10, 100, 0, 7)
.attr(HealStatusEffectAttr, false, StatusEffect.BURN)
.soundBased()
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.ICE_HAMMER, Type.ICE, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 7)
- .attr(StatChangeAttr, BattleStat.SPD, -1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1, true)
.punchingMove(),
new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7)
.attr(BoostHealAttr, 0.5, 2/3, true, false, (user, target, move) => user.scene.arena.terrain?.terrainType === TerrainType.GRASSY)
@@ -8315,8 +8433,8 @@ export function initMoves() {
new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7),
new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7)
.attr(HitHealAttr, null, Stat.ATK)
- .attr(StatChangeAttr, BattleStat.ATK, -1)
- .condition((user, target, move) => target.summonData.battleStats[BattleStat.ATK] > -6)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
+ .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6)
.triageMove(),
new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", {pokemonName: "{USER}"}))
@@ -8328,11 +8446,11 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false),
new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7)
.attr(StatusEffectAttr, StatusEffect.POISON)
- .attr(StatChangeAttr, BattleStat.SPD, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1),
new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
.target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))),
new AttackMove(Moves.THROAT_CHOP, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7)
@@ -8347,11 +8465,11 @@ export function initMoves() {
.attr(TerrainChangeAttr, TerrainType.PSYCHIC)
.target(MoveTarget.BOTH_SIDES),
new AttackMove(Moves.LUNGE, Type.BUG, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new AttackMove(Moves.FIRE_LASH, Type.FIRE, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7)
- .attr(StatChangeAttr, BattleStat.DEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.POWER_TRIP, Type.DARK, MoveCategory.PHYSICAL, 20, 100, 10, -1, 0, 7)
- .attr(StatChangeCountPowerAttr),
+ .attr(PositiveStatStagePowerAttr),
new AttackMove(Moves.BURN_UP, Type.FIRE, MoveCategory.SPECIAL, 130, 100, 5, -1, 0, 7)
.condition((user) => {
const userTypes = user.getTypes(true);
@@ -8362,7 +8480,7 @@ export function initMoves() {
user.scene.queueMessage(i18next.t("moveTriggers:burnedItselfOut", {pokemonName: getPokemonNameWithAffix(user)}));
}),
new StatusMove(Moves.SPEED_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 7)
- .unimplemented(),
+ .attr(SwapStatAttr, Stat.SPD),
new AttackMove(Moves.SMART_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7),
new StatusMove(Moves.PURIFY, Type.POISON, -1, 20, -1, 0, 7)
.condition(
@@ -8377,7 +8495,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES)
.attr(SuppressAbilitiesIfActedAttr),
new AttackMove(Moves.TROP_KICK, Type.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
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, -3, 7)
@@ -8385,7 +8503,7 @@ export function initMoves() {
.ballBombMove()
.makesContact(false),
new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7)
- .attr(StatChangeAttr, BattleStat.DEF, -1, true, null, true, false, MoveEffectTrigger.HIT, true)
+ .attr(StatStageChangeAttr, [ Stat.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),
@@ -8419,7 +8537,7 @@ export function initMoves() {
.partial()
.ignoresVirtual(),
new SelfStatusMove(Moves.EXTREME_EVOBOOST, Type.NORMAL, -1, 1, -1, 0, 7)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 2, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true)
.ignoresVirtual(),
new AttackMove(Moves.GENESIS_SUPERNOVA, Type.PSYCHIC, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7)
.attr(TerrainChangeAttr, TerrainType.PSYCHIC)
@@ -8431,18 +8549,18 @@ export function initMoves() {
// Fails if the user was not hit by a physical attack during the turn
.condition((user, target, move) => user.getTag(ShellTrapTag)?.activated === true),
new AttackMove(Moves.FLEUR_CANNON, Type.FAIRY, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 7)
- .attr(StatChangeAttr, BattleStat.SPATK, -2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
new AttackMove(Moves.PSYCHIC_FANGS, Type.PSYCHIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
.bitingMove()
.attr(RemoveScreensAttr),
new AttackMove(Moves.STOMPING_TANTRUM, Type.GROUND, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 7)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1),
new AttackMove(Moves.SHADOW_BONE, Type.GHOST, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7)
- .attr(StatChangeAttr, BattleStat.DEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.makesContact(false),
new AttackMove(Moves.ACCELEROCK, Type.ROCK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 1, 7),
new AttackMove(Moves.LIQUIDATION, Type.WATER, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7)
- .attr(StatChangeAttr, BattleStat.DEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
.attr(RechargeAttr),
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
@@ -8454,7 +8572,7 @@ export function initMoves() {
.ignoresAbilities()
.partial(),
new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1),
new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7)
.attr(FlinchAttr),
new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7)
@@ -8496,7 +8614,7 @@ export function initMoves() {
.makesContact(false)
.ignoresVirtual(),
new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES)
.partial()
@@ -8563,18 +8681,18 @@ export function initMoves() {
.bitingMove(),
new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8) // TODO: Stuff Cheeks should not be selectable when the user does not have a berry, see wiki
.attr(EatBerryAttr)
- .attr(StatChangeAttr, BattleStat.DEF, 2, true)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.condition((user) => {
const userBerries = user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer());
return userBerries.length > 0;
})
.partial(),
new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, -1, 0, 8)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat
new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.partial(),
new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8)
.attr(ChangeTypeAttr, Type.PSYCHIC)
@@ -8669,16 +8787,16 @@ export function initMoves() {
.ignoresVirtual(),
/* End Unused */
new SelfStatusMove(Moves.CLANGOROUS_SOUL, Type.DRAGON, 100, 5, -1, 0, 8)
- .attr(CutHpStatBoostAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, 3)
+ .attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, 3)
.soundBased()
.danceMove(),
new AttackMove(Moves.BODY_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 8)
.attr(DefAtkAttr),
new StatusMove(Moves.DECORATE, Type.FAIRY, -1, 15, -1, 0, 8)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 2)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 2)
.ignoresProtect(),
new AttackMove(Moves.DRUM_BEATING, Type.GRASS, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.makesContact(false),
new AttackMove(Moves.SNAP_TRAP, Type.GRASS, MoveCategory.PHYSICAL, 35, 100, 15, -1, 0, 8)
.attr(TrapAttr, BattlerTagType.SNAP_TRAP),
@@ -8691,25 +8809,25 @@ export function initMoves() {
.slicingMove(),
new AttackMove(Moves.BEHEMOTH_BASH, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 8),
new AttackMove(Moves.AURA_WHEEL, Type.ELECTRIC, MoveCategory.PHYSICAL, 110, 100, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.SPD, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true)
.makesContact(false)
.attr(AuraWheelTypeAttr)
.condition((user, target, move) => [user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.MORPEKO)), // Missing custom fail message
new AttackMove(Moves.BREAKING_SWIPE, Type.DRAGON, MoveCategory.PHYSICAL, 60, 100, 15, 100, 0, 8)
.target(MoveTarget.ALL_NEAR_ENEMIES)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new AttackMove(Moves.BRANCH_POKE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 8),
new AttackMove(Moves.OVERDRIVE, Type.ELECTRIC, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 8)
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.APPLE_ACID, Type.GRASS, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.SPDEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1),
new AttackMove(Moves.GRAV_APPLE, Type.GRASS, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.DEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTag(ArenaTagType.GRAVITY) ? 1.5 : 1)
.makesContact(false),
new AttackMove(Moves.SPIRIT_BREAK, Type.FAIRY, MoveCategory.PHYSICAL, 75, 100, 15, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.SPATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1),
new AttackMove(Moves.STRANGE_STEAM, Type.FAIRY, MoveCategory.SPECIAL, 90, 95, 10, 20, 0, 8)
.attr(ConfuseAttr),
new StatusMove(Moves.LIFE_DEW, Type.WATER, -1, 10, -1, 0, 8)
@@ -8733,14 +8851,14 @@ export function initMoves() {
.attr(ClearTerrainAttr)
.condition((user, target, move) => !!user.scene.arena.terrain),
new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8)
- //.attr(StatChangeAttr, BattleStat.SPD, 1, true) // TODO: Have boosts only apply at end of move, not after every hit
- //.attr(StatChangeAttr, BattleStat.DEF, -1, true)
+ //.attr(StatStageChangeAttr, Stat.SPD, 1, true) // TODO: Have boosts only apply at end of move, not after every hit
+ //.attr(StatStageChangeAttr, Stat.DEF, -1, true)
.attr(MultiHitAttr)
.makesContact(false)
.partial(),
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", {pokemonName: "{USER}"}), null, true)
- .attr(StatChangeAttr, BattleStat.SPATK, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.ignoresVirtual(),
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr)
@@ -8761,12 +8879,12 @@ export function initMoves() {
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() !== TerrainType.NONE && user.isGrounded() ? 2 : 1)
.pulseMove(),
new AttackMove(Moves.SKITTER_SMACK, Type.BUG, MoveCategory.PHYSICAL, 70, 90, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.SPATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1),
new AttackMove(Moves.BURNING_JEALOUSY, Type.FIRE, MoveCategory.SPECIAL, 70, 100, 5, 100, 0, 8)
.attr(StatusIfBoostedAttr, StatusEffect.BURN)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.LASH_OUT, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
- .attr(MovePowerMultiplierAttr, (user, target, move) => user.turnData.battleStatsDecreased ? 2 : 1),
+ .attr(MovePowerMultiplierAttr, (user, _target, _move) => user.turnData.statStagesDecreased ? 2 : 1),
new AttackMove(Moves.POLTERGEIST, Type.GHOST, MoveCategory.PHYSICAL, 110, 90, 5, -1, 0, 8)
.attr(AttackedByItemAttr)
.makesContact(false),
@@ -8774,7 +8892,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS)
.unimplemented(),
new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1)
.target(MoveTarget.NEAR_ALLY),
new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8)
.attr(ForceSwitchOutAttr, true, false),
@@ -8810,7 +8928,7 @@ export function initMoves() {
.attr(FlinchAttr)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.THUNDEROUS_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.DEF, -1),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.GLACIAL_LANCE, Type.ICE, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 8)
.target(MoveTarget.ALL_NEAR_ENEMIES)
.makesContact(false),
@@ -8822,18 +8940,18 @@ export function initMoves() {
new AttackMove(Moves.DIRE_CLAW, Type.POISON, MoveCategory.PHYSICAL, 80, 100, 15, 50, 0, 8)
.attr(MultiStatusEffectAttr, [StatusEffect.POISON, StatusEffect.PARALYSIS, StatusEffect.SLEEP]),
new AttackMove(Moves.PSYSHIELD_BASH, Type.PSYCHIC, MoveCategory.PHYSICAL, 70, 90, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.DEF, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new SelfStatusMove(Moves.POWER_SHIFT, Type.NORMAL, -1, 10, -1, 0, 8)
.unimplemented(),
new AttackMove(Moves.STONE_AXE, Type.ROCK, MoveCategory.PHYSICAL, 65, 90, 15, 100, 0, 8)
.attr(AddArenaTrapTagHitAttr, ArenaTagType.STEALTH_ROCK)
.slicingMove(),
new AttackMove(Moves.SPRINGTIDE_STORM, Type.FAIRY, MoveCategory.SPECIAL, 100, 80, 5, 30, 0, 8)
- .attr(StatChangeAttr, BattleStat.ATK, -1)
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.MYSTICAL_POWER, Type.PSYCHIC, MoveCategory.SPECIAL, 70, 90, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.SPATK, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true),
new AttackMove(Moves.RAGING_FURY, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 8)
.makesContact(false)
.attr(FrenzyAttr)
@@ -8849,10 +8967,10 @@ export function initMoves() {
.makesContact(false)
.attr(FlinchAttr),
new SelfStatusMove(Moves.VICTORY_DANCE, Type.FIGHTING, -1, 10, -1, 0, 8)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPD ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPD ], 1, true)
.danceMove(),
new AttackMove(Moves.HEADLONG_RUSH, Type.GROUND, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 8)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true)
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true)
.punchingMove(),
new AttackMove(Moves.BARB_BARRAGE, Type.POISON, MoveCategory.PHYSICAL, 60, 100, 10, 50, 0, 8)
.makesContact(false)
@@ -8860,15 +8978,15 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.POISON),
new AttackMove(Moves.ESPER_WING, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 8)
.attr(HighCritAttr)
- .attr(StatChangeAttr, BattleStat.SPD, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true),
new AttackMove(Moves.BITTER_MALICE, Type.GHOST, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new SelfStatusMove(Moves.SHELTER, Type.STEEL, -1, 10, 100, 0, 8)
- .attr(StatChangeAttr, BattleStat.DEF, 2, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8)
.makesContact(false)
.attr(HighCritAttr)
- .attr(StatChangeAttr, BattleStat.DEF, -1)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.attr(FlinchAttr)
.partial(),
new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8)
@@ -8879,7 +8997,7 @@ export function initMoves() {
.slicingMove(),
new AttackMove(Moves.BLEAKWIND_STORM, Type.FLYING, MoveCategory.SPECIAL, 100, 80, 10, 30, 0, 8)
.attr(StormAccuracyAttr)
- .attr(StatChangeAttr, BattleStat.SPD, -1)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.WILDBOLT_STORM, Type.ELECTRIC, MoveCategory.SPECIAL, 100, 80, 10, 20, 0, 8)
@@ -8898,7 +9016,7 @@ export function initMoves() {
.target(MoveTarget.USER_AND_ALLIES)
.triageMove(),
new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8)
- .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF ], 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true)
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP),
/* Unused
new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8)
@@ -9005,7 +9123,7 @@ export function initMoves() {
.attr(TeraBlastCategoryAttr)
.attr(TeraBlastTypeAttr)
.attr(TeraBlastPowerAttr)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR))
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR))
.partial(),
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP),
@@ -9018,17 +9136,17 @@ export function initMoves() {
.attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 100))
.makesContact(false),
new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
- .attr(StatChangeAttr, BattleStat.SPDEF, -2),
+ .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9)
.makesContact(false)
.partial(),
new AttackMove(Moves.JET_PUNCH, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9)
.punchingMove(),
new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, -1, 0, 9)
- .attr(StatChangeAttr, BattleStat.ATK, 2)
- .attr(StatChangeAttr, BattleStat.DEF, -2),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], 2)
+ .attr(StatStageChangeAttr, [ Stat.DEF ], -2),
new AttackMove(Moves.SPIN_OUT, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9)
- .attr(StatChangeAttr, BattleStat.SPD, -2, true),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -2, true),
new AttackMove(Moves.POPULATION_BOMB, Type.NORMAL, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 9)
.attr(MultiHitAttr, MultiHitType._10)
.slicingMove()
@@ -9070,24 +9188,24 @@ export function initMoves() {
new StatusMove(Moves.DOODLE, Type.NORMAL, 100, 10, -1, 0, 9)
.attr(AbilityCopyAttr, true),
new SelfStatusMove(Moves.FILLET_AWAY, Type.NORMAL, -1, 10, -1, 0, 9)
- .attr(CutHpStatBoostAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], 2, 2),
+ .attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 2, 2),
new AttackMove(Moves.KOWTOW_CLEAVE, Type.DARK, MoveCategory.PHYSICAL, 85, -1, 10, -1, 0, 9)
.slicingMove(),
new AttackMove(Moves.FLOWER_TRICK, Type.GRASS, MoveCategory.PHYSICAL, 70, -1, 10, 100, 0, 9)
.attr(CritOnlyAttr)
.makesContact(false),
new AttackMove(Moves.TORCH_SONG, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
- .attr(StatChangeAttr, BattleStat.SPATK, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.soundBased(),
new AttackMove(Moves.AQUA_STEP, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9)
- .attr(StatChangeAttr, BattleStat.SPD, 1, true)
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true)
.danceMove(),
new AttackMove(Moves.RAGING_BULL, Type.NORMAL, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 9)
.attr(RagingBullTypeAttr)
.attr(RemoveScreensAttr),
new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(MoneyAttr)
- .attr(StatChangeAttr, BattleStat.SPATK, -1, true, null, true, false, MoveEffectTrigger.HIT, true)
+ .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1)
@@ -9109,17 +9227,17 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, true, false)
.target(MoveTarget.BOTH_SIDES),
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
- .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPD ], 1, true, null, true, true)
+ .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true)
.attr(RemoveArenaTrapAttr, true),
new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9)
.attr(WeatherChangeAttr, WeatherType.SNOW)
.target(MoveTarget.BOTH_SIDES),
new AttackMove(Moves.POUNCE, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 9)
- .attr(StatChangeAttr, BattleStat.SPD, -1),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1),
new AttackMove(Moves.TRAILBLAZE, Type.GRASS, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 9)
- .attr(StatChangeAttr, BattleStat.SPD, 1, true),
+ .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true),
new AttackMove(Moves.CHILLING_WATER, Type.WATER, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 9)
- .attr(StatChangeAttr, BattleStat.ATK, -1),
+ .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new AttackMove(Moves.HYPER_DRILL, Type.NORMAL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9)
.ignoresProtect(),
new AttackMove(Moves.TWIN_BEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9)
@@ -9128,7 +9246,7 @@ export function initMoves() {
.attr(HitCountPowerAttr)
.punchingMove(),
new AttackMove(Moves.ARMOR_CANNON, Type.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
- .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true),
+ .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true),
new AttackMove(Moves.BITTER_BLADE, Type.FIRE, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 9)
.attr(HitHealAttr)
.slicingMove()
@@ -9183,7 +9301,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES)
.triageMove(),
new AttackMove(Moves.SYRUP_BOMB, Type.GRASS, MoveCategory.SPECIAL, 60, 85, 10, -1, 0, 9)
- .attr(StatChangeAttr, BattleStat.SPD, -1) //Temporary
+ .attr(StatStageChangeAttr, [ Stat.SPD ], -1) //Temporary
.ballBombMove()
.partial(),
new AttackMove(Moves.IVY_CUDGEL, Type.GRASS, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 9)
@@ -9235,7 +9353,7 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.TOXIC)
);
allMoves.map(m => {
- if (m.getAttrs(StatChangeAttr).some(a => a.selfTarget && a.levels < 0)) {
+ if (m.getAttrs(StatStageChangeAttr).some(a => a.selfTarget && a.stages < 0)) {
selfStatLowerMoves.push(m.id);
}
});
diff --git a/src/data/nature.ts b/src/data/nature.ts
index 72e5bb7863c..c614be465c3 100644
--- a/src/data/nature.ts
+++ b/src/data/nature.ts
@@ -1,9 +1,9 @@
-import { Stat, getStatName } from "./pokemon-stat";
import * as Utils from "../utils";
import { TextStyle, getBBCodeFrag } from "../ui/text";
import { Nature } from "#enums/nature";
import { UiTheme } from "#enums/ui-theme";
import i18next from "i18next";
+import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#app/enums/stat";
export { Nature };
@@ -14,10 +14,9 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals
ret = i18next.t("nature:" + ret as any);
}
if (includeStatEffects) {
- const stats = Utils.getEnumValues(Stat).slice(1);
let increasedStat: Stat | null = null;
let decreasedStat: Stat | null = null;
- for (const stat of stats) {
+ for (const stat of EFFECTIVE_STATS) {
const multiplier = getNatureStatMultiplier(nature, stat);
if (multiplier > 1) {
increasedStat = stat;
@@ -28,7 +27,7 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals
const textStyle = forStarterSelect ? TextStyle.SUMMARY_ALT : TextStyle.WINDOW;
const getTextFrag = !ignoreBBCode ? (text: string, style: TextStyle) => getBBCodeFrag(text, style, uiTheme) : (text: string, style: TextStyle) => text;
if (increasedStat && decreasedStat) {
- ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${getStatName(increasedStat, true)}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${getStatName(decreasedStat, true)}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`;
+ ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${i18next.t(getShortenedStatKey(increasedStat))}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${i18next.t(getShortenedStatKey(decreasedStat))}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`;
} else {
ret = getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(-)`, textStyle);
}
diff --git a/src/data/pokemon-evolutions.ts b/src/data/pokemon-evolutions.ts
index 315e75e53e1..6479d620182 100644
--- a/src/data/pokemon-evolutions.ts
+++ b/src/data/pokemon-evolutions.ts
@@ -1,7 +1,7 @@
import { Gender } from "./gender";
import { PokeballType } from "./pokeball";
import Pokemon from "../field/pokemon";
-import { Stat } from "./pokemon-stat";
+import { Stat } from "#enums/stat";
import { Type } from "./type";
import * as Utils from "../utils";
import { SpeciesFormKey } from "./pokemon-species";
diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts
index 17f2de794ae..8930c7053a3 100644
--- a/src/data/pokemon-species.ts
+++ b/src/data/pokemon-species.ts
@@ -14,7 +14,7 @@ import { GrowthRate } from "./exp";
import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions";
import { Type } from "./type";
import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "./pokemon-level-moves";
-import { Stat } from "./pokemon-stat";
+import { Stat } from "#enums/stat";
import { Variant, VariantSet, variantColorCache, variantData } from "./variant";
export enum Region {
diff --git a/src/data/pokemon-stat.ts b/src/data/pokemon-stat.ts
deleted file mode 100644
index 16570785a62..00000000000
--- a/src/data/pokemon-stat.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Stat } from "#enums/stat";
-import i18next from "i18next";
-
-export { Stat };
-
-export function getStatName(stat: Stat, shorten: boolean = false) {
- let ret: string = "";
- switch (stat) {
- case Stat.HP:
- ret = !shorten ? i18next.t("pokemonInfo:Stat.HP") : i18next.t("pokemonInfo:Stat.HPshortened");
- break;
- case Stat.ATK:
- ret = !shorten ? i18next.t("pokemonInfo:Stat.ATK") : i18next.t("pokemonInfo:Stat.ATKshortened");
- break;
- case Stat.DEF:
- ret = !shorten ? i18next.t("pokemonInfo:Stat.DEF") : i18next.t("pokemonInfo:Stat.DEFshortened");
- break;
- case Stat.SPATK:
- ret = !shorten ? i18next.t("pokemonInfo:Stat.SPATK") : i18next.t("pokemonInfo:Stat.SPATKshortened");
- break;
- case Stat.SPDEF:
- ret = !shorten ? i18next.t("pokemonInfo:Stat.SPDEF") : i18next.t("pokemonInfo:Stat.SPDEFshortened");
- break;
- case Stat.SPD:
- ret = !shorten ? i18next.t("pokemonInfo:Stat.SPD") : i18next.t("pokemonInfo:Stat.SPDshortened");
- break;
- }
- return ret;
-}
diff --git a/src/data/temp-battle-stat.ts b/src/data/temp-battle-stat.ts
deleted file mode 100644
index 2d461a1d647..00000000000
--- a/src/data/temp-battle-stat.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { BattleStat, getBattleStatName } from "./battle-stat";
-import i18next from "i18next";
-
-export enum TempBattleStat {
- ATK,
- DEF,
- SPATK,
- SPDEF,
- SPD,
- ACC,
- CRIT
-}
-
-export function getTempBattleStatName(tempBattleStat: TempBattleStat) {
- if (tempBattleStat === TempBattleStat.CRIT) {
- return i18next.t("modifierType:TempBattleStatBoosterStatName.CRIT");
- }
- return getBattleStatName(tempBattleStat as integer as BattleStat);
-}
-
-export function getTempBattleStatBoosterItemName(tempBattleStat: TempBattleStat) {
- switch (tempBattleStat) {
- case TempBattleStat.ATK:
- return "X Attack";
- case TempBattleStat.DEF:
- return "X Defense";
- case TempBattleStat.SPATK:
- return "X Sp. Atk";
- case TempBattleStat.SPDEF:
- return "X Sp. Def";
- case TempBattleStat.SPD:
- return "X Speed";
- case TempBattleStat.ACC:
- return "X Accuracy";
- case TempBattleStat.CRIT:
- return "Dire Hit";
- }
-}
diff --git a/src/enums/stat.ts b/src/enums/stat.ts
index a40319664d6..a12d53e8559 100644
--- a/src/enums/stat.ts
+++ b/src/enums/stat.ts
@@ -1,8 +1,75 @@
+/** Enum that comprises all possible stat-related attributes, in-battle and permanent, of a Pokemon. */
export enum Stat {
+ /** Hit Points */
HP = 0,
+ /** Attack */
ATK,
+ /** Defense */
DEF,
+ /** Special Attack */
SPATK,
+ /** Special Defense */
SPDEF,
+ /** Speed */
SPD,
+ /** Accuracy */
+ ACC,
+ /** Evasiveness */
+ EVA
+}
+
+/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode PermanentStat}. */
+export const PERMANENT_STATS = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const;
+/** Type used to describe the core, permanent stats of a Pokemon. */
+export type PermanentStat = typeof PERMANENT_STATS[number];
+
+/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode EFfectiveStat}. */
+export const EFFECTIVE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const;
+/** Type used to describe the intersection of core stats and stats that have stages in battle. */
+export type EffectiveStat = typeof EFFECTIVE_STATS[number];
+
+/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode BattleStat}. */
+export const BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC, Stat.EVA ] as const;
+/** Type used to describe the stats that have stages which can be incremented and decremented in battle. */
+export type BattleStat = typeof BATTLE_STATS[number];
+
+/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode TempBattleStat}. */
+export const TEMP_BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC ] as const;
+/** Type used to describe the stats that have X item (`TEMP_STAT_STAGE_BOOSTER`) equivalents. */
+export type TempBattleStat = typeof TEMP_BATTLE_STATS[number];
+
+/**
+ * Provides the translation key corresponding to the amount of stat stages and whether those stat stages
+ * are positive or negative.
+ * @param stages the amount of stages
+ * @param isIncrease dictates a negative (`false`) or a positive (`true`) stat stage change
+ * @returns the translation key fitting the conditions described by {@linkcode stages} and {@linkcode isIncrease}
+ */
+export function getStatStageChangeDescriptionKey(stages: number, isIncrease: boolean) {
+ if (stages === 1) {
+ return isIncrease ? "battle:statRose" : "battle:statFell";
+ } else if (stages === 2) {
+ return isIncrease ? "battle:statSharplyRose" : "battle:statHarshlyFell";
+ } else if (stages <= 6) {
+ return isIncrease ? "battle:statRoseDrastically" : "battle:statSeverelyFell";
+ }
+ return isIncrease ? "battle:statWontGoAnyHigher" : "battle:statWontGoAnyLower";
+}
+
+/**
+ * Provides the translation key corresponding to a given stat which can be translated into its full name.
+ * @param stat the {@linkcode Stat} to be translated
+ * @returns the translation key corresponding to the given {@linkcode Stat}
+ */
+export function getStatKey(stat: Stat) {
+ return `pokemonInfo:Stat.${Stat[stat]}`;
+}
+
+/**
+ * Provides the translation key corresponding to a given stat which can be translated into its shortened name.
+ * @param stat the {@linkcode Stat} to be translated
+ * @returns the translation key corresponding to the given {@linkcode Stat}
+ */
+export function getShortenedStatKey(stat: PermanentStat) {
+ return `pokemonInfo:Stat.${Stat[stat]}shortened`;
}
diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts
index 4da756547bf..52c20f5f512 100644
--- a/src/field/pokemon.ts
+++ b/src/field/pokemon.ts
@@ -3,26 +3,24 @@ import BattleScene, { AnySound } from "../battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
-import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move";
+import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Constructor } from "#app/utils";
import * as Utils from "../utils";
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type";
import { getLevelTotalExp } from "../data/exp";
-import { Stat } from "../data/pokemon-stat";
-import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
+import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
+import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
import { PokeballType } from "../data/pokeball";
import { Gender } from "../data/gender";
import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
-import { BattleStat } from "../data/battle-stat";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather";
-import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
-import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
+import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
import PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui";
@@ -40,7 +38,7 @@ import Overrides from "#app/overrides";
import i18next from "i18next";
import { speciesEggMoves } from "../data/egg-moves";
import { ModifierTier } from "../modifier/modifier-tier";
-import { applyChallenges, ChallengeType } from "#app/data/challenge.js";
+import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec";
@@ -49,17 +47,17 @@ import { BerryType } from "#enums/berry-type";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
+import { getPokemonNameWithAffix } from "#app/messages";
+import { DamagePhase } from "#app/phases/damage-phase";
+import { FaintPhase } from "#app/phases/faint-phase";
+import { LearnMovePhase } from "#app/phases/learn-move-phase";
+import { MoveEffectPhase } from "#app/phases/move-effect-phase";
+import { MoveEndPhase } from "#app/phases/move-end-phase";
+import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
+import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
+import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { Challenges } from "#enums/challenges";
-import { getPokemonNameWithAffix } from "#app/messages.js";
-import { DamagePhase } from "#app/phases/damage-phase.js";
-import { FaintPhase } from "#app/phases/faint-phase.js";
-import { LearnMovePhase } from "#app/phases/learn-move-phase.js";
-import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
-import { MoveEndPhase } from "#app/phases/move-end-phase.js";
-import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase.js";
-import { StatChangePhase } from "#app/phases/stat-change-phase.js";
-import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js";
-import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase.js";
export enum FieldPosition {
CENTER,
@@ -119,6 +117,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public maskEnabled: boolean;
public maskSprite: Phaser.GameObjects.Sprite | null;
+ public usedTMs: Moves[];
+
private shinySparkle: Phaser.GameObjects.Sprite;
constructor(scene: BattleScene, x: number, y: number, species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData) {
@@ -195,6 +195,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = dataSource.fusionVariant || 0;
this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck;
+ this.usedTMs = dataSource.usedTMs ?? [];
} else {
this.id = Utils.randSeedInt(4294967296);
this.ivs = ivs || Utils.getIvsFromId(this.id);
@@ -673,49 +674,139 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
});
}
- getStat(stat: Stat): integer {
+ /**
+ * Retrieves the entire set of stats of the {@linkcode Pokemon}.
+ * @param bypassSummonData prefer actual stats (`true` by default) or in-battle overriden stats (`false`)
+ * @returns the numeric values of the {@linkcode Pokemon}'s stats
+ */
+ getStats(bypassSummonData: boolean = true): number[] {
+ if (!bypassSummonData && this.summonData?.stats) {
+ return this.summonData.stats;
+ }
+ return this.stats;
+ }
+
+ /**
+ * Retrieves the corresponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
+ * @param stat the desired {@linkcode PermanentStat}
+ * @param bypassSummonData prefer actual stats (`true` by default) or in-battle overridden stats (`false`)
+ * @returns the numeric value of the desired {@linkcode Stat}
+ */
+ getStat(stat: PermanentStat, bypassSummonData: boolean = true): number {
+ if (!bypassSummonData && this.summonData && (this.summonData.stats[stat] !== 0)) {
+ return this.summonData.stats[stat];
+ }
return this.stats[stat];
}
- getBattleStat(stat: Stat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer {
- if (stat === Stat.HP) {
- return this.getStat(Stat.HP);
- }
- const battleStat = (stat - 1) as BattleStat;
- const statLevel = new Utils.IntegerHolder(this.summonData.battleStats[battleStat]);
- if (opponent) {
- if (isCritical) {
- switch (stat) {
- case Stat.ATK:
- case Stat.SPATK:
- statLevel.value = Math.max(statLevel.value, 0);
- break;
- case Stat.DEF:
- case Stat.SPDEF:
- statLevel.value = Math.min(statLevel.value, 0);
- break;
- }
- }
- applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, false, statLevel);
- if (move) {
- applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel);
+ /**
+ * Writes the value to the corrseponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
+ *
+ * Note that this does nothing if {@linkcode value} is less than 0.
+ * @param stat the desired {@linkcode PermanentStat} to be overwritten
+ * @param value the desired numeric value
+ * @param bypassSummonData write to actual stats (`true` by default) or in-battle overridden stats (`false`)
+ */
+ setStat(stat: PermanentStat, value: number, bypassSummonData: boolean = true): void {
+ if (value >= 0) {
+ if (!bypassSummonData && this.summonData) {
+ this.summonData.stats[stat] = value;
+ } else {
+ this.stats[stat] = value;
}
}
- if (this.isPlayer()) {
- this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel);
+ }
+
+ /**
+ * Retrieves the entire set of in-battle stat stages of the {@linkcode Pokemon}.
+ * @returns the numeric values of the {@linkcode Pokemon}'s in-battle stat stages if available, a fresh stat stage array otherwise
+ */
+ getStatStages(): number[] {
+ return this.summonData ? this.summonData.statStages : [ 0, 0, 0, 0, 0, 0, 0 ];
+ }
+
+ /**
+ * Retrieves the in-battle stage of the specified {@linkcode BattleStat}.
+ * @param stat the {@linkcode BattleStat} whose stage is desired
+ * @returns the stage of the desired {@linkcode BattleStat} if available, 0 otherwise
+ */
+ getStatStage(stat: BattleStat): number {
+ return this.summonData ? this.summonData.statStages[stat - 1] : 0;
+ }
+
+ /**
+ * Writes the value to the in-battle stage of the corresponding {@linkcode BattleStat} of the {@linkcode Pokemon}.
+ *
+ * Note that, if the value is not within a range of [-6, 6], it will be forced to the closest range bound.
+ * @param stat the {@linkcode BattleStat} whose stage is to be overwritten
+ * @param value the desired numeric value
+ */
+ setStatStage(stat: BattleStat, value: number): void {
+ if (this.summonData) {
+ if (value >= -6) {
+ this.summonData.statStages[stat - 1] = Math.min(value, 6);
+ } else {
+ this.summonData.statStages[stat - 1] = Math.max(value, -6);
+ }
}
- const statValue = new Utils.NumberHolder(this.getStat(stat));
+ }
+
+ /**
+ * Retrieves the critical-hit stage considering the move used and the Pokemon
+ * who used it.
+ * @param source the {@linkcode Pokemon} who using the move
+ * @param move the {@linkcode Move} being used
+ * @returns the final critical-hit stage value
+ */
+ getCritStage(source: Pokemon, move: Move): number {
+ const critStage = new Utils.IntegerHolder(0);
+ applyMoveAttrs(HighCritAttr, source, this, move, critStage);
+ this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
+ this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
+ const bonusCrit = new Utils.BooleanHolder(false);
+ //@ts-ignore
+ if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
+ if (bonusCrit.value) {
+ critStage.value += 1;
+ }
+ }
+ const critBoostTag = source.getTag(CritBoostTag);
+ if (critBoostTag) {
+ if (critBoostTag instanceof DragonCheerTag) {
+ critStage.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1;
+ } else {
+ critStage.value += 2;
+ }
+ }
+
+ console.log(`crit stage: +${critStage.value}`);
+ return critStage.value;
+ }
+
+ /**
+ * Calculates and retrieves the final value of a stat considering any held
+ * items, move effects, opponent abilities, and whether there was a critical
+ * hit.
+ * @param stat the desired {@linkcode EffectiveStat}
+ * @param opponent the target {@linkcode Pokemon}
+ * @param move the {@linkcode Move} being used
+ * @param isCritical determines whether a critical hit has occurred or not (`false` by default)
+ * @returns the final in-battle value of a stat
+ */
+ getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer {
+ const statValue = new Utils.NumberHolder(this.getStat(stat, false));
this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
const fieldApplied = new Utils.BooleanHolder(false);
for (const pokemon of this.scene.getField(true)) {
- applyFieldBattleStatMultiplierAbAttrs(FieldMultiplyBattleStatAbAttr, pokemon, stat, statValue, this, fieldApplied);
+ applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied);
if (fieldApplied.value) {
break;
}
}
- applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue);
- let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value));
+ applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue);
+ let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, isCritical);
+
switch (stat) {
case Stat.ATK:
if (this.getTag(BattlerTagType.SLOW_START)) {
@@ -762,24 +853,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!this.stats) {
this.stats = [ 0, 0, 0, 0, 0, 0 ];
}
- const baseStats = this.getSpeciesForm().baseStats.slice(0);
- if (this.fusionSpecies) {
- const fusionBaseStats = this.getFusionSpeciesForm().baseStats;
- for (let s = 0; s < this.stats.length; s++) {
+
+ // Get and manipulate base stats
+ const baseStats = this.getSpeciesForm(true).baseStats.slice();
+ if (this.isFusion()) {
+ const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats;
+ for (const s of PERMANENT_STATS) {
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
}
} else if (this.scene.gameMode.isSplicedOnly) {
- for (let s = 0; s < this.stats.length; s++) {
+ for (const s of PERMANENT_STATS) {
baseStats[s] = Math.ceil(baseStats[s] / 2);
}
}
- this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats);
- const stats = Utils.getEnumValues(Stat);
- for (const s of stats) {
- const isHp = s === Stat.HP;
- const baseStat = baseStats[s];
- let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01);
- if (isHp) {
+ this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats);
+
+ // Using base stats, calculate and store stats one by one
+ for (const s of PERMANENT_STATS) {
+ let value = Math.floor(((2 * baseStats[s] + this.ivs[s]) * this.level) * 0.01);
+ if (s === Stat.HP) {
value = value + this.level + 10;
if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) {
value = 1;
@@ -800,7 +892,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
value = Math.max(Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](value * natureStatMultiplier.value), 1);
}
}
- this.stats[s] = value;
+
+ this.setStat(s, value);
}
}
@@ -935,7 +1028,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.metBiome === -1 && !this.scene.gameMode.isFreshStartChallenge() && !this.scene.gameMode.isDaily) {
levelMoves = this.getUnlockedEggMoves().concat(levelMoves);
}
- return levelMoves.filter(lm => !this.moveset.some(m => m?.moveId === lm));
+ if (Array.isArray(this.usedTMs) && this.usedTMs.length > 0) {
+ levelMoves = this.usedTMs.filter(m => !levelMoves.includes(m)).concat(levelMoves);
+ }
+ levelMoves = levelMoves.filter(lm => !this.moveset.some(m => m?.moveId === lm));
+ return levelMoves;
}
/**
@@ -1371,7 +1468,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const types = this.getTypes(true);
const enemyTypes = opponent.getTypes(true, true);
/** Is this Pokemon faster than the opponent? */
- const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, opponent) : this.getStat(Stat.SPD)) >= opponent.getBattleStat(Stat.SPD, this);
+ const outspeed = (this.isActive(true) ? this.getEffectiveStat(Stat.SPD, opponent) : this.getStat(Stat.SPD, false)) >= opponent.getEffectiveStat(Stat.SPD, this);
/**
* Based on how effective this Pokemon's types are offensively against the opponent's types.
* This score is increased by 25 percent if this Pokemon is faster than the opponent.
@@ -1750,7 +1847,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]);
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttrOnHit) ? 0.5 : 1)]);
// Trainers get a weight bump to stat buffing moves
- movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatChangeAttr).some(a => a.levels > 1 && a.selfTarget) ? 1.25 : 1)]);
+ movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1)]);
// Trainers get a weight decrease to multiturn moves
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1)]);
}
@@ -1762,8 +1859,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]);
// Weight damaging moves against the lower stat
- const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
- const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.ATK];
+ const atk = this.getStat(Stat.ATK);
+ const spAtk = this.getStat(Stat.SPATK);
+ const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
+ const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk;
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]);
let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight.
@@ -1949,6 +2048,48 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField();
}
+ /**
+ * Calculates the stat stage multiplier of the user against an opponent.
+ *
+ * Note that this does not apply to evasion or accuracy
+ * @see {@linkcode getAccuracyMultiplier}
+ * @param stat the desired {@linkcode EffectiveStat}
+ * @param opponent the target {@linkcode Pokemon}
+ * @param move the {@linkcode Move} being used
+ * @param isCritical determines whether a critical hit has occurred or not (`false` by default)
+ * @return the stat stage multiplier to be used for effective stat calculation
+ */
+ getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): number {
+ const statStage = new Utils.IntegerHolder(this.getStatStage(stat));
+ const ignoreStatStage = new Utils.BooleanHolder(false);
+
+ if (opponent) {
+ if (isCritical) {
+ switch (stat) {
+ case Stat.ATK:
+ case Stat.SPATK:
+ statStage.value = Math.max(statStage.value, 0);
+ break;
+ case Stat.DEF:
+ case Stat.SPDEF:
+ statStage.value = Math.min(statStage.value, 0);
+ break;
+ }
+ }
+ applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, false, stat, ignoreStatStage);
+ if (move) {
+ applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, opponent, move, ignoreStatStage);
+ }
+ }
+
+ if (!ignoreStatStage.value) {
+ const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value));
+ this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier);
+ return Math.min(statStageMultiplier.value, 4);
+ }
+ return 1;
+ }
+
/**
* Calculates the accuracy multiplier of the user against a target.
*
@@ -1965,34 +2106,38 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return 1;
}
- const userAccuracyLevel = new Utils.IntegerHolder(this.summonData.battleStats[BattleStat.ACC]);
- const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]);
+ const userAccStage = new Utils.IntegerHolder(this.getStatStage(Stat.ACC));
+ const targetEvaStage = new Utils.IntegerHolder(target.getStatStage(Stat.EVA));
- applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, false, userAccuracyLevel);
- applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, false, targetEvasionLevel);
- applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, false, targetEvasionLevel);
- applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel);
- this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel);
+ const ignoreAccStatStage = new Utils.BooleanHolder(false);
+ const ignoreEvaStatStage = new Utils.BooleanHolder(false);
+
+ applyAbAttrs(IgnoreOpponentStatStagesAbAttr, target, null, false, Stat.ACC, ignoreAccStatStage);
+ applyAbAttrs(IgnoreOpponentStatStagesAbAttr, this, null, false, Stat.EVA, ignoreEvaStatStage);
+ applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, target, sourceMove, ignoreEvaStatStage);
+
+ this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage);
+
+ userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6);
+ targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value;
if (target.findTag(t => t instanceof ExposedTag)) {
- targetEvasionLevel.value = Math.min(0, targetEvasionLevel.value);
+ targetEvaStage.value = Math.min(0, targetEvaStage.value);
}
const accuracyMultiplier = new Utils.NumberHolder(1);
- if (userAccuracyLevel.value !== targetEvasionLevel.value) {
- accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value
- ? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3
- : 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6));
+ if (userAccStage.value !== targetEvaStage.value) {
+ accuracyMultiplier.value = userAccStage.value > targetEvaStage.value
+ ? (3 + Math.min(userAccStage.value - targetEvaStage.value, 6)) / 3
+ : 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6));
}
- applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, false, sourceMove);
+ applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, false, sourceMove);
const evasionMultiplier = new Utils.NumberHolder(1);
- applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier);
+ applyStatMultiplierAbAttrs(StatMultiplierAbAttr, target, Stat.EVA, evasionMultiplier);
- accuracyMultiplier.value /= evasionMultiplier.value;
-
- return accuracyMultiplier.value;
+ return accuracyMultiplier.value / evasionMultiplier.value;
}
/**
@@ -2079,29 +2224,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (critOnly.value || critAlways) {
isCritical = true;
} else {
- const critLevel = new Utils.IntegerHolder(0);
- applyMoveAttrs(HighCritAttr, source, this, move, critLevel);
- this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critLevel);
- this.scene.applyModifiers(TempBattleStatBoosterModifier, source.isPlayer(), TempBattleStat.CRIT, critLevel);
- const bonusCrit = new Utils.BooleanHolder(false);
- //@ts-ignore
- if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
- if (bonusCrit.value) {
- critLevel.value += 1;
- }
- }
-
- const critBoostTag = source.getTag(CritBoostTag);
- if (critBoostTag) {
- if (critBoostTag instanceof DragonCheerTag) {
- critLevel.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1;
- } else {
- critLevel.value += 2;
- }
- }
-
- console.log(`crit stage: +${critLevel.value}`);
- const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))];
+ const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))];
isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance);
if (Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false;
@@ -2115,8 +2238,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
isCritical = false;
}
}
- const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical));
- const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical));
+ const sourceAtk = new Utils.IntegerHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical));
+ const targetDef = new Utils.IntegerHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical));
const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1);
applyAbAttrs(MultCritAbAttr, source, null, false, criticalMultiplier);
const screenMultiplier = new Utils.NumberHolder(1);
@@ -2527,10 +2650,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass
*/
transferSummon(source: Pokemon): void {
- const battleStats = Utils.getEnumValues(BattleStat);
- for (const stat of battleStats) {
- this.summonData.battleStats[stat] = source.summonData.battleStats[stat];
+ // Copy all stat stages
+ for (const s of BATTLE_STATS) {
+ const sourceStage = source.getStatStage(s);
+ if ((this instanceof PlayerPokemon) && (sourceStage === 6)) {
+ this.scene.validateAchv(achvs.TRANSFER_MAX_STAT_STAGE);
+ }
+ this.setStatStage(s, sourceStage);
}
+
for (const tag of source.summonData.tags) {
// bypass those can not be passed via Baton Pass
@@ -2542,9 +2670,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.summonData.tags.push(tag);
}
- if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) {
- this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT);
- }
+
this.updateInfo();
}
@@ -3357,6 +3483,7 @@ export default interface Pokemon {
export class PlayerPokemon extends Pokemon {
public compatibleTms: Moves[];
+ public usedTms: Moves[];
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData) {
super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
@@ -3380,6 +3507,7 @@ export class PlayerPokemon extends Pokemon {
}
}
this.generateCompatibleTms();
+ this.usedTms = [];
}
initBattleInfo(): void {
@@ -3638,6 +3766,9 @@ export class PlayerPokemon extends Pokemon {
newPokemon.moveset = this.moveset.slice();
newPokemon.moveset = this.copyMoveset();
newPokemon.luck = this.luck;
+ newPokemon.metLevel = this.metLevel;
+ newPokemon.metBiome = this.metBiome;
+ newPokemon.metSpecies = this.metSpecies;
newPokemon.fusionSpecies = this.fusionSpecies;
newPokemon.fusionFormIndex = this.fusionFormIndex;
newPokemon.fusionAbilityIndex = this.fusionAbilityIndex;
@@ -3717,16 +3848,17 @@ export class PlayerPokemon extends Pokemon {
this.scene.gameData.gameStats.pokemonFused++;
// Store the average HP% that each Pokemon has
- const newHpPercent = ((pokemon.hp / pokemon.stats[Stat.HP]) + (this.hp / this.stats[Stat.HP])) / 2;
+ const maxHp = this.getMaxHp();
+ const newHpPercent = ((pokemon.hp / pokemon.getMaxHp()) + (this.hp / maxHp)) / 2;
this.generateName();
this.calculateStats();
// Set this Pokemon's HP to the average % of both fusion components
- this.hp = Math.round(this.stats[Stat.HP] * newHpPercent);
+ this.hp = Math.round(maxHp * newHpPercent);
if (!this.isFainted()) {
// If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum
- this.hp = Math.min(this.hp, this.stats[Stat.HP]);
+ this.hp = Math.min(this.hp, maxHp);
this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
} else if (!pokemon.isFainted()) {
// If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero
@@ -4176,7 +4308,7 @@ export class EnemyPokemon extends Pokemon {
//console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1)));
}
- damage = hpRemainder + Math.round(segmentSize * segmentsBypassed);
+ damage = Utils.toDmgValue(this.hp - hpThreshold + segmentSize * segmentsBypassed);
clearedBossSegmentIndex = s - segmentsBypassed;
}
break;
@@ -4219,43 +4351,40 @@ export class EnemyPokemon extends Pokemon {
handleBossSegmentCleared(segmentIndex: integer): void {
while (segmentIndex - 1 < this.bossSegmentIndex) {
- let boostedStat = BattleStat.RAND;
+ // Filter out already maxed out stat stages and weigh the rest based on existing stats
+ const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6);
+ const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false));
- const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3);
- const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1));
- const statThresholds: integer[] = [];
+ let boostedStat: EffectiveStat;
+ const statThresholds: number[] = [];
let totalWeight = 0;
- for (const bs of battleStats) {
- totalWeight += statWeights[bs];
+
+ for (const i in statWeights) {
+ totalWeight += statWeights[i];
statThresholds.push(totalWeight);
}
+ // Pick a random stat from the leftover stats to increase its stages
const randInt = Utils.randSeedInt(totalWeight);
-
- for (const bs of battleStats) {
- if (randInt < statThresholds[bs]) {
- boostedStat = bs;
+ for (const i in statThresholds) {
+ if (randInt < statThresholds[i]) {
+ boostedStat = leftoverStats[i];
break;
}
}
- let statLevels = 1;
+ let stages = 1;
- switch (segmentIndex) {
- case 1:
- if (this.bossSegments >= 3) {
- statLevels++;
- }
- break;
- case 2:
- if (this.bossSegments >= 5) {
- statLevels++;
- }
- break;
+ // increase the boost if the boss has at least 3 segments and we passed last shield
+ if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) {
+ stages++;
+ }
+ // increase the boost if the boss has at least 5 segments and we passed the second to last shield
+ if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) {
+ stages++;
}
- this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true));
-
+ this.scene.unshiftPhase(new StatStageChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat! ], stages, true, true));
this.bossSegmentIndex--;
}
}
@@ -4331,7 +4460,7 @@ export interface AttackMoveResult {
}
export class PokemonSummonData {
- public battleStats: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
+ public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
public moveQueue: QueuedMove[] = [];
public disabledMove: Moves = Moves.NONE;
public disabledTurns: number = 0;
@@ -4344,7 +4473,7 @@ export class PokemonSummonData {
public ability: Abilities = Abilities.NONE;
public gender: Gender;
public fusionGender: Gender;
- public stats: number[];
+ public stats: number[] = [ 0, 0, 0, 0, 0, 0 ];
public moveset: (PokemonMove | null)[];
// If not initialized this value will not be populated from save data.
public types: Type[] = [];
@@ -4375,8 +4504,8 @@ export class PokemonTurnData {
public damageTaken: number = 0;
public attacksReceived: AttackMoveResult[] = [];
public order: number;
- public battleStatsIncreased: boolean = false;
- public battleStatsDecreased: boolean = false;
+ public statStagesIncreased: boolean = false;
+ public statStagesDecreased: boolean = false;
}
export enum AiType {
diff --git a/src/interfaces/locales.ts b/src/interfaces/locales.ts
index 5f7c52100c1..4405095e0fe 100644
--- a/src/interfaces/locales.ts
+++ b/src/interfaces/locales.ts
@@ -37,8 +37,7 @@ export interface ModifierTypeTranslationEntries {
ModifierType: { [key: string]: ModifierTypeTranslationEntry },
SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry },
AttackTypeBoosterItem: SimpleTranslationEntries,
- TempBattleStatBoosterItem: SimpleTranslationEntries,
- TempBattleStatBoosterStatName: SimpleTranslationEntries,
+ TempStatStageBoosterItem: SimpleTranslationEntries,
BaseStatBoosterItem: SimpleTranslationEntries,
EvolutionItem: SimpleTranslationEntries,
FormChangeItem: SimpleTranslationEntries,
diff --git a/src/loading-scene.ts b/src/loading-scene.ts
index e4a73414cd1..4d67ec01ccd 100644
--- a/src/loading-scene.ts
+++ b/src/loading-scene.ts
@@ -98,6 +98,8 @@ export class LoadingScene extends SceneBase {
this.loadImage("ha_capsule", "ui", "ha_capsule.png");
this.loadImage("champion_ribbon", "ui", "champion_ribbon.png");
this.loadImage("icon_spliced", "ui");
+ this.loadImage("icon_lock", "ui", "icon_lock.png");
+ this.loadImage("icon_stop", "ui", "icon_stop.png");
this.loadImage("icon_tera", "ui");
this.loadImage("type_tera", "ui");
this.loadAtlas("type_bgs", "ui");
diff --git a/src/locales/de/achv.json b/src/locales/de/achv.json
index d2e56089720..21a1d89f9d6 100644
--- a/src/locales/de/achv.json
+++ b/src/locales/de/achv.json
@@ -89,7 +89,7 @@
"name": "Bänder-Meister",
"name_female": "Bänder-Meisterin"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "Teamwork",
"description": "Nutze Staffette, während der Anwender mindestens eines Statuswertes maximiert hat."
},
@@ -274,4 +274,4 @@
"name": "Spieglein, Spieglein an der Wand",
"description": "Schließe die 'Umkehrkampf' Herausforderung ab"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/de/dialogue-misc.json b/src/locales/de/dialogue-misc.json
index 1529831d7c5..69c704c66c6 100644
--- a/src/locales/de/dialogue-misc.json
+++ b/src/locales/de/dialogue-misc.json
@@ -1,6 +1,6 @@
{
- "ending": "@c{smile}Oh? Du hast gewonnen?@d{96} @c{smile_eclosed}Ich schätze, das hätte ich wissen sollen.\n$Aber, du bist jetzt zurück.\n$@c{smile}Es ist vorbei.@d{64} Du hast die Schleife beendet.\n$@c{serious_smile_fists}Du hast auch deinen Traum erfüllt, nicht wahr?\nDu hast nicht einmal verloren.\n$@c{neutral}Ich bin der Einzige, der sich daran erinnern wird, was du getan hast.@d{96}\n$Ich schätze, das ist in Ordnung, oder?\n$@c{serious_smile_fists}Deine Legende wird immer in unseren Herzen weiterleben.\n$@c{smile_eclosed}Wie auch immer, ich habe genug von diesem Ort, oder nicht? Lass uns nach Hause gehen.\n$@c{serious_smile_fists}Vielleicht können wir, wenn wir zurück sind, noch einen Kampf haben?\n$Wenn du dazu bereit bist.",
- "ending_female": "@c{shock}Du bist zurück?@d{32} Bedeutet das…@d{96} du hast gewonnen?!\n$@c{smile_ehalf}Ich hätte wissen sollen, dass du es in dir hast.\n$@c{smile_eclosed}Natürlich… ich hatte immer dieses Gefühl.\n$@c{smile}Es ist jetzt vorbei, richtig? Du hast die Schleife beendet.\n$@c{smile_ehalf}Du hast auch deinen Traum erfüllt, nicht wahr?\n$Du hast nicht einmal verloren.\n$Ich werde die Einzige sein, die sich daran erinnert, was du getan hast.\n$@c{angry_mopen}Ich werde versuchen, es nicht zu vergessen!\n$@c{smile_wave_wink}Nur ein Scherz!@d{64} @c{smile}Ich würde es nie vergessen.@d{32}\n$Deine Legende wird in unseren Herzen weiterleben.\n$@c{smile_wave}Wie auch immer,@d{64} es wird spät…@d{96} denke ich?\nEs ist schwer zu sagen an diesem Ort.\n$Lass uns nach Hause gehen. \n$@c{smile_wave_wink}Vielleicht können wir morgen noch einen Kampf haben, der alten Zeiten willen?",
+ "ending": "@c{shock}Du bist zurück?@d{32} Bedeutet das…@d{96} du hast gewonnen?!\n$@c{smile_ehalf}Ich hätte wissen sollen, dass du es in dir hast.\n$@c{smile_eclosed}Natürlich… ich hatte immer dieses Gefühl.\n$@c{smile}Es ist jetzt vorbei, richtig? Du hast die Schleife beendet.\n$@c{smile_ehalf}Du hast auch deinen Traum erfüllt, nicht wahr?\n$Du hast nicht einmal verloren.\n$Ich werde die Einzige sein, die sich daran erinnert, was du getan hast.\n$@c{angry_mopen}Ich werde versuchen, es nicht zu vergessen!\n$@c{smile_wave_wink}Nur ein Scherz!@d{64} @c{smile}Ich würde es nie vergessen.@d{32}\n$Deine Legende wird in unseren Herzen weiterleben.\n$@c{smile_wave}Wie auch immer,@d{64} es wird spät…@d{96} denke ich?\nEs ist schwer zu sagen an diesem Ort.\n$Lass uns nach Hause gehen. \n$@c{smile_wave_wink}Vielleicht können wir morgen noch einen Kampf haben, der alten Zeiten willen?",
+ "ending_female": "@c{smile}Oh? Du hast gewonnen?@d{96} @c{smile_eclosed}Ich schätze, das hätte ich wissen sollen.\n$Aber, du bist jetzt zurück.\n$@c{smile}Es ist vorbei.@d{64} Du hast die Schleife beendet.\n$@c{serious_smile_fists}Du hast auch deinen Traum erfüllt, nicht wahr?\nDu hast nicht einmal verloren.\n$@c{neutral}Ich bin der Einzige, der sich daran erinnern wird, was du getan hast.@d{96}\n$Ich schätze, das ist in Ordnung, oder?\n$@c{serious_smile_fists}Deine Legende wird immer in unseren Herzen weiterleben.\n$@c{smile_eclosed}Wie auch immer, ich habe genug von diesem Ort, oder nicht? Lass uns nach Hause gehen.\n$@c{serious_smile_fists}Vielleicht können wir, wenn wir zurück sind, noch einen Kampf haben?\n$Wenn du dazu bereit bist.",
"ending_endless": "Glückwunsch! Du hast das aktuelle Ende erreicht!\nWir arbeiten an mehr Spielinhalten.",
"ending_name": "Entwickler"
}
diff --git a/src/locales/de/modifier-type.json b/src/locales/de/modifier-type.json
index 9298a78614a..8e2372cb447 100644
--- a/src/locales/de/modifier-type.json
+++ b/src/locales/de/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "Verdoppelt die Wahrscheinlichkeit, dass die nächsten {{battleCount}} Begegnungen mit wilden Pokémon ein Doppelkampf sind."
},
- "TempBattleStatBoosterModifierType": {
- "description": "Erhöht die {{tempBattleStatName}} aller Teammitglieder für 5 Kämpfe um eine Stufe."
+ "TempStatStageBoosterModifierType": {
+ "description": "Erhöht die {{stat}} aller Teammitglieder für 5 Kämpfe um eine Stufe."
},
"AttackTypeBoosterModifierType": {
"description": "Erhöht die Stärke aller {{moveType}}-Attacken eines Pokémon um 20%."
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "Erhöht das Level aller Teammitglieder um {{levels}}."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "Erhöht den {{statName}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist."
+ "BaseStatBoosterModifierType": {
+ "description": "Erhöht den {{stat}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist."
},
"AllPokemonFullHpRestoreModifierType": {
"description": "Stellt 100% der KP aller Pokémon her."
@@ -248,6 +248,12 @@
"name": "Scope-Linse",
"description": "Ein Item zum Tragen. Es erhöht die Volltrefferquote."
},
+ "DIRE_HIT": {
+ "name": "X-Volltreffer",
+ "extra": {
+ "raises": "Volltrefferquote"
+ }
+ },
"LEEK": {
"name": "Lauchstange",
"description": "Ein Item, das von Porenta getragen werden kann. Diese lange Lauchstange erhöht die Volltrefferquote stark."
@@ -411,25 +417,13 @@
"description": "Ein Item, das Ditto zum Tragen gegeben werden kann. Fein und doch hart, erhöht dieses sonderbare Pulver die Initiative."
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "X-Angriff",
"x_defense": "X-Verteidigung",
"x_sp_atk": "X-Sp.-Ang.",
"x_sp_def": "X-Sp.-Vert.",
"x_speed": "X-Tempo",
- "x_accuracy": "X-Treffer",
- "dire_hit": "X-Volltreffer"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "Angriff",
- "DEF": "Verteidigung",
- "SPATK": "Sp. Ang",
- "SPDEF": "Sp. Vert",
- "SPD": "Initiative",
- "ACC": "Genauigkeit",
- "CRIT": "Volltrefferquote",
- "EVA": "Fluchtwert",
- "DEFAULT": "???"
+ "x_accuracy": "X-Treffer"
},
"AttackTypeBoosterItem": {
"silk_scarf": "Seidenschal",
@@ -606,4 +600,4 @@
"FAIRY_MEMORY": "Feen-Disc",
"NORMAL_MEMORY": "Normal-Disc"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/de/modifier.json b/src/locales/de/modifier.json
index 22053b1da63..37227973410 100644
--- a/src/locales/de/modifier.json
+++ b/src/locales/de/modifier.json
@@ -3,7 +3,7 @@
"turnHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!",
"hitHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} wurde durch {{typeName}} wiederbelebt!",
- "pokemonResetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!",
+ "resetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!",
"moneyInterestApply": "Du erhählst {{moneyAmount}} ₽ durch das Item {{typeName}}!",
"turnHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} absorbiert!",
"contactHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} geklaut!",
diff --git a/src/locales/de/move-trigger.json b/src/locales/de/move-trigger.json
index 163e8014d8b..61283c9e62e 100644
--- a/src/locales/de/move-trigger.json
+++ b/src/locales/de/move-trigger.json
@@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} nutzt seine KP um seine Attacke zu verstärken!",
"absorbedElectricity": "{{pokemonName}} absorbiert elektrische Energie!",
"switchedStatChanges": "{{pokemonName}} tauschte die Statuswerteveränderungen mit dem Ziel!",
+ "switchedTwoStatChanges": "{{pokemonName}} tauscht Veränderungen an {{firstStat}} und {{secondStat}} mit dem Ziel!",
+ "switchedStat": "{{pokemonName}} tauscht seinen {{stat}}-Wert mit dem des Zieles!",
+ "sharedGuard": "{{pokemonName}} addiert seine Schutzkräfte mit jenen des Zieles und teilt sie gerecht auf!",
+ "sharedPower": "{{pokemonName}} addiert seine Kräfte mit jenen des Zieles und teilt sie gerecht auf!",
"goingAllOutForAttack": "{{pokemonName}} legt sich ins Zeug!",
"regainedHealth": "{{pokemonName}} erholt sich!",
"keptGoingAndCrashed": "{{pokemonName}} springt daneben und verletzt sich!",
@@ -63,4 +67,4 @@
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!",
"exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!",
"safeguard": "{{targetName}} wird durch Bodyguard geschützt!"
-}
\ No newline at end of file
+}
diff --git a/src/locales/de/pokemon-form-battle.json b/src/locales/de/pokemon-form-battle.json
index 8651b3d1318..35060c33d0b 100644
--- a/src/locales/de/pokemon-form-battle.json
+++ b/src/locales/de/pokemon-form-battle.json
@@ -10,5 +10,5 @@
"eternamaxChange": "{{preName}} hat sich zu {{pokemonName}} unendynamaximiert!",
"revertChange": "{{pokemonName}} hat seine ursprüngliche Form zurückerlangt!",
"formChange": "{{preName}} hat seine Form geändert!",
- "disguiseChange": "Its disguise served it as a decoy!"
+ "disguiseChange": "Sein Kostüm hat die Attacke absorbiert!"
}
\ No newline at end of file
diff --git a/src/locales/de/pokemon-info.json b/src/locales/de/pokemon-info.json
index a559001f663..2d625d52ba7 100644
--- a/src/locales/de/pokemon-info.json
+++ b/src/locales/de/pokemon-info.json
@@ -1,7 +1,6 @@
{
"Stat": {
"HP": "KP",
- "HPStat": "KP",
"HPshortened": "KP",
"ATK": "Angriff",
"ATKshortened": "Ang",
diff --git a/src/locales/en/achv-female.json b/src/locales/en/achv-female.json
deleted file mode 100644
index edcd8c53fb7..00000000000
--- a/src/locales/en/achv-female.json
+++ /dev/null
@@ -1,268 +0,0 @@
-{
- "Achievements": {
- "name": "Achievements"
- },
- "Locked": {
- "name": "Locked"
- },
- "MoneyAchv": {
- "description": "Accumulate a total of ₽{{moneyAmount}}"
- },
- "10K_MONEY": {
- "name": "Money Haver"
- },
- "100K_MONEY": {
- "name": "Rich"
- },
- "1M_MONEY": {
- "name": "Millionaire"
- },
- "10M_MONEY": {
- "name": "One Percenter"
- },
- "DamageAchv": {
- "description": "Inflict {{damageAmount}} damage in one hit"
- },
- "250_DMG": {
- "name": "Hard Hitter"
- },
- "1000_DMG": {
- "name": "Harder Hitter"
- },
- "2500_DMG": {
- "name": "That's a Lotta Damage!"
- },
- "10000_DMG": {
- "name": "One Punch Man"
- },
- "HealAchv": {
- "description": "Heal {{healAmount}} {{HP}} at once with a move, ability, or held item"
- },
- "250_HEAL": {
- "name": "Novice Healer"
- },
- "1000_HEAL": {
- "name": "Big Healer"
- },
- "2500_HEAL": {
- "name": "Cleric"
- },
- "10000_HEAL": {
- "name": "Recovery Master"
- },
- "LevelAchv": {
- "description": "Level up a Pokémon to Lv{{level}}"
- },
- "LV_100": {
- "name": "But Wait, There's More!"
- },
- "LV_250": {
- "name": "Elite"
- },
- "LV_1000": {
- "name": "To Go Even Further Beyond"
- },
- "RibbonAchv": {
- "description": "Accumulate a total of {{ribbonAmount}} Ribbons"
- },
- "10_RIBBONS": {
- "name": "Pokémon League Champion"
- },
- "25_RIBBONS": {
- "name": "Great League Champion"
- },
- "50_RIBBONS": {
- "name": "Ultra League Champion"
- },
- "75_RIBBONS": {
- "name": "Rogue League Champion"
- },
- "100_RIBBONS": {
- "name": "Master League Champion"
- },
- "TRANSFER_MAX_BATTLE_STAT": {
- "name": "Teamwork",
- "description": "Baton pass to another party member with at least one stat maxed out"
- },
- "MAX_FRIENDSHIP": {
- "name": "Friendmaxxing",
- "description": "Reach max friendship on a Pokémon"
- },
- "MEGA_EVOLVE": {
- "name": "Megamorph",
- "description": "Mega evolve a Pokémon"
- },
- "GIGANTAMAX": {
- "name": "Absolute Unit",
- "description": "Gigantamax a Pokémon"
- },
- "TERASTALLIZE": {
- "name": "STAB Enthusiast",
- "description": "Terastallize a Pokémon"
- },
- "STELLAR_TERASTALLIZE": {
- "name": "The Hidden Type",
- "description": "Stellar Terastallize a Pokémon"
- },
- "SPLICE": {
- "name": "Infinite Fusion",
- "description": "Splice two Pokémon together with DNA Splicers"
- },
- "MINI_BLACK_HOLE": {
- "name": "A Hole Lot of Items",
- "description": "Acquire a Mini Black Hole"
- },
- "CATCH_MYTHICAL": {
- "name": "Mythical",
- "description": "Catch a mythical Pokémon"
- },
- "CATCH_SUB_LEGENDARY": {
- "name": "(Sub-)Legendary",
- "description": "Catch a sub-legendary Pokémon"
- },
- "CATCH_LEGENDARY": {
- "name": "Legendary",
- "description": "Catch a legendary Pokémon"
- },
- "SEE_SHINY": {
- "name": "Shiny",
- "description": "Find a shiny Pokémon in the wild"
- },
- "SHINY_PARTY": {
- "name": "That's Dedication",
- "description": "Have a full party of shiny Pokémon"
- },
- "HATCH_MYTHICAL": {
- "name": "Mythical Egg",
- "description": "Hatch a mythical Pokémon from an egg"
- },
- "HATCH_SUB_LEGENDARY": {
- "name": "Sub-Legendary Egg",
- "description": "Hatch a sub-legendary Pokémon from an egg"
- },
- "HATCH_LEGENDARY": {
- "name": "Legendary Egg",
- "description": "Hatch a legendary Pokémon from an egg"
- },
- "HATCH_SHINY": {
- "name": "Shiny Egg",
- "description": "Hatch a shiny Pokémon from an egg"
- },
- "HIDDEN_ABILITY": {
- "name": "Hidden Potential",
- "description": "Catch a Pokémon with a hidden ability"
- },
- "PERFECT_IVS": {
- "name": "Certificate of Authenticity",
- "description": "Get perfect IVs on a Pokémon"
- },
- "CLASSIC_VICTORY": {
- "name": "Undefeated",
- "description": "Beat the game in classic mode"
- },
- "UNEVOLVED_CLASSIC_VICTORY": {
- "name": "Bring Your Child To Work Day",
- "description": "Beat the game in Classic Mode with at least one unevolved party member."
- },
- "MONO_GEN_ONE": {
- "name": "The Original Rival",
- "description": "Complete the generation one only challenge."
- },
- "MONO_GEN_TWO": {
- "name": "Generation 1.5",
- "description": "Complete the generation two only challenge."
- },
- "MONO_GEN_THREE": {
- "name": "Too much water?",
- "description": "Complete the generation three only challenge."
- },
- "MONO_GEN_FOUR": {
- "name": "Is she really the hardest?",
- "description": "Complete the generation four only challenge."
- },
- "MONO_GEN_FIVE": {
- "name": "All Original",
- "description": "Complete the generation five only challenge."
- },
- "MONO_GEN_SIX": {
- "name": "Almost Royalty",
- "description": "Complete the generation six only challenge."
- },
- "MONO_GEN_SEVEN": {
- "name": "Only Technically",
- "description": "Complete the generation seven only challenge."
- },
- "MONO_GEN_EIGHT": {
- "name": "A Champion Time!",
- "description": "Complete the generation eight only challenge."
- },
- "MONO_GEN_NINE": {
- "name": "She was going easy on you",
- "description": "Complete the generation nine only challenge."
- },
- "MonoType": {
- "description": "Complete the {{type}} monotype challenge."
- },
- "MONO_NORMAL": {
- "name": "Extra Ordinary"
- },
- "MONO_FIGHTING": {
- "name": "I Know Kung Fu"
- },
- "MONO_FLYING": {
- "name": "Angry Birds"
- },
- "MONO_POISON": {
- "name": "Kanto's Favourite"
- },
- "MONO_GROUND": {
- "name": "Forecast: Earthquakes"
- },
- "MONO_ROCK": {
- "name": "Brock Hard"
- },
- "MONO_BUG": {
- "name": "You Like Jazz?"
- },
- "MONO_GHOST": {
- "name": "Who You Gonna Call?"
- },
- "MONO_STEEL": {
- "name": "Iron Giant"
- },
- "MONO_FIRE": {
- "name": "I Cast Fireball!"
- },
- "MONO_WATER": {
- "name": "When It Rains, It Pours"
- },
- "MONO_GRASS": {
- "name": "Can't Touch This"
- },
- "MONO_ELECTRIC": {
- "name": "Aim For The Horn!"
- },
- "MONO_PSYCHIC": {
- "name": "Big Brain Energy"
- },
- "MONO_ICE": {
- "name": "Walking On Thin Ice"
- },
- "MONO_DRAGON": {
- "name": "Pseudo-Legend Club"
- },
- "MONO_DARK": {
- "name": "It's Just A Phase"
- },
- "MONO_FAIRY": {
- "name": "Hey! Listen!"
- },
- "FRESH_START": {
- "name": "First Try!",
- "description": "Complete the Fresh Start challenge."
- },
- "INVERSE_BATTLE": {
- "name": "Mirror rorriM",
- "description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
- }
-}
\ No newline at end of file
diff --git a/src/locales/en/achv.json b/src/locales/en/achv.json
index fae786e034a..32d519fbf78 100644
--- a/src/locales/en/achv.json
+++ b/src/locales/en/achv.json
@@ -97,9 +97,9 @@
"name": "Master League Champion",
"name_female": "Master League Champion"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "Teamwork",
- "description": "Baton pass to another party member with at least one stat maxed out"
+ "description": "Baton pass to another party member with at least one stat stage maxed out"
},
"MAX_FRIENDSHIP": {
"name": "Friendmaxxing",
@@ -284,4 +284,4 @@
"name": "Mirror rorriM",
"description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/en/dialogue-misc.json b/src/locales/en/dialogue-misc.json
index f5c63a85410..2f333b5f383 100644
--- a/src/locales/en/dialogue-misc.json
+++ b/src/locales/en/dialogue-misc.json
@@ -1,6 +1,6 @@
{
- "ending": "@c{smile}Oh? You won?@d{96} @c{smile_eclosed}I guess I should've known.\nBut, you're back now.\n$@c{smile}It's over.@d{64} You ended the loop.\n$@c{serious_smile_fists}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$@c{neutral}I'm the only one who'll remember what you did.@d{96}\nI guess that's okay, isn't it?\n$@c{serious_smile_fists}Your legend will always live on in our hearts.\n$@c{smile_eclosed}Anyway, I've had about enough of this place, haven't you? Let's head home.\n$@c{serious_smile_fists}Maybe when we get back, we can have another battle?\nIf you're up to it.",
- "ending_female": "@c{shock}You're back?@d{32} Does that mean…@d{96} you won?!\n@c{smile_ehalf}I should have known you had it in you.\n$@c{smile_eclosed}Of course… I always had that feeling.\n@c{smile}It's over now, right? You ended the loop.\n$@c{smile_ehalf}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$I'll be the only one to remember what you did.\n@c{angry_mopen}I'll try not to forget!\n$@c{smile_wave_wink}Just kidding!@d{64} @c{smile}I'd never forget.@d{32}\nYour legend will live on in our hearts.\n$@c{smile_wave}Anyway,@d{64} it's getting late…@d{96} I think?\nIt's hard to tell in this place.\n$Let's go home. @c{smile_wave_wink}Maybe tomorrow, we can have another battle, for old time's sake?",
+ "ending": "@c{shock}You're back?@d{32} Does that mean…@d{96} you won?!\n@c{smile_ehalf}I should have known you had it in you.\n$@c{smile_eclosed}Of course… I always had that feeling.\n@c{smile}It's over now, right? You ended the loop.\n$@c{smile_ehalf}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$I'll be the only one to remember what you did.\n@c{angry_mopen}I'll try not to forget!\n$@c{smile_wave_wink}Just kidding!@d{64} @c{smile}I'd never forget.@d{32}\nYour legend will live on in our hearts.\n$@c{smile_wave}Anyway,@d{64} it's getting late…@d{96} I think?\nIt's hard to tell in this place.\n$Let's go home. @c{smile_wave_wink}Maybe tomorrow, we can have another battle, for old time's sake?",
+ "ending_female": "@c{smile}Oh? You won?@d{96} @c{smile_eclosed}I guess I should've known.\nBut, you're back now.\n$@c{smile}It's over.@d{64} You ended the loop.\n$@c{serious_smile_fists}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$@c{neutral}I'm the only one who'll remember what you did.@d{96}\nI guess that's okay, isn't it?\n$@c{serious_smile_fists}Your legend will always live on in our hearts.\n$@c{smile_eclosed}Anyway, I've had about enough of this place, haven't you? Let's head home.\n$@c{serious_smile_fists}Maybe when we get back, we can have another battle?\nIf you're up to it.",
"ending_endless": "Congratulations on reaching the current end!\nMore content is coming soon.",
"ending_name": "Devs"
-}
\ No newline at end of file
+}
diff --git a/src/locales/en/dialogue.json b/src/locales/en/dialogue.json
index 90b03a176d1..cd544395a27 100644
--- a/src/locales/en/dialogue.json
+++ b/src/locales/en/dialogue.json
@@ -699,6 +699,7 @@
"encounter": {
"1": "I'll fight you with all I have to wipe you out!",
"2": "I don't care if you're a kid or what. I'll send you flying if you threaten us!",
+ "2_female": "I don't care if you're a kid or what. I'll send you flying if you threaten us!",
"3": "I was told to turn away Trainers, whomever they might be!",
"4": "I'll show you the power of Aether Paradise!",
"5": "Now that you've learned of the darkness at the heart of Aether Paradise, we'll need you to conveniently disappear!"
@@ -715,11 +716,13 @@
"encounter": {
"1": "I, Branch Chief Faba, shall show you the harshness of the real world!",
"2": "The man who is called Aether Paradise's last line of defense is to battle a mere child?",
+ "2_female": "The man who is called Aether Paradise's last line of defense is to battle a mere child?",
"3": "I, Faba, am the Aether Branch Chief. The only one in the world, I'm irreplaceable."
},
"victory": {
"1": "Aiyee!",
"2": "H-h-how can this be?! How could this child...",
+ "2_female": "H-h-how can this be?! How could this child...",
"3": "This is why... This is why I can't bring myself to like children."
}
},
@@ -727,9 +730,12 @@
"encounter": {
"1": "We're not bad-we're just hard!",
"2": "You want some? That's how we say hello! Nice knowing you, punks!",
+ "2_female": "You want some? That's how we say hello! Nice knowing you, punks!",
"3": "We're just a bunch of guys and gals with a great interest in other people's Pokémon!",
"4": "Why you trying to act hard when we're already hard as bones out here, homie?",
- "5": "Team Skull represent! We can't pay the rent! Had a lot of fun, but our youth was misspent!"
+ "4_female": "Why you trying to act hard when we're already hard as bones out here, homie?",
+ "5": "Team Skull represent! We can't pay the rent! Had a lot of fun, but our youth was misspent!",
+ "5_female": "Team Skull represent! We can't pay the rent! Had a lot of fun, but our youth was misspent!"
},
"victory": {
"1": "Huh? Is it over already?",
@@ -742,11 +748,13 @@
"plumeria": {
"encounter": {
"1": " ...Hmph. You don't look like anything special to me.",
+ "1_female": " ...Hmph. You don't look like anything special to me.",
"2": "It takes these dumb Grunts way too long to deal with you kids...",
"3": "Mess with anyone in Team Skull, and I'll show you how serious I can get."
},
"victory": {
"1": "Hmmph! You're pretty strong. I'll give you that.",
+ "1_female": "Hmmph! You're pretty strong. I'll give you that.",
"2": "Hmmph. Guess you are pretty tough. Now I understand why my Grunts waste so much time battling kids.",
"3": "Hmmph! I guess I just have to hold that loss."
}
@@ -755,6 +763,7 @@
"encounter": {
"1": "It looks like this is the end of the line for you!",
"2": "You are a trainer aren't you? I'm afraid that doesn't give you the right to interfere in our work.",
+ "2_female": "You are a trainer aren't you? I'm afraid that doesn't give you the right to interfere in our work.",
"3": "I'm from Macro Cosmos Insurance! Do you have a life insurance policy?"
},
"victory": {
@@ -772,6 +781,7 @@
"victory": {
"1": "*sigh* I wasn't able to win... Oleana...you really are a hopeless woman.",
"2": "Arghhh! This is inexcusable... What was I thinking... Any trainer who's made it this far would be no pushover..",
+ "2_female": "Arghhh! This is inexcusable... What was I thinking... Any trainer who's made it this far would be no pushover..",
"3": "*sigh* I am one tired Oleana..."
}
},
diff --git a/src/locales/en/menu-ui-handler.json b/src/locales/en/menu-ui-handler.json
index fccf9cd3002..0536fa12c2e 100644
--- a/src/locales/en/menu-ui-handler.json
+++ b/src/locales/en/menu-ui-handler.json
@@ -24,6 +24,7 @@
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "Cancel",
+ "donate": "Donate",
"losingProgressionWarning": "You will lose any progress since the beginning of the battle. Proceed?",
"noEggs": "You are not hatching\nany eggs at the moment!"
}
\ No newline at end of file
diff --git a/src/locales/en/modifier-type.json b/src/locales/en/modifier-type.json
index 15b9fb8f46d..f73a3dcccae 100644
--- a/src/locales/en/modifier-type.json
+++ b/src/locales/en/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "Doubles the chance of an encounter being a double battle for {{battleCount}} battles."
},
- "TempBattleStatBoosterModifierType": {
- "description": "Increases the {{tempBattleStatName}} of all party members by 1 stage for 5 battles."
+ "TempStatStageBoosterModifierType": {
+ "description": "Increases the {{stat}} of all party members by 1 stage for 5 battles."
},
"AttackTypeBoosterModifierType": {
"description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%."
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "Increases all party members' level by {{levels}}."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "Increases the holder's base {{statName}} by 10%. The higher your IVs, the higher the stack limit."
+ "BaseStatBoosterModifierType": {
+ "description": "Increases the holder's base {{stat}} by 10%. The higher your IVs, the higher the stack limit."
},
"AllPokemonFullHpRestoreModifierType": {
"description": "Restores 100% HP for all Pokémon."
@@ -183,6 +183,7 @@
"SOOTHE_BELL": { "name": "Soothe Bell" },
"SCOPE_LENS": { "name": "Scope Lens", "description": "It's a lens for scoping out weak points. It boosts the holder's critical-hit ratio."},
+ "DIRE_HIT": { "name": "Dire Hit", "extra": { "raises": "Critical Hit Ratio" } },
"LEEK": { "name": "Leek", "description": "This very long and stiff stalk of leek boosts the critical-hit ratio of Farfetch'd's moves."},
"EVIOLITE": { "name": "Eviolite", "description": "This mysterious evolutionary lump boosts the Defense and Sp. Def stats when held by a Pokémon that can still evolve." },
@@ -250,28 +251,14 @@
"METAL_POWDER": { "name": "Metal Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Defense stat." },
"QUICK_POWDER": { "name": "Quick Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Speed stat." }
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "X Attack",
"x_defense": "X Defense",
"x_sp_atk": "X Sp. Atk",
"x_sp_def": "X Sp. Def",
"x_speed": "X Speed",
- "x_accuracy": "X Accuracy",
- "dire_hit": "Dire Hit"
+ "x_accuracy": "X Accuracy"
},
-
- "TempBattleStatBoosterStatName": {
- "ATK": "Attack",
- "DEF": "Defense",
- "SPATK": "Sp. Atk",
- "SPDEF": "Sp. Def",
- "SPD": "Speed",
- "ACC": "Accuracy",
- "CRIT": "Critical Hit Ratio",
- "EVA": "Evasiveness",
- "DEFAULT": "???"
- },
-
"AttackTypeBoosterItem": {
"silk_scarf": "Silk Scarf",
"black_belt": "Black Belt",
diff --git a/src/locales/en/modifier.json b/src/locales/en/modifier.json
index 473be0e8bfa..47944c8adb7 100644
--- a/src/locales/en/modifier.json
+++ b/src/locales/en/modifier.json
@@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!",
"hitHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} was revived\nby its {{typeName}}!",
- "pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!",
+ "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!",
"moneyInterestApply": "You received interest of ₽{{moneyAmount}}\nfrom the {{typeName}}!",
"turnHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was absorbed\nby {{pokemonName}}'s {{typeName}}!",
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was snatched\nby {{pokemonName}}'s {{typeName}}!",
diff --git a/src/locales/en/move-trigger.json b/src/locales/en/move-trigger.json
index baddbaa34bf..110d3dc68c7 100644
--- a/src/locales/en/move-trigger.json
+++ b/src/locales/en/move-trigger.json
@@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} cut its own HP to power up its move!",
"absorbedElectricity": "{{pokemonName}} absorbed electricity!",
"switchedStatChanges": "{{pokemonName}} switched stat changes with the target!",
+ "switchedTwoStatChanges": "{{pokemonName}} switched all changes to its {{firstStat}}\nand {{secondStat}} with its target!",
+ "switchedStat": "{{pokemonName}} switched {{stat}} with its target!",
+ "sharedGuard": "{{pokemonName}} shared its guard with the target!",
+ "sharedPower": "{{pokemonName}} shared its power with the target!",
"goingAllOutForAttack": "{{pokemonName}} is going all out for this attack!",
"regainedHealth": "{{pokemonName}} regained\nhealth!",
"keptGoingAndCrashed": "{{pokemonName}} kept going\nand crashed!",
diff --git a/src/locales/en/pokemon-info.json b/src/locales/en/pokemon-info.json
index 87d2f7ad17b..b79daaed621 100644
--- a/src/locales/en/pokemon-info.json
+++ b/src/locales/en/pokemon-info.json
@@ -1,7 +1,7 @@
{
"Stat": {
"HP": "Max. HP",
- "HPshortened": "MaxHP",
+ "HPshortened": "HP",
"ATK": "Attack",
"ATKshortened": "Atk",
"DEF": "Defense",
@@ -13,8 +13,7 @@
"SPD": "Speed",
"SPDshortened": "Spd",
"ACC": "Accuracy",
- "EVA": "Evasiveness",
- "HPStat": "HP"
+ "EVA": "Evasiveness"
},
"Type": {
"UNKNOWN": "Unknown",
@@ -38,4 +37,4 @@
"FAIRY": "Fairy",
"STELLAR": "Stellar"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/es/achv.json b/src/locales/es/achv.json
index c94b8858233..14501dbdb6b 100644
--- a/src/locales/es/achv.json
+++ b/src/locales/es/achv.json
@@ -91,7 +91,7 @@
"name": "Campeón Liga Master",
"name_female": "Campeona Liga Master"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "Trabajo en Equipo",
"description": "Haz relevo a otro miembro del equipo con al menos una estadística al máximo."
},
@@ -175,4 +175,4 @@
"name": "Espejo ojepsE",
"description": "Completa el reto de Combate Inverso.\n.osrevnI etabmoC ed oter le atelpmoC"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/es/modifier-type.json b/src/locales/es/modifier-type.json
index 9c36b8da767..e18cb19244d 100644
--- a/src/locales/es/modifier-type.json
+++ b/src/locales/es/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "Duplica la posibilidad de que un encuentro sea una combate doble durante {{battleCount}} combates."
},
- "TempBattleStatBoosterModifierType": {
- "description": "Aumenta la est. {{tempBattleStatName}} de todos los miembros del equipo en 1 nivel durante 5 combates."
+ "TempStatStageBoosterModifierType": {
+ "description": "Aumenta la est. {{stat}} de todos los miembros del equipo en 1 nivel durante 5 combates."
},
"AttackTypeBoosterModifierType": {
"description": "Aumenta la potencia de los movimientos de tipo {{moveType}} de un Pokémon en un 20%."
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "Aumenta el nivel de todos los miembros del equipo en {{levels}}."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "Aumenta la est. {{statName}} base del portador en un 10%.\nCuanto mayores sean tus IVs, mayor será el límite de acumulación."
+ "BaseStatBoosterModifierType": {
+ "description": "Aumenta la est. {{stat}} base del portador en un 10%.\nCuanto mayores sean tus IVs, mayor será el límite de acumulación."
},
"AllPokemonFullHpRestoreModifierType": {
"description": "Restaura el 100% de los PS de todos los Pokémon."
@@ -248,6 +248,12 @@
"name": "Periscopio",
"description": "Aumenta la probabilidad de asestar un golpe crítico."
},
+ "DIRE_HIT": {
+ "name": "Crítico X",
+ "extra": {
+ "raises": "Critical Hit Ratio"
+ }
+ },
"LEEK": {
"name": "Puerro",
"description": "Puerro muy largo y duro que aumenta la probabilidad de asestar un golpe crítico. Debe llevarlo Farfetch'd."
@@ -411,25 +417,13 @@
"description": "Polvo muy fino, pero a la vez poderoso, que aumenta la Velocidad. Debe llevarlo Ditto."
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "Ataque X",
"x_defense": "Defensa X",
"x_sp_atk": "Ataq. Esp. X",
"x_sp_def": "Def. Esp. X",
"x_speed": "Velocidad X",
- "x_accuracy": "Precisión X",
- "dire_hit": "Crítico X"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "Ataque",
- "DEF": "Defensa",
- "SPATK": "Ataq. Esp.",
- "SPDEF": "Def. Esp.",
- "SPD": "Velocidad",
- "ACC": "Precisión",
- "CRIT": "Tasa de crítico",
- "EVA": "Evasión",
- "DEFAULT": "???"
+ "x_accuracy": "Precisión X"
},
"AttackTypeBoosterItem": {
"silk_scarf": "Pañuelo seda",
diff --git a/src/locales/es/move-trigger.json b/src/locales/es/move-trigger.json
index 52a6f86d930..f92b7950a07 100644
--- a/src/locales/es/move-trigger.json
+++ b/src/locales/es/move-trigger.json
@@ -1,4 +1,8 @@
{
+ "switchedTwoStatChanges": "{{pokemonName}} ha intercambiado los cambios en {{firstStat}} y {{secondStat}} con los del objetivo!",
+ "switchedStat": "{{pokemonName}} cambia su {{stat}} por la de su objetivo!",
+ "sharedGuard": "{{pokemonName}} suma su capacidad defensiva a la del objetivo y la reparte equitativamente!",
+ "sharedPower": "{{pokemonName}} suma su capacidad ofensiva a la del objetivo y la reparte equitativamente!",
"isChargingPower": "¡{{pokemonName}} está acumulando energía!",
"burnedItselfOut": "¡El fuego interior de {{pokemonName}} se ha extinguido!",
"startedHeatingUpBeak": "¡{{pokemonName}} empieza\na calentar su pico!",
@@ -9,4 +13,4 @@
"statEliminated": "¡Los cambios en estadísticas fueron eliminados!",
"revivalBlessing": "¡{{pokemonName}} ha revivido!",
"safeguard": "¡{{targetName}} está protegido por Velo Sagrado!"
-}
\ No newline at end of file
+}
diff --git a/src/locales/es/pokemon-form-battle.json b/src/locales/es/pokemon-form-battle.json
index 5266baa7049..d6eed9e93cc 100644
--- a/src/locales/es/pokemon-form-battle.json
+++ b/src/locales/es/pokemon-form-battle.json
@@ -10,5 +10,5 @@
"eternamaxChange": "¡{{preName}} ha eternamaxizado a {{pokemonName}}!",
"revertChange": "¡{{pokemonName}} ha revertido a su forma original!",
"formChange": "¡{{preName}} ha cambiado de forma!",
- "disguiseChange": "¡El disfraz ha actuado como señuelo!\t"
+ "disguiseChange": "¡El disfraz ha actuado como señuelo!"
}
diff --git a/src/locales/fr/achv.json b/src/locales/fr/achv.json
index 60655ae22cf..3e95f9326ca 100644
--- a/src/locales/fr/achv.json
+++ b/src/locales/fr/achv.json
@@ -92,7 +92,7 @@
"name": "Master Maitre de la Ligue",
"name_female": "Master Maitresse de la Ligue"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "Travail d’équipe",
"description": "Utiliser Relais avec au moins une statistique montée à fond."
},
diff --git a/src/locales/fr/dialogue-misc.json b/src/locales/fr/dialogue-misc.json
index 359c2dfb46b..c8c781002b9 100644
--- a/src/locales/fr/dialogue-misc.json
+++ b/src/locales/fr/dialogue-misc.json
@@ -1,6 +1,6 @@
{
- "ending": "@c{smile}Oh ? T’as gagné ?@d{96} @c{smile_eclosed}J’aurais dû le savoir.\nMais de voilà de retour.\n$@c{smile}C’est terminé.@d{64} T’as brisé ce cycle infernal.\n$@c{serious_smile_fists}T’as aussi accompli ton rêve non ?\nTu n’as pas connu la moindre défaite.\n$@c{neutral}Je suis le seul à me souvenir de ce que t’as fait.@d{96}\nJe pense que ça ira, non ?\n$@c{serious_smile_fists}Ta légende vivra à jamais dans nos cœurs.\n$@c{smile_eclosed}Bref, j’en ai un peu marre de ce endroit, pas toi ? Rentrons à la maison.\n$@c{serious_smile_fists}On se fera un p’tit combat une fois rentrés ?\nSi t’es d’accord.",
- "ending_female": "@c{shock}T’es revenu ?@d{32} Ça veut dire…@d{96} que t’as gagné ?!\n@c{smile_ehalf}J’aurais dû le savoir.\n$@c{smile_eclosed}Bien sûr… J’ai toujours eu ce sentiment.\n@c{smile}C’est fini maitenant hein ? T’as brisé ce cycle.\n$@c{smile_ehalf}T’as aussi accompli ton rêve non ?\nTu n’as pas connu la moindre défaite.\n$Je serai la seule à me souvenir de ce que t’as fait.\n@c{angry_mopen}Je tâcherai de ne pas oublier !\n$@c{smile_wave_wink}J’déconne !@d{64} @c{smile}Jamais j’oublierai.@d{32}\nTa légende vivra à jamais dans nos cœurs.\n$@c{smile_wave}Bon,@d{64} il se fait tard…@d{96} je crois ?\nDifficile à dire ici.\n$Rentrons, @c{smile_wave_wink}et demain on se fera un p’tit combat, comme au bon vieux temps ?",
+ "ending": "@c{shock}T’es revenu ?@d{32} Ça veut dire…@d{96} que t’as gagné ?!\n@c{smile_ehalf}J’aurais dû m’en douter.\n$@c{smile_eclosed}Bien sûr… J’ai toujours eu ce sentiment.\n@c{smile}C’est fini maintenant hein ? T’as brisé ce cycle.\n$@c{smile_ehalf}T’as aussi accompli ton rêve non ?\nTu n’as pas connu la moindre défaite.\n$Je serai la seule à me souvenir de ce que t’as fait.\n@c{angry_mopen}Je tâcherai de ne pas oublier !\n$@c{smile_wave_wink}J’déconne !@d{64} @c{smile}Jamais j’oublierai.@d{32}\nTa légende vivra à jamais dans nos cœurs.\n$@c{smile_wave}Bon,@d{64} il se fait tard…@d{96} je crois ?\nDifficile à dire ici.\n$Rentrons, @c{smile_wave_wink}et demain on se fera un p’tit combat, comme au bon vieux temps ?",
+ "ending_female": "@c{smile}Oh ? T’as gagné ?@d{96} @c{smile_eclosed}J’aurais dû m’en douter.\nMais te voilà enfin de retour.\n$@c{smile}C’est terminé.@d{64} T’as brisé ce cycle infernal.\n$@c{serious_smile_fists}T’as aussi accompli ton rêve non ?\nTu n’as pas connu la moindre défaite.\n$@c{neutral}Je suis le seul à me souvenir de ce que t’as fait.@d{96}\nJe pense que ça ira, non ?\n$@c{serious_smile_fists}Ta légende vivra à jamais dans nos cœurs.\n$@c{smile_eclosed}Bref, j’en ai un peu marre de ce endroit, pas toi ? Rentrons à la maison.\n$@c{serious_smile_fists}On se fera un p’tit combat une fois rentrés ?\nSi t’es d’accord.",
"ending_endless": "Félicitations ! Vous avez atteint la fin actuelle.\nPlus de contenu à venir bientôt !",
"ending_name": "Les devs"
}
diff --git a/src/locales/fr/dialogue.json b/src/locales/fr/dialogue.json
index c9bb3c417c7..d9d13a8f1e8 100644
--- a/src/locales/fr/dialogue.json
+++ b/src/locales/fr/dialogue.json
@@ -9,7 +9,7 @@
"6": "Allez, c’est parti !",
"7": "Attention, me voilà !\nTu vas voir comment j’suis fort !",
"8": "Coucou… Tu veux voir mes bô Pokémon ?",
- "9": "Trève de mondanités. Ramène-toi quand tu le sens !",
+ "9": "Trêve de mondanités. Ramène-toi quand tu le sens !",
"10": "Baisse pas ta garde si tu veux pas pleurer d’avoir perdu face à un gamin.",
"11": "J’ai tout donné pour élever mes Pokémon. Attention à toi si tu leur fait du mal !",
"12": "Incroyable que t’y sois parvenu ! Mais la suite va pas être une partie de plaisir.",
@@ -68,7 +68,7 @@
"3": "Hum, t’es pas trop laxiste avec tes Pokémon ?\nTrop les chouchouter n’est pas bon."
},
"victory": {
- "1": "Il est primordial de nourir et développer toutes les caractéristiques de chaque Pokémon.",
+ "1": "Il est primordial de nourrir et développer toutes les caractéristiques de chaque Pokémon.",
"2": "Contrairement à moi, ces Pokémon ont un bon fond.",
"3": "Trop d’éloges peut ruiner les Pokémon et les gens."
},
@@ -229,7 +229,7 @@
"encounter": {
"1": "Ne te mets pas en travers de la Team Galaxie !",
"2": "Sois témoin de la puissance de notre technologie et du futur qui se profile !",
- "3": "Au nom de la Team Galaxie, j’éliminerai quiconque se mettera sur notre route !",
+ "3": "Au nom de la Team Galaxie, j’éliminerai quiconque se mettra sur notre route !",
"4": "Prépare ta défaite !",
"5": "J’espère que t’es prêt à te prendre une raclée de l’espace !",
"5_female": "J’espère que t’es prête à te prendre une raclée de l’espace !"
@@ -244,7 +244,7 @@
},
"plasma_grunt": {
"encounter": {
- "1": "Pas de quatiers à ceux qui ne suivent pas notre idéal !",
+ "1": "Pas de quartiers à quiconque ne suit pas notre idéal !",
"2": "Si je gagne, tu relâches tous tes Pokémon !",
"3": "Si tu te mets en travers de la Team Plasma, je m’occuperai de toi personnellement !",
"4": "La Team Plasma va libérer les Pokémon de tous les humains égoïstes dans ton genre !",
@@ -275,6 +275,96 @@
"5": "J’appelle pas ça perdre, j’appelle ça échouer avec panache !"
}
},
+ "aether_grunt": {
+ "encounter": {
+ "1": "Je vais te mettre ta raclée !",
+ "2": "J’en ai rien à faire que tu sois une gosse. Tu vas tutoyer les étoiles si tu nous menaces !",
+ "2_female": "J’en ai rien à faire que tu sois une gosse. Tu vas tutoyer les étoiles si tu nous menaces !",
+ "3": "J’ai pour ordre de ne laisser passer aucun Dresseur, peu importe qui c’est !",
+ "4": "Je vais te montrer le pouvoir du Paradis Æther !",
+ "5": "Maintenant que t’es au courant de ce qu’il se passe au cœur du Paradis Æther, fais-moi une faveur et disparait !"
+ },
+ "victory": {
+ "1": "C’est plutôt toi qui devrait m’apprendre à en mettre…",
+ "2": "Pardon ? J’ai pas compris…",
+ "3": "Peu importe les ordres, jamais j’aurais pu te retenir en fait…",
+ "4": "Mhh… Il semblerait que j’ai perdu.",
+ "5": "C’est plutôt moi qui va disparaitre je crois."
+ }
+ },
+ "faba": {
+ "encounter": {
+ "1": "Moi, Directeur Saubohne, je vais te montrer de quel bois je me chauffe !",
+ "2": "Donc là, l’homme supposé être la dernière ligne défense du Paradis Æther doit affronter un mioche ?",
+ "2_female": "Donc là, l’homme supposé être la dernière ligne défense du Paradis Æther doit affronter un mioche ?",
+ "3": "S’il n’y a qu’un seul nom à retenir au sein de la Fondation Æther, c’est le mien : Saubohne !"
+ },
+ "victory": {
+ "1": "Gloups !",
+ "2": "Malheur ! J’ai perdu face à un simple enfant ?!",
+ "2_female": "Malheur ! J’ai perdu face à une simple enfant ?!",
+ "3": "J’ai HORREUR des enfants !"
+ }
+ },
+ "skull_grunt": {
+ "encounter": {
+ "1": "Oush oush ! On est pas méchants, sauf si tu viens nous allumer la mèche-han !",
+ "2": "Ce manque de respect, j’hallucine ! T’es allé trop loin, le mioche !",
+ "2_female": "Ce manque de respect, j’hallucine ! T’es allée trop loin, la mioche !",
+ "3": "On est juste des gars et des meufs normaux, on voit un Pokémon on le prend !",
+ "4": "Pourquoi tu te la joue comme ça ? C'est avec tes dents que t’vas jouer frérot.",
+ "4_female": "Pourquoi tu te la joue comme ça ? C'est avec tes dents que t’vas jouer ma reus.",
+ "5": "Cousin, écoute-nous bien ! ♪\nSe taper dessus, ça sert à rien ! ♪\n$Tu t’incrustes chez nous, ça s’fait pas ! ♪\n$Mais on est sympa, on a un plan pour toi ! ♪",
+ "5_female": "Cousine, écoute-nous bien ! ♪\nSe taper dessus, ça sert à rien ! ♪\n$Tu t’incrustes chez nous, ça s’fait pas ! ♪\n$Mais on est sympa, on a un plan pour toi ! ♪"
+ },
+ "victory": {
+ "1": "Hein ? C’est déjà terminé ?",
+ "2": "… Ça craint grave ! On s’tire !",
+ "3": "Ouais de toute on en avait pas b’soin de ton Pokémon… Ah ah…",
+ "4": "Ouh là, c’est bon, j’en demandais pas tant…",
+ "5": "On pèse plus que des Pokémon, t’entends ?\nAlors tu vas nous respecter, oush !"
+ }
+ },
+ "plumeria": {
+ "encounter": {
+ "1": "Tsk. T’es un gamin tout ce qu’il y a de plus banal, en fait.",
+ "1_female": "Tsk. T’es une gamine tout ce qu’il y a de plus banal, en fait.",
+ "2": "Abrutis de sbires. Trop incompétents pour arriver à se débarasser de gamins…",
+ "3": "Si tu touches encore à un cheveu de mes lascars, tu vas pas comprendre c’qui t’arrive !"
+ },
+ "victory": {
+ "1": "Tsk. T’es pas mauvais. J’te l’accorde.",
+ "1_female": "Tsk. T’es pas mauvaise. J’te l’accorde.",
+ "2": "Tsk. J’dois reconnaitre que t’en as dans le ventre.\n$Maintenant, j’comprends pourquoi mes gars n’arrêtent pas de se faire battre par toi.",
+ "3": "Tsk. J’crois que j'ai plus qu’à assumer ma défaite."
+ }
+ },
+ "macro_grunt": {
+ "encounter": {
+ "1": "Hop hop hop ! Terminus !",
+ "2": "T’es un Dresseur n’est-ce pas ?\n$J’ai bien peur ce que ne soit pas une excuse suffisante pour nous interrompre dans notre travail.",
+ "2_female": "T’es une Dresseuse n’est-ce pas ?\n$J’ai bien peur ce que ne soit pas une excuse suffisante pour nous interrompre dans notre travail.",
+ "3": "Je travaille à Macro Cosmos Assurances !\nBesoin d’une assurance-vie ?"
+ },
+ "victory": {
+ "1": "Je n’ai d’autre choix que respectueusement me retirer.",
+ "2": "Mon argent de poche…\nPlus qu’à manger des pâtes pour la fin du mois…",
+ "3": "Chez Macro Cosmos, rien n’est comparable à notre dévotion au travail !"
+ }
+ },
+ "oleana": {
+ "encounter": {
+ "1": "Je ne laisserai personne interférer avec les projets du président Shehroz.",
+ "2": "Je vois que vous avez su vous défaire de mes subalternes.\n$Mais assez joué. Il est temps de rentrer chez vous, maintenant.",
+ "3": "Je gagnerai en votre nom, monsieur le président."
+ },
+ "victory": {
+ "1": "*soupir* Comment ai-je fait pour perdre ainsi… ?\nJe ne suis vraiment pas à la hauteur…",
+ "2": "Ah ! Quelle erreur… Je n’aurais pas dû sous-estimer un Dresseur de ton calibre…",
+ "2_female": "Ah ! Quelle erreur… Je n’aurais pas dû sous-estimer une Dresseuse de ton calibre…",
+ "3": "*soupir* Je suis fatiguée parton…"
+ }
+ },
"rocket_boss_giovanni_1": {
"encounter": {
"1": "Bien. Je dois admettre que je suis impressionné de te voir ici !"
@@ -468,7 +558,7 @@
"4": "Voir un tel jardin rempli de fleurs est si apaisant…"
},
"victory": {
- "1": "Bien joué, c’est mértié.",
+ "1": "Bien joué, c’est mérité.",
"2": "Dommage, on s’amusait si bien…",
"3": "Oh non, le combat est terminé…",
"4": "Aaah, ça fait du bien !\nMerci, j’en avais besoin."
@@ -505,15 +595,15 @@
},
"rival": {
"encounter": {
- "1": "@c{smile}Ah, je te cherchais ! Je savais que t’étais pressé de partir, mais je m’attendais quand même à un au revoir…\n$@c{smile_eclosed}T’as finalement décidé de réaliser ton rêve ?\nJ’ai peine à y croire.\n$@c{serious_smile_fists}Vu que t’es là, ça te dis un petit combat ?\nJe voudrais quand même m’assurer que t’es prêt.\n$@c{serious_mopen_fists}Surtout ne te retiens pas et donne-moi tout ce que t’as !"
+ "1": "@c{smile}Ah, je te cherchais ! Je savais que t’étais pressée de partir, mais je m’attendais quand même à un au revoir…\n$@c{smile_eclosed}T’as finalement décidé de réaliser ton rêve ?\nJ’ai peine à y croire.\n$@c{serious_smile_fists}Vu que t’es là, ça te dis un petit combat ?\nJe voudrais quand même m’assurer que t’es prête.\n$@c{serious_mopen_fists}Surtout ne te retiens pas et donne-moi tout ce que t’as !"
},
"victory": {
- "1": "@c{shock}Wah… Tu m’as vraiment lavé.\nT’es vraiment un débutant ?\n$@c{smile}T’as peut-être eu de la chance, mais…\nPeut-être que t’arriveras jusqu’au bout du chemin.\n$D’ailleurs, le prof m’a demandé de te filer ces objets.\nIls ont l’air sympas.\n$@c{serious_smile_fists}Bonne chance à toi !"
+ "1": "@c{shock}Wah… Tu m’as vraiment lavé.\nT’es vraiment une débutante ?\n$@c{smile}T’as peut-être eu de la chance, mais…\nPeut-être que t’arriveras jusqu’au bout du chemin.\n$D’ailleurs, le prof m’a demandé de te filer ces objets.\nIls ont l’air sympas.\n$@c{serious_smile_fists}Bonne chance à toi !"
}
},
"rival_female": {
"encounter": {
- "1": "@c{smile_wave}Ah, te voilà ! Je t’ai cherché partout !\n@c{angry_mopen}On oublie de dire au revoir à sa meilleure amie ?\n$@c{smile_ehalf}T’as décidé de réaliser ton rêve, hein ?\nCe jour est donc vraiment arrivé…\n$@c{smile}Je veux bien te pardonner de m’avoir oubliée,\nà une conditon. @c{smile_wave_wink}Que tu m’affronte !\n$@c{angry_mopen}Donne tout ! Ce serait dommage que ton aventure finisse avant d’avoir commencé, hein ?"
+ "1": "@c{smile_wave}Ah, te voilà ! Je t’ai cherché partout !\n@c{angry_mopen}On oublie de dire au revoir à sa meilleure amie ?\n$@c{smile_ehalf}T’as décidé de réaliser ton rêve, hein ?\nCe jour est donc vraiment arrivé…\n$@c{smile}Je veux bien te pardonner de m’avoir oubliée,\nà une condition. @c{smile_wave_wink}Que tu m’affronte !\n$@c{angry_mopen}Donne tout ! Ce serait dommage que ton aventure finisse avant d’avoir commencé, hein ?"
},
"victory": {
"1": "@c{shock}Tu viens de commencer et t’es déjà si fort ?!@d{96}\n@c{angry}T’as triché non ? Avoue !\n$@c{smile_wave_wink}J’déconne !@d{64} @c{smile_eclosed}J’ai perdu dans les règles…\nJ’ai le sentiment que tu vas très bien t’en sortir.\n$@c{smile}D’ailleurs, le prof veut que je te donne ces quelques objets. Ils te seront utiles, pour sûr !\n$@c{smile_wave}Fais de ton mieux, comme toujours !\nJe crois fort en toi !"
@@ -521,10 +611,10 @@
},
"rival_2": {
"encounter": {
- "1": "@c{smile}Hé, toi aussi t’es là ?\n@c{smile_eclosed}Toujours invaincu, hein… ?\n$@c{serious_mopen_fists}Je sais que j’ai l’air de t’avoir suivi ici, mais c’est pas complètement vrai.\n$@c{serious_smile_fists}Pour être honnête, ça me démangeait d’avoir une revanche depuis que tu m’as battu.\n$Je me suis beaucoup entrainé, alors sois sure que je vais pas retenir mes coups cette fois.\n$@c{serious_mopen_fists}Et comme la dernière fois, ne te retiens pas !\nC’est parti !"
+ "1": "@c{smile}Hé, toi aussi t’es là ?\n@c{smile_eclosed}Toujours invaincue, hein… ?\n$@c{serious_mopen_fists}Je sais que j’ai l’air de t’avoir suivie ici, mais c’est pas complètement vrai.\n$@c{serious_smile_fists}Pour être honnête, ça me démangeait d’avoir une revanche depuis que tu m’as battu.\n$Je me suis beaucoup entrainé, alors sois sure que je vais pas retenir mes coups cette fois.\n$@c{serious_mopen_fists}Et comme la dernière fois, ne te retiens pas !\nC’est parti !"
},
"victory": {
- "1": "@c{neutral_eclosed}Oh. Je crois que j’ai trop pris la confiance.\n$@c{smile}Pas grave, c’est OK. Je me doutais que ça arriverait.\n@c{serious_mopen_fists}Je vais juste devoir encore plus m’entrainer !\n\n$@c{smile}Ah, et pas que t’aies réellement besoin d’aide, mais j’ai ça en trop sur moi qui pourrait t’intéresser.\n\n$@c{serious_smile_fists}Mais n’espère plus en avoir d’autres !\nJe peux pas passer mon temps à aider mon adversaire.\n$@c{smile}Bref, prends soin de toi et profite bien de l’évènement !"
+ "1": "@c{neutral_eclosed}Oh. Je crois que j’ai trop pris la confiance.\n$@c{smile}Pas grave, c’est OK. Je me doutais que ça arriverait.\n@c{serious_mopen_fists}Je vais juste devoir encore plus m’entrainer !\n\n$@c{smile}Ah, et pas que t’aies réellement besoin d’aide, mais j’ai ça en trop sur moi qui pourrait t’intéresser.\n\n$@c{serious_smile_fists}Mais n’espère plus en avoir d’autres !\nJe peux pas passer mon temps à aider mon adversaire.\n$@c{smile}Bref, prends soin de toi !"
}
},
"rival_2_female": {
@@ -532,7 +622,7 @@
"1": "@c{smile_wave}Hé, sympa de te croiser ici. T’as toujours l’air invaincu. @c{angry_mopen}Eh… Pas mal !\n$@c{angry_mopen}Je sais à quoi tu penses et non, je t’espionne pas.\n@c{smile_eclosed}C’est juste que j’étais aussi dans le coin.\n$@c{smile_ehalf}Heureuse pour toi, mais je veux juste te rappeler que c’est pas grave de perdre parfois.\n$@c{smile}On apprend de nos erreurs, souvent plus que si on ne connaissait que le succès.\n$@c{angry_mopen}Dans tous les cas je me suis bien entrainée pour cette revanche, t’as intérêt à tout donner !"
},
"victory": {
- "1": "@c{neutral}Je… J’étais pas encore supposée perdre…\n$@c{smile}Bon. Ça veut juste dire que je vais devoir encore plus m’entrainer !\n$@c{smile_wave}J’ai aussi ça en rab pour toi !\n@c{smile_wave_wink}Inutile de me remercier ~.\n$@c{angry_mopen}C’étaient les derniers, terminé les cadeaux après ceux-là !\n$@c{smile_wave}Allez, tiens le coup et profite bien de l’évènement !"
+ "1": "@c{neutral}Je… J’étais pas encore supposée perdre…\n$@c{smile}Bon. Ça veut juste dire que je vais devoir encore plus m’entrainer !\n$@c{smile_wave}J’ai aussi ça en rab pour toi !\n@c{smile_wave_wink}Inutile de me remercier ~.\n$@c{angry_mopen}C’étaient les derniers, terminé les cadeaux après ceux-là !\n$@c{smile_wave}Allez, tiens le coup !"
},
"defeat": {
"1": "Je suppose que c’est parfois normal de perdre…"
@@ -540,7 +630,7 @@
},
"rival_3": {
"encounter": {
- "1": "@c{smile}Hé, mais qui voilà ! Ça fait un bail.\n@c{neutral}T’es… toujours invaincu ? Incroyable.\n$@c{neutral_eclosed}Tout est devenu un peu… étrange.\nC’est plus pareil sans toi au village.\n$@c{serious}Je sais que c’est égoïste, mais j’ai besoin d’expier ça.\n@c{neutral_eclosed}Je crois que tout ça te dépasse.\n$@c{serious}Ne jamais perdre, c’est juste irréaliste.\nGrandir, c’est parfois aussi savoir perdre.\n$@c{neutral_eclosed}T’as un beau parcours, mais il y a encore tellement à venir et ça va pas s’arranger. @c{neutral}T’es prêt pour ça ?\n$@c{serious_mopen_fists}Si tu l’es, alors prouve-le."
+ "1": "@c{smile}Hé, mais qui voilà ! Ça fait un bail.\n@c{neutral}T’es… toujours invaincue ? Incroyable.\n$@c{neutral_eclosed}Tout est devenu un peu… étrange.\nC’est plus pareil sans toi au village.\n$@c{serious}Je sais que c’est égoïste, mais j’ai besoin d’expier ça.\n@c{neutral_eclosed}Je crois que tout ça te dépasse.\n$@c{serious}Ne jamais perdre, c’est juste irréaliste.\nGrandir, c’est parfois aussi savoir perdre.\n$@c{neutral_eclosed}T’as un beau parcours, mais il y a encore tellement à venir et ça va pas s’arranger. @c{neutral}T’es prête pour ça ?\n$@c{serious_mopen_fists}Si tu l’es, alors prouve-le."
},
"victory": {
"1": "@c{angry_mhalf}C’est lunaire… J’ai presque fait que m’entrainer…\nAlors pourquoi il y a encore un tel écart entre nous ?"
@@ -559,7 +649,7 @@
},
"rival_4": {
"encounter": {
- "1": "@c{neutral}Hé.\n$Je vais pas y aller par quatre chemins avec toi.\n@c{neutral_eclosed}Je suis là pour gagner. Simple, basique.\n$@c{serious_mhalf_fists}J’ai appris à maximiser tout mon potentiel en m’entrainant d’arrachepied.\n$@c{smile}C’est fou tout le temps que tu peux te dégager si tu dors pas en sacrifiant ta vie sociale.\n$@c{serious_mopen_fists}Plus rien n’a d’importance désormais, pas tant que j’aurai pas gagné.\n$@c{neutral_eclosed}J’ai atteint un stade où je ne peux plus perdre.\n@c{smile_eclosed}Je présume que ta philosophie était pas si fausse finalement.\n$@c{angry_mhalf}La défaite, c’est pour les faibles, et je ne suis plus un faible.\n$@c{serious_mopen_fists}Tiens-toi prêt."
+ "1": "@c{neutral}Hé.\n$Je vais pas y aller par quatre chemins avec toi.\n@c{neutral_eclosed}Je suis là pour gagner. Simple, basique.\n$@c{serious_mhalf_fists}J’ai appris à maximiser tout mon potentiel en m’entrainant d’arrachepied.\n$@c{smile}C’est fou tout le temps que tu peux te dégager si tu dors pas en sacrifiant ta vie sociale.\n$@c{serious_mopen_fists}Plus rien n’a d’importance désormais, pas tant que j’aurai pas gagné.\n$@c{neutral_eclosed}J’ai atteint un stade où je ne peux plus perdre.\n@c{smile_eclosed}Je présume que ta philosophie était pas si fausse finalement.\n$@c{angry_mhalf}La défaite, c’est pour les faibles, et je ne suis plus un faible.\n$@c{serious_mopen_fists}Tiens-toi prête."
},
"victory": {
"1": "@c{neutral}Que…@d{64} Qui es-tu ?"
@@ -597,7 +687,7 @@
},
"rival_6": {
"encounter": {
- "1": "@c{smile_eclosed}Nous y revoilà.\n$@c{neutral}J’ai eu du temps pour réfléchir à tout ça.\nIl y a une raison à pourquoi tout semble étrange.\n$@c{neutral_eclosed}Ton rêve, ma volonté de te battre…\nFont partie de quelque chose de plus grand.\n$@c{serious}C’est même pas à propos de moi, ni de toi… Mais du monde, @c{serious_mhalf_fists}et te repousser dans tes limites est ma mission.\n$@c{neutral_eclosed}J’ignore si je serai capable de l’accomplir, mais je ferai tout ce qui est en mon pouvoir.\n$@c{neutral}Cet endroit est terrifiant… Et pourtant il m’a l’air familier, comme si j’y avais déjà mis les pieds.\n$@c{serious_mhalf_fists}Tu ressens la même chose, pas vrai ?\n$@c{serious}… et c’est comme si quelque chose ici me parlait.\n$Comme si c’était tout ce que ce monde avait toujours connu.\n$Ces précieux moments ensemble semblent si proches ne sont rien de plus qu’un lointain souvenir.\n$@c{neutral_eclosed}D’ailleurs, qui peut dire aujourd’hui qu’ils ont pu être réels ?\n$@c{serious_mopen_fists}Il faut que tu persévères. Si tu t’arrêtes, ça n’aura jamais de fin et t’es le seul à en être capable.\n$@c{serious_smile_fists}Difficile de comprendre le sens de tout ça, je sais juste que c’est la réalité.\n$@c{serious_mopen_fists}Si tu ne parviens pas à me battre ici et maintenant, tu n’as aucune chance."
+ "1": "@c{smile_eclosed}Nous y revoilà.\n$@c{neutral}J’ai eu du temps pour réfléchir à tout ça.\nIl y a une raison à pourquoi tout semble étrange.\n$@c{neutral_eclosed}Ton rêve, ma volonté de te battre…\nFont partie de quelque chose de plus grand.\n$@c{serious}C’est même pas à propos de moi, ni de toi… Mais du monde, @c{serious_mhalf_fists}et te repousser dans tes limites est ma mission.\n$@c{neutral_eclosed}J’ignore si je serai capable de l’accomplir, mais je ferai tout ce qui est en mon pouvoir.\n$@c{neutral}Cet endroit est terrifiant… Et pourtant il m’a l’air familier, comme si j’y avais déjà mis les pieds.\n$@c{serious_mhalf_fists}Tu ressens la même chose, pas vrai ?\n$@c{serious}… et c’est comme si quelque chose ici me parlait.\n$Comme si c’était tout ce que ce monde avait toujours connu.\n$Ces précieux moments ensemble semblent si proches ne sont rien de plus qu’un lointain souvenir.\n$@c{neutral_eclosed}D’ailleurs, qui peut dire aujourd’hui qu’ils ont pu être réels ?\n$@c{serious_mopen_fists}Il faut que tu persévères. Si tu t’arrêtes, ça n’aura jamais de fin et t’es la seule à en être capable.\n$@c{serious_smile_fists}Difficile de comprendre le sens de tout ça, je sais juste que c’est la réalité.\n$@c{serious_mopen_fists}Si tu ne parviens pas à me battre ici et maintenant, tu n’as aucune chance."
},
"victory": {
"1": "@c{smile_eclosed}J’ai fait ce que j’avais à faire.\n$Promets-moi juste une chose.\n@c{smile}Après avoir réparé ce monde… Rentre à la maison."
diff --git a/src/locales/fr/modifier-type.json b/src/locales/fr/modifier-type.json
index 935deeb5f62..509a8b11112 100644
--- a/src/locales/fr/modifier-type.json
+++ b/src/locales/fr/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "Double les chances de tomber sur un combat double pendant {{battleCount}} combats."
},
- "TempBattleStatBoosterModifierType": {
- "description": "Augmente d’un cran {{tempBattleStatName}} pour toute l’équipe pendant 5 combats."
+ "TempStatStageBoosterModifierType": {
+ "description": "Augmente d’un cran {{stat}} pour toute l’équipe pendant 5 combats."
},
"AttackTypeBoosterModifierType": {
"description": "Augmente de 20% la puissance des capacités de type {{moveType}} d’un Pokémon."
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "Fait monter toute l’équipe de {{levels}} niveau·x."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "Augmente de 10% {{statName}} de base de son porteur. Plus les IV sont hauts, plus il peut en porter."
+ "BaseStatBoosterModifierType": {
+ "description": "Augmente de 10% {{stat}} de base de son porteur. Plus les IV sont hauts, plus il peut en porter."
},
"AllPokemonFullHpRestoreModifierType": {
"description": "Restaure tous les PV de toute l’équipe."
@@ -183,6 +183,7 @@
"SOOTHE_BELL": { "name": "Grelot Zen" },
"SCOPE_LENS": { "name": "Lentilscope", "description": "Une lentille qui augmente d’un cran le taux de critiques du porteur." },
+ "DIRE_HIT": { "name": "Muscle +", "extra": { "raises": "Taux de critique" } },
"LEEK": { "name": "Poireau", "description": "À faire tenir à Canarticho ou Palarticho. Un poireau très long et solide qui augmente de 2 crans le taux de critiques." },
"EVIOLITE": { "name": "Évoluroc", "description": "Augmente de 50% la Défense et Déf. Spé. si le porteur peut évoluer, 25% aux fusions dont une moitié le peut encore." },
@@ -250,28 +251,14 @@
"METAL_POWDER": { "name": "Poudre Métal", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Défense." },
"QUICK_POWDER": { "name": "Poudre Vite", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Vitesse." }
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "Attaque +",
"x_defense": "Défense +",
"x_sp_atk": "Atq. Spé. +",
"x_sp_def": "Déf. Spé. +",
"x_speed": "Vitesse +",
- "x_accuracy": "Précision +",
- "dire_hit": "Muscle +"
+ "x_accuracy": "Précision +"
},
-
- "TempBattleStatBoosterStatName": {
- "ATK": "l’Attaque",
- "DEF": "la Défense",
- "SPATK": "l’Atq. Spé.",
- "SPDEF": "la Déf. Spé.",
- "SPD": "la Vitesse",
- "ACC": "la précision",
- "CRIT": "le taux de critique",
- "EVA": "l’esquive",
- "DEFAULT": "???"
- },
-
"AttackTypeBoosterItem": {
"silk_scarf": "Mouchoir Soie",
"black_belt": "Ceinture Noire",
diff --git a/src/locales/fr/modifier.json b/src/locales/fr/modifier.json
index 8a15c9e5ddf..0ec228a22c2 100644
--- a/src/locales/fr/modifier.json
+++ b/src/locales/fr/modifier.json
@@ -3,7 +3,7 @@
"turnHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par les {{typeName}} !",
"hitHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par le {{typeName}} !",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} a repris connaissance\navec sa {{typeName}} et est prêt à se battre de nouveau !",
- "pokemonResetNegativeStatStageApply": "Les stats baissées de {{pokemonNameWithAffix}}\nsont restaurées par l’{{typeName}} !",
+ "resetNegativeStatStageApply": "Les stats baissées de {{pokemonNameWithAffix}}\nsont restaurées par l’{{typeName}} !",
"moneyInterestApply": "La {{typeName}} vous rapporte\n{{moneyAmount}} ₽ d’intérêts !",
"turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est absorbé·e\npar le {{typeName}} de {{pokemonName}} !",
"contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est volé·e\npar l’{{typeName}} de {{pokemonName}} !",
diff --git a/src/locales/fr/move-trigger.json b/src/locales/fr/move-trigger.json
index 5c814745a8e..b9bc929c619 100644
--- a/src/locales/fr/move-trigger.json
+++ b/src/locales/fr/move-trigger.json
@@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} sacrifie des PV\net augmente la puissance ses capacités !",
"absorbedElectricity": "{{pokemonName}} absorbe de l’électricité !",
"switchedStatChanges": "{{pokemonName}} permute\nles changements de stats avec ceux de sa cible !",
+ "switchedTwoStatChanges": "{{pokemonName}} permute les changements de {{firstStat} et de {{secondStat}} avec ceux de sa cible !",
+ "switchedStat": "{{pokemonName}} et sa cible échangent leur {{stat}} !",
+ "sharedGuard": "{{pokemonName}} additionne sa garde à celle de sa cible et redistribue le tout équitablement !",
+ "sharedPower": "{{pokemonName}} additionne sa force à celle de sa cible et redistribue le tout équitablement !",
"goingAllOutForAttack": "{{pokemonName}} a pris\ncette capacité au sérieux !",
"regainedHealth": "{{pokemonName}}\nrécupère des PV !",
"keptGoingAndCrashed": "{{pokemonName}}\ns’écrase au sol !",
diff --git a/src/locales/it/achv.json b/src/locales/it/achv.json
index 98e41005c46..d1607f6c548 100644
--- a/src/locales/it/achv.json
+++ b/src/locales/it/achv.json
@@ -80,7 +80,7 @@
"100_RIBBONS": {
"name": "Campione Lega Assoluta"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "Lavoro di Squadra",
"description": "Trasferisci almeno sei bonus statistiche tramite staffetta"
},
@@ -261,4 +261,4 @@
"name": "Buona la prima!",
"description": "Completa la modalità sfida 'Un nuovo inizio'."
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/it/modifier-type.json b/src/locales/it/modifier-type.json
index b466e5bb9a3..99c06bb2038 100644
--- a/src/locales/it/modifier-type.json
+++ b/src/locales/it/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "Raddoppia la possibilità di imbattersi in doppie battaglie per {{battleCount}} battaglie."
},
- "TempBattleStatBoosterModifierType": {
- "description": "Aumenta {{tempBattleStatName}} di un livello a tutti i Pokémon nel gruppo per 5 battaglie."
+ "TempStatStageBoosterModifierType": {
+ "description": "Aumenta la statistica {{stat}} di un livello\na tutti i Pokémon nel gruppo per 5 battaglie."
},
"AttackTypeBoosterModifierType": {
"description": "Aumenta la potenza delle mosse di tipo {{moveType}} del 20% per un Pokémon."
@@ -59,10 +59,10 @@
"description": "Aumenta il livello di un Pokémon di {{levels}}."
},
"AllPokemonLevelIncrementModifierType": {
- "description": "Aumenta i livell di tutti i Pokémon della squadra di {{levels}}."
+ "description": "Aumenta il livello di tutti i Pokémon della squadra di {{levels}}."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "Aumenta {{statName}} di base del possessore del 10%."
+ "BaseStatBoosterModifierType": {
+ "description": "Aumenta l'/la {{stat}} di base del possessore del 10%."
},
"AllPokemonFullHpRestoreModifierType": {
"description": "Restituisce il 100% dei PS a tutti i Pokémon."
@@ -248,6 +248,12 @@
"name": "Mirino",
"description": "Lente che aumenta la probabilità di sferrare brutti colpi."
},
+ "DIRE_HIT": {
+ "name": "Supercolpo",
+ "extra": {
+ "raises": "Tasso di brutti colpi"
+ }
+ },
"LEEK": {
"name": "Porro",
"description": "Strumento da dare a Farfetch'd. Lungo gambo di porro che aumenta la probabilità di sferrare brutti colpi."
@@ -411,25 +417,13 @@
"description": "Strumento da dare a Ditto. Questa strana polvere, fine e al contempo dura, aumenta la Velocità."
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "Attacco X",
"x_defense": "Difesa X",
"x_sp_atk": "Att. Speciale X",
"x_sp_def": "Dif. Speciale X",
"x_speed": "Velocità X",
- "x_accuracy": "Precisione X",
- "dire_hit": "Supercolpo"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "Attacco",
- "DEF": "Difesa",
- "SPATK": "Att. Speciale",
- "SPDEF": "Dif. Speciale",
- "SPD": "Velocità",
- "ACC": "Precisione",
- "CRIT": "Tasso di brutti colpi",
- "EVA": "Elusione",
- "DEFAULT": "???"
+ "x_accuracy": "Precisione X"
},
"AttackTypeBoosterItem": {
"silk_scarf": "Sciarpa seta",
@@ -606,4 +600,4 @@
"FAIRY_MEMORY": "ROM Folletto",
"NORMAL_MEMORY": "ROM Normale"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/it/modifier.json b/src/locales/it/modifier.json
index 397a1f21f9a..c42bf04bc8a 100644
--- a/src/locales/it/modifier.json
+++ b/src/locales/it/modifier.json
@@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!",
"hitHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} torna in forze\ngrazie al/alla suo/a {{typeName}}!",
- "pokemonResetNegativeStatStageApply": "La riduzione alle statistiche di {{pokemonNameWithAffix}}\nviene annullata grazie al/alla suo/a {{typeName}}!",
+ "resetNegativeStatStageApply": "La riduzione alle statistiche di {{pokemonNameWithAffix}}\nviene annullata grazie al/alla suo/a {{typeName}}!",
"moneyInterestApply": "Ricevi un interesse pari a {{moneyAmount}}₽\ngrazie al/allo/a {{typeName}}!",
"turnHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato assorbito\ndal {{typeName}} di {{pokemonName}}!",
"contactHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato rubato\nda {{pokemonName}} con {{typeName}}!",
diff --git a/src/locales/it/move-trigger.json b/src/locales/it/move-trigger.json
index 58b7b1a4c5b..785972b90f9 100644
--- a/src/locales/it/move-trigger.json
+++ b/src/locales/it/move-trigger.json
@@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} riduce i suoi PS per potenziare la sua mossa!",
"absorbedElectricity": "{{pokemonName}} assorbe elettricità!",
"switchedStatChanges": "{{pokemonName}} scambia con il bersaglio le modifiche alle statistiche!",
+ "switchedTwoStatChanges": "{{pokemonName}} scambia con il bersaglio le modifiche a {{firstStat}} e {{secondStat}}!",
+ "switchedStat": "{{pokemonName}} scambia la sua {{stat}} con quella del bersaglio!",
+ "sharedGuard": "{{pokemonName}} somma le sue capacità difensive con quelle del bersaglio e le ripartisce equamente!",
+ "sharedPower": "{{pokemonName}} somma le sue capacità offensive con quelle del bersaglio e le ripartisce equamente!",
"goingAllOutForAttack": "{{pokemonName}} fa sul serio!",
"regainedHealth": "{{pokemonName}} s'è\nripreso!",
"keptGoingAndCrashed": "{{pokemonName}} si sbilancia e\nsi schianta!",
diff --git a/src/locales/ja/achv.json b/src/locales/ja/achv.json
index 809375e5c7e..182da0aed2e 100644
--- a/src/locales/ja/achv.json
+++ b/src/locales/ja/achv.json
@@ -81,7 +81,7 @@
"100_RIBBONS": {
"name": "マスターリーグチャンピオン"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "同力",
"description": "少なくとも 一つの 能力を 最大まで あげて\n他の 手持ちポケモンに バトンタッチする"
},
diff --git a/src/locales/ja/modifier-type.json b/src/locales/ja/modifier-type.json
index 6effb1d9d82..f1fcc4d3005 100644
--- a/src/locales/ja/modifier-type.json
+++ b/src/locales/ja/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "バトル{{battleCount}}かいのあいだ ダブルバトルになるかくりつを2ばいにする"
},
- "TempBattleStatBoosterModifierType": {
- "description": "すべてのパーティメンバーの {{tempBattleStatName}}を5かいのバトルのあいだ 1だんかいあげる"
+ "TempStatStageBoosterModifierType": {
+ "description": "すべてのパーティメンバーの {{stat}}を5かいのバトルのあいだ 1だんかいあげる"
},
"AttackTypeBoosterModifierType": {
"description": "ポケモンの {{moveType}}タイプのわざのいりょくを20パーセントあげる"
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "すべてのパーティメンバーのレベルを1あげる"
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "ポケモンの{{statName}}のきほんステータスを10パーセントあげる。こたいちがたかいほどスタックのげんかいもたかくなる。"
+ "BaseStatBoosterModifierType": {
+ "description": "ポケモンの{{stat}}のきほんステータスを10パーセントあげる。こたいちがたかいほどスタックのげんかいもたかくなる。"
},
"AllPokemonFullHpRestoreModifierType": {
"description": "すべてのポケモンのHPを100パーセントかいふくする"
@@ -248,6 +248,12 @@
"name": "ピントレンズ",
"description": "弱点が 見える レンズ。持たせた ポケモンの技が 急所に 当たりやすくなる。"
},
+ "DIRE_HIT": {
+ "name": "クリティカット",
+ "extra": {
+ "raises": "きゅうしょりつ"
+ }
+ },
"LEEK": {
"name": "ながねぎ",
"description": "とても長くて 硬いクキ。カモネギに 持たせると 技が 急所に 当たりやすくなる。"
@@ -411,25 +417,13 @@
"description": "メタモンに 持たせると 素早さが あがる 不思議 粉。とても こまかくて 硬い。"
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "プラスパワー",
"x_defense": "ディフェンダー",
"x_sp_atk": "スペシャルアップ",
"x_sp_def": "スペシャルガード",
"x_speed": "スピーダー",
- "x_accuracy": "ヨクアタール",
- "dire_hit": "クリティカット"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "こうげき",
- "DEF": "ぼうぎょ",
- "SPATK": "とくこう",
- "SPDEF": "とくぼう",
- "SPD": "すばやさ",
- "ACC": "めいちゅう",
- "CRIT": "きゅうしょりつ",
- "EVA": "かいひ",
- "DEFAULT": "???"
+ "x_accuracy": "ヨクアタール"
},
"AttackTypeBoosterItem": {
"silk_scarf": "シルクのスカーフ",
@@ -569,4 +563,4 @@
"DOUSE_DRIVE": "アクアカセット",
"ULTRANECROZIUM_Z": "ウルトラネクロZ"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/ja/modifier.json b/src/locales/ja/modifier.json
index 30d5270d94f..a42a849e232 100644
--- a/src/locales/ja/modifier.json
+++ b/src/locales/ja/modifier.json
@@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!",
"hitHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 復活した!",
- "pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った!",
+ "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った!",
"moneyInterestApply": "{{typeName}}から {{moneyAmount}}円 取得した!",
"turnHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を 吸い取った!",
"contactHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を うばい取った!",
diff --git a/src/locales/ja/move-trigger.json b/src/locales/ja/move-trigger.json
index 0c404feeed6..11a327c01d7 100644
--- a/src/locales/ja/move-trigger.json
+++ b/src/locales/ja/move-trigger.json
@@ -2,7 +2,9 @@
"hitWithRecoil": "{{pokemonName}}は\nはんどうによる ダメージを うけた!",
"cutHpPowerUpMove": "{{pokemonName}}は\nたいりょくを けずって パワーぜんかい!",
"absorbedElectricity": "{{pokemonName}}は\n でんきを きゅうしゅうした!",
- "switchedStatChanges": "{{pokemonName}}は あいてと じぶんのn\nのうりょくへんかを いれかえた!",
+ "switchedStatChanges": "{{pokemonName}}は あいてと じぶんの\nのうりょくへんかを いれかえた!",
+ "sharedGuard": "{{pokemonName}}は\nおたがいのガードを シェアした!",
+ "sharedPower": "{{pokemonName}}は\nおたがいのパワーを シェアした!",
"goingAllOutForAttack": "{{pokemonName}}は\nほんきを だした!",
"regainedHealth": "{{pokemonName}}は\nたいりょくを かいふくした!",
"keptGoingAndCrashed": "いきおいあまって {{pokemonName}}は\nじめんに ぶつかった!",
@@ -59,4 +61,4 @@
"suppressAbilities": "{{pokemonName}}の とくせいが きかなくなった!",
"revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった!",
"swapArenaTags": "{{pokemonName}}は\nおたがいの ばのこうかを いれかえた!"
-}
\ No newline at end of file
+}
diff --git a/src/locales/ja/trainer-classes.json b/src/locales/ja/trainer-classes.json
index 9e26dfeeb6e..aba294fbbbd 100644
--- a/src/locales/ja/trainer-classes.json
+++ b/src/locales/ja/trainer-classes.json
@@ -1 +1,130 @@
-{}
\ No newline at end of file
+{
+ "ace_trainer": "エリートトレーナー",
+ "ace_trainer_female": "エリートトレーナー",
+ "ace_duo": "エリートコンビ",
+ "artist": "芸術家",
+ "artist_female": "芸術家",
+ "backers": "ファンクラブ",
+ "backpacker": "バックパッカー",
+ "backpacker_female": "バックパッカー",
+ "backpackers": "バックパッカーズ",
+ "baker": "ベーカリー",
+ "battle_girl": "バトルガール",
+ "beauty": "大人のおねえさん",
+ "beginners": "初心者",
+ "biker": "暴走族",
+ "black_belt": "カラテ王",
+ "breeder": "ポケモンブリーダー",
+ "breeder_female": "ポケモンブリーダー",
+ "breeders": "ブリーダーコンビ",
+ "clerk": "ビジネスマン",
+ "clerk_female": "OL",
+ "colleagues": "ビジネスパートナー",
+ "crush_kin": "格闘兄妹",
+ "cyclist": "サイクリング",
+ "cyclist_female": "サイクリング",
+ "cyclists": "サイクリングチーム",
+ "dancer": "ダンサー",
+ "dancer_female": "ダンサー",
+ "depot_agent": "鉄道員",
+ "doctor": "ドクター",
+ "doctor_female": "ドクター",
+ "firebreather": "火吹きやろう",
+ "fisherman": "釣り人",
+ "fisherman_female": "釣り人",
+ "gentleman": "ジェントルマン",
+ "guitarist": "ギタリスト",
+ "guitarist_female": "ギタリスト",
+ "harlequin": "クラウン",
+ "hiker": "山男",
+ "hooligans": "バッドチーム",
+ "hoopster": "バスケ選手",
+ "infielder": "野球選手",
+ "janitor": "清掃員",
+ "lady": "お嬢さま",
+ "lass": "ミニスカート",
+ "linebacker": "フットボーラー",
+ "maid": "メイド",
+ "madame": "マダム",
+ "medical_team": "医療チーム",
+ "musician": "ミュージシャン",
+ "hex_maniac": "オカルトマニア",
+ "nurse": "ナース",
+ "nursery_aide": "保育士",
+ "officer": "お巡りさん",
+ "parasol_lady": "パラソルおねえさん",
+ "pilot": "パイロット",
+ "pokéfan": "大好きクラブ",
+ "pokéfan_female": "大好きクラブ",
+ "pokéfan_family": "大好き夫婦",
+ "preschooler": "園児",
+ "preschooler_female": "園児",
+ "preschoolers": "園児たち",
+ "psychic": "サイキッカー",
+ "psychic_female": "サイキッカー",
+ "psychics": "サイキッ家",
+ "pokémon_ranger": "ポケモンレンジャー",
+ "pokémon_ranger_female": "ポケモンレンジャー",
+ "pokémon_rangers": "レンジャーズ",
+ "ranger": "レンジャー",
+ "restaurant_staff": "レストランスタッフ",
+ "rich": "お金持ち",
+ "rich_female": "お金持ち",
+ "rich_boy": "お坊っちゃま",
+ "rich_couple": "お二人さま",
+ "rich_kid": "ブルジョワ男子",
+ "rich_kid_female": "ブルジョワ女子",
+ "rich_kids": "ブルジョワ子達",
+ "roughneck": "スキンヘッズ",
+ "sailor": "船乗り",
+ "scientist": "研究員",
+ "scientist_female": "研究員",
+ "scientists": "研究チーム",
+ "smasher": "テニスプレイヤー",
+ "snow_worker": "冷凍作業員",
+ "snow_worker_female": "冷凍作業員",
+ "striker": "サッカー選手",
+ "school_kid": "塾帰り",
+ "school_kid_female": "塾帰り",
+ "school_kids": "塾生たち",
+ "swimmer": "海パンやろう",
+ "swimmer_female": "ビキニのおねえさん",
+ "swimmers": "水着カップル",
+ "twins": "双子ちゃん",
+ "veteran": "ベテラントレーナー",
+ "veteran_female": "ベテラントレーナー",
+ "veteran_duo": "ベテランコンビ",
+ "waiter": "ウエーター",
+ "waitress": "ウエートレス",
+ "worker": "作業員",
+ "worker_female": "作業員",
+ "workers": "作業班",
+ "youngster": "短パン小僧",
+ "rocket_grunt": "ロケット団の下っ端",
+ "rocket_grunts": " ロケット団の下っ端",
+ "rocket_grunt_female": "ロケット団の下っ端",
+ "magma_grunt": "マグマ団の下っ端",
+ "magma_grunt_female": "マグマ団の下っ端",
+ "magma_grunts": "マグマ団の下っ端",
+ "aqua_grunt": "アクア団の下っ端",
+ "aqua_grunt_female": "アクア団の下っ端",
+ "aqua_grunts": "アクア団の下っ端",
+ "galactic_grunt": "ギンガ団の下っ端",
+ "galactic_grunt_female": "ギンガ団の下っ端",
+ "galactic_grunts": "ギンガ団の下っ端",
+ "plasma_grunt": "プラスマ団の下っ端",
+ "plasma_grunt_female": "プラズマ団の下っ端",
+ "plasma_grunts": "プラズマ団の下っ端",
+ "flare_grunt": "フレア団の下っ端",
+ "flare_grunt_female": "フレア団の下っ端",
+ "flare_grunts": "フレア団の下っ端",
+ "aether_grunt": "エーテル財団の職員",
+ "aether_grunt_female": "エーテル財団の職員",
+ "aether_grunts": "エーテル財団の職員",
+ "skull_grunt": "スカル団の下っ端",
+ "skull_grunt_female": "スカル団の下っ端",
+ "skull_grunts": "スカル団の下っ端",
+ "macro_grunt": "マクロコスモスのトレーナ",
+ "macro_grunt_female": "マクロコスモスのトレーナ",
+ "macro_grunts": "マクロコスモスのトレーナ"
+}
diff --git a/src/locales/ja/trainer-names.json b/src/locales/ja/trainer-names.json
index 9e26dfeeb6e..70841734b5b 100644
--- a/src/locales/ja/trainer-names.json
+++ b/src/locales/ja/trainer-names.json
@@ -1 +1,164 @@
-{}
\ No newline at end of file
+{
+ "brock": "タケシ",
+ "misty": "カスミ",
+ "lt_surge": "マチス",
+ "erika": "エリカ",
+ "janine": "アンズ",
+ "sabrina": "ナツメ",
+ "blaine": "カツラ",
+ "giovanni": "サカキ",
+ "falkner": "ハヤト",
+ "bugsy": "ツクシ",
+ "whitney": "アカネ",
+ "morty": "マツバ",
+ "chuck": "シジマ",
+ "jasmine": "ミカン",
+ "pryce": "ヤナギ",
+ "clair": "イブキ",
+ "roxanne": "ツツジ",
+ "brawly": "トウキ",
+ "wattson": "テッセン",
+ "flannery": "アスナ",
+ "norman": "センリ",
+ "winona": "ナギ",
+ "tate": "フウ",
+ "liza": "ラン",
+ "juan": "アダン",
+ "roark": "ヒョウタ",
+ "gardenia": "ナタネ",
+ "maylene": "スモモ",
+ "crasher_wake": "マキシ",
+ "fantina": "メリッサ",
+ "byron": "トウガン",
+ "candice": "スズナ",
+ "volkner": "デンジ",
+ "cilan": "デント",
+ "chili": "ポッド",
+ "cress": "コーン",
+ "cheren": "チェレン",
+ "lenora": "アロエ",
+ "roxie": "ホミカ",
+ "burgh": "アーティ",
+ "elesa": "カミツレ",
+ "clay": "ヤーコン",
+ "skyla": "フウロ",
+ "brycen": "ハチク",
+ "drayden": "シャガ",
+ "marlon": "シズイ",
+ "viola": "ビオラ",
+ "grant": "ザクロ",
+ "korrina": "コルニ",
+ "ramos": "フクジ",
+ "clemont": "シトロン",
+ "valerie": "マーシュ",
+ "olympia": "ゴジカ",
+ "wulfric": "ウルップ",
+ "milo": "ヤロー",
+ "nessa": "ルリナ",
+ "kabu": "カブ",
+ "bea": "サイトウ",
+ "allister": "オニオン",
+ "opal": "ポプラ",
+ "bede": "ビート",
+ "gordie": "マクワ",
+ "melony": "メロン",
+ "piers": "ネズ",
+ "marnie": "マリィ",
+ "raihan": "キバナ",
+ "katy": "カエデ",
+ "brassius": "コルサ",
+ "iono": " ナンジャモ",
+ "kofu": "ハイダイ",
+ "larry": "アオキ",
+ "ryme": "ライム",
+ "tulip": "リップ",
+ "grusha": "グルーシャ",
+ "lorelei": "カンナ",
+ "bruno": "シバ",
+ "agatha": "キクコ",
+ "lance": "ワタル",
+ "will": "イツキ",
+ "koga": "キョウ",
+ "karen": "カリン",
+ "sidney": "カゲツ",
+ "phoebe": "フヨウ",
+ "glacia": "プリム",
+ "drake": "ゲンジ",
+ "aaron": "リョウ",
+ "bertha": "キクノ",
+ "flint": "オーバ",
+ "lucian": "ゴヨウ",
+ "shauntal": "シキミ",
+ "marshal": "レンブ",
+ "grimsley": "ギーマ",
+ "caitlin": "カトレア",
+ "malva": "パキラ",
+ "siebold": "ズミ",
+ "wikstrom": "ガンピ",
+ "drasna": "ドラセナ",
+ "hala": "ハラ",
+ "molayne": "マーレイン",
+ "olivia": "ライチ",
+ "acerola": "アセロラ",
+ "kahili": "カヒリ",
+ "rika": "チリ",
+ "poppy": "ポピー",
+ "hassel": "ハッサク",
+ "crispin": "アカマツ",
+ "amarys": "ネリネ",
+ "lacey": "タロ",
+ "drayton": "カキツバタ",
+ "blue": "グリーン",
+ "red": "レッド",
+ "steven": "ダイゴ",
+ "wallace": "ミクリ",
+ "cynthia": "シロナ",
+ "alder": "アデク",
+ "iris": "アイリス",
+ "diantha": "カルネ",
+ "hau": "ハウ",
+ "geeta": "オモダカ",
+ "nemona": "ネモ",
+ "kieran": "スグリ",
+ "leon": "ダンデ",
+ "rival": "フィン",
+ "rival_female": "アイヴィー",
+ "archer": "アポロ",
+ "ariana": "アテナ",
+ "proton": "ランス",
+ "petrel": "ラムダ",
+ "tabitha": "ホムラ",
+ "courtney": "カガリ",
+ "shelly": "イズミ",
+ "matt": "ウシオ",
+ "mars": "マーズ",
+ "jupiter": "ジュピター",
+ "saturn": "サターン",
+ "zinzolin": "ヴィオ",
+ "rood": "ロット",
+ "xerosic": "クセロシキ",
+ "bryony": "バラ",
+ "faba": "ザオボー",
+ "plumeria": "プルメリ",
+ "oleana": "オリーヴ",
+
+ "maxie": "マツブサ",
+ "archie": "アオギリ",
+ "cyrus": "アカギ",
+ "ghetsis": "ゲーチス",
+ "lysandre": "フラダリ",
+ "lusamine": "ルザミーネ",
+ "guzma": "グズマ",
+ "rose": "ローズ",
+
+ "blue_red_double": "グリーンとレッド",
+ "red_blue_double": "レッドとグリーン",
+ "tate_liza_double": "フウとラン",
+ "liza_tate_double": "ランとフウ",
+ "steven_wallace_double": "ダイゴとミクリ",
+ "wallace_steven_double": "ミクリとダイゴ",
+ "alder_iris_double": "アデクとアイリス",
+ "iris_alder_double": "アイリスとアデク",
+ "marnie_piers_double": "マリィとネズ",
+ "piers_marnie_double": "ネズとマリィ"
+}
diff --git a/src/locales/ja/trainer-titles.json b/src/locales/ja/trainer-titles.json
index 9e26dfeeb6e..b3829c701e5 100644
--- a/src/locales/ja/trainer-titles.json
+++ b/src/locales/ja/trainer-titles.json
@@ -1 +1,38 @@
-{}
\ No newline at end of file
+{
+ "elite_four": "四天王",
+ "elite_four_female": "四天王",
+ "gym_leader": "ジムリーダー",
+ "gym_leader_female": "ジムリーダー",
+ "gym_leader_double": "ジムリーダーコンビ",
+ "champion": "チャンピオン",
+ "champion_female": "チャンピオン",
+ "champion_double": "チャンピオンコンビ",
+ "rival": "ライバル",
+ "professor": "ポケモン博士",
+ "frontier_brain": "フロンティアブレーン",
+ "rocket_boss": "ロケット団ボス",
+ "magma_boss": "マグマ団リーダー",
+ "aqua_boss": "アクア団リーダー",
+ "galactic_boss": "ギンガ団ボス",
+ "plasma_boss": "プラズマ団ボス",
+ "flare_boss": "フレア団ボス",
+ "aether_boss": "エーテル代表",
+ "skull_boss": "スカル団ボス",
+ "macro_boss": "マクロコスモス社長",
+
+ "rocket_admin": "ロケット団幹部",
+ "rocket_admin_female": "ロケット団幹部",
+ "magma_admin": "マグマ団幹部",
+ "magma_admin_female": "マグマロケット団幹部",
+ "aqua_admin": "アクア団幹部",
+ "aqua_admin_female": "アクア団幹部",
+ "galactic_commander": "ギンガ団幹部",
+ "galactic_commander_female": "ギンガ団幹部",
+ "plasma_sage": "プラズマ団賢人",
+ "plasma_admin": "プラズマ団賢人",
+ "flare_admin": "フレア団幹部",
+ "flare_admin_female": "フレア団幹部",
+ "aether_admin": "エーテル支部長",
+ "skull_admin": "スカル団幹部",
+ "macro_admin": "マクロコスモス"
+}
diff --git a/src/locales/ko/achv.json b/src/locales/ko/achv.json
index b9fd327ef3b..9364c1c55b6 100644
--- a/src/locales/ko/achv.json
+++ b/src/locales/ko/achv.json
@@ -80,7 +80,7 @@
"100_RIBBONS": {
"name": "마스터 리그 챔피언"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "팀워크",
"description": "한 개 이상의 능력치가 최대 랭크일 때 배턴터치 사용"
},
diff --git a/src/locales/ko/dialogue-misc.json b/src/locales/ko/dialogue-misc.json
index 8445c5c4810..f24fc79ea99 100644
--- a/src/locales/ko/dialogue-misc.json
+++ b/src/locales/ko/dialogue-misc.json
@@ -1,6 +1,6 @@
{
- "ending": "@c{smile}오? 이긴거야?@d{96} @c{smile_eclosed}진즉 알았어야 했는데.\n아무튼, 돌아왔구나.\n$@c{smile}다 끝난거야.@d{64} 네가 굴레를 끝장냈어.\n$@c{serious_smile_fists}네 꿈도 이뤄졌고말야.\n진짜로 한 번도 안 졌잖아.\n$@c{neutral}기억하는 건 우리들 뿐일 모양이지만.@d{96}\n그래도, 괜찮지?\n$@c{serious_smile_fists}오늘의 일은\n너와 나의 마음 속에 항상 함께할 거야.\n$@c{smile_eclosed}여기 구경도 충분히 했으니\n이제 집에 가자.\n$@c{serious_smile_fists}되돌아가서, 다시 배틀을 할 수도 있지 않을까?\n네가 원한다면 말야.",
- "ending_female": "@c{shock}돌아왔구나?@d{32} 그 말은…@d{96} 이겼어?!\n@c{smile_ehalf}그럴 줄 알았다니까.\n$@c{smile_eclosed}물론… 언제나 느껴왔지.\n@c{smile}끝난 거, 맞지? 이 굴레를 말이야.\n$@c{smile_ehalf}네 꿈도 이뤘고 말이야.\n어떻게 한번도 안 졌대?\n$네가 한 일은 나만 기억하게 될 모양이지만.\n@c{angry_mopen}나, 안 까먹어볼 테니까!\n$@c{smile_wave_wink}농담이야!@d{64} @c{smile}절대 안 잊어버릴 거야.@d{32}\n마음 속엔 쭉 남아있을 수 있게.\n$@c{smile_wave}어쨌든,@d{64} 시간이 좀 늦었어…@d{96}\n이런 곳에서 할 말은 아닌가?\n$집에 가자. @c{smile_wave_wink}아마 내일은,\n추억을 되짚어보기 위한 배틀을 해볼 수 있을 거야.",
+ "ending": "@c{shock}돌아왔구나?@d{32} 그 말은…@d{96} 이겼어?!\n@c{smile_ehalf}그럴 줄 알았다니까.\n$@c{smile_eclosed}물론… 언제나 느껴왔지.\n@c{smile}끝난 거, 맞지? 이 굴레를 말이야.\n$@c{smile_ehalf}네 꿈도 이뤘고 말이야.\n어떻게 한번도 안 졌대?\n$네가 한 일은 나만 기억하게 될 모양이지만.\n@c{angry_mopen}나, 안 까먹어볼 테니까!\n$@c{smile_wave_wink}농담이야!@d{64} @c{smile}절대 안 잊어버릴 거야.@d{32}\n마음 속엔 쭉 남아있을 수 있게.\n$@c{smile_wave}어쨌든,@d{64} 시간이 좀 늦었어…@d{96}\n이런 곳에서 할 말은 아닌가?\n$집에 가자. @c{smile_wave_wink}아마 내일은,\n추억을 되짚어보기 위한 배틀을 해볼 수 있을 거야.",
+ "ending_female": "@c{smile}오? 이긴거야?@d{96} @c{smile_eclosed}진즉 알았어야 했는데.\n아무튼, 돌아왔구나.\n$@c{smile}다 끝난거야.@d{64} 네가 굴레를 끝장냈어.\n$@c{serious_smile_fists}네 꿈도 이뤄졌고말야.\n진짜로 한 번도 안 졌잖아.\n$@c{neutral}기억하는 건 우리들 뿐일 모양이지만.@d{96}\n그래도, 괜찮지?\n$@c{serious_smile_fists}오늘의 일은\n너와 나의 마음 속에 항상 함께할 거야.\n$@c{smile_eclosed}여기 구경도 충분히 했으니\n이제 집에 가자.\n$@c{serious_smile_fists}되돌아가서, 다시 배틀을 할 수도 있지 않을까?\n네가 원한다면 말야.",
"ending_endless": "끝에 도달하신 것을 축하드립니다!\n더 많은 컨텐츠를 기다려주세요.",
"ending_name": "Devs"
-}
\ No newline at end of file
+}
diff --git a/src/locales/ko/modifier-type.json b/src/locales/ko/modifier-type.json
index a5b3405b33f..d94837bb0d2 100644
--- a/src/locales/ko/modifier-type.json
+++ b/src/locales/ko/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "{{battleCount}}번의 배틀 동안 더블 배틀이 등장할 확률이 두 배가 된다."
},
- "TempBattleStatBoosterModifierType": {
- "description": "자신의 모든 포켓몬이 5번의 배틀 동안 {{tempBattleStatName}}[[가]] 한 단계 증가한다."
+ "TempStatStageBoosterModifierType": {
+ "description": "자신의 모든 포켓몬이 5번의 배틀 동안 {{stat}}[[가]] 한 단계 증가한다."
},
"AttackTypeBoosterModifierType": {
"description": "지니게 하면 {{moveType}}타입 기술의 위력이 20% 상승한다."
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "자신의 모든 포켓몬의 레벨이 {{levels}}만큼 상승한다."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "지니게 하면 {{statName}} 종족값을 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다."
+ "BaseStatBoosterModifierType": {
+ "description": "지니게 하면 {{stat}} 종족값을 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다."
},
"AllPokemonFullHpRestoreModifierType": {
"description": "자신의 포켓몬의 HP를 모두 회복한다."
@@ -248,6 +248,12 @@
"name": "초점렌즈",
"description": "약점이 보이는 렌즈. 지니게 한 포켓몬의 기술이 급소에 맞기 쉬워진다."
},
+ "DIRE_HIT": {
+ "name": "크리티컬커터",
+ "extra": {
+ "raises": "급소율"
+ }
+ },
"LEEK": {
"name": "대파",
"description": "매우 길고 단단한 줄기. 파오리에게 지니게 하면 기술이 급소에 맞기 쉬워진다."
@@ -411,25 +417,13 @@
"description": "메타몽에게 지니게 하면 스피드가 올라가는 이상한 가루. 매우 잘고 단단하다."
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "플러스파워",
"x_defense": "디펜드업",
"x_sp_atk": "스페셜업",
"x_sp_def": "스페셜가드",
"x_speed": "스피드업",
- "x_accuracy": "잘-맞히기",
- "dire_hit": "크리티컬커터"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "공격",
- "DEF": "방어",
- "SPATK": "특수공격",
- "SPDEF": "특수방어",
- "SPD": "스피드",
- "ACC": "명중률",
- "CRIT": "급소율",
- "EVA": "회피율",
- "DEFAULT": "???"
+ "x_accuracy": "잘-맞히기"
},
"AttackTypeBoosterItem": {
"silk_scarf": "실크스카프",
@@ -606,4 +600,4 @@
"FAIRY_MEMORY": "페어리메모리",
"NORMAL_MEMORY": "일반메모리"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/ko/move-trigger.json b/src/locales/ko/move-trigger.json
index f0e0fbd6a56..2a38bb13b0a 100644
--- a/src/locales/ko/move-trigger.json
+++ b/src/locales/ko/move-trigger.json
@@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}}[[는]]\n체력을 깎아서 자신의 기술을 강화했다!",
"absorbedElectricity": "{{pokemonName}}는(은)\n전기를 흡수했다!",
"switchedStatChanges": "{{pokemonName}}[[는]] 상대와 자신의\n능력 변화를 바꿨다!",
+ "switchedTwoStatChanges": "{{pokemonName}} 상대와 자신의 {{firstStat}}과 {{secondStat}}의 능력 변화를 바꿨다!",
+ "switchedStat": "{{pokemonName}} 서로의 {{stat}}를 교체했다!",
+ "sharedGuard": "{{pokemonName}} 서로의 가드를 셰어했다!",
+ "sharedPower": "{{pokemonName}} 서로의 파워를 셰어했다!",
"goingAllOutForAttack": "{{pokemonName}}[[는]]\n전력을 다하기 시작했다!",
"regainedHealth": "{{pokemonName}}[[는]]\n기력을 회복했다!",
"keptGoingAndCrashed": "{{pokemonName}}[[는]]\n의욕이 넘쳐서 땅에 부딪쳤다!",
@@ -63,4 +67,4 @@
"swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!",
"exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!",
"safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!"
-}
\ No newline at end of file
+}
diff --git a/src/locales/pt_BR/achv.json b/src/locales/pt_BR/achv.json
index acdec1ae306..93e982b60ea 100644
--- a/src/locales/pt_BR/achv.json
+++ b/src/locales/pt_BR/achv.json
@@ -84,7 +84,7 @@
"100_RIBBONS": {
"name": "Fita de Diamante"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "Trabalho em Equipe",
"description": "Use Baton Pass com pelo menos um atributo aumentado ao máximo"
},
@@ -269,4 +269,4 @@
"name": "A torre da derrotA",
"description": "Complete o desafio da Batalha Inversa.\n.asrevnI ahlataB ad oifased o etelpmoC"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/pt_BR/dialogue-misc.json b/src/locales/pt_BR/dialogue-misc.json
index 18eb2ba8c91..10e50aaa7e1 100644
--- a/src/locales/pt_BR/dialogue-misc.json
+++ b/src/locales/pt_BR/dialogue-misc.json
@@ -1,6 +1,6 @@
{
- "ending": "@c{smile}Oh? Você venceu?@d{96} @c{smile_eclosed}Acho que eu deveria saber.\nMas, você está de volta agora.\n$@c{smile}Acabou.@d{64} Você quebrou o ciclo.\n$@c{serious_smile_fists}Você também realizou seu sonho, não é?\nVocê não perdeu nenhuma vez.\n$@c{neutral}Eu sou o único que vai lembrar o que você fez.@d{96}\nAcho que está tudo bem, não é?\n$@c{serious_smile_fists}Sua lenda sempre viverá em nossos corações.\n$@c{smile_eclosed}Enfim, já tive o suficiente deste lugar, não é? Vamos para casa.\n$@c{serious_smile_fists}Talvez quando voltarmos, possamos ter outra batalha?\nSe você estiver disposto.",
- "ending_female": "@c{shock}Você está de volta?@d{32} Isso significa que…@d{96} você venceu?!\n@c{smile_ehalf}Eu deveria saber que você conseguiria.\n$@c{smile_eclosed}Claro… Eu sempre tive essa sensação.\n@c{smile}Acabou agora, certo? Você quebrou o ciclo.\n$@c{smile_ehalf}Você também realizou seu sonho, não foi?\nVocê não perdeu nenhuma vez.\n$Eu serei a única a lembrar o que você fez.\n@c{angry_mopen}Eu tentarei não esquecer!\n$@c{smile_wave_wink}Brincadeirinha!@d{64} @c{smile}Eu nunca esqueceria.@d{32}\nSua lenda viverá em nossos corações.\n$@c{smile_wave}De qualquer forma,@d{64} está ficando tarde…@d{96} Eu acho?\nÉ difícil dizer neste lugar.\n$Vamos para casa. @c{smile_wave_wink}Talvez amanhã possamos ter outra batalha, pelos velhos tempos?",
+ "ending": "@c{shock}Você está de volta?@d{32} Isso significa que…@d{96} você venceu?!\n@c{smile_ehalf}Eu deveria saber que você conseguiria.\n$@c{smile_eclosed}Claro… Eu sempre tive essa sensação.\n@c{smile}Acabou agora, certo? Você quebrou o ciclo.\n$@c{smile_ehalf}Você também realizou seu sonho, não foi?\nVocê não perdeu nenhuma vez.\n$Eu serei a única a lembrar o que você fez.\n@c{angry_mopen}Eu tentarei não esquecer!\n$@c{smile_wave_wink}Brincadeirinha!@d{64} @c{smile}Eu nunca esqueceria.@d{32}\nSua lenda viverá em nossos corações.\n$@c{smile_wave}De qualquer forma,@d{64} está ficando tarde…@d{96} Eu acho?\nÉ difícil dizer neste lugar.\n$Vamos para casa. @c{smile_wave_wink}Talvez amanhã possamos ter outra batalha, pelos velhos tempos?",
+ "ending_female": "@c{smile}Oh? Você venceu?@d{96} @c{smile_eclosed}Acho que eu deveria saber.\nMas, você está de volta agora.\n$@c{smile}Acabou.@d{64} Você quebrou o ciclo.\n$@c{serious_smile_fists}Você também realizou seu sonho, não é?\nVocê não perdeu nenhuma vez.\n$@c{neutral}Eu sou o único que vai lembrar o que você fez.@d{96}\nAcho que está tudo bem, não é?\n$@c{serious_smile_fists}Sua lenda sempre viverá em nossos corações.\n$@c{smile_eclosed}Enfim, já tive o suficiente deste lugar, não é? Vamos para casa.\n$@c{serious_smile_fists}Talvez quando voltarmos, possamos ter outra batalha?\nSe você estiver disposto.",
"ending_endless": "Parabéns por alcançar o final atual!\nMais conteúdo chegará em breve.",
"ending_name": "Desenvolvedores"
-}
\ No newline at end of file
+}
diff --git a/src/locales/pt_BR/modifier-type.json b/src/locales/pt_BR/modifier-type.json
index b02281a53b8..823d6b35e16 100644
--- a/src/locales/pt_BR/modifier-type.json
+++ b/src/locales/pt_BR/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "Dobra as chances de encontrar uma batalha em dupla por {{battleCount}} batalhas."
},
- "TempBattleStatBoosterModifierType": {
- "description": "Aumenta o atributo de {{tempBattleStatName}} para todos os membros da equipe por 5 batalhas."
+ "TempStatStageBoosterModifierType": {
+ "description": "Aumenta o atributo de {{stat}} para todos os membros da equipe por 5 batalhas."
},
"AttackTypeBoosterModifierType": {
"description": "Aumenta o poder dos ataques do tipo {{moveType}} de um Pokémon em 20%."
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "Aumenta em {{levels}} o nível de todos os membros da equipe."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "Aumenta o atributo base de {{statName}} em 10%. Quanto maior os IVs, maior o limite de aumento."
+ "BaseStatBoosterModifierType": {
+ "description": "Aumenta o atributo base de {{stat}} em 10%. Quanto maior os IVs, maior o limite de aumento."
},
"AllPokemonFullHpRestoreModifierType": {
"description": "Restaura totalmente os PS de todos os Pokémon."
@@ -248,6 +248,12 @@
"name": "Lentes de Mira",
"description": "Estas lentes facilitam o foco em pontos fracos. Aumenta a chance de acerto crítico de quem a segurar."
},
+ "DIRE_HIT": {
+ "name": "Direto",
+ "extra": {
+ "raises": "Chance de Acerto Crítico"
+ }
+ },
"LEEK": {
"name": "Alho-poró",
"description": "Esse talo de alho-poró muito longo e rígido aumenta a taxa de acerto crítico dos movimentos do Farfetch'd."
@@ -411,25 +417,13 @@
"description": "Extremamente fino, porém duro, este pó estranho aumenta o atributo de Velocidade de Ditto."
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "Ataque X",
"x_defense": "Defesa X",
"x_sp_atk": "Ataque Esp. X",
"x_sp_def": "Defesa Esp. X",
"x_speed": "Velocidade X",
- "x_accuracy": "Precisão X",
- "dire_hit": "Direto"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "Ataque",
- "DEF": "Defesa",
- "SPATK": "Ataque Esp.",
- "SPDEF": "Defesa Esp.",
- "SPD": "Velocidade",
- "ACC": "Precisão",
- "CRIT": "Chance de Acerto Crítico",
- "EVA": "Evasão",
- "DEFAULT": "???"
+ "x_accuracy": "Precisão X"
},
"AttackTypeBoosterItem": {
"silk_scarf": "Lenço de Seda",
diff --git a/src/locales/pt_BR/modifier.json b/src/locales/pt_BR/modifier.json
index 602a0be3a5b..38622de579e 100644
--- a/src/locales/pt_BR/modifier.json
+++ b/src/locales/pt_BR/modifier.json
@@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsuas {{typeName}}!",
"hitHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsua {{typeName}}!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} foi reanimado\npor sua {{typeName}}!",
- "pokemonResetNegativeStatStageApply": "Os atributos diminuídos de {{pokemonNameWithAffix}} foram\nrestaurados por seu(sua) {{typeName}}!",
+ "resetNegativeStatStageApply": "Os atributos diminuídos de {{pokemonNameWithAffix}} foram\nrestaurados por seu(sua) {{typeName}}!",
"moneyInterestApply": "Você recebeu um juros de ₽{{moneyAmount}}\nde sua {{typeName}}!",
"turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi absorvido(a)\npelo {{typeName}} de {{pokemonName}}!",
"contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi pego(a)\npela {{typeName}} de {{pokemonName}}!",
diff --git a/src/locales/pt_BR/move-trigger.json b/src/locales/pt_BR/move-trigger.json
index ea320412a24..9aa13dedad5 100644
--- a/src/locales/pt_BR/move-trigger.json
+++ b/src/locales/pt_BR/move-trigger.json
@@ -63,4 +63,4 @@
"swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!",
"exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!",
"safeguard": "{{targetName}} está protegido por Safeguard!"
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh_CN/achv.json b/src/locales/zh_CN/achv.json
index 8de0c48a2c3..90dfda0e3c1 100644
--- a/src/locales/zh_CN/achv.json
+++ b/src/locales/zh_CN/achv.json
@@ -86,7 +86,7 @@
"name": "大师球联盟冠军"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "团队协作",
"description": "在一项属性强化至最大时用接力棒传递给其他宝可梦"
},
diff --git a/src/locales/zh_CN/dialogue-misc.json b/src/locales/zh_CN/dialogue-misc.json
index e9ac66b7955..07aa336d4f6 100644
--- a/src/locales/zh_CN/dialogue-misc.json
+++ b/src/locales/zh_CN/dialogue-misc.json
@@ -1,4 +1,4 @@
{
- "ending": "@c{smile}哦?你赢了?@d{96} @c{smile_eclosed}我应该早猜到了\n你回来了。\n$@c{smile}结束了。@d{64} 你终结了这个循环。\n$@c{serious_smile_fists}你也完成了自己的梦想,不是吗?\n你甚至一次都没失败。\n$@c{neutral}我是唯一能够记得你所作所为的人@d{96}\n我觉得这应该也还行吧?\n$@c{serious_smile_fists}你的传奇将永远留存于我们心中。\n$@c{smile_eclosed}不管了,我真是受够这个地方了,你也一样吗?我们回家吧。\n$@c{serious_smile_fists}可能等我们回家以后,再打一场?\n要是你想的话",
- "ending_female": "@c{shock}你回来了?@d{32} 也就是说…@d{96} 你赢了呀!?\n@c{smile_ehalf}我应该早料到了。\n$@c{smile_eclosed}当然…我一直有这种感觉\n@c{smile}一切都结束了,对么? 你打破了循环。\n$@c{smile_ehalf}你也完成了自己的梦想,不是吗?\n你甚至一次都没失败。\n$我是唯一能够记得你所作所为的人\n@c{angry_mopen}我会努力不忘掉哒!\n$@c{smile_wave_wink}开玩笑啦,@d{64} @c{smile}我才不会忘呢。@d{32}\n你的传奇将永远留存于我们心中。\n$@c{smile_wave}不管了,@d{64} 时候不早了@d{96} ,应该吧?\n在这地方还真搞不清楚。\n$一起回家吧。 @c{smile_wave_wink}可能明天,我们再来打一场,为了重温回忆嘛~"
-}
\ No newline at end of file
+ "ending": "@c{shock}你回来了?@d{32} 也就是说…@d{96} 你赢了呀!?\n@c{smile_ehalf}我应该早料到了。\n$@c{smile_eclosed}当然…我一直有这种感觉\n@c{smile}一切都结束了,对么? 你打破了循环。\n$@c{smile_ehalf}你也完成了自己的梦想,不是吗?\n你甚至一次都没失败。\n$我是唯一能够记得你所作所为的人\n@c{angry_mopen}我会努力不忘掉哒!\n$@c{smile_wave_wink}开玩笑啦,@d{64} @c{smile}我才不会忘呢。@d{32}\n你的传奇将永远留存于我们心中。\n$@c{smile_wave}不管了,@d{64} 时候不早了@d{96} ,应该吧?\n在这地方还真搞不清楚。\n$一起回家吧。 @c{smile_wave_wink}可能明天,我们再来打一场,为了重温回忆嘛~",
+ "ending_female": "@c{smile}哦?你赢了?@d{96} @c{smile_eclosed}我应该早猜到了\n你回来了。\n$@c{smile}结束了。@d{64} 你终结了这个循环。\n$@c{serious_smile_fists}你也完成了自己的梦想,不是吗?\n你甚至一次都没失败。\n$@c{neutral}我是唯一能够记得你所作所为的人@d{96}\n我觉得这应该也还行吧?\n$@c{serious_smile_fists}你的传奇将永远留存于我们心中。\n$@c{smile_eclosed}不管了,我真是受够这个地方了,你也一样吗?我们回家吧。\n$@c{serious_smile_fists}可能等我们回家以后,再打一场?\n要是你想的话"
+}
diff --git a/src/locales/zh_CN/modifier-type.json b/src/locales/zh_CN/modifier-type.json
index 4a982b77cea..5d6184640b1 100644
--- a/src/locales/zh_CN/modifier-type.json
+++ b/src/locales/zh_CN/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "接下来的{{battleCount}}场战斗是双打的概率翻倍。"
},
- "TempBattleStatBoosterModifierType": {
- "description": "为所有成员宝可梦提升一级{{tempBattleStatName}},持续5场战斗。"
+ "TempStatStageBoosterModifierType": {
+ "description": "为所有成员宝可梦提升一级{{stat}},持续5场战斗。"
},
"AttackTypeBoosterModifierType": {
"description": "一只宝可梦的{{moveType}}系招式威力提升20%。"
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "使一只寶可夢的等級提升{{levels}}級。"
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "增加10%持有者的{{statName}},\n个体值越高堆叠上限越高。"
+ "BaseStatBoosterModifierType": {
+ "description": "增加10%持有者的{{stat}},\n个体值越高堆叠上限越高。"
},
"AllPokemonFullHpRestoreModifierType": {
"description": "所有宝可梦完全回复HP。"
@@ -248,6 +248,12 @@
"name": "焦点镜",
"description": "能看见弱点的镜片。携带它的宝可梦的招式\n会变得容易击中要害。"
},
+ "DIRE_HIT": {
+ "name": "要害攻击",
+ "extra": {
+ "raises": "会心"
+ }
+ },
"LEEK": {
"name": "大葱",
"description": "非常长且坚硬的茎。让大葱鸭携带后,\n招式会变得容易击中要害。"
@@ -411,25 +417,13 @@
"description": "让百变怪携带后,速度就会提高的神奇粉末。\n非常细腻坚硬。"
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "力量强化",
"x_defense": "防御强化",
"x_sp_atk": "特攻强化",
"x_sp_def": "特防强化",
"x_speed": "速度强化",
- "x_accuracy": "命中强化",
- "dire_hit": "要害攻击"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "攻击",
- "DEF": "防御",
- "SPATK": "特攻",
- "SPDEF": "特防",
- "SPD": "速度",
- "ACC": "命中",
- "CRIT": "会心",
- "EVA": "闪避",
- "DEFAULT": "???"
+ "x_accuracy": "命中强化"
},
"AttackTypeBoosterItem": {
"silk_scarf": "丝绸围巾",
@@ -606,4 +600,4 @@
"FAIRY_MEMORY": "妖精存储碟",
"NORMAL_MEMORY": "一般存储碟"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh_CN/modifier.json b/src/locales/zh_CN/modifier.json
index 707fab20ecc..a50cdd35bc1 100644
--- a/src/locales/zh_CN/modifier.json
+++ b/src/locales/zh_CN/modifier.json
@@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力!",
"hitHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}}用{{typeName}}\n恢复了活力!",
- "pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}降低的能力被{{typeName}}\n复原了!",
+ "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}降低的能力被{{typeName}}\n复原了!",
"moneyInterestApply": "用{{typeName}}\n获得了 ₽{{moneyAmount}} 利息!",
"turnHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}吸收了!",
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}夺取了!",
diff --git a/src/locales/zh_CN/move-trigger.json b/src/locales/zh_CN/move-trigger.json
index 44705d54e76..1eb4c397f45 100644
--- a/src/locales/zh_CN/move-trigger.json
+++ b/src/locales/zh_CN/move-trigger.json
@@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}}\n削减了体力并提升了招式威力!",
"absorbedElectricity": "{{pokemonName}}\n吸收了电力!",
"switchedStatChanges": "{{pokemonName}}和对手互换了\n自己的能力变化!",
+ "switchedTwoStatChanges": "{{pokemonName}} 和对手互换了自己的{{firstStat}}和{{secondStat}}的能力变化!",
+ "switchedStat": "{{pokemonName}} 互换了各自的{{stat}}!",
+ "sharedGuard": "{{pokemonName}} 平分了各自的防守!",
+ "sharedPower": "{{pokemonName}} 平分了各自的力量!",
"goingAllOutForAttack": "{{pokemonName}}拿出全力了!",
"regainedHealth": "{{pokemonName}}的\n体力回复了!",
"keptGoingAndCrashed": "{{pokemonName}}因势头过猛\n而撞到了地面!",
diff --git a/src/locales/zh_CN/pokemon-info.json b/src/locales/zh_CN/pokemon-info.json
index 5194189c806..a21a8156e4c 100644
--- a/src/locales/zh_CN/pokemon-info.json
+++ b/src/locales/zh_CN/pokemon-info.json
@@ -1,7 +1,7 @@
{
"Stat": {
"HP": "最大HP",
- "HPshortened": "最大HP",
+ "HPshortened": "HP",
"ATK": "攻击",
"ATKshortened": "攻击",
"DEF": "防御",
@@ -37,4 +37,4 @@
"FAIRY": "妖精",
"STELLAR": "星晶"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh_TW/achv.json b/src/locales/zh_TW/achv.json
index 6587394cf41..9edce2e368d 100644
--- a/src/locales/zh_TW/achv.json
+++ b/src/locales/zh_TW/achv.json
@@ -80,7 +80,7 @@
"100_RIBBONS": {
"name": "大師球聯盟冠軍"
},
- "TRANSFER_MAX_BATTLE_STAT": {
+ "TRANSFER_MAX_STAT_STAGE": {
"name": "團隊協作",
"description": "在一項屬性強化至最大時用接力棒傳遞給其他寶可夢"
},
@@ -257,4 +257,4 @@
"name": "鏡子子鏡",
"description": "完成逆轉之戰挑戰\n戰挑戰之轉逆成完"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh_TW/dialogue-misc.json b/src/locales/zh_TW/dialogue-misc.json
index 24e2109e5b3..408bcac546b 100644
--- a/src/locales/zh_TW/dialogue-misc.json
+++ b/src/locales/zh_TW/dialogue-misc.json
@@ -1,4 +1,4 @@
{
- "ending": "@c{smile}哦?你贏了?@d{96} @c{smile_eclosed}我應該早猜到了\n你回來了。\n$@c{smile}結束了。@d{64} 你終結了這個循環。\n$@c{serious_smile_fists}你也完成了自己的夢想,不是嗎?\n你甚至一次都沒失敗。\n$@c{neutral}我是唯一能夠記得你所作所為的人@d{96}\n我覺得這應該也還行吧?\n$@c{serious_smile_fists}你的傳奇將永遠留存於我們心中。\n$@c{smile_eclosed}不管了,我真是受夠這個地方了,你也一樣嗎?我們回家吧。\n$@c{serious_smile_fists}可能等我們回家以後,再打一場?\n要是你想的話",
- "ending_female": "@c{shock}你回來了?@d{32} 也就是說…@d{96} 你贏了呀!?\n@c{smile_ehalf}我應該早料到了。\n$@c{smile_eclosed}當然…我一直有這種感覺\n@c{smile}一切都結束了,對麼? 你打破了循環。\n$@c{smile_ehalf}你也完成了自己的夢想,不是嗎?\n你甚至一次都沒失敗。\n$我是唯一能夠記得你所作所為的人\n@c{angry_mopen}我會努力不忘掉哒!\n$@c{smile_wave_wink}開玩笑啦,@d{64} @c{smile}我才不會忘呢。@d{32}\n你的傳奇將永遠留存於我們心中。\n$@c{smile_wave}不管了,@d{64} 時候不早了@d{96} ,應該吧?\n在這地方還真搞不清楚。\n$一起回家吧。 @c{smile_wave_wink}可能明天,我們再來打一場,為了重溫回憶嘛~"
-}
\ No newline at end of file
+ "ending": "@c{shock}你回來了?@d{32} 也就是說…@d{96} 你贏了呀!?\n@c{smile_ehalf}我應該早料到了。\n$@c{smile_eclosed}當然…我一直有這種感覺\n@c{smile}一切都結束了,對麼? 你打破了循環。\n$@c{smile_ehalf}你也完成了自己的夢想,不是嗎?\n你甚至一次都沒失敗。\n$我是唯一能夠記得你所作所為的人\n@c{angry_mopen}我會努力不忘掉哒!\n$@c{smile_wave_wink}開玩笑啦,@d{64} @c{smile}我才不會忘呢。@d{32}\n你的傳奇將永遠留存於我們心中。\n$@c{smile_wave}不管了,@d{64} 時候不早了@d{96} ,應該吧?\n在這地方還真搞不清楚。\n$一起回家吧。 @c{smile_wave_wink}可能明天,我們再來打一場,為了重溫回憶嘛~",
+ "ending_female": "@c{smile}哦?你贏了?@d{96} @c{smile_eclosed}我應該早猜到了\n你回來了。\n$@c{smile}結束了。@d{64} 你終結了這個循環。\n$@c{serious_smile_fists}你也完成了自己的夢想,不是嗎?\n你甚至一次都沒失敗。\n$@c{neutral}我是唯一能夠記得你所作所為的人@d{96}\n我覺得這應該也還行吧?\n$@c{serious_smile_fists}你的傳奇將永遠留存於我們心中。\n$@c{smile_eclosed}不管了,我真是受夠這個地方了,你也一樣嗎?我們回家吧。\n$@c{serious_smile_fists}可能等我們回家以後,再打一場?\n要是你想的話"
+}
diff --git a/src/locales/zh_TW/modifier-type.json b/src/locales/zh_TW/modifier-type.json
index 847ede7001e..68881a206cb 100644
--- a/src/locales/zh_TW/modifier-type.json
+++ b/src/locales/zh_TW/modifier-type.json
@@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": {
"description": "接下來的{{battleCount}}場戰鬥是雙打的概率翻倍。"
},
- "TempBattleStatBoosterModifierType": {
- "description": "爲所有成員寶可夢提升一級{{tempBattleStatName}},持續5場戰鬥。"
+ "TempStatStageBoosterModifierType": {
+ "description": "爲所有成員寶可夢提升一級{{stat}},持續5場戰鬥。"
},
"AttackTypeBoosterModifierType": {
"description": "一隻寶可夢的{{moveType}}系招式威力提升20%。"
@@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": {
"description": "Increases all party members' level by {{levels}}."
},
- "PokemonBaseStatBoosterModifierType": {
- "description": "增加持有者的{{statName}}10%,個體值越高堆疊\n上限越高。"
+ "BaseStatBoosterModifierType": {
+ "description": "增加持有者的{{stat}}10%,個體值越高堆疊\n上限越高。"
},
"AllPokemonFullHpRestoreModifierType": {
"description": "所有寶可夢完全恢復HP。"
@@ -244,6 +244,12 @@
"name": "焦點鏡",
"description": "能看見弱點的鏡片。攜帶它的寶可夢的招式 會變得容易擊中要害。"
},
+ "DIRE_HIT": {
+ "name": "要害攻擊",
+ "extra": {
+ "raises": "會心"
+ }
+ },
"LEEK": {
"name": "大蔥",
"description": "非常長且堅硬的莖。讓大蔥鴨攜帶後,招式會 變得容易擊中要害。"
@@ -407,25 +413,13 @@
"description": "讓百變怪攜帶後,速度就會提高的神奇粉末。非常細緻堅硬。"
}
},
- "TempBattleStatBoosterItem": {
+ "TempStatStageBoosterItem": {
"x_attack": "力量強化",
"x_defense": "防禦強化",
"x_sp_atk": "特攻強化",
"x_sp_def": "特防強化",
"x_speed": "速度強化",
- "x_accuracy": "命中強化",
- "dire_hit": "要害攻擊"
- },
- "TempBattleStatBoosterStatName": {
- "ATK": "攻擊",
- "DEF": "防禦",
- "SPATK": "特攻",
- "SPDEF": "特防",
- "SPD": "速度",
- "ACC": "命中",
- "CRIT": "會心",
- "EVA": "閃避",
- "DEFAULT": "???"
+ "x_accuracy": "命中強化"
},
"AttackTypeBoosterItem": {
"silk_scarf": "絲綢圍巾",
@@ -602,4 +596,4 @@
"FAIRY_MEMORY": "妖精記憶碟",
"NORMAL_MEMORY": "一般記憶碟"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh_TW/modifier.json b/src/locales/zh_TW/modifier.json
index eb4b5107cff..1c0d4760e6f 100644
--- a/src/locales/zh_TW/modifier.json
+++ b/src/locales/zh_TW/modifier.json
@@ -8,4 +8,4 @@
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}奪取了!",
"enemyTurnHealApply": "{{pokemonNameWithAffix}}\n回復了一些體力!",
"bypassSpeedChanceApply": "{{pokemonName}}用了{{itemName}}後,行動變快了!"
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh_TW/move-trigger.json b/src/locales/zh_TW/move-trigger.json
index 60dcc1eab7a..d6d0ce659ea 100644
--- a/src/locales/zh_TW/move-trigger.json
+++ b/src/locales/zh_TW/move-trigger.json
@@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}}\n削減體力並提升了招式威力!",
"absorbedElectricity": "{{pokemonName}}\n吸收了电力!",
"switchedStatChanges": "{{pokemonName}}和對手互換了\n自身的能力變化!",
+ "switchedTwoStatChanges": "{{pokemonName}} 和對手互換了自身的{{firstStat}}和{{secondStat}}的能力變化!",
+ "switchedStat": "{{pokemonName}} 互換了各自的{{stat}}!",
+ "sharedGuard": "{{pokemonName}} 平分了各自的防守!",
+ "sharedPower": "{{pokemonName}} 平分了各自的力量!",
"goingAllOutForAttack": "{{pokemonName}}拿出全力了!",
"regainedHealth": "{{pokemonName}}的\n體力回復了!",
"keptGoingAndCrashed": "{{pokemonName}}因勢頭過猛\n而撞到了地面!",
diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts
index e693ca01052..fe586074c79 100644
--- a/src/modifier/modifier-type.ts
+++ b/src/modifier/modifier-type.ts
@@ -3,12 +3,10 @@ import { AttackMove, allMoves, selfStatLowerMoves } from "../data/move";
import { MAX_PER_TYPE_POKEBALLS, PokeballType, getPokeballCatchMultiplier, getPokeballName } from "../data/pokeball";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions";
-import { Stat, getStatName } from "../data/pokemon-stat";
import { tmPoolTiers, tmSpecies } from "../data/tms";
import { Type } from "../data/type";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "../ui/party-ui-handler";
import * as Utils from "../utils";
-import { TempBattleStat, getTempBattleStatBoosterItemName, getTempBattleStatName } from "../data/temp-battle-stat";
import { getBerryEffectDescription, getBerryName } from "../data/berry";
import { Unlockables } from "../system/unlockables";
import { StatusEffect, getStatusEffectDescriptor } from "../data/status-effect";
@@ -28,6 +26,7 @@ import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { getPokemonNameWithAffix } from "#app/messages.js";
+import { PermanentStat, TEMP_BATTLE_STATS, TempBattleStat, Stat, getStatKey } from "#app/enums/stat";
const outputModifierData = false;
const useMaxWeightForOutput = false;
@@ -447,26 +446,28 @@ export class DoubleBattleChanceBoosterModifierType extends ModifierType {
}
}
-export class TempBattleStatBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType {
- public tempBattleStat: TempBattleStat;
+export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType {
+ private stat: TempBattleStat;
+ private key: string;
- constructor(tempBattleStat: TempBattleStat) {
- super("", getTempBattleStatBoosterItemName(tempBattleStat).replace(/\./g, "").replace(/[ ]/g, "_").toLowerCase(),
- (_type, _args) => new Modifiers.TempBattleStatBoosterModifier(this, this.tempBattleStat));
+ constructor(stat: TempBattleStat) {
+ const key = TempStatStageBoosterModifierTypeGenerator.items[stat];
+ super("", key, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat));
- this.tempBattleStat = tempBattleStat;
+ this.stat = stat;
+ this.key = key;
}
get name(): string {
- return i18next.t(`modifierType:TempBattleStatBoosterItem.${getTempBattleStatBoosterItemName(this.tempBattleStat).replace(/\./g, "").replace(/[ ]/g, "_").toLowerCase()}`);
+ return i18next.t(`modifierType:TempStatStageBoosterItem.${this.key}`);
}
- getDescription(scene: BattleScene): string {
- return i18next.t("modifierType:ModifierType.TempBattleStatBoosterModifierType.description", { tempBattleStatName: getTempBattleStatName(this.tempBattleStat) });
+ getDescription(_scene: BattleScene): string {
+ return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) });
}
getPregenArgs(): any[] {
- return [ this.tempBattleStat ];
+ return [ this.stat ];
}
}
@@ -611,40 +612,24 @@ export class AllPokemonLevelIncrementModifierType extends ModifierType {
}
}
-function getBaseStatBoosterItemName(stat: Stat) {
- switch (stat) {
- case Stat.HP:
- return "HP Up";
- case Stat.ATK:
- return "Protein";
- case Stat.DEF:
- return "Iron";
- case Stat.SPATK:
- return "Calcium";
- case Stat.SPDEF:
- return "Zinc";
- case Stat.SPD:
- return "Carbos";
- }
-}
+export class BaseStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
+ private stat: PermanentStat;
+ private key: string;
-export class PokemonBaseStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
- private localeName: string;
- private stat: Stat;
+ constructor(stat: PermanentStat) {
+ const key = BaseStatBoosterModifierTypeGenerator.items[stat];
+ super("", key, (_type, args) => new Modifiers.BaseStatModifier(this, (args[0] as Pokemon).id, this.stat));
- constructor(localeName: string, stat: Stat) {
- super("", localeName.replace(/[ \-]/g, "_").toLowerCase(), (_type, args) => new Modifiers.PokemonBaseStatModifier(this, (args[0] as Pokemon).id, this.stat));
-
- this.localeName = localeName;
this.stat = stat;
+ this.key = key;
}
get name(): string {
- return i18next.t(`modifierType:BaseStatBoosterItem.${this.localeName.replace(/[ \-]/g, "_").toLowerCase()}`);
+ return i18next.t(`modifierType:BaseStatBoosterItem.${this.key}`);
}
- getDescription(scene: BattleScene): string {
- return i18next.t("modifierType:ModifierType.PokemonBaseStatBoosterModifierType.description", { statName: getStatName(this.stat) });
+ getDescription(_scene: BattleScene): string {
+ return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) });
}
getPregenArgs(): any[] {
@@ -922,6 +907,48 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator {
}
}
+class BaseStatBoosterModifierTypeGenerator extends ModifierTypeGenerator {
+ public static readonly items: Record = {
+ [Stat.HP]: "hp_up",
+ [Stat.ATK]: "protein",
+ [Stat.DEF]: "iron",
+ [Stat.SPATK]: "calcium",
+ [Stat.SPDEF]: "zinc",
+ [Stat.SPD]: "carbos"
+ };
+
+ constructor() {
+ super((_party: Pokemon[], pregenArgs?: any[]) => {
+ if (pregenArgs) {
+ return new BaseStatBoosterModifierType(pregenArgs[0]);
+ }
+ const randStat: PermanentStat = Utils.randSeedInt(Stat.SPD + 1);
+ return new BaseStatBoosterModifierType(randStat);
+ });
+ }
+}
+
+class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator {
+ public static readonly items: Record = {
+ [Stat.ATK]: "x_attack",
+ [Stat.DEF]: "x_defense",
+ [Stat.SPATK]: "x_sp_atk",
+ [Stat.SPDEF]: "x_sp_def",
+ [Stat.SPD]: "x_speed",
+ [Stat.ACC]: "x_accuracy"
+ };
+
+ constructor() {
+ super((_party: Pokemon[], pregenArgs?: any[]) => {
+ if (pregenArgs && (pregenArgs.length === 1) && TEMP_BATTLE_STATS.includes(pregenArgs[0])) {
+ return new TempStatStageBoosterModifierType(pregenArgs[0]);
+ }
+ const randStat: TempBattleStat = Utils.randSeedInt(Stat.ACC, Stat.ATK);
+ return new TempStatStageBoosterModifierType(randStat);
+ });
+ }
+}
+
/**
* Modifier type generator for {@linkcode SpeciesStatBoosterModifierType}, which
* encapsulates the logic for weighting the most useful held item from
@@ -930,7 +957,7 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator {
*/
class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator {
/** Object comprised of the currently available species-based stat boosting held items */
- public static items = {
+ public static readonly items = {
LIGHT_BALL: { stats: [Stat.ATK, Stat.SPATK], multiplier: 2, species: [Species.PIKACHU] },
THICK_CLUB: { stats: [Stat.ATK], multiplier: 2, species: [Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK] },
METAL_POWDER: { stats: [Stat.DEF], multiplier: 2, species: [Species.DITTO] },
@@ -1233,7 +1260,7 @@ export type GeneratorModifierOverride = {
type?: SpeciesStatBoosterItem;
}
| {
- name: keyof Pick;
+ name: keyof Pick;
type?: TempBattleStat;
}
| {
@@ -1306,7 +1333,7 @@ export const modifierTypes = {
SACRED_ASH: () => new AllPokemonFullReviveModifierType("modifierType:ModifierType.SACRED_ASH", "sacred_ash"),
REVIVER_SEED: () => new PokemonHeldItemModifierType("modifierType:ModifierType.REVIVER_SEED", "reviver_seed", (type, args) => new Modifiers.PokemonInstantReviveModifier(type, (args[0] as Pokemon).id)),
- WHITE_HERB: () => new PokemonHeldItemModifierType("modifierType:ModifierType.WHITE_HERB", "white_herb", (type, args) => new Modifiers.PokemonResetNegativeStatStageModifier(type, (args[0] as Pokemon).id)),
+ WHITE_HERB: () => new PokemonHeldItemModifierType("modifierType:ModifierType.WHITE_HERB", "white_herb", (type, args) => new Modifiers.ResetNegativeStatStageModifier(type, (args[0] as Pokemon).id)),
ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.ETHER", "ether", 10),
MAX_ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.MAX_ETHER", "max_ether", -1),
@@ -1327,23 +1354,15 @@ export const modifierTypes = {
SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(),
- TEMP_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
- if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in TempBattleStat)) {
- return new TempBattleStatBoosterModifierType(pregenArgs[0] as TempBattleStat);
- }
- const randTempBattleStat = Utils.randSeedInt(6) as TempBattleStat;
- return new TempBattleStatBoosterModifierType(randTempBattleStat);
- }),
- DIRE_HIT: () => new TempBattleStatBoosterModifierType(TempBattleStat.CRIT),
+ TEMP_STAT_STAGE_BOOSTER: () => new TempStatStageBoosterModifierTypeGenerator(),
- BASE_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
- if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in Stat)) {
- const stat = pregenArgs[0] as Stat;
- return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(stat), stat);
+ DIRE_HIT: () => new class extends ModifierType {
+ getDescription(_scene: BattleScene): string {
+ return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises") });
}
- const randStat = Utils.randSeedInt(6) as Stat;
- return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(randStat), randStat);
- }),
+ }("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new Modifiers.TempCritBoosterModifier(type)),
+
+ BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(),
ATTACK_TYPE_BOOSTER: () => new AttackTypeBoosterModifierTypeGenerator(),
@@ -1513,7 +1532,7 @@ const modifierPool: ModifierPool = {
return thresholdPartyMemberCount;
}, 3),
new WeightedModifierType(modifierTypes.LURE, skipInLastClassicWaveOrDefault(2)),
- new WeightedModifierType(modifierTypes.TEMP_STAT_BOOSTER, 4),
+ new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4),
new WeightedModifierType(modifierTypes.BERRY, 2),
new WeightedModifierType(modifierTypes.TM_COMMON, 2),
].map(m => {
@@ -1626,7 +1645,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.WHITE_HERB, (party: Pokemon[]) => {
const checkedAbilities = [Abilities.WEAK_ARMOR, Abilities.CONTRARY, Abilities.MOODY, Abilities.ANGER_SHELL, Abilities.COMPETITIVE, Abilities.DEFIANT];
const weightMultiplier = party.filter(
- p => !p.getHeldItems().some(i => i instanceof Modifiers.PokemonResetNegativeStatStageModifier && i.stackCount >= i.getMaxHeldItemCount(p)) &&
+ p => !p.getHeldItems().some(i => i instanceof Modifiers.ResetNegativeStatStageModifier && i.stackCount >= i.getMaxHeldItemCount(p)) &&
(checkedAbilities.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && selfStatLowerMoves.includes(m.moveId)))).length;
// If a party member has one of the above moves or abilities and doesn't have max herbs, the herb will appear more frequently
return 0 * (weightMultiplier ? 2 : 1) + (weightMultiplier ? weightMultiplier * 0 : 0);
@@ -2225,7 +2244,7 @@ export class ModifierTypeOption {
}
export function getPartyLuckValue(party: Pokemon[]): integer {
- const luck = Phaser.Math.Clamp(party.map(p => p.isFainted() ? 0 : p.getLuck())
+ const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() : 0)
.reduce((total: integer, value: integer) => total += value, 0), 0, 14);
return luck || 0;
}
diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts
index ca0b18ce18b..84d8a1385af 100644
--- a/src/modifier/modifier.ts
+++ b/src/modifier/modifier.ts
@@ -3,14 +3,12 @@ import BattleScene from "../battle-scene";
import { getLevelTotalExp } from "../data/exp";
import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball";
import Pokemon, { PlayerPokemon } from "../field/pokemon";
-import { Stat } from "../data/pokemon-stat";
import { addTextObject, TextStyle } from "../ui/text";
import { Type } from "../data/type";
import { EvolutionPhase } from "../phases/evolution-phase";
import { FusionSpeciesFormEvolution, pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions";
import { getPokemonNameWithAffix } from "../messages";
import * as Utils from "../utils";
-import { TempBattleStat } from "../data/temp-battle-stat";
import { getBerryEffectFunc, getBerryPredicate } from "../data/berry";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
@@ -23,6 +21,7 @@ import Overrides from "#app/overrides";
import { ModifierType, modifierTypes } from "./modifier-type";
import { Command } from "#app/ui/command-ui-handler";
import { Species } from "#enums/species";
+import { Stat, type PermanentStat, type TempBattleStat, BATTLE_STATS, TEMP_BATTLE_STATS } from "#app/enums/stat";
import i18next from "i18next";
import { allMoves } from "#app/data/move";
@@ -362,41 +361,160 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier
}
}
-export class TempBattleStatBoosterModifier extends LapsingPersistentModifier {
- private tempBattleStat: TempBattleStat;
+/**
+ * Modifier used for party-wide items, specifically the X items, that
+ * temporarily increases the stat stage multiplier of the corresponding
+ * {@linkcode TempBattleStat}.
+ * @extends LapsingPersistentModifier
+ * @see {@linkcode apply}
+ */
+export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
+ private stat: TempBattleStat;
+ private multiplierBoost: number;
- constructor(type: ModifierTypes.TempBattleStatBoosterModifierType, tempBattleStat: TempBattleStat, battlesLeft?: integer, stackCount?: integer) {
- super(type, battlesLeft || 5, stackCount);
+ constructor(type: ModifierType, stat: TempBattleStat, battlesLeft?: number, stackCount?: number) {
+ super(type, battlesLeft ?? 5, stackCount);
- this.tempBattleStat = tempBattleStat;
+ this.stat = stat;
+ // Note that, because we want X Accuracy to maintain its original behavior,
+ // it will increment as it did previously, directly to the stat stage.
+ this.multiplierBoost = stat !== Stat.ACC ? 0.3 : 1;
}
match(modifier: Modifier): boolean {
- if (modifier instanceof TempBattleStatBoosterModifier) {
- return (modifier as TempBattleStatBoosterModifier).tempBattleStat === this.tempBattleStat
- && (modifier as TempBattleStatBoosterModifier).battlesLeft === this.battlesLeft;
+ if (modifier instanceof TempStatStageBoosterModifier) {
+ const modifierInstance = modifier as TempStatStageBoosterModifier;
+ return (modifierInstance.stat === this.stat);
}
return false;
}
- clone(): TempBattleStatBoosterModifier {
- return new TempBattleStatBoosterModifier(this.type as ModifierTypes.TempBattleStatBoosterModifierType, this.tempBattleStat, this.battlesLeft, this.stackCount);
+ clone() {
+ return new TempStatStageBoosterModifier(this.type, this.stat, this.battlesLeft, this.stackCount);
}
getArgs(): any[] {
- return [ this.tempBattleStat, this.battlesLeft ];
+ return [ this.stat, this.battlesLeft ];
}
- apply(args: any[]): boolean {
- const tempBattleStat = args[0] as TempBattleStat;
+ /**
+ * Checks if {@linkcode args} contains the necessary elements and if the
+ * incoming stat is matches {@linkcode stat}.
+ * @param args [0] {@linkcode TempBattleStat} being checked at the time
+ * [1] {@linkcode Utils.NumberHolder} N/A
+ * @returns true if the modifier can be applied, false otherwise
+ */
+ shouldApply(args: any[]): boolean {
+ return args && (args.length === 2) && TEMP_BATTLE_STATS.includes(args[0]) && (args[0] === this.stat) && (args[1] instanceof Utils.NumberHolder);
+ }
- if (tempBattleStat === this.tempBattleStat) {
- const statLevel = args[1] as Utils.IntegerHolder;
- statLevel.value = Math.min(statLevel.value + 1, 6);
- return true;
+ /**
+ * Increases the incoming stat stage matching {@linkcode stat} by {@linkcode multiplierBoost}.
+ * @param args [0] {@linkcode TempBattleStat} N/A
+ * [1] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat stage multiplier
+ */
+ apply(args: any[]): boolean {
+ (args[1] as Utils.NumberHolder).value += this.multiplierBoost;
+ return true;
+ }
+
+ /**
+ * Goes through existing modifiers for any that match the selected modifier,
+ * which will then either add it to the existing modifiers if none were found
+ * or, if one was found, it will refresh {@linkcode battlesLeft}.
+ * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
+ * @param _virtual N/A
+ * @param _scene N/A
+ * @returns true if the modifier was successfully added or applied, false otherwise
+ */
+ add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
+ for (const modifier of modifiers) {
+ if (this.match(modifier)) {
+ const modifierInstance = modifier as TempStatStageBoosterModifier;
+ if (modifierInstance.getBattlesLeft() < 5) {
+ modifierInstance.battlesLeft = 5;
+ return true;
+ }
+ // should never get here
+ return false;
+ }
}
- return false;
+ modifiers.push(this);
+ return true;
+ }
+
+ getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
+ return 1;
+ }
+}
+
+/**
+ * Modifier used for party-wide items, namely Dire Hit, that
+ * temporarily increments the critical-hit stage
+ * @extends LapsingPersistentModifier
+ * @see {@linkcode apply}
+ */
+export class TempCritBoosterModifier extends LapsingPersistentModifier {
+ constructor(type: ModifierType, battlesLeft?: integer, stackCount?: number) {
+ super(type, battlesLeft || 5, stackCount);
+ }
+
+ clone() {
+ return new TempCritBoosterModifier(this.type, this.stackCount);
+ }
+
+ match(modifier: Modifier): boolean {
+ return (modifier instanceof TempCritBoosterModifier);
+ }
+
+ /**
+ * Checks if {@linkcode args} contains the necessary elements.
+ * @param args [1] {@linkcode Utils.NumberHolder} N/A
+ * @returns true if the critical-hit stage boost applies successfully
+ */
+ shouldApply(args: any[]): boolean {
+ return args && (args.length === 1) && (args[0] instanceof Utils.NumberHolder);
+ }
+
+ /**
+ * Increases the current critical-hit stage value by 1.
+ * @param args [0] {@linkcode Utils.IntegerHolder} that holds the resulting critical-hit level
+ * @returns true if the critical-hit stage boost applies successfully
+ */
+ apply(args: any[]): boolean {
+ (args[0] as Utils.NumberHolder).value++;
+ return true;
+ }
+
+ /**
+ * Goes through existing modifiers for any that match the selected modifier,
+ * which will then either add it to the existing modifiers if none were found
+ * or, if one was found, it will refresh {@linkcode battlesLeft}.
+ * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
+ * @param _virtual N/A
+ * @param _scene N/A
+ * @returns true if the modifier was successfully added or applied, false otherwise
+ */
+ add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
+ for (const modifier of modifiers) {
+ if (this.match(modifier)) {
+ const modifierInstance = modifier as TempCritBoosterModifier;
+ if (modifierInstance.getBattlesLeft() < 5) {
+ modifierInstance.battlesLeft = 5;
+ return true;
+ }
+ // should never get here
+ return false;
+ }
+ }
+
+ modifiers.push(this);
+ return true;
+ }
+
+ getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
+ return 1;
}
}
@@ -663,24 +781,30 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier {
}
}
-export class PokemonBaseStatModifier extends PokemonHeldItemModifier {
- protected stat: Stat;
+/**
+ * Modifier used for held items, specifically vitamins like Carbos, Hp Up, etc., that
+ * increase the value of a given {@linkcode PermanentStat}.
+ * @extends LapsingPersistentModifier
+ * @see {@linkcode apply}
+ */
+export class BaseStatModifier extends PokemonHeldItemModifier {
+ protected stat: PermanentStat;
readonly isTransferrable: boolean = false;
- constructor(type: ModifierTypes.PokemonBaseStatBoosterModifierType, pokemonId: integer, stat: Stat, stackCount?: integer) {
+ constructor(type: ModifierType, pokemonId: integer, stat: PermanentStat, stackCount?: integer) {
super(type, pokemonId, stackCount);
this.stat = stat;
}
matchType(modifier: Modifier): boolean {
- if (modifier instanceof PokemonBaseStatModifier) {
- return (modifier as PokemonBaseStatModifier).stat === this.stat;
+ if (modifier instanceof BaseStatModifier) {
+ return (modifier as BaseStatModifier).stat === this.stat;
}
return false;
}
clone(): PersistentModifier {
- return new PokemonBaseStatModifier(this.type as ModifierTypes.PokemonBaseStatBoosterModifierType, this.pokemonId, this.stat, this.stackCount);
+ return new BaseStatModifier(this.type, this.pokemonId, this.stat, this.stackCount);
}
getArgs(): any[] {
@@ -688,12 +812,12 @@ export class PokemonBaseStatModifier extends PokemonHeldItemModifier {
}
shouldApply(args: any[]): boolean {
- return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array;
+ return super.shouldApply(args) && args.length === 2 && Array.isArray(args[1]);
}
apply(args: any[]): boolean {
- args[1][this.stat] = Math.min(Math.floor(args[1][this.stat] * (1 + this.getStackCount() * 0.1)), 999999);
-
+ const baseStats = args[1] as number[];
+ baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + this.getStackCount() * 0.1));
return true;
}
@@ -1398,42 +1522,48 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier {
}
/**
- * Modifier used for White Herb, which resets negative {@linkcode Stat} changes
+ * Modifier used for held items, namely White Herb, that restore adverse stat
+ * stages in battle.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
-export class PokemonResetNegativeStatStageModifier extends PokemonHeldItemModifier {
+export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier {
constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) {
super(type, pokemonId, stackCount);
}
matchType(modifier: Modifier) {
- return modifier instanceof PokemonResetNegativeStatStageModifier;
+ return modifier instanceof ResetNegativeStatStageModifier;
}
clone() {
- return new PokemonResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount);
+ return new ResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount);
}
/**
- * Restores any negative stat stages of the mon to 0
- * @param args args[0] is the {@linkcode Pokemon} whose stat stages are being checked
- * @returns true if any stat changes were applied (item was used), false otherwise
+ * Goes through the holder's stat stages and, if any are negative, resets that
+ * stat stage back to 0.
+ * @param args [0] {@linkcode Pokemon} that holds the held item
+ * @returns true if any stat stages were reset, false otherwise
*/
apply(args: any[]): boolean {
const pokemon = args[0] as Pokemon;
- const loweredStats = pokemon.summonData.battleStats.filter(s => s < 0);
- if (loweredStats.length) {
- for (let s = 0; s < pokemon.summonData.battleStats.length; s++) {
- pokemon.summonData.battleStats[s] = Math.max(0, pokemon.summonData.battleStats[s]);
+ let statRestored = false;
+
+ for (const s of BATTLE_STATS) {
+ if (pokemon.getStatStage(s) < 0) {
+ pokemon.setStatStage(s, 0);
+ statRestored = true;
}
- pokemon.scene.queueMessage(i18next.t("modifier:pokemonResetNegativeStatStageApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }));
- return true;
}
- return false;
+
+ if (statRestored) {
+ pokemon.scene.queueMessage(i18next.t("modifier:resetNegativeStatStageApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }));
+ }
+ return statRestored;
}
- getMaxHeldItemCount(pokemon: Pokemon): integer {
+ getMaxHeldItemCount(_pokemon: Pokemon): integer {
return 2;
}
}
@@ -1625,7 +1755,7 @@ export class TmModifier extends ConsumablePokemonModifier {
apply(args: any[]): boolean {
const pokemon = args[0] as PlayerPokemon;
- pokemon.scene.unshiftPhase(new LearnMovePhase(pokemon.scene, pokemon.scene.getParty().indexOf(pokemon), (this.type as ModifierTypes.TmModifierType).moveId));
+ pokemon.scene.unshiftPhase(new LearnMovePhase(pokemon.scene, pokemon.scene.getParty().indexOf(pokemon), (this.type as ModifierTypes.TmModifierType).moveId, true));
return true;
}
@@ -2745,7 +2875,7 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier {
* - The player
* - The enemy
* @param scene current {@linkcode BattleScene}
- * @param isPlayer {@linkcode boolean} for whether the the player (`true`) or enemy (`false`) is being overridden
+ * @param isPlayer {@linkcode boolean} for whether the player (`true`) or enemy (`false`) is being overridden
*/
export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true): void {
const modifiersOverride: ModifierTypes.ModifierOverride[] = isPlayer ? Overrides.STARTING_MODIFIER_OVERRIDE : Overrides.OPP_MODIFIER_OVERRIDE;
@@ -2760,13 +2890,22 @@ export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true):
modifiersOverride.forEach(item => {
const modifierFunc = modifierTypes[item.name];
- const modifier = modifierFunc().withIdFromFunc(modifierFunc).newModifier() as PersistentModifier;
- modifier.stackCount = item.count || 1;
+ let modifierType: ModifierType | null = modifierFunc();
- if (isPlayer) {
- scene.addModifier(modifier, true, false, false, true);
- } else {
- scene.addEnemyModifier(modifier, true, true);
+ if (modifierType instanceof ModifierTypes.ModifierTypeGenerator) {
+ const pregenArgs = ("type" in item) && (item.type !== null) ? [item.type] : undefined;
+ modifierType = modifierType.generateType([], pregenArgs);
+ }
+
+ const modifier = modifierType && modifierType.withIdFromFunc(modifierFunc).newModifier() as PersistentModifier;
+ if (modifier) {
+ modifier.stackCount = item.count || 1;
+
+ if (isPlayer) {
+ scene.addModifier(modifier, true, false, false, true);
+ } else {
+ scene.addEnemyModifier(modifier, true, true);
+ }
}
});
}
diff --git a/src/overrides.ts b/src/overrides.ts
index 32ff116f41d..48c118b55bc 100644
--- a/src/overrides.ts
+++ b/src/overrides.ts
@@ -116,6 +116,14 @@ class DefaultOverrides {
readonly OPP_VARIANT_OVERRIDE: Variant = 0;
readonly OPP_IVS_OVERRIDE: number | number[] = [];
readonly OPP_FORM_OVERRIDES: Partial> = {};
+ /**
+ * Override to give the enemy Pokemon a given amount of health segments
+ *
+ * 0 (default): the health segments will be handled normally based on wave, level and species
+ * 1: the Pokemon will have a single health segment and therefore will not be a boss
+ * 2+: the Pokemon will be a boss with the given number of health segments
+ */
+ readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0;
// -------------
// EGG OVERRIDES
diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts
index 07bfd72a8bf..6e0658d4ccb 100644
--- a/src/phases/encounter-phase.ts
+++ b/src/phases/encounter-phase.ts
@@ -26,6 +26,7 @@ import { ScanIvsPhase } from "./scan-ivs-phase";
import { ShinySparklePhase } from "./shiny-sparkle-phase";
import { SummonPhase } from "./summon-phase";
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
+import Overrides from "#app/overrides";
export class EncounterPhase extends BattlePhase {
private loaded: boolean;
@@ -112,10 +113,11 @@ export class EncounterPhase extends BattlePhase {
if (battle.battleType === BattleType.TRAINER) {
loadEnemyAssets.push(battle.trainer?.loadAssets().then(() => battle.trainer?.initSprite())!); // TODO: is this bang correct?
} else {
- // This block only applies for double battles to init the boss segments (idk why it's split up like this)
- if (battle.enemyParty.filter(p => p.isBoss()).length > 1) {
+ const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1;
+ // for double battles, reduce the health segments for boss Pokemon unless there is an override
+ if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) {
for (const enemyPokemon of battle.enemyParty) {
- // If the enemy pokemon is a boss and wasn't populated from data source, then set it up
+ // If the enemy pokemon is a boss and wasn't populated from data source, then update the number of segments
if (enemyPokemon.isBoss() && !enemyPokemon.isPopulatedFromDataSource) {
enemyPokemon.setBoss(true, Math.ceil(enemyPokemon.bossSegments * (enemyPokemon.getSpeciesForm().baseTotal / totalBst)));
enemyPokemon.initBattleInfo();
diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts
index 66946d268cb..d5dd9f61340 100644
--- a/src/phases/faint-phase.ts
+++ b/src/phases/faint-phase.ts
@@ -1,14 +1,14 @@
-import BattleScene from "#app/battle-scene.js";
-import { BattlerIndex, BattleType } from "#app/battle.js";
-import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability.js";
-import { BattlerTagLapseType } from "#app/data/battler-tags.js";
-import { battleSpecDialogue } from "#app/data/dialogue.js";
-import { allMoves, PostVictoryStatChangeAttr } from "#app/data/move.js";
-import { BattleSpec } from "#app/enums/battle-spec.js";
-import { StatusEffect } from "#app/enums/status-effect.js";
-import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon.js";
-import { getPokemonNameWithAffix } from "#app/messages.js";
-import { PokemonInstantReviveModifier } from "#app/modifier/modifier.js";
+import BattleScene from "#app/battle-scene";
+import { BattlerIndex, BattleType } from "#app/battle";
+import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability";
+import { BattlerTagLapseType } from "#app/data/battler-tags";
+import { battleSpecDialogue } from "#app/data/dialogue";
+import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move";
+import { BattleSpec } from "#app/enums/battle-spec";
+import { StatusEffect } from "#app/enums/status-effect";
+import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon";
+import { getPokemonNameWithAffix } from "#app/messages";
+import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import i18next from "i18next";
import { DamagePhase } from "./damage-phase";
import { PokemonPhase } from "./pokemon-phase";
@@ -72,7 +72,7 @@ export class FaintPhase extends PokemonPhase {
if (defeatSource?.isOnField()) {
applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource);
const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move];
- const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr);
+ const pvattrs = pvmove.getAttrs(PostVictoryStatStageChangeAttr);
if (pvattrs.length) {
for (const pvattr of pvattrs) {
pvattr.applyPostVictory(defeatSource, defeatSource, pvmove);
diff --git a/src/phases/field-phase.ts b/src/phases/field-phase.ts
index 02d1f1395d3..b65e903a32b 100644
--- a/src/phases/field-phase.ts
+++ b/src/phases/field-phase.ts
@@ -1,5 +1,5 @@
+import Pokemon from "#app/field/pokemon.js";
import { BattlePhase } from "./battle-phase";
-import Pokemon from "#app/field/pokemon";
type PokemonFunc = (pokemon: Pokemon) => void;
diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts
index 5a9a16b6f5e..201019e8860 100644
--- a/src/phases/learn-move-phase.ts
+++ b/src/phases/learn-move-phase.ts
@@ -12,11 +12,13 @@ import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-pha
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
private moveId: Moves;
+ private fromTM: boolean;
- constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves) {
+ constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, fromTM?: boolean) {
super(scene, partyMemberIndex);
this.moveId = moveId;
+ this.fromTM = fromTM ?? false;
}
start() {
@@ -41,6 +43,9 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
if (emptyMoveIndex > -1) {
pokemon.setMove(emptyMoveIndex, this.moveId);
+ if (this.fromTM) {
+ pokemon.usedTMs.push(this.moveId);
+ }
initMoveAnim(this.scene, this.moveId).then(() => {
loadMoveAnimAssets(this.scene, [this.moveId], true)
.then(() => {
@@ -85,6 +90,9 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
this.scene.ui.showText(i18next.t("battle:countdownPoof"), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveForgetSuccess", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: pokemon.moveset[moveIndex]!.getName() }), null, () => { // TODO: is the bang correct?
this.scene.ui.showText(i18next.t("battle:learnMoveAnd"), null, () => {
+ if (this.fromTM) {
+ pokemon.usedTMs.push(this.moveId);
+ }
pokemon.setMove(moveIndex, Moves.NONE);
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId));
this.end();
diff --git a/src/phases/stat-change-phase.ts b/src/phases/stat-change-phase.ts
deleted file mode 100644
index 3116c49e8ef..00000000000
--- a/src/phases/stat-change-phase.ts
+++ /dev/null
@@ -1,248 +0,0 @@
-import { BattlerIndex } from "#app/battle";
-import BattleScene from "#app/battle-scene";
-import { applyAbAttrs, applyPostStatChangeAbAttrs, applyPreStatChangeAbAttrs, PostStatChangeAbAttr, ProtectStatAbAttr, StatChangeCopyAbAttr, StatChangeMultiplierAbAttr } from "#app/data/ability";
-import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
-import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat";
-import Pokemon from "#app/field/pokemon";
-import { getPokemonNameWithAffix } from "#app/messages";
-import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier";
-import { handleTutorial, Tutorial } from "#app/tutorial";
-import * as Utils from "#app/utils";
-import i18next from "i18next";
-import { PokemonPhase } from "./pokemon-phase";
-
-export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
-
-export class StatChangePhase extends PokemonPhase {
- private stats: BattleStat[];
- private selfTarget: boolean;
- private levels: integer;
- private showMessage: boolean;
- private ignoreAbilities: boolean;
- private canBeCopied: boolean;
- private onChange: StatChangeCallback | null;
-
-
- constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatChangeCallback | null = null) {
- super(scene, battlerIndex);
-
- this.selfTarget = selfTarget;
- this.stats = stats;
- this.levels = levels;
- this.showMessage = showMessage;
- this.ignoreAbilities = ignoreAbilities;
- this.canBeCopied = canBeCopied;
- this.onChange = onChange;
- }
-
- start() {
- const pokemon = this.getPokemon();
-
- let random = false;
-
- if (this.stats.length === 1 && this.stats[0] === BattleStat.RAND) {
- this.stats[0] = this.getRandomStat();
- random = true;
- }
-
- this.aggregateStatChanges(random);
-
- if (!pokemon.isActive(true)) {
- return this.end();
- }
-
- const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : this.getRandomStat()).filter(stat => {
- const cancelled = new Utils.BooleanHolder(false);
-
- if (!this.selfTarget && this.levels < 0) {
- this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
- }
-
- if (!cancelled.value && !this.selfTarget && this.levels < 0) {
- applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled);
- }
-
- return !cancelled.value;
- });
-
- const levels = new Utils.IntegerHolder(this.levels);
-
- if (!this.ignoreAbilities) {
- applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, false, levels);
- }
-
- const battleStats = this.getPokemon().summonData.battleStats;
- const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]);
-
- this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
-
- const end = () => {
- if (this.showMessage) {
- const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels);
- for (const message of messages) {
- this.scene.queueMessage(message);
- }
- }
-
- for (const stat of filteredStats) {
- if (levels.value > 0 && pokemon.summonData.battleStats[stat] < 6) {
- if (!pokemon.turnData) {
- // Temporary fix for missing turn data struct on turn 1
- pokemon.resetTurnData();
- }
- pokemon.turnData.battleStatsIncreased = true;
- } else if (levels.value < 0 && pokemon.summonData.battleStats[stat] > -6) {
- if (!pokemon.turnData) {
- // Temporary fix for missing turn data struct on turn 1
- pokemon.resetTurnData();
- }
- pokemon.turnData.battleStatsDecreased = true;
- }
-
- pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6);
- }
-
- if (levels.value > 0 && this.canBeCopied) {
- for (const opponent of pokemon.getOpponents()) {
- applyAbAttrs(StatChangeCopyAbAttr, opponent, null, false, this.stats, levels.value);
- }
- }
-
- applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget);
-
- // Look for any other stat change phases; if this is the last one, do White Herb check
- const existingPhase = this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex);
- if (!(existingPhase instanceof StatChangePhase)) {
- // Apply White Herb if needed
- const whiteHerb = this.scene.applyModifier(PokemonResetNegativeStatStageModifier, this.player, pokemon) as PokemonResetNegativeStatStageModifier;
- // If the White Herb was applied, consume it
- if (whiteHerb) {
- --whiteHerb.stackCount;
- if (whiteHerb.stackCount <= 0) {
- this.scene.removeModifier(whiteHerb);
- }
- this.scene.updateModifiers(this.player);
- }
- }
-
- pokemon.updateInfo();
-
- handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end());
- };
-
- if (relLevels.filter(l => l).length && this.scene.moveAnimations) {
- pokemon.enableMask();
- const pokemonMaskSprite = pokemon.maskSprite;
-
- const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale;
- const tileY = ((this.player ? 148 : 84) + (levels.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale;
- const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale();
- const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale();
-
- // On increase, show the red sprite located at ATK
- // On decrease, show the blue sprite located at SPD
- const spriteColor = levels.value >= 1 ? BattleStat[BattleStat.ATK].toLowerCase() : BattleStat[BattleStat.SPD].toLowerCase();
- const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor);
- statSprite.setPipeline(this.scene.fieldSpritePipeline);
- statSprite.setAlpha(0);
- statSprite.setScale(6);
- statSprite.setOrigin(0.5, 1);
-
- this.scene.playSound(`se/stat_${levels.value >= 1 ? "up" : "down"}`);
-
- statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined));
-
- this.scene.tweens.add({
- targets: statSprite,
- duration: 250,
- alpha: 0.8375,
- onComplete: () => {
- this.scene.tweens.add({
- targets: statSprite,
- delay: 1000,
- duration: 250,
- alpha: 0
- });
- }
- });
-
- this.scene.tweens.add({
- targets: statSprite,
- duration: 1500,
- y: `${levels.value >= 1 ? "-" : "+"}=${160 * 6}`
- });
-
- this.scene.time.delayedCall(1750, () => {
- pokemon.disableMask();
- end();
- });
- } else {
- end();
- }
- }
-
- getRandomStat(): BattleStat {
- const allStats = Utils.getEnumValues(BattleStat);
- return this.getPokemon() ? allStats[this.getPokemon()!.randSeedInt(BattleStat.SPD + 1)] : BattleStat.ATK; // TODO: return default ATK on random? idk...
- }
-
- aggregateStatChanges(random: boolean = false): void {
- const isAccEva = [BattleStat.ACC, BattleStat.EVA].some(s => this.stats.includes(s));
- let existingPhase: StatChangePhase;
- if (this.stats.length === 1) {
- while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1
- && (p.stats[0] === this.stats[0] || (random && p.stats[0] === BattleStat.RAND))
- && p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) {
- if (existingPhase.stats[0] === BattleStat.RAND) {
- existingPhase.stats[0] = this.getRandomStat();
- if (existingPhase.stats[0] !== this.stats[0]) {
- continue;
- }
- }
- this.levels += existingPhase.levels;
-
- if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
- break;
- }
- }
- }
- while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget
- && ([BattleStat.ACC, BattleStat.EVA].some(s => p.stats.includes(s)) === isAccEva)
- && p.levels === this.levels && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) {
- this.stats.push(...existingPhase.stats);
- if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
- break;
- }
- }
- }
-
- getStatChangeMessages(stats: BattleStat[], levels: integer, relLevels: integer[]): string[] {
- const messages: string[] = [];
-
- const relLevelStatIndexes = {};
- for (let rl = 0; rl < relLevels.length; rl++) {
- const relLevel = relLevels[rl];
- if (!relLevelStatIndexes[relLevel]) {
- relLevelStatIndexes[relLevel] = [];
- }
- relLevelStatIndexes[relLevel].push(rl);
- }
-
- Object.keys(relLevelStatIndexes).forEach(rl => {
- const relLevelStats = stats.filter((_, i) => relLevelStatIndexes[rl].includes(i));
- let statsFragment = "";
-
- if (relLevelStats.length > 1) {
- statsFragment = relLevelStats.length >= 5
- ? i18next.t("battle:stats")
- : `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`;
- messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1, relLevelStats.length));
- } else {
- statsFragment = getBattleStatName(relLevelStats[0]);
- messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1, relLevelStats.length));
- }
- });
-
- return messages;
- }
-}
diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts
new file mode 100644
index 00000000000..55faaa29903
--- /dev/null
+++ b/src/phases/stat-stage-change-phase.ts
@@ -0,0 +1,244 @@
+import { BattlerIndex } from "#app/battle";
+import BattleScene from "#app/battle-scene";
+import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability";
+import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
+import Pokemon from "#app/field/pokemon";
+import { getPokemonNameWithAffix } from "#app/messages";
+import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
+import { handleTutorial, Tutorial } from "#app/tutorial";
+import * as Utils from "#app/utils";
+import i18next from "i18next";
+import { PokemonPhase } from "./pokemon-phase";
+import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
+
+export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
+
+export class StatStageChangePhase extends PokemonPhase {
+ private stats: BattleStat[];
+ private selfTarget: boolean;
+ private stages: integer;
+ private showMessage: boolean;
+ private ignoreAbilities: boolean;
+ private canBeCopied: boolean;
+ private onChange: StatStageChangeCallback | null;
+
+
+ constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) {
+ super(scene, battlerIndex);
+
+ this.selfTarget = selfTarget;
+ this.stats = stats;
+ this.stages = stages;
+ this.showMessage = showMessage;
+ this.ignoreAbilities = ignoreAbilities;
+ this.canBeCopied = canBeCopied;
+ this.onChange = onChange;
+ }
+
+ start() {
+ const pokemon = this.getPokemon();
+
+ if (!pokemon.isActive(true)) {
+ return this.end();
+ }
+
+ let simulate = false;
+
+ const filteredStats = this.stats.filter(stat => {
+ const cancelled = new Utils.BooleanHolder(false);
+
+ if (!this.selfTarget && this.stages < 0) {
+ // TODO: Include simulate boolean when tag applications can be simulated
+ this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
+ }
+
+ if (!cancelled.value && !this.selfTarget && this.stages < 0) {
+ applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
+ }
+
+ // If one stat stage decrease is cancelled, simulate the rest of the applications
+ if (cancelled.value) {
+ simulate = true;
+ }
+
+ return !cancelled.value;
+ });
+
+ const stages = new Utils.IntegerHolder(this.stages);
+
+ if (!this.ignoreAbilities) {
+ applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
+ }
+
+ const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
+
+ this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
+
+ const end = () => {
+ if (this.showMessage) {
+ const messages = this.getStatStageChangeMessages(filteredStats, stages.value, relLevels);
+ for (const message of messages) {
+ this.scene.queueMessage(message);
+ }
+ }
+
+ for (const s of filteredStats) {
+ if (stages.value > 0 && pokemon.getStatStage(s) < 6) {
+ if (!pokemon.turnData) {
+ // Temporary fix for missing turn data struct on turn 1
+ pokemon.resetTurnData();
+ }
+ pokemon.turnData.statStagesIncreased = true;
+ } else if (stages.value < 0 && pokemon.getStatStage(s) > -6) {
+ if (!pokemon.turnData) {
+ // Temporary fix for missing turn data struct on turn 1
+ pokemon.resetTurnData();
+ }
+ pokemon.turnData.statStagesDecreased = true;
+ }
+
+ pokemon.setStatStage(s, pokemon.getStatStage(s) + stages.value);
+ }
+
+ if (stages.value > 0 && this.canBeCopied) {
+ for (const opponent of pokemon.getOpponents()) {
+ applyAbAttrs(StatStageChangeCopyAbAttr, opponent, null, false, this.stats, stages.value);
+ }
+ }
+
+ applyPostStatStageChangeAbAttrs(PostStatStageChangeAbAttr, pokemon, filteredStats, this.stages, this.selfTarget);
+
+ // Look for any other stat change phases; if this is the last one, do White Herb check
+ const existingPhase = this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex);
+ if (!(existingPhase instanceof StatStageChangePhase)) {
+ // Apply White Herb if needed
+ const whiteHerb = this.scene.applyModifier(ResetNegativeStatStageModifier, this.player, pokemon) as ResetNegativeStatStageModifier;
+ // If the White Herb was applied, consume it
+ if (whiteHerb) {
+ whiteHerb.stackCount--;
+ if (whiteHerb.stackCount <= 0) {
+ this.scene.removeModifier(whiteHerb);
+ }
+ this.scene.updateModifiers(this.player);
+ }
+ }
+
+ pokemon.updateInfo();
+
+ handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end());
+ };
+
+ if (relLevels.filter(l => l).length && this.scene.moveAnimations) {
+ pokemon.enableMask();
+ const pokemonMaskSprite = pokemon.maskSprite;
+
+ const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale;
+ const tileY = ((this.player ? 148 : 84) + (stages.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale;
+ const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale();
+ const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale();
+
+ // On increase, show the red sprite located at ATK
+ // On decrease, show the blue sprite located at SPD
+ const spriteColor = stages.value >= 1 ? Stat[Stat.ATK].toLowerCase() : Stat[Stat.SPD].toLowerCase();
+ const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor);
+ statSprite.setPipeline(this.scene.fieldSpritePipeline);
+ statSprite.setAlpha(0);
+ statSprite.setScale(6);
+ statSprite.setOrigin(0.5, 1);
+
+ this.scene.playSound(`se/stat_${stages.value >= 1 ? "up" : "down"}`);
+
+ statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined));
+
+ this.scene.tweens.add({
+ targets: statSprite,
+ duration: 250,
+ alpha: 0.8375,
+ onComplete: () => {
+ this.scene.tweens.add({
+ targets: statSprite,
+ delay: 1000,
+ duration: 250,
+ alpha: 0
+ });
+ }
+ });
+
+ this.scene.tweens.add({
+ targets: statSprite,
+ duration: 1500,
+ y: `${stages.value >= 1 ? "-" : "+"}=${160 * 6}`
+ });
+
+ this.scene.time.delayedCall(1750, () => {
+ pokemon.disableMask();
+ end();
+ });
+ } else {
+ end();
+ }
+ }
+
+ aggregateStatStageChanges(): void {
+ const accEva: BattleStat[] = [ Stat.ACC, Stat.EVA ];
+ const isAccEva = accEva.some(s => this.stats.includes(s));
+ let existingPhase: StatStageChangePhase;
+ if (this.stats.length === 1) {
+ while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1
+ && (p.stats[0] === this.stats[0])
+ && p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) {
+ this.stages += existingPhase.stages;
+
+ if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
+ break;
+ }
+ }
+ }
+ while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget
+ && (accEva.some(s => p.stats.includes(s)) === isAccEva)
+ && p.stages === this.stages && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) {
+ this.stats.push(...existingPhase.stats);
+ if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
+ break;
+ }
+ }
+ }
+
+ getStatStageChangeMessages(stats: BattleStat[], stages: integer, relStages: integer[]): string[] {
+ const messages: string[] = [];
+
+ const relStageStatIndexes = {};
+ for (let rl = 0; rl < relStages.length; rl++) {
+ const relStage = relStages[rl];
+ if (!relStageStatIndexes[relStage]) {
+ relStageStatIndexes[relStage] = [];
+ }
+ relStageStatIndexes[relStage].push(rl);
+ }
+
+ Object.keys(relStageStatIndexes).forEach(rl => {
+ const relStageStats = stats.filter((_, i) => relStageStatIndexes[rl].includes(i));
+ let statsFragment = "";
+
+ if (relStageStats.length > 1) {
+ statsFragment = relStageStats.length >= 5
+ ? i18next.t("battle:stats")
+ : `${relStageStats.slice(0, -1).map(s => i18next.t(getStatKey(s))).join(", ")}${relStageStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${i18next.t(getStatKey(relStageStats[relStageStats.length - 1]))}`;
+ messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), {
+ pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()),
+ stats: statsFragment,
+ count: relStageStats.length
+ }));
+ } else {
+ statsFragment = i18next.t(getStatKey(relStageStats[0]));
+ messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), {
+ pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()),
+ stats: statsFragment,
+ count: relStageStats.length
+ }));
+ }
+ });
+
+ return messages;
+ }
+}
diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts
index b2545e9ee30..5c1af4228c6 100644
--- a/src/phases/turn-start-phase.ts
+++ b/src/phases/turn-start-phase.ts
@@ -43,8 +43,8 @@ export class TurnStartPhase extends FieldPhase {
}, this.scene.currentBattle.turn, this.scene.waveSeed);
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
- const aSpeed = a?.getBattleStat(Stat.SPD) || 0;
- const bSpeed = b?.getBattleStat(Stat.SPD) || 0;
+ const aSpeed = a?.getEffectiveStat(Stat.SPD) || 0;
+ const bSpeed = b?.getEffectiveStat(Stat.SPD) || 0;
return bSpeed - aSpeed;
});
diff --git a/src/system/achv.ts b/src/system/achv.ts
index de2862c2813..89e5493eb2e 100644
--- a/src/system/achv.ts
+++ b/src/system/achv.ts
@@ -5,9 +5,10 @@ import { pokemonEvolutions } from "#app/data/pokemon-evolutions";
import i18next from "i18next";
import * as Utils from "../utils";
import { PlayerGender } from "#enums/player-gender";
-import { Challenge, FreshStartChallenge, InverseBattleChallenge, SingleGenerationChallenge, SingleTypeChallenge } from "#app/data/challenge";
-import { Challenges } from "#app/enums/challenges";
+import { Challenge, FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge, InverseBattleChallenge } from "#app/data/challenge";
import { ConditionFn } from "#app/@types/common";
+import { Stat, getShortenedStatKey } from "#app/enums/stat";
+import { Challenges } from "#app/enums/challenges";
export enum AchvTier {
COMMON,
@@ -172,13 +173,13 @@ export function getAchievementDescription(localizationKey: string): string {
case "10000_DMG":
return i18next.t("achv:DamageAchv.description", {context: genderStr, "damageAmount": achvs._10000_DMG.damageAmount.toLocaleString("en-US")});
case "250_HEAL":
- return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")});
+ return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "1000_HEAL":
- return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")});
+ return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "2500_HEAL":
- return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")});
+ return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "10000_HEAL":
- return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")});
+ return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "LV_100":
return i18next.t("achv:LevelAchv.description", {context: genderStr, "level": achvs.LV_100.level});
case "LV_250":
@@ -195,7 +196,7 @@ export function getAchievementDescription(localizationKey: string): string {
return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._75_RIBBONS.ribbonAmount.toLocaleString("en-US")});
case "100_RIBBONS":
return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US")});
- case "TRANSFER_MAX_BATTLE_STAT":
+ case "TRANSFER_MAX_STAT_STAGE":
return i18next.t("achv:TRANSFER_MAX_BATTLE_STAT.description", { context: genderStr });
case "MAX_FRIENDSHIP":
return i18next.t("achv:MAX_FRIENDSHIP.description", { context: genderStr });
@@ -305,7 +306,7 @@ export const achvs = {
_50_RIBBONS: new RibbonAchv("50_RIBBONS", "", 50, "ultra_ribbon", 50).setSecret(true),
_75_RIBBONS: new RibbonAchv("75_RIBBONS", "", 75, "rogue_ribbon", 75).setSecret(true),
_100_RIBBONS: new RibbonAchv("100_RIBBONS", "", 100, "master_ribbon", 100).setSecret(true),
- TRANSFER_MAX_BATTLE_STAT: new Achv("TRANSFER_MAX_BATTLE_STAT", "", "TRANSFER_MAX_BATTLE_STAT.description", "baton", 20),
+ TRANSFER_MAX_STAT_STAGE: new Achv("TRANSFER_MAX_STAT_STAGE", "", "TRANSFER_MAX_STAT_STAGE.description", "baton", 20),
MAX_FRIENDSHIP: new Achv("MAX_FRIENDSHIP", "", "MAX_FRIENDSHIP.description", "soothe_bell", 25),
MEGA_EVOLVE: new Achv("MEGA_EVOLVE", "", "MEGA_EVOLVE.description", "mega_bracelet", 50),
GIGANTAMAX: new Achv("GIGANTAMAX", "", "GIGANTAMAX.description", "dynamax_band", 50),
diff --git a/src/system/game-data.ts b/src/system/game-data.ts
index a4c276fa770..29bb6057722 100644
--- a/src/system/game-data.ts
+++ b/src/system/game-data.ts
@@ -1488,7 +1488,7 @@ export class GameData {
};
}
- const defaultStarterAttr = DexAttr.NON_SHINY | DexAttr.MALE | DexAttr.DEFAULT_VARIANT | DexAttr.DEFAULT_FORM;
+ const defaultStarterAttr = DexAttr.NON_SHINY | DexAttr.MALE | DexAttr.FEMALE | DexAttr.DEFAULT_VARIANT | DexAttr.DEFAULT_FORM;
const defaultStarterNatures: Nature[] = [];
@@ -1919,6 +1919,7 @@ export class GameData {
fixStarterData(systemData: SystemSaveData): void {
for (const starterId of defaultStarterSpecies) {
systemData.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
+ systemData.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
}
diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts
index 8f094379434..9a743ceb1d2 100644
--- a/src/system/pokemon-data.ts
+++ b/src/system/pokemon-data.ts
@@ -42,6 +42,7 @@ export default class PokemonData {
public luck: integer;
public pauseEvolutions: boolean;
public pokerus: boolean;
+ public usedTMs: Moves[];
public fusionSpecies: Species;
public fusionFormIndex: integer;
@@ -98,6 +99,7 @@ export default class PokemonData {
this.fusionVariant = source.fusionVariant;
this.fusionGender = source.fusionGender;
this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0);
+ this.usedTMs = source.usedTMs ?? [];
if (!forHistory) {
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
@@ -122,7 +124,8 @@ export default class PokemonData {
this.summonData = new PokemonSummonData();
if (!forHistory && source.summonData) {
- this.summonData.battleStats = source.summonData.battleStats;
+ this.summonData.stats = source.summonData.stats;
+ this.summonData.statStages = source.summonData.statStages;
this.summonData.moveQueue = source.summonData.moveQueue;
this.summonData.disabledMove = source.summonData.disabledMove;
this.summonData.disabledTurns = source.summonData.disabledTurns;
diff --git a/src/test/abilities/beast_boost.test.ts b/src/test/abilities/beast_boost.test.ts
new file mode 100644
index 00000000000..cfe015c822e
--- /dev/null
+++ b/src/test/abilities/beast_boost.test.ts
@@ -0,0 +1,97 @@
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
+import { Abilities } from "#enums/abilities";
+import { Moves } from "#enums/moves";
+import { Species } from "#enums/species";
+import Phaser from "phaser";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { SPLASH_ONLY } from "../utils/testUtils";
+import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
+import { VictoryPhase } from "#app/phases/victory-phase";
+import { TurnStartPhase } from "#app/phases/turn-start-phase";
+import { BattlerIndex } from "#app/battle";
+
+describe("Abilities - Beast Boost", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemySpecies(Species.BULBASAUR)
+ .enemyAbility(Abilities.BEAST_BOOST)
+ .ability(Abilities.BEAST_BOOST)
+ .startingLevel(2000)
+ .moveset([ Moves.FLAMETHROWER ])
+ .enemyMoveset(SPLASH_ONLY);
+ });
+
+ // Note that both MOXIE and BEAST_BOOST test for their current implementation and not their mainline behavior.
+ it("should prefer highest stat to boost its corresponding stat stage by 1 when winning a battle", async() => {
+ // SLOWBRO's highest stat is DEF, so it should be picked here
+ await game.startBattle([
+ Species.SLOWBRO
+ ]);
+
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+
+ expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0);
+
+ game.move.select(Moves.FLAMETHROWER);
+ await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
+
+ expect(playerPokemon.getStatStage(Stat.DEF)).toBe(1);
+ }, 20000);
+
+ it("should use in-battle overriden stats when determining the stat stage to raise by 1", async() => {
+ // If the opponent can GUARD_SPLIT, SLOWBRO's second highest stat should be SPATK
+ game.override.enemyMoveset(new Array(4).fill(Moves.GUARD_SPLIT));
+
+ await game.startBattle([
+ Species.SLOWBRO
+ ]);
+
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0);
+
+ game.move.select(Moves.FLAMETHROWER);
+
+ await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
+
+ await game.phaseInterceptor.runFrom(TurnStartPhase).to(VictoryPhase);
+
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
+ }, 20000);
+
+ it("should have order preference in case of stat ties", async() => {
+ // Order preference follows the order of EFFECTIVE_STAT
+ await game.startBattle([
+ Species.SLOWBRO
+ ]);
+
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+
+ // Set up tie between SPATK, SPDEF, and SPD, where SPATK should win
+ vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([ 10000, 1, 1, 100, 100, 100 ]);
+
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0);
+
+ game.move.select(Moves.FLAMETHROWER);
+
+ await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
+
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
+ }, 20000);
+});
diff --git a/src/test/abilities/contrary.test.ts b/src/test/abilities/contrary.test.ts
new file mode 100644
index 00000000000..19ecc7e0240
--- /dev/null
+++ b/src/test/abilities/contrary.test.ts
@@ -0,0 +1,42 @@
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
+import { Abilities } from "#enums/abilities";
+import { Species } from "#enums/species";
+import Phaser from "phaser";
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { SPLASH_ONLY } from "../utils/testUtils";
+
+describe("Abilities - Contrary", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemySpecies(Species.BULBASAUR)
+ .enemyAbility(Abilities.CONTRARY)
+ .ability(Abilities.INTIMIDATE)
+ .enemyMoveset(SPLASH_ONLY);
+ });
+
+ it("should invert stat changes when applied", async() => {
+ await game.startBattle([
+ Species.SLOWBRO
+ ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
+ }, 20000);
+});
diff --git a/src/test/abilities/costar.test.ts b/src/test/abilities/costar.test.ts
index 9a4baeef1fb..96ec775f2a0 100644
--- a/src/test/abilities/costar.test.ts
+++ b/src/test/abilities/costar.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
@@ -35,7 +35,7 @@ describe("Abilities - COSTAR", () => {
test(
- "ability copies positive stat changes",
+ "ability copies positive stat stages",
async () => {
game.override.enemyAbility(Abilities.BALL_FETCH);
@@ -48,8 +48,8 @@ describe("Abilities - COSTAR", () => {
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
- expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2);
- expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0);
+ expect(leftPokemon.getStatStage(Stat.SPATK)).toBe(2);
+ expect(rightPokemon.getStatStage(Stat.SPATK)).toBe(0);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(CommandPhase);
@@ -57,14 +57,14 @@ describe("Abilities - COSTAR", () => {
await game.phaseInterceptor.to(MessagePhase);
[leftPokemon, rightPokemon] = game.scene.getPlayerField();
- expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2);
- expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2);
+ expect(leftPokemon.getStatStage(Stat.SPATK)).toBe(2);
+ expect(rightPokemon.getStatStage(Stat.SPATK)).toBe(2);
},
TIMEOUT,
);
test(
- "ability copies negative stat changes",
+ "ability copies negative stat stages",
async () => {
game.override.enemyAbility(Abilities.INTIMIDATE);
@@ -72,8 +72,8 @@ describe("Abilities - COSTAR", () => {
let [leftPokemon, rightPokemon] = game.scene.getPlayerField();
- expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
- expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
+ expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2);
+ expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(CommandPhase);
@@ -81,8 +81,8 @@ describe("Abilities - COSTAR", () => {
await game.phaseInterceptor.to(MessagePhase);
[leftPokemon, rightPokemon] = game.scene.getPlayerField();
- expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
- expect(rightPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
+ expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2);
+ expect(rightPokemon.getStatStage(Stat.ATK)).toBe(-2);
},
TIMEOUT,
);
diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts
index bbb0a20dc1a..f7c45e91724 100644
--- a/src/test/abilities/disguise.test.ts
+++ b/src/test/abilities/disguise.test.ts
@@ -1,8 +1,8 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { StatusEffect } from "#app/data/status-effect";
import { toDmgValue } from "#app/utils";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
+import { StatusEffect } from "#app/data/status-effect";
+import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
@@ -36,7 +36,7 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT);
it("takes no damage from attacking move and transforms to Busted form, takes 1/8 max HP damage from the disguise breaking", async () => {
- await game.startBattle();
+ await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!;
const maxHp = mimikyu.getMaxHp();
@@ -53,7 +53,7 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT);
it("doesn't break disguise when attacked with ineffective move", async () => {
- await game.startBattle();
+ await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!;
@@ -67,9 +67,9 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT);
it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => {
- game.override.moveset([Moves.SURGING_STRIKES]);
+ game.override.moveset([ Moves.SURGING_STRIKES ]);
game.override.enemyLevel(5);
- await game.startBattle();
+ await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!;
const maxHp = mimikyu.getMaxHp();
@@ -91,7 +91,7 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT);
it("takes effects from status moves and damage from status effects", async () => {
- await game.startBattle();
+ await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!;
expect(mimikyu.hp).toBe(mimikyu.getMaxHp());
@@ -102,7 +102,7 @@ describe("Abilities - Disguise", () => {
expect(mimikyu.formIndex).toBe(disguisedForm);
expect(mimikyu.status?.effect).toBe(StatusEffect.POISON);
- expect(mimikyu.summonData.battleStats[BattleStat.SPD]).toBe(-1);
+ expect(mimikyu.getStatStage(Stat.SPD)).toBe(-1);
expect(mimikyu.hp).toBeLessThan(mimikyu.getMaxHp());
}, TIMEOUT);
@@ -110,7 +110,7 @@ describe("Abilities - Disguise", () => {
game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK));
game.override.starterSpecies(0);
- await game.startBattle([Species.MIMIKYU, Species.FURRET]);
+ await game.classicMode.startBattle([ Species.MIMIKYU, Species.FURRET ]);
const mimikyu = game.scene.getPlayerPokemon()!;
const maxHp = mimikyu.getMaxHp();
@@ -136,7 +136,7 @@ describe("Abilities - Disguise", () => {
game.override.starterForms({
[Species.MIMIKYU]: bustedForm
});
- await game.startBattle([Species.FURRET, Species.MIMIKYU]);
+ await game.classicMode.startBattle([ Species.FURRET, Species.MIMIKYU ]);
const mimikyu = game.scene.getParty()[1]!;
expect(mimikyu.formIndex).toBe(bustedForm);
@@ -155,7 +155,7 @@ describe("Abilities - Disguise", () => {
[Species.MIMIKYU]: bustedForm
});
- await game.startBattle();
+ await game.classicMode.startBattle();
const mimikyu = game.scene.getPlayerPokemon()!;
@@ -175,7 +175,7 @@ describe("Abilities - Disguise", () => {
[Species.MIMIKYU]: bustedForm
});
- await game.startBattle([Species.MIMIKYU, Species.FURRET]);
+ await game.classicMode.startBattle([ Species.MIMIKYU, Species.FURRET ]);
const mimikyu1 = game.scene.getPlayerPokemon()!;
@@ -194,7 +194,7 @@ describe("Abilities - Disguise", () => {
it("doesn't faint twice when fainting due to Disguise break damage, nor prevent faint from Disguise break damage if using Endure", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.ENDURE));
- await game.startBattle();
+ await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!;
mimikyu.hp = 1;
diff --git a/src/test/abilities/flower_gift.test.ts b/src/test/abilities/flower_gift.test.ts
index f8c1141386d..de07bd29478 100644
--- a/src/test/abilities/flower_gift.test.ts
+++ b/src/test/abilities/flower_gift.test.ts
@@ -49,16 +49,16 @@ describe("Abilities - Flower Gift", () => {
});
// TODO: Uncomment expect statements when the ability is implemented - currently does not increase stats of allies
- it("increases the Attack and Special Defense stats of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => {
+ it("increases the ATK and SPDEF stat stages of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]);
const [ cherrim ] = game.scene.getPlayerField();
- const cherrimAtkStat = cherrim.getBattleStat(Stat.ATK);
- const cherrimSpDefStat = cherrim.getBattleStat(Stat.SPDEF);
+ const cherrimAtkStat = cherrim.getEffectiveStat(Stat.ATK);
+ const cherrimSpDefStat = cherrim.getEffectiveStat(Stat.SPDEF);
- // const magikarpAtkStat = magikarp.getBattleStat(Stat.ATK);;
- // const magikarpSpDefStat = magikarp.getBattleStat(Stat.SPDEF);
+ // const magikarpAtkStat = magikarp.getEffectiveStat(Stat.ATK);;
+ // const magikarpSpDefStat = magikarp.getEffectiveStat(Stat.SPDEF);
game.move.select(Moves.SUNNY_DAY, 0);
game.move.select(Moves.SPLASH, 1);
@@ -67,10 +67,10 @@ describe("Abilities - Flower Gift", () => {
await game.phaseInterceptor.to("TurnEndPhase");
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
- expect(cherrim.getBattleStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5));
- expect(cherrim.getBattleStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5));
- // expect(magikarp.getBattleStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5));
- // expect(magikarp.getBattleStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5));
+ expect(cherrim.getEffectiveStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5));
+ expect(cherrim.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5));
+ // expect(magikarp.getEffectiveStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5));
+ // expect(magikarp.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5));
});
it("changes the Pokemon's form during Harsh Sunlight", async () => {
diff --git a/src/test/abilities/gulp_missile.test.ts b/src/test/abilities/gulp_missile.test.ts
index a451d290906..286c3af1c56 100644
--- a/src/test/abilities/gulp_missile.test.ts
+++ b/src/test/abilities/gulp_missile.test.ts
@@ -1,4 +1,3 @@
-import { BattleStat } from "#app/data/battle-stat";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { StatusEffect } from "#app/enums/status-effect";
import Pokemon from "#app/field/pokemon";
@@ -13,6 +12,7 @@ import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
+import { Stat } from "#enums/stat";
describe("Abilities - Gulp Missile", () => {
let phaserGame: Phaser.Game;
@@ -107,7 +107,7 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
- it("deals ¼ of the attacker's maximum HP when hit by a damaging attack", async () => {
+ it("deals 1/4 of the attacker's maximum HP when hit by a damaging attack", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.startBattle([Species.CRAMORANT]);
@@ -139,7 +139,7 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
- it("lowers the attacker's Defense by 1 stage when hit in Gulping form", async () => {
+ it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.startBattle([Species.CRAMORANT]);
@@ -158,7 +158,7 @@ describe("Abilities - Gulp Missile", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
- expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1);
+ expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
expect(cramorant.formIndex).toBe(NORMAL_FORM);
});
@@ -219,7 +219,7 @@ describe("Abilities - Gulp Missile", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemy.hp).toBe(enemyHpPreEffect);
- expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1);
+ expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
expect(cramorant.formIndex).toBe(NORMAL_FORM);
});
diff --git a/src/test/abilities/hustle.test.ts b/src/test/abilities/hustle.test.ts
index 276edb691c9..ff96b98c7ac 100644
--- a/src/test/abilities/hustle.test.ts
+++ b/src/test/abilities/hustle.test.ts
@@ -26,7 +26,7 @@ describe("Abilities - Hustle", () => {
game = new GameManager(phaserGame);
game.override
.ability(Abilities.HUSTLE)
- .moveset([Moves.TACKLE, Moves.GIGA_DRAIN, Moves.FISSURE])
+ .moveset([ Moves.TACKLE, Moves.GIGA_DRAIN, Moves.FISSURE ])
.disableCrits()
.battleType("single")
.enemyMoveset(SPLASH_ONLY)
@@ -39,13 +39,13 @@ describe("Abilities - Hustle", () => {
const pikachu = game.scene.getPlayerPokemon()!;
const atk = pikachu.stats[Stat.ATK];
- vi.spyOn(pikachu, "getBattleStat");
+ vi.spyOn(pikachu, "getEffectiveStat");
game.move.select(Moves.TACKLE);
await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase");
- expect(pikachu.getBattleStat).toHaveReturnedWith(Math.floor(atk * 1.5));
+ expect(pikachu.getEffectiveStat).toHaveReturnedWith(Math.floor(atk * 1.5));
});
it("lowers the accuracy of the user's physical moves by 20%", async () => {
@@ -65,13 +65,13 @@ describe("Abilities - Hustle", () => {
const pikachu = game.scene.getPlayerPokemon()!;
const spatk = pikachu.stats[Stat.SPATK];
- vi.spyOn(pikachu, "getBattleStat");
+ vi.spyOn(pikachu, "getEffectiveStat");
vi.spyOn(pikachu, "getAccuracyMultiplier");
game.move.select(Moves.GIGA_DRAIN);
await game.phaseInterceptor.to("DamagePhase");
- expect(pikachu.getBattleStat).toHaveReturnedWith(spatk);
+ expect(pikachu.getEffectiveStat).toHaveReturnedWith(spatk);
expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1);
});
diff --git a/src/test/abilities/hyper_cutter.test.ts b/src/test/abilities/hyper_cutter.test.ts
index 28fcc2f6085..64e04ac2fd3 100644
--- a/src/test/abilities/hyper_cutter.test.ts
+++ b/src/test/abilities/hyper_cutter.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@@ -51,7 +51,7 @@ describe("Abilities - Hyper Cutter", () => {
game.move.select(Moves.STRING_SHOT);
await game.toNextTurn();
- expect(enemy.summonData.battleStats[BattleStat.ATK]).toEqual(0);
- [BattleStat.ACC, BattleStat.DEF, BattleStat.EVA, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD].forEach((stat: number) => expect(enemy.summonData.battleStats[stat]).toBeLessThan(0));
+ expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
+ [Stat.ACC, Stat.DEF, Stat.EVA, Stat.SPATK, Stat.SPDEF, Stat.SPD].forEach((stat: number) => expect(enemy.getStatStage(stat)).toBeLessThan(0));
});
});
diff --git a/src/test/abilities/imposter.test.ts b/src/test/abilities/imposter.test.ts
new file mode 100644
index 00000000000..2857f80632a
--- /dev/null
+++ b/src/test/abilities/imposter.test.ts
@@ -0,0 +1,101 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#app/test/utils/gameManager";
+import { Species } from "#enums/species";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
+import { Abilities } from "#enums/abilities";
+import { SPLASH_ONLY } from "../utils/testUtils";
+
+// TODO: Add more tests once Imposter is fully implemented
+describe("Abilities - Imposter", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemySpecies(Species.MEW)
+ .enemyLevel(200)
+ .enemyAbility(Abilities.BEAST_BOOST)
+ .enemyPassiveAbility(Abilities.BALL_FETCH)
+ .enemyMoveset(SPLASH_ONLY)
+ .ability(Abilities.IMPOSTER)
+ .moveset(SPLASH_ONLY);
+ });
+
+ it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
+ await game.startBattle([
+ Species.DITTO
+ ]);
+
+ game.move.select(Moves.SPLASH);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
+ expect(player.getAbility()).toBe(enemy.getAbility());
+ expect(player.getGender()).toBe(enemy.getGender());
+
+ expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP));
+ for (const s of EFFECTIVE_STATS) {
+ expect(player.getStat(s, false)).toBe(enemy.getStat(s, false));
+ }
+
+ for (const s of BATTLE_STATS) {
+ expect(player.getStatStage(s)).toBe(enemy.getStatStage(s));
+ }
+
+ const playerMoveset = player.getMoveset();
+ const enemyMoveset = player.getMoveset();
+
+ for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) {
+ // TODO: Checks for 5 PP should be done here when that gets addressed
+ expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
+ }
+
+ const playerTypes = player.getTypes();
+ const enemyTypes = enemy.getTypes();
+
+ for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
+ expect(playerTypes[i]).toBe(enemyTypes[i]);
+ }
+ }, 20000);
+
+ it("should copy in-battle overridden stats", async () => {
+ game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT));
+
+ await game.startBattle([
+ Species.DITTO
+ ]);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2);
+ const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2);
+
+ game.move.select(Moves.TACKLE);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStat(Stat.ATK, false)).toBe(avgAtk);
+ expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk);
+
+ expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ });
+});
diff --git a/src/test/abilities/intimidate.test.ts b/src/test/abilities/intimidate.test.ts
index bc5b5ab1a7d..f90ba6c0e1e 100644
--- a/src/test/abilities/intimidate.test.ts
+++ b/src/test/abilities/intimidate.test.ts
@@ -1,17 +1,13 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { Status, StatusEffect } from "#app/data/status-effect";
-import { GameModes, getGameMode } from "#app/game-mode";
-import { EncounterPhase } from "#app/phases/encounter-phase";
-import { SelectStarterPhase } from "#app/phases/select-starter-phase";
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#test/utils/gameManager";
import { Mode } from "#app/ui/ui";
+import { Stat } from "#enums/stat";
+import { getMovePosition } from "#test/utils/gameManagerUtils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
-import { generateStarter } from "#test/utils/gameManagerUtils";
import { SPLASH_ONLY } from "#test/utils/testUtils";
-import Phaser from "phaser";
-import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Intimidate", () => {
let phaserGame: Phaser.Game;
@@ -29,257 +25,113 @@ describe("Abilities - Intimidate", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
- game.override
- .battleType("single")
- .enemySpecies(Species.MAGIKARP)
+ game.override.battleType("single")
+ .enemySpecies(Species.RATTATA)
.enemyAbility(Abilities.INTIMIDATE)
+ .enemyPassiveAbility(Abilities.HYDRATION)
.ability(Abilities.INTIMIDATE)
- .moveset([Moves.SPLASH, Moves.AERIAL_ACE])
+ .startingWave(3)
.enemyMoveset(SPLASH_ONLY);
});
- it("single - wild with switch", async () => {
- await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
-
- let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
-
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- game.doSwitchPokemon(1);
- await game.phaseInterceptor.to("CommandPhase");
-
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
-
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
- }, 20000);
-
- it("single - boss should only trigger once then switch", async () => {
- game.override.startingWave(10);
- await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
-
- let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
-
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- game.doSwitchPokemon(1);
- await game.phaseInterceptor.to("CommandPhase");
-
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
-
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
- }, 20000);
-
- it("single - trainer should only trigger once with switch", async () => {
- game.override.startingWave(5);
- await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
-
- let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
-
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- game.doSwitchPokemon(1);
- await game.phaseInterceptor.to("CommandPhase");
-
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
-
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
- }, 200000);
-
- it("double - trainer should only trigger once per pokemon", async () => {
- game.override
- .battleType("double")
- .startingWave(5);
- await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
-
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
-
- const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
- expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
-
- const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
-
- const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats;
- expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2);
- }, 20000);
-
- it("double - wild: should only trigger once per pokemon", async () => {
- game.override.battleType("double");
- await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
-
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
-
- const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
- expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
-
- const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
-
- const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats;
- expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2);
- }, 20000);
-
- it("double - boss: should only trigger once per pokemon", async () => {
- game.override
- .battleType("double")
- .startingWave(10);
- await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
-
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
-
- const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
- expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
-
- const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
-
- const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats;
- expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2);
- }, 20000);
-
- it("single - wild next wave opp triger once, us: none", async () => {
- await game.startBattle([Species.MIGHTYENA]);
-
- let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
-
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- game.move.select(Moves.AERIAL_ACE);
- await game.phaseInterceptor.to("DamagePhase");
- await game.doKillOpponents();
- await game.toNextWave();
-
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
-
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
- }, 20000);
-
- it("single - wild next turn - no retrigger on next turn", async () => {
- await game.startBattle([Species.MIGHTYENA]);
-
- let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
-
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- game.move.select(Moves.SPLASH);
- await game.toNextTurn();
-
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
- }, 20000);
-
- it("single - trainer should only trigger once and each time he switch", async () => {
- game.override
- .enemyMoveset(Array(4).fill(Moves.VOLT_SWITCH))
- .startingWave(5);
- await game.startBattle([Species.MIGHTYENA]);
-
- let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
-
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- game.move.select(Moves.SPLASH);
- await game.toNextTurn();
-
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
-
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
-
- game.move.select(Moves.SPLASH);
- await game.toNextTurn();
-
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-3);
-
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
- }, 200000);
-
- it("single - trainer should only trigger once whatever turn we are", async () => {
- game.override.startingWave(5);
- await game.startBattle([Species.MIGHTYENA]);
-
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
-
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
-
- game.move.select(Moves.SPLASH);
- await game.toNextTurn();
-
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
- }, 20000);
-
- it("double - wild vs only 1 on player side", async () => {
- game.override.battleType("double");
- await game.classicMode.runToSummon([Species.MIGHTYENA]);
+ it("should lower ATK stat stage by 1 of enemy Pokemon on entry and player switch", async () => {
+ await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
+ game.onNextPrompt(
+ "CheckSwitchPhase",
+ Mode.CONFIRM,
+ () => {
+ game.setMode(Mode.MESSAGE);
+ game.endPhase();
+ },
+ () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase")
+ );
await game.phaseInterceptor.to("CommandPhase", false);
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
+ let playerPokemon = game.scene.getPlayerPokemon()!;
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
- const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
- expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1);
+ expect(playerPokemon.species.speciesId).toBe(Species.MIGHTYENA);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
- const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
+ game.doSwitchPokemon(1);
+ await game.phaseInterceptor.run("CommandPhase");
+ await game.phaseInterceptor.to("CommandPhase");
+
+ playerPokemon = game.scene.getPlayerPokemon()!;
+ expect(playerPokemon.species.speciesId).toBe(Species.POOCHYENA);
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
}, 20000);
- it("double - wild vs only 1 alive on player side", async () => {
- game.override.battleType("double");
- await game.runToTitle();
-
- game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
- game.scene.gameMode = getGameMode(GameModes.CLASSIC);
- const starters = generateStarter(game.scene, [Species.MIGHTYENA, Species.POOCHYENA]);
- const selectStarterPhase = new SelectStarterPhase(game.scene);
- game.scene.pushPhase(new EncounterPhase(game.scene, false));
- selectStarterPhase.initBattle(starters);
- game.scene.getParty()[1].hp = 0;
- game.scene.getParty()[1].status = new Status(StatusEffect.FAINT);
- });
-
- await game.phaseInterceptor.run(EncounterPhase);
-
+ it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => {
+ game.override.battleType("double")
+ .startingWave(3);
+ await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
+ game.onNextPrompt(
+ "CheckSwitchPhase",
+ Mode.CONFIRM,
+ () => {
+ game.setMode(Mode.MESSAGE);
+ game.endPhase();
+ },
+ () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase")
+ );
await game.phaseInterceptor.to("CommandPhase", false);
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
+ const playerField = game.scene.getPlayerField()!;
+ const enemyField = game.scene.getEnemyField()!;
- const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
- expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1);
-
- const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
+ expect(enemyField[0].getStatStage(Stat.ATK)).toBe(-2);
+ expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2);
+ expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2);
+ expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2);
}, 20000);
+
+ it("should not activate again if there is no switch or new entry", async () => {
+ game.override.startingWave(2);
+ game.override.moveset([Moves.SPLASH]);
+ await game.classicMode.startBattle([ Species.MIGHTYENA, Species.POOCHYENA ]);
+
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
+
+ game.move.select(Moves.SPLASH);
+ await game.toNextTurn();
+
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
+ }, 20000);
+
+ it("should lower ATK stat stage by 1 for every switch", async () => {
+ game.override.moveset([Moves.SPLASH])
+ .enemyMoveset(new Array(4).fill(Moves.VOLT_SWITCH))
+ .startingWave(5);
+ await game.classicMode.startBattle([ Species.MIGHTYENA, Species.POOCHYENA ]);
+
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+ let enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
+
+ game.move.select(getMovePosition(game.scene, 0, Moves.SPLASH));
+ await game.toNextTurn();
+
+ enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
+
+ game.move.select(Moves.SPLASH);
+ await game.toNextTurn();
+
+ enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
+ }, 200000);
});
diff --git a/src/test/abilities/intrepid_sword.test.ts b/src/test/abilities/intrepid_sword.test.ts
index 18d6c04adbc..7bf0654276c 100644
--- a/src/test/abilities/intrepid_sword.test.ts
+++ b/src/test/abilities/intrepid_sword.test.ts
@@ -1,8 +1,8 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
import { CommandPhase } from "#app/phases/command-phase";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@@ -29,14 +29,17 @@ describe("Abilities - Intrepid Sword", () => {
game.override.ability(Abilities.INTREPID_SWORD);
});
- it("INTREPID SWORD on player", async() => {
+ it("should raise ATK stat stage by 1 on entry", async() => {
await game.classicMode.runToSummon([
Species.ZACIAN,
]);
+
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
await game.phaseInterceptor.to(CommandPhase, false);
- const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(1);
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(1);
+
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000);
});
diff --git a/src/test/abilities/moody.test.ts b/src/test/abilities/moody.test.ts
index 9e936e8100a..5c46ea68ec5 100644
--- a/src/test/abilities/moody.test.ts
+++ b/src/test/abilities/moody.test.ts
@@ -1,18 +1,16 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
-import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Moody", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
- const battleStatsArray = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD];
-
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
@@ -30,63 +28,61 @@ describe("Abilities - Moody", () => {
.battleType("single")
.enemySpecies(Species.RATTATA)
.enemyAbility(Abilities.BALL_FETCH)
- .enemyPassiveAbility(Abilities.HYDRATION)
.ability(Abilities.MOODY)
.enemyMoveset(SPLASH_ONLY)
.moveset(SPLASH_ONLY);
});
- it(
- "should increase one BattleStat by 2 stages and decrease a different BattleStat by 1 stage",
+ it("should increase one stat stage by 2 and decrease a different stat stage by 1",
async () => {
- await game.startBattle();
+ await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// Find the increased and decreased stats, make sure they are different.
- const statChanges = playerPokemon.summonData.battleStats;
- const changedStats = battleStatsArray.filter(bs => statChanges[bs] === 2 || statChanges[bs] === -1);
+ const changedStats = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === 2 || playerPokemon.getStatStage(s) === -1);
expect(changedStats).toBeTruthy();
expect(changedStats.length).toBe(2);
expect(changedStats[0] !== changedStats[1]).toBeTruthy();
});
- it(
- "should only increase one BattleStat by 2 stages if all BattleStats are at -6",
+ it("should only increase one stat stage by 2 if all stat stages are at -6",
async () => {
- await game.startBattle();
+ await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
- // Set all BattleStats to -6
- battleStatsArray.forEach(bs => playerPokemon.summonData.battleStats[bs] = -6);
+
+ // Set all stat stages to -6
+ vi.spyOn(playerPokemon.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(-6));
game.move.select(Moves.SPLASH);
await game.toNextTurn();
- // Should increase one BattleStat by 2 (from -6, meaning it will be -4)
- const increasedStat = battleStatsArray.filter(bs => playerPokemon.summonData.battleStats[bs] === -4);
+ // Should increase one stat stage by 2 (from -6, meaning it will be -4)
+ const increasedStat = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === -4);
expect(increasedStat).toBeTruthy();
expect(increasedStat.length).toBe(1);
});
- it(
- "should only decrease one BattleStat by 1 stage if all BattleStats are at 6",
+ it("should only decrease one stat stage by 1 stage if all stat stages are at 6",
async () => {
- await game.startBattle();
+ await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
- // Set all BattleStats to 6
- battleStatsArray.forEach(bs => playerPokemon.summonData.battleStats[bs] = 6);
+
+ // Set all stat stages to 6
+ vi.spyOn(playerPokemon.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(6));
game.move.select(Moves.SPLASH);
await game.toNextTurn();
- // Should decrease one BattleStat by 1 (from 6, meaning it will be 5)
- const decreasedStat = battleStatsArray.filter(bs => playerPokemon.summonData.battleStats[bs] === 5);
+ // Should decrease one stat stage by 1 (from 6, meaning it will be 5)
+ const decreasedStat = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === 5);
+
expect(decreasedStat).toBeTruthy();
expect(decreasedStat.length).toBe(1);
});
diff --git a/src/test/abilities/moxie.test.ts b/src/test/abilities/moxie.test.ts
index 6a1838c9a98..e713d78f39e 100644
--- a/src/test/abilities/moxie.test.ts
+++ b/src/test/abilities/moxie.test.ts
@@ -1,14 +1,15 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { Stat } from "#app/data/pokemon-stat";
-import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
-import { VictoryPhase } from "#app/phases/victory-phase";
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
-
+import { SPLASH_ONLY } from "../utils/testUtils";
+import { BattlerIndex } from "#app/battle";
+import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
+import { VictoryPhase } from "#app/phases/victory-phase";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
describe("Abilities - Moxie", () => {
let phaserGame: Phaser.Game;
@@ -32,23 +33,47 @@ describe("Abilities - Moxie", () => {
game.override.enemyAbility(Abilities.MOXIE);
game.override.ability(Abilities.MOXIE);
game.override.startingLevel(2000);
- game.override.moveset([moveToUse]);
- game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
+ game.override.moveset([ moveToUse ]);
+ game.override.enemyMoveset(SPLASH_ONLY);
});
- it("MOXIE", async () => {
+ it("should raise ATK stat stage by 1 when winning a battle", async() => {
const moveToUse = Moves.AERIAL_ACE;
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[Stat.ATK]).toBe(0);
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
game.move.select(moveToUse);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.ATK]).toBe(1);
+
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
+ }, 20000);
+
+ // TODO: Activate this test when MOXIE is corrected to work on faint and not on battle victory
+ it.todo("should raise ATK stat stage by 1 when defeating an ally Pokemon", async() => {
+ game.override.battleType("double");
+ const moveToUse = Moves.AERIAL_ACE;
+ await game.startBattle([
+ Species.MIGHTYENA,
+ Species.MIGHTYENA,
+ ]);
+
+ const [ firstPokemon, secondPokemon ] = game.scene.getPlayerField();
+
+ expect(firstPokemon.getStatStage(Stat.ATK)).toBe(0);
+
+ secondPokemon.hp = 1;
+
+ game.move.select(moveToUse);
+ game.selectTarget(BattlerIndex.PLAYER_2);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(firstPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000);
});
diff --git a/src/test/abilities/mycelium_might.test.ts b/src/test/abilities/mycelium_might.test.ts
index d5bea185f59..d8947935880 100644
--- a/src/test/abilities/mycelium_might.test.ts
+++ b/src/test/abilities/mycelium_might.test.ts
@@ -1,10 +1,10 @@
-import { BattleStat } from "#app/data/battle-stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
+import { Stat } from "#enums/stat";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@@ -58,8 +58,9 @@ describe("Abilities - Mycelium Might", () => {
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
await game.phaseInterceptor.to(TurnEndPhase);
- // Despite the opponent's ability (Clear Body), its attack stat is still reduced.
- expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1);
+
+ // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
+ expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
}, 20000);
it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
@@ -81,8 +82,8 @@ describe("Abilities - Mycelium Might", () => {
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
await game.phaseInterceptor.to(TurnEndPhase);
- // Despite the opponent's ability (Clear Body), its attack stat is still reduced.
- expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1);
+ // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
+ expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
}, 20000);
it("will not affect non-status moves", async () => {
diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts
index 1404f597ccf..e3c6c8ec5bb 100644
--- a/src/test/abilities/parental_bond.test.ts
+++ b/src/test/abilities/parental_bond.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { StatusEffect } from "#app/data/status-effect";
import { Type } from "#app/data/type";
import { BattlerTagType } from "#app/enums/battler-tag-type";
@@ -96,7 +96,7 @@ describe("Abilities - Parental Bond", () => {
await game.phaseInterceptor.to(BerryPhase, false);
expect(leadPokemon.turnData.hitCount).toBe(2);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
}, TIMEOUT
);
@@ -116,7 +116,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.BABY_DOLL_EYES);
await game.phaseInterceptor.to(BerryPhase, false);
- expect(enemyPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, TIMEOUT
);
@@ -568,7 +568,7 @@ describe("Abilities - Parental Bond", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, TIMEOUT
);
@@ -590,7 +590,7 @@ describe("Abilities - Parental Bond", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- expect(enemyPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, TIMEOUT
);
diff --git a/src/test/abilities/sand_veil.test.ts b/src/test/abilities/sand_veil.test.ts
index 2336e2b50de..da9fdcc01ab 100644
--- a/src/test/abilities/sand_veil.test.ts
+++ b/src/test/abilities/sand_veil.test.ts
@@ -1,5 +1,5 @@
-import { BattleStatMultiplierAbAttr, allAbilities } from "#app/data/ability";
-import { BattleStat } from "#app/data/battle-stat";
+import { StatMultiplierAbAttr, allAbilities } from "#app/data/ability";
+import { Stat } from "#enums/stat";
import { WeatherType } from "#app/data/weather";
import { CommandPhase } from "#app/phases/command-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
@@ -49,10 +49,10 @@ describe("Abilities - Sand Veil", () => {
vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[Abilities.SAND_VEIL]);
- const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(BattleStatMultiplierAbAttr)[0];
- vi.spyOn(sandVeilAttr, "applyBattleStat").mockImplementation(
- (pokemon, passive, simulated, battleStat, statValue, args) => {
- if (battleStat === BattleStat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) {
+ const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(StatMultiplierAbAttr)[0];
+ vi.spyOn(sandVeilAttr, "applyStatStage").mockImplementation(
+ (_pokemon, _passive, _simulated, stat, statValue, _args) => {
+ if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) {
statValue.value *= -1; // will make all attacks miss
return true;
}
diff --git a/src/test/abilities/sap_sipper.test.ts b/src/test/abilities/sap_sipper.test.ts
index f9c20e85eab..2d70ede3530 100644
--- a/src/test/abilities/sap_sipper.test.ts
+++ b/src/test/abilities/sap_sipper.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { TerrainType } from "#app/data/terrain";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
@@ -9,6 +9,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { SPLASH_ONLY } from "../utils/testUtils";
// See also: TypeImmunityAbAttr
describe("Abilities - Sap Sipper", () => {
@@ -31,52 +32,55 @@ describe("Abilities - Sap Sipper", () => {
game.override.disableCrits();
});
- it("raise attack 1 level and block effects when activated against a grass attack", async () => {
+ it("raises ATK stat stage by 1 and block effects when activated against a grass attack", async() => {
const moveToUse = Moves.LEAFAGE;
const enemyAbility = Abilities.SAP_SIPPER;
- game.override.moveset([moveToUse]);
- game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]);
+ game.override.moveset([ moveToUse ]);
+ game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.DUSKULL);
game.override.enemyAbility(enemyAbility);
await game.startBattle();
- const startingOppHp = game.scene.currentBattle.enemyParty[0].hp;
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+ const initialEnemyHp = enemyPokemon.hp;
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0);
- expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1);
+ expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
});
- it("raise attack 1 level and block effects when activated against a grass status move", async () => {
+ it("raises ATK stat stage by 1 and block effects when activated against a grass status move", async() => {
const moveToUse = Moves.SPORE;
const enemyAbility = Abilities.SAP_SIPPER;
- game.override.moveset([moveToUse]);
- game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]);
+ game.override.moveset([ moveToUse ]);
+ game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(enemyAbility);
await game.startBattle();
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(game.scene.getEnemyParty()[0].status).toBeUndefined();
- expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1);
+ expect(enemyPokemon.status).toBeUndefined();
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
});
it("do not activate against status moves that target the field", async () => {
const moveToUse = Moves.GRASSY_TERRAIN;
const enemyAbility = Abilities.SAP_SIPPER;
- game.override.moveset([moveToUse]);
- game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]);
+ game.override.moveset([ moveToUse ]);
+ game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(enemyAbility);
@@ -88,51 +92,54 @@ describe("Abilities - Sap Sipper", () => {
expect(game.scene.arena.terrain).toBeDefined();
expect(game.scene.arena.terrain!.terrainType).toBe(TerrainType.GRASSY);
- expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
it("activate once against multi-hit grass attacks", async () => {
const moveToUse = Moves.BULLET_SEED;
const enemyAbility = Abilities.SAP_SIPPER;
- game.override.moveset([moveToUse]);
- game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]);
+ game.override.moveset([ moveToUse ]);
+ game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(enemyAbility);
await game.startBattle();
- const startingOppHp = game.scene.currentBattle.enemyParty[0].hp;
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+ const initialEnemyHp = enemyPokemon.hp;
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0);
- expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1);
+ expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
});
it("do not activate against status moves that target the user", async () => {
const moveToUse = Moves.SPIKY_SHIELD;
const ability = Abilities.SAP_SIPPER;
- game.override.moveset([moveToUse]);
+ game.override.moveset([ moveToUse ]);
game.override.ability(ability);
- game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]);
+ game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(Abilities.NONE);
await game.startBattle();
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+
game.move.select(moveToUse);
await game.phaseInterceptor.to(MoveEndPhase);
- expect(game.scene.getParty()[0].getTag(BattlerTagType.SPIKY_SHIELD)).toBeDefined();
+ expect(playerPokemon.getTag(BattlerTagType.SPIKY_SHIELD)).toBeDefined();
await game.phaseInterceptor.to(TurnEndPhase);
- expect(game.scene.getParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
});
@@ -149,13 +156,14 @@ describe("Abilities - Sap Sipper", () => {
await game.startBattle();
- const startingOppHp = game.scene.currentBattle.enemyParty[0].hp;
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+ const initialEnemyHp = enemyPokemon.hp;
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0);
- expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1);
+ expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
});
});
diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts
index 7316b2ea920..e06288b9de9 100644
--- a/src/test/abilities/serene_grace.test.ts
+++ b/src/test/abilities/serene_grace.test.ts
@@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";
diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts
index f73b749dac2..69b47e1eaae 100644
--- a/src/test/abilities/sheer_force.test.ts
+++ b/src/test/abilities/sheer_force.test.ts
@@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability";
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";
diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts
index 14770c49427..8a0b769827d 100644
--- a/src/test/abilities/shield_dust.test.ts
+++ b/src/test/abilities/shield_dust.test.ts
@@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";
diff --git a/src/test/abilities/simple.test.ts b/src/test/abilities/simple.test.ts
new file mode 100644
index 00000000000..4310c5d45d1
--- /dev/null
+++ b/src/test/abilities/simple.test.ts
@@ -0,0 +1,42 @@
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
+import { Abilities } from "#enums/abilities";
+import { Species } from "#enums/species";
+import Phaser from "phaser";
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { SPLASH_ONLY } from "../utils/testUtils";
+
+describe("Abilities - Simple", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemySpecies(Species.BULBASAUR)
+ .enemyAbility(Abilities.SIMPLE)
+ .ability(Abilities.INTIMIDATE)
+ .enemyMoveset(SPLASH_ONLY);
+ });
+
+ it("should double stat changes when applied", async() => {
+ await game.startBattle([
+ Species.SLOWBRO
+ ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
+ }, 20000);
+});
diff --git a/src/test/abilities/volt_absorb.test.ts b/src/test/abilities/volt_absorb.test.ts
index d9c3fe34c24..7f3e160c7d0 100644
--- a/src/test/abilities/volt_absorb.test.ts
+++ b/src/test/abilities/volt_absorb.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
@@ -41,12 +41,14 @@ describe("Abilities - Volt Absorb", () => {
await game.startBattle();
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+
game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(game.scene.getParty()[0].summonData.battleStats[BattleStat.SPDEF]).toBe(1);
- expect(game.scene.getParty()[0].getTag(BattlerTagType.CHARGED)).toBeDefined();
+ expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(1);
+ expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
});
});
diff --git a/src/test/abilities/wind_rider.test.ts b/src/test/abilities/wind_rider.test.ts
index e11b3b39723..7a1fee6794a 100644
--- a/src/test/abilities/wind_rider.test.ts
+++ b/src/test/abilities/wind_rider.test.ts
@@ -1,8 +1,8 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@@ -31,56 +31,38 @@ describe("Abilities - Wind Rider", () => {
.enemyMoveset(SPLASH_ONLY);
});
- it("takes no damage from wind moves and its Attack is increased by one stage when hit by one", async () => {
- await game.classicMode.startBattle([Species.MAGIKARP]);
+ it("takes no damage from wind moves and its ATK stat stage is raised by 1 when hit by one", async () => {
+ await game.classicMode.startBattle([ Species.MAGIKARP ]);
const shiftry = game.scene.getEnemyPokemon()!;
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.PETAL_BLIZZARD);
await game.phaseInterceptor.to("TurnEndPhase");
expect(shiftry.isFullHp()).toBe(true);
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(1);
});
- it("Attack is increased by one stage when Tailwind is present on its side", async () => {
- game.override.ability(Abilities.WIND_RIDER);
- game.override.enemySpecies(Species.MAGIKARP);
+ it("ATK stat stage is raised by 1 when Tailwind is present on its side", async () => {
+ game.override
+ .enemySpecies(Species.MAGIKARP)
+ .ability(Abilities.WIND_RIDER);
await game.classicMode.startBattle([Species.SHIFTRY]);
const shiftry = game.scene.getPlayerPokemon()!;
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.TAILWIND);
await game.phaseInterceptor.to("TurnEndPhase");
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(1);
});
- it("does not increase Attack when Tailwind is present on opposing side", async () => {
- game.override.ability(Abilities.WIND_RIDER);
- game.override.enemySpecies(Species.MAGIKARP);
-
- await game.classicMode.startBattle([Species.SHIFTRY]);
- const magikarp = game.scene.getEnemyPokemon()!;
- const shiftry = game.scene.getPlayerPokemon()!;
-
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0);
-
- game.move.select(Moves.TAILWIND);
-
- await game.phaseInterceptor.to("TurnEndPhase");
-
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1);
- expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0);
- });
-
- it("does not increase Attack when Tailwind is present on opposing side", async () => {
+ it("does not raise ATK stat stage when Tailwind is present on opposing side", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.ability(Abilities.WIND_RIDER);
@@ -89,15 +71,35 @@ describe("Abilities - Wind Rider", () => {
const magikarp = game.scene.getEnemyPokemon()!;
const shiftry = game.scene.getPlayerPokemon()!;
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
+ expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.TAILWIND);
await game.phaseInterceptor.to("TurnEndPhase");
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1);
- expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(1);
+ expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
+ });
+
+ it("does not raise ATK stat stage when Tailwind is present on opposing side", async () => {
+ game.override
+ .enemySpecies(Species.MAGIKARP)
+ .ability(Abilities.WIND_RIDER);
+
+ await game.classicMode.startBattle([Species.SHIFTRY]);
+ const magikarp = game.scene.getEnemyPokemon()!;
+ const shiftry = game.scene.getPlayerPokemon()!;
+
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
+ expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
+
+ game.move.select(Moves.TAILWIND);
+
+ await game.phaseInterceptor.to("TurnEndPhase");
+
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(1);
+ expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
});
it("does not interact with Sandstorm", async () => {
@@ -106,14 +108,14 @@ describe("Abilities - Wind Rider", () => {
await game.classicMode.startBattle([Species.SHIFTRY]);
const shiftry = game.scene.getPlayerPokemon()!;
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
expect(shiftry.isFullHp()).toBe(true);
game.move.select(Moves.SANDSTORM);
await game.phaseInterceptor.to("TurnEndPhase");
- expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
expect(shiftry.hp).lessThan(shiftry.getMaxHp());
});
});
diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts
index 677d998e876..fd378647184 100644
--- a/src/test/abilities/zen_mode.test.ts
+++ b/src/test/abilities/zen_mode.test.ts
@@ -1,6 +1,5 @@
+import { Stat } from "#enums/stat";
import { BattlerIndex } from "#app/battle";
-import { Stat } from "#app/data/pokemon-stat";
-import { Status, StatusEffect } from "#app/data/status-effect";
import { DamagePhase } from "#app/phases/damage-phase";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { MessagePhase } from "#app/phases/message-phase";
@@ -18,6 +17,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
+import { Status, StatusEffect } from "#app/data/status-effect";
const TIMEOUT = 20 * 1000;
diff --git a/src/test/achievements/achievement.test.ts b/src/test/achievements/achievement.test.ts
index 36c20ae2248..24d00a3e77b 100644
--- a/src/test/achievements/achievement.test.ts
+++ b/src/test/achievements/achievement.test.ts
@@ -224,7 +224,7 @@ describe("achvs", () => {
expect(achvs._50_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._75_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._100_RIBBONS).toBeInstanceOf(RibbonAchv);
- expect(achvs.TRANSFER_MAX_BATTLE_STAT).toBeInstanceOf(Achv);
+ expect(achvs.TRANSFER_MAX_STAT_STAGE).toBeInstanceOf(Achv);
expect(achvs.MAX_FRIENDSHIP).toBeInstanceOf(Achv);
expect(achvs.MEGA_EVOLVE).toBeInstanceOf(Achv);
expect(achvs.GIGANTAMAX).toBeInstanceOf(Achv);
diff --git a/src/test/battle-scene.test.ts b/src/test/battle-scene.test.ts
index 9e28ec99791..4da75cea197 100644
--- a/src/test/battle-scene.test.ts
+++ b/src/test/battle-scene.test.ts
@@ -1,5 +1,5 @@
import { LoadingScene } from "#app/loading-scene";
-import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import GameManager from "./utils/gameManager";
describe("BattleScene", () => {
@@ -24,4 +24,12 @@ describe("BattleScene", () => {
// `BattleScene.create()` is called during the `new GameManager()` call
expect(game.scene.scene.remove).toHaveBeenCalledWith(LoadingScene.KEY);
});
+
+ it("should also reset RNG on reset", () => {
+ vi.spyOn(game.scene, "resetSeed");
+
+ game.scene.reset();
+
+ expect(game.scene.resetSeed).toHaveBeenCalled();
+ });
});
diff --git a/src/test/battle-stat.spec.ts b/src/test/battle-stat.spec.ts
deleted file mode 100644
index 16fce962838..00000000000
--- a/src/test/battle-stat.spec.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat";
-import { describe, expect, it } from "vitest";
-import { arrayOfRange, mockI18next } from "./utils/testUtils";
-
-const TEST_BATTLE_STAT = -99 as unknown as BattleStat;
-const TEST_POKEMON = "Testmon";
-const TEST_STAT = "Teststat";
-
-describe("battle-stat", () => {
- describe("getBattleStatName", () => {
- it("should return the correct name for each BattleStat", () => {
- mockI18next();
-
- expect(getBattleStatName(BattleStat.ATK)).toBe("pokemonInfo:Stat.ATK");
- expect(getBattleStatName(BattleStat.DEF)).toBe("pokemonInfo:Stat.DEF");
- expect(getBattleStatName(BattleStat.SPATK)).toBe(
- "pokemonInfo:Stat.SPATK"
- );
- expect(getBattleStatName(BattleStat.SPDEF)).toBe(
- "pokemonInfo:Stat.SPDEF"
- );
- expect(getBattleStatName(BattleStat.SPD)).toBe("pokemonInfo:Stat.SPD");
- expect(getBattleStatName(BattleStat.ACC)).toBe("pokemonInfo:Stat.ACC");
- expect(getBattleStatName(BattleStat.EVA)).toBe("pokemonInfo:Stat.EVA");
- });
-
- it("should fall back to ??? for an unknown BattleStat", () => {
- expect(getBattleStatName(TEST_BATTLE_STAT)).toBe("???");
- });
- });
-
- describe("getBattleStatLevelChangeDescription", () => {
- it("should return battle:statRose for +1", () => {
- mockI18next();
-
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- 1,
- true
- );
-
- expect(message).toBe("battle:statRose");
- });
-
- it("should return battle:statSharplyRose for +2", () => {
- mockI18next();
-
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- 2,
- true
- );
-
- expect(message).toBe("battle:statSharplyRose");
- });
-
- it("should return battle:statRoseDrastically for +3 to +6", () => {
- mockI18next();
-
- arrayOfRange(3, 6).forEach((n) => {
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- n,
- true
- );
-
- expect(message).toBe("battle:statRoseDrastically");
- });
- });
-
- it("should return battle:statWontGoAnyHigher for 7 or higher", () => {
- mockI18next();
-
- arrayOfRange(7, 10).forEach((n) => {
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- n,
- true
- );
-
- expect(message).toBe("battle:statWontGoAnyHigher");
- });
- });
-
- it("should return battle:statFell for -1", () => {
- mockI18next();
-
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- 1,
- false
- );
-
- expect(message).toBe("battle:statFell");
- });
-
- it("should return battle:statHarshlyFell for -2", () => {
- mockI18next();
-
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- 2,
- false
- );
-
- expect(message).toBe("battle:statHarshlyFell");
- });
-
- it("should return battle:statSeverelyFell for -3 to -6", () => {
- mockI18next();
-
- arrayOfRange(3, 6).forEach((n) => {
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- n,
- false
- );
-
- expect(message).toBe("battle:statSeverelyFell");
- });
- });
-
- it("should return battle:statWontGoAnyLower for -7 or lower", () => {
- mockI18next();
-
- arrayOfRange(7, 10).forEach((n) => {
- const message = getBattleStatLevelChangeDescription(
- TEST_POKEMON,
- TEST_STAT,
- n,
- false
- );
-
- expect(message).toBe("battle:statWontGoAnyLower");
- });
- });
- });
-});
diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts
index be89fdeb2af..25dfbc765bd 100644
--- a/src/test/battle/battle.test.ts
+++ b/src/test/battle/battle.test.ts
@@ -1,5 +1,5 @@
import { allSpecies } from "#app/data/pokemon-species";
-import { TempBattleStat } from "#app/data/temp-battle-stat";
+import { Stat } from "#enums/stat";
import { GameModes, getGameMode } from "#app/game-mode";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { CommandPhase } from "#app/phases/command-phase";
@@ -320,7 +320,7 @@ describe("Test Battle Phase", () => {
.startingLevel(100)
.moveset([moveToUse])
.enemyMoveset(SPLASH_ONLY)
- .startingHeldItems([{ name: "TEMP_STAT_BOOSTER", type: TempBattleStat.ACC }]);
+ .startingHeldItems([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]);
await game.startBattle();
game.scene.getPlayerPokemon()!.hp = 1;
diff --git a/src/test/battlerTags/octolock.test.ts b/src/test/battlerTags/octolock.test.ts
index fa491589f09..7b1f9264370 100644
--- a/src/test/battlerTags/octolock.test.ts
+++ b/src/test/battlerTags/octolock.test.ts
@@ -1,16 +1,16 @@
import BattleScene from "#app/battle-scene";
-import { BattleStat } from "#app/data/battle-stat";
-import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
-import { BattlerTagType } from "#app/enums/battler-tag-type";
-import Pokemon from "#app/field/pokemon";
-import { StatChangePhase } from "#app/phases/stat-change-phase";
import { describe, expect, it, vi } from "vitest";
+import Pokemon from "#app/field/pokemon";
+import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
+import { BattlerTagType } from "#app/enums/battler-tag-type";
+import { Stat } from "#enums/stat";
vi.mock("#app/battle-scene.js");
describe("BattlerTag - OctolockTag", () => {
describe("lapse behavior", () => {
- it("unshifts a StatChangePhase with expected stat changes", { timeout: 10000 }, async () => {
+ it("unshifts a StatStageChangePhase with expected stat stage changes", { timeout: 10000 }, async () => {
const mockPokemon = {
scene: new BattleScene(),
getBattlerIndex: () => 0,
@@ -19,9 +19,9 @@ describe("BattlerTag - OctolockTag", () => {
const subject = new OctolockTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(-1);
- expect((phase as StatChangePhase)["stats"]).toEqual([BattleStat.DEF, BattleStat.SPDEF]);
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(-1);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual([ Stat.DEF, Stat.SPDEF ]);
});
subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END);
diff --git a/src/test/battlerTags/stockpiling.test.ts b/src/test/battlerTags/stockpiling.test.ts
index fef1e938c09..e568016dfef 100644
--- a/src/test/battlerTags/stockpiling.test.ts
+++ b/src/test/battlerTags/stockpiling.test.ts
@@ -1,10 +1,10 @@
import BattleScene from "#app/battle-scene";
-import { BattleStat } from "#app/data/battle-stat";
-import { StockpilingTag } from "#app/data/battler-tags";
-import Pokemon, { PokemonSummonData } from "#app/field/pokemon";
-import * as messages from "#app/messages";
-import { StatChangePhase } from "#app/phases/stat-change-phase";
import { beforeEach, describe, expect, it, vi } from "vitest";
+import Pokemon, { PokemonSummonData } from "#app/field/pokemon";
+import { StockpilingTag } from "#app/data/battler-tags";
+import { Stat } from "#enums/stat";
+import * as messages from "#app/messages";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
beforeEach(() => {
vi.spyOn(messages, "getPokemonNameWithAffix").mockImplementation(() => "");
@@ -12,7 +12,7 @@ beforeEach(() => {
describe("BattlerTag - StockpilingTag", () => {
describe("onAdd", () => {
- it("unshifts a StatChangePhase with expected stat changes on add", { timeout: 10000 }, async () => {
+ it("unshifts a StatStageChangePhase with expected stat stage changes on add", { timeout: 10000 }, async () => {
const mockPokemon = {
scene: vi.mocked(new BattleScene()) as BattleScene,
getBattlerIndex: () => 0,
@@ -23,11 +23,11 @@ describe("BattlerTag - StockpilingTag", () => {
const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(1);
- expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
- (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]);
+ (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [Stat.DEF, Stat.SPDEF], [1, 1]);
});
subject.onAdd(mockPokemon);
@@ -35,7 +35,7 @@ describe("BattlerTag - StockpilingTag", () => {
expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1);
});
- it("unshifts a StatChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => {
+ it("unshifts a StatStageChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => {
const mockPokemon = {
scene: new BattleScene(),
summonData: new PokemonSummonData(),
@@ -44,17 +44,17 @@ describe("BattlerTag - StockpilingTag", () => {
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
- mockPokemon.summonData.battleStats[BattleStat.DEF] = 6;
- mockPokemon.summonData.battleStats[BattleStat.SPDEF] = 5;
+ mockPokemon.summonData.statStages[Stat.DEF - 1] = 6;
+ mockPokemon.summonData.statStages[Stat.SPD - 1] = 5;
const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(1);
- expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.DEF, Stat.SPDEF]));
- (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]);
+ (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]);
});
subject.onAdd(mockPokemon);
@@ -64,7 +64,7 @@ describe("BattlerTag - StockpilingTag", () => {
});
describe("onOverlap", () => {
- it("unshifts a StatChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => {
+ it("unshifts a StatStageChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => {
const mockPokemon = {
scene: new BattleScene(),
getBattlerIndex: () => 0,
@@ -75,11 +75,11 @@ describe("BattlerTag - StockpilingTag", () => {
const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(1);
- expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
- (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]);
+ (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]);
});
subject.onOverlap(mockPokemon);
@@ -98,39 +98,39 @@ describe("BattlerTag - StockpilingTag", () => {
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
- mockPokemon.summonData.battleStats[BattleStat.DEF] = 5;
- mockPokemon.summonData.battleStats[BattleStat.SPDEF] = 4;
+ mockPokemon.summonData.statStages[Stat.DEF - 1] = 5;
+ mockPokemon.summonData.statStages[Stat.SPD - 1] = 4;
const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(1);
- expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
// def doesn't change
- (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.SPDEF], [1]);
+ (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]);
});
subject.onAdd(mockPokemon);
expect(subject.stockpiledCount).toBe(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(1);
- expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
// def doesn't change
- (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.SPDEF], [1]);
+ (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]);
});
subject.onOverlap(mockPokemon);
expect(subject.stockpiledCount).toBe(2);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(1);
- expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
// neither stat changes, stack count should still increase
});
@@ -138,20 +138,20 @@ describe("BattlerTag - StockpilingTag", () => {
subject.onOverlap(mockPokemon);
expect(subject.stockpiledCount).toBe(3);
- vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
+ vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(_phase => {
throw new Error("Should not be called a fourth time");
});
// fourth stack should not be applied
subject.onOverlap(mockPokemon);
expect(subject.stockpiledCount).toBe(3);
- expect(subject.statChangeCounts).toMatchObject({ [BattleStat.DEF]: 0, [BattleStat.SPDEF]: 2 });
+ expect(subject.statChangeCounts).toMatchObject({ [ Stat.DEF ]: 0, [Stat.SPDEF]: 2 });
// removing tag should reverse stat changes
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
- expect(phase).toBeInstanceOf(StatChangePhase);
- expect((phase as StatChangePhase)["levels"]).toEqual(-2);
- expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.SPDEF]));
+ expect(phase).toBeInstanceOf(StatStageChangePhase);
+ expect((phase as StatStageChangePhase)["stages"]).toEqual(-2);
+ expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.SPDEF]));
});
subject.onRemove(mockPokemon);
diff --git a/src/test/boss-pokemon.test.ts b/src/test/boss-pokemon.test.ts
new file mode 100644
index 00000000000..f8437932580
--- /dev/null
+++ b/src/test/boss-pokemon.test.ts
@@ -0,0 +1,215 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import GameManager from "./utils/gameManager";
+import { Species } from "#app/enums/species";
+import { getPokemonSpecies } from "#app/data/pokemon-species";
+import { SPLASH_ONLY } from "./utils/testUtils";
+import { Abilities } from "#app/enums/abilities";
+import { Moves } from "#app/enums/moves";
+import { EFFECTIVE_STATS } from "#app/enums/stat";
+import { EnemyPokemon } from "#app/field/pokemon";
+import { toDmgValue } from "#app/utils";
+
+describe("Boss Pokemon / Shields", () => {
+ const TIMEOUT = 2500;
+
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+
+ game.override
+ .battleType("single")
+ .disableTrainerWaves()
+ .disableCrits()
+ .enemySpecies(Species.RATTATA)
+ .enemyMoveset(SPLASH_ONLY)
+ .enemyHeldItems([])
+ .startingLevel(1000)
+ .moveset([Moves.FALSE_SWIPE, Moves.SUPER_FANG, Moves.SPLASH])
+ .ability(Abilities.NO_GUARD);
+ });
+
+ it("Pokemon should get shields based on their Species and level and the current wave", async () => {
+ let level = 50;
+ let wave = 5;
+
+ // On normal waves, no shields...
+ expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(0);
+ // ... expect (sub)-legendary and mythical Pokemon who always get shields
+ expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.MEW))).toBe(2);
+ // Pokemon with 670+ BST get an extra shield
+ expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.MEWTWO))).toBe(3);
+
+ // Every 10 waves will always be a boss Pokemon with shield(s)
+ wave = 50;
+ expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(2);
+ // Every extra 250 waves adds a shield
+ wave += 250;
+ expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(3);
+ wave += 750;
+ expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(6);
+
+ // Pokemon above level 100 get an extra shield
+ level = 100;
+ expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(7);
+ }, TIMEOUT);
+
+ it("should reduce the number of shields if we are in a double battle", async () => {
+ game.override
+ .battleType("double")
+ .startingWave(150); // Floor 150 > 2 shields / 3 health segments
+
+ await game.classicMode.startBattle([ Species.MEWTWO ]);
+
+ const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!;
+ const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!;
+ expect(boss1.isBoss()).toBe(true);
+ expect(boss1.bossSegments).toBe(2);
+ expect(boss2.isBoss()).toBe(true);
+ expect(boss2.bossSegments).toBe(2);
+ }, TIMEOUT);
+
+ it("shields should stop overflow damage and give stat stage boosts when broken", async () => {
+ game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments
+
+ await game.classicMode.startBattle([ Species.MEWTWO ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+ const segmentHp = enemyPokemon.getMaxHp() / enemyPokemon.bossSegments;
+ expect(enemyPokemon.isBoss()).toBe(true);
+ expect(enemyPokemon.bossSegments).toBe(3);
+ expect(getTotalStatStageBoosts(enemyPokemon)).toBe(0);
+
+ game.move.select(Moves.SUPER_FANG); // Enough to break the first shield
+ await game.toNextTurn();
+
+ // Broke 1st of 2 shields, health at 2/3rd
+ expect(enemyPokemon.bossSegmentIndex).toBe(1);
+ expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(segmentHp));
+ // Breaking the shield gives a +1 boost to ATK, DEF, SP ATK, SP DEF or SPD
+ expect(getTotalStatStageBoosts(enemyPokemon)).toBe(1);
+
+ game.move.select(Moves.FALSE_SWIPE); // Enough to break last shield but not kill
+ await game.toNextTurn();
+
+ expect(enemyPokemon.bossSegmentIndex).toBe(0);
+ expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(2 * segmentHp));
+ // Breaking the last shield gives a +2 boost to ATK, DEF, SP ATK, SP DEF or SPD
+ expect(getTotalStatStageBoosts(enemyPokemon)).toBe(3);
+
+ }, TIMEOUT);
+
+ it("breaking multiple shields at once requires extra damage", async () => {
+ game.override
+ .battleType("double")
+ .enemyHealthSegments(5);
+
+ await game.classicMode.startBattle([ Species.MEWTWO ]);
+
+ // In this test we want to break through 3 shields at once
+ const brokenShields = 3;
+
+ const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!;
+ const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments;
+ const requiredDamageBoss1 = boss1SegmentHp * (1 + Math.pow(2, brokenShields));
+ expect(boss1.isBoss()).toBe(true);
+ expect(boss1.bossSegments).toBe(5);
+ expect(boss1.bossSegmentIndex).toBe(4);
+
+ // Not enough damage to break through all shields
+ boss1.damageAndUpdate(Math.floor(requiredDamageBoss1 - 5));
+ expect(boss1.bossSegmentIndex).toBe(1);
+ expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * 3));
+
+ const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!;
+ const boss2SegmentHp = boss2.getMaxHp() / boss2.bossSegments;
+ const requiredDamageBoss2 = boss2SegmentHp * (1 + Math.pow(2, brokenShields));
+
+ expect(boss2.isBoss()).toBe(true);
+ expect(boss2.bossSegments).toBe(5);
+
+ // Enough damage to break through all shields
+ boss2.damageAndUpdate(Math.ceil(requiredDamageBoss2));
+ expect(boss2.bossSegmentIndex).toBe(0);
+ expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * 4));
+
+ }, TIMEOUT);
+
+ it("the number of stat stage boosts is consistent when several shields are broken at once", async () => {
+ const shieldsToBreak = 4;
+
+ game.override
+ .battleType("double")
+ .enemyHealthSegments(shieldsToBreak + 1);
+
+ await game.classicMode.startBattle([ Species.MEWTWO ]);
+
+ const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!;
+ const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments;
+ const singleShieldDamage = Math.ceil(boss1SegmentHp);
+ expect(boss1.isBoss()).toBe(true);
+ expect(boss1.bossSegments).toBe(shieldsToBreak + 1);
+ expect(boss1.bossSegmentIndex).toBe(shieldsToBreak);
+ expect(getTotalStatStageBoosts(boss1)).toBe(0);
+
+
+ let totalStatStages = 0;
+
+ // Break the shields one by one
+ for (let i = 1; i <= shieldsToBreak; i++) {
+ boss1.damageAndUpdate(singleShieldDamage);
+ expect(boss1.bossSegmentIndex).toBe(shieldsToBreak - i);
+ expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * i));
+ // Do nothing and go to next turn so that the StatStageChangePhase gets applied
+ game.move.select(Moves.SPLASH);
+ await game.toNextTurn();
+ // All broken shields give +1 stat boost, except the last two that gives +2
+ totalStatStages += i >= shieldsToBreak -1? 2 : 1;
+ expect(getTotalStatStageBoosts(boss1)).toBe(totalStatStages);
+ }
+
+ const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!;
+ const boss2SegmentHp = boss2.getMaxHp() / boss2.bossSegments;
+ const requiredDamage = boss2SegmentHp * (1 + Math.pow(2, shieldsToBreak - 1));
+
+ expect(boss2.isBoss()).toBe(true);
+ expect(boss2.bossSegments).toBe(shieldsToBreak + 1);
+ expect(boss2.bossSegmentIndex).toBe(shieldsToBreak);
+ expect(getTotalStatStageBoosts(boss2)).toBe(0);
+
+ // Enough damage to break all shields at once
+ boss2.damageAndUpdate(Math.ceil(requiredDamage));
+ expect(boss2.bossSegmentIndex).toBe(0);
+ expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * shieldsToBreak));
+ // Do nothing and go to next turn so that the StatStageChangePhase gets applied
+ game.move.select(Moves.SPLASH);
+ await game.toNextTurn();
+ expect(getTotalStatStageBoosts(boss2)).toBe(totalStatStages);
+
+ }, TIMEOUT);
+
+ /**
+ * Gets the sum of the effective stat stage boosts for the given Pokemon
+ * @param enemyPokemon the pokemon to get stats from
+ * @returns the total stats boosts
+ */
+ function getTotalStatStageBoosts(enemyPokemon: EnemyPokemon): number {
+ let boosts = 0;
+ for (const s of EFFECTIVE_STATS) {
+ boosts += enemyPokemon.getStatStage(s);
+ }
+ return boosts;
+ }
+});
+
diff --git a/src/test/eggs/egg.test.ts b/src/test/eggs/egg.test.ts
index 28f1b7f0a6c..4f00e843b47 100644
--- a/src/test/eggs/egg.test.ts
+++ b/src/test/eggs/egg.test.ts
@@ -12,6 +12,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
describe("Egg Generation Tests", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
+ const EGG_HATCH_COUNT: integer = 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
@@ -47,14 +48,21 @@ describe("Egg Generation Tests", () => {
expect(result).toBe(expectedSpecies);
});
- it("should hatch an Arceus. Set from legendary gacha", async () => {
+ it("should hatch an Arceus around half the time. Set from legendary gacha", async () => {
const scene = game.scene;
const timestamp = new Date(2024, 6, 10, 15, 0, 0, 0).getTime();
const expectedSpecies = Species.ARCEUS;
+ let gachaSpeciesCount = 0;
- const result = new Egg({ scene, timestamp, sourceType: EggSourceType.GACHA_LEGENDARY, tier: EggTier.MASTER }).generatePlayerPokemon(scene).species.speciesId;
+ for (let i = 0; i < EGG_HATCH_COUNT; i++) {
+ const result = new Egg({ scene, timestamp, sourceType: EggSourceType.GACHA_LEGENDARY, tier: EggTier.MASTER }).generatePlayerPokemon(scene).species.speciesId;
+ if (result === expectedSpecies) {
+ gachaSpeciesCount++;
+ }
+ }
- expect(result).toBe(expectedSpecies);
+ expect(gachaSpeciesCount).toBeGreaterThan(0.4 * EGG_HATCH_COUNT);
+ expect(gachaSpeciesCount).toBeLessThan(0.6 * EGG_HATCH_COUNT);
});
it("should hatch an Arceus. Set from species", () => {
const scene = game.scene;
@@ -156,7 +164,7 @@ describe("Egg Generation Tests", () => {
const scene = game.scene;
const eggMoveIndex = new Egg({ scene }).eggMoveIndex;
- const result = eggMoveIndex && eggMoveIndex >= 0 && eggMoveIndex <= 3;
+ const result = !Utils.isNullOrUndefined(eggMoveIndex) && eggMoveIndex >= 0 && eggMoveIndex <= 3;
expect(result).toBe(true);
});
@@ -309,4 +317,63 @@ describe("Egg Generation Tests", () => {
expect(egg.variantTier).toBe(VariantTier.EPIC);
});
+
+ it("should generate egg moves, species, shinyness, and ability unpredictably for the egg gacha", () => {
+ const scene = game.scene;
+ scene.setSeed("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ scene.resetSeed();
+
+ const firstEgg = new Egg({scene, sourceType: EggSourceType.GACHA_SHINY, tier: EggTier.COMMON});
+ const firstHatch = firstEgg.generatePlayerPokemon(scene);
+ let diffEggMove = false;
+ let diffSpecies = false;
+ let diffShiny = false;
+ let diffAbility = false;
+ for (let i = 0; i < EGG_HATCH_COUNT; i++) {
+ scene.gameData.unlockPity[EggTier.COMMON] = 0;
+ scene.setSeed("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ scene.resetSeed(); // Make sure that eggs are unpredictable even if using same seed
+
+ const newEgg = new Egg({scene, sourceType: EggSourceType.GACHA_SHINY, tier: EggTier.COMMON});
+ const newHatch = newEgg.generatePlayerPokemon(scene);
+ diffEggMove = diffEggMove || (newEgg.eggMoveIndex !== firstEgg.eggMoveIndex);
+ diffSpecies = diffSpecies || (newHatch.species.speciesId !== firstHatch.species.speciesId);
+ diffShiny = diffShiny || (newHatch.shiny !== firstHatch.shiny);
+ diffAbility = diffAbility || (newHatch.abilityIndex !== firstHatch.abilityIndex);
+ }
+
+ expect(diffEggMove).toBe(true);
+ expect(diffSpecies).toBe(true);
+ expect(diffShiny).toBe(true);
+ expect(diffAbility).toBe(true);
+ });
+
+ it("should generate egg moves, shinyness, and ability unpredictably for species eggs", () => {
+ const scene = game.scene;
+ scene.setSeed("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ scene.resetSeed();
+
+ const firstEgg = new Egg({scene, species: Species.BULBASAUR});
+ const firstHatch = firstEgg.generatePlayerPokemon(scene);
+ let diffEggMove = false;
+ let diffSpecies = false;
+ let diffShiny = false;
+ let diffAbility = false;
+ for (let i = 0; i < EGG_HATCH_COUNT; i++) {
+ scene.setSeed("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ scene.resetSeed(); // Make sure that eggs are unpredictable even if using same seed
+
+ const newEgg = new Egg({scene, species: Species.BULBASAUR});
+ const newHatch = newEgg.generatePlayerPokemon(scene);
+ diffEggMove = diffEggMove || (newEgg.eggMoveIndex !== firstEgg.eggMoveIndex);
+ diffSpecies = diffSpecies || (newHatch.species.speciesId !== firstHatch.species.speciesId);
+ diffShiny = diffShiny || (newHatch.shiny !== firstHatch.shiny);
+ diffAbility = diffAbility || (newHatch.abilityIndex !== firstHatch.abilityIndex);
+ }
+
+ expect(diffEggMove).toBe(true);
+ expect(diffSpecies).toBe(false);
+ expect(diffShiny).toBe(true);
+ expect(diffAbility).toBe(true);
+ });
});
diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts
index 5844e92ab8d..9f0806b8e24 100644
--- a/src/test/evolution.test.ts
+++ b/src/test/evolution.test.ts
@@ -79,12 +79,15 @@ describe("Evolution", () => {
const nincada = game.scene.getPlayerPokemon()!;
nincada.abilityIndex = 2;
+ nincada.metBiome = -1;
nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm());
const ninjask = game.scene.getParty()[0];
const shedinja = game.scene.getParty()[1];
expect(ninjask.abilityIndex).toBe(2);
expect(shedinja.abilityIndex).toBe(1);
+ // Regression test for https://github.com/pagefaultgames/pokerogue/issues/3842
+ expect(shedinja.metBiome).toBe(-1);
}, TIMEOUT);
it("should set wild delay to NONE by default", () => {
diff --git a/src/test/items/dire_hit.test.ts b/src/test/items/dire_hit.test.ts
new file mode 100644
index 00000000000..c43091d1f03
--- /dev/null
+++ b/src/test/items/dire_hit.test.ts
@@ -0,0 +1,97 @@
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Species } from "#enums/species";
+import GameManager from "#test/utils/gameManager";
+import Phase from "phaser";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { SPLASH_ONLY } from "../utils/testUtils";
+import { BattleEndPhase } from "#app/phases/battle-end-phase";
+import { TempCritBoosterModifier } from "#app/modifier/modifier";
+import { Mode } from "#app/ui/ui";
+import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
+import { Button } from "#app/enums/buttons";
+import { CommandPhase } from "#app/phases/command-phase";
+import { NewBattlePhase } from "#app/phases/new-battle-phase";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
+
+describe("Items - Dire Hit", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phase.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+
+ game.override
+ .enemySpecies(Species.MAGIKARP)
+ .enemyMoveset(SPLASH_ONLY)
+ .moveset([ Moves.POUND ])
+ .startingHeldItems([{ name: "DIRE_HIT" }])
+ .battleType("single")
+ .disableCrits();
+
+ }, 20000);
+
+ it("should raise CRIT stage by 1", async () => {
+ await game.startBattle([
+ Species.GASTLY
+ ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ vi.spyOn(enemyPokemon, "getCritStage");
+
+ game.move.select(Moves.POUND);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(enemyPokemon.getCritStage).toHaveReturnedWith(1);
+ }, 20000);
+
+ it("should renew how many battles are left of existing DIRE_HIT when picking up new DIRE_HIT", async() => {
+ game.override.itemRewards([{ name: "DIRE_HIT" }]);
+
+ await game.startBattle([
+ Species.PIKACHU
+ ]);
+
+ game.move.select(Moves.SPLASH);
+
+ await game.doKillOpponents();
+
+ await game.phaseInterceptor.to(BattleEndPhase);
+
+ const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier;
+ expect(modifier.getBattlesLeft()).toBe(4);
+
+ // Forced DIRE_HIT to spawn in the first slot with override
+ game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
+ const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
+ // Traverse to first modifier slot
+ handler.processInput(Button.LEFT);
+ handler.processInput(Button.UP);
+ handler.processInput(Button.ACTION);
+ }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true);
+
+ await game.phaseInterceptor.to(TurnInitPhase);
+
+ // Making sure only one booster is in the modifier list even after picking up another
+ let count = 0;
+ for (const m of game.scene.modifiers) {
+ if (m instanceof TempCritBoosterModifier) {
+ count++;
+ expect((m as TempCritBoosterModifier).getBattlesLeft()).toBe(5);
+ }
+ }
+ expect(count).toBe(1);
+ }, 20000);
+});
diff --git a/src/test/items/eviolite.test.ts b/src/test/items/eviolite.test.ts
index e491784acec..83b00583893 100644
--- a/src/test/items/eviolite.test.ts
+++ b/src/test/items/eviolite.test.ts
@@ -1,4 +1,4 @@
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { EvolutionStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
@@ -37,29 +37,29 @@ describe("Items - Eviolite", () => {
const partyMember = game.scene.getParty()[0];
- // Checking consoe log to make sure Eviolite is applied when getBattleStat (with the appropriate stat) is called
- partyMember.getBattleStat(Stat.DEF);
+ // Checking console log to make sure Eviolite is applied when getEffectiveStat (with the appropriate stat) is called
+ partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log("");
- partyMember.getBattleStat(Stat.SPDEF);
+ partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.ATK);
+ partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPATK);
+ partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPD);
+ partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
});
diff --git a/src/test/items/leek.test.ts b/src/test/items/leek.test.ts
index 7505b6374a0..af20516ef83 100644
--- a/src/test/items/leek.test.ts
+++ b/src/test/items/leek.test.ts
@@ -1,7 +1,4 @@
-import { BattlerIndex } from "#app/battle";
-import { CritBoosterModifier } from "#app/modifier/modifier";
-import { modifierTypes } from "#app/modifier/modifier-type";
-import { MoveEffectPhase } from "#app/phases/move-effect-phase";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
import * as Utils from "#app/utils";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@@ -26,91 +23,64 @@ describe("Items - Leek", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
- game.override.enemySpecies(Species.MAGIKARP);
- game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
- game.override.disableCrits();
-
- game.override.battleType("single");
+ game.override
+ .enemySpecies(Species.MAGIKARP)
+ .enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH])
+ .startingHeldItems([{ name: "LEEK" }])
+ .moveset([ Moves.TACKLE ])
+ .disableCrits()
+ .battleType("single");
});
- it("LEEK activates in battle correctly", async () => {
- game.override.startingHeldItems([{ name: "LEEK" }]);
- game.override.moveset([Moves.POUND]);
- const consoleSpy = vi.spyOn(console, "log");
+ it("should raise CRIT stage by 2 when held by FARFETCHD", async () => {
await game.startBattle([
Species.FARFETCHD
]);
- game.move.select(Moves.POUND);
+ const enemyMember = game.scene.getEnemyPokemon()!;
- await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
+ vi.spyOn(enemyMember, "getCritStage");
- await game.phaseInterceptor.to(MoveEffectPhase);
+ game.move.select(Moves.TACKLE);
- expect(consoleSpy).toHaveBeenCalledWith("Applied", "Leek", "");
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
- it("LEEK held by FARFETCHD", async () => {
- await game.startBattle([
- Species.FARFETCHD
- ]);
-
- const partyMember = game.scene.getPlayerPokemon()!;
-
- // Making sure modifier is not applied without holding item
- const critLevel = new Utils.IntegerHolder(0);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
-
- expect(critLevel.value).toBe(0);
-
- // Giving Leek to party member and testing if it applies
- partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
-
- expect(critLevel.value).toBe(2);
- }, 20000);
-
- it("LEEK held by GALAR_FARFETCHD", async () => {
+ it("should raise CRIT stage by 2 when held by GALAR_FARFETCHD", async () => {
await game.startBattle([
Species.GALAR_FARFETCHD
]);
- const partyMember = game.scene.getPlayerPokemon()!;
+ const enemyMember = game.scene.getEnemyPokemon()!;
- // Making sure modifier is not applied without holding item
- const critLevel = new Utils.IntegerHolder(0);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ vi.spyOn(enemyMember, "getCritStage");
- expect(critLevel.value).toBe(0);
+ game.move.select(Moves.TACKLE);
- // Giving Leek to party member and testing if it applies
- partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ await game.phaseInterceptor.to(TurnEndPhase);
- expect(critLevel.value).toBe(2);
+ expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
- it("LEEK held by SIRFETCHD", async () => {
+ it("should raise CRIT stage by 2 when held by SIRFETCHD", async () => {
await game.startBattle([
Species.SIRFETCHD
]);
- const partyMember = game.scene.getPlayerPokemon()!;
+ const enemyMember = game.scene.getEnemyPokemon()!;
- // Making sure modifier is not applied without holding item
- const critLevel = new Utils.IntegerHolder(0);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ vi.spyOn(enemyMember, "getCritStage");
- expect(critLevel.value).toBe(0);
+ game.move.select(Moves.TACKLE);
- // Giving Leek to party member and testing if it applies
- partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ await game.phaseInterceptor.to(TurnEndPhase);
- expect(critLevel.value).toBe(2);
+ expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
- it("LEEK held by fused FARFETCHD line (base)", async () => {
+ it("should raise CRIT stage by 2 when held by FARFETCHD line fused with Pokemon", async () => {
// Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@@ -119,9 +89,7 @@ describe("Items - Leek", () => {
Species.PIKACHU,
]);
- const party = game.scene.getParty();
- const partyMember = party[0];
- const ally = party[1];
+ const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@@ -132,20 +100,18 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
- // Making sure modifier is not applied without holding item
- const critLevel = new Utils.IntegerHolder(0);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ const enemyMember = game.scene.getEnemyPokemon()!;
- expect(critLevel.value).toBe(0);
+ vi.spyOn(enemyMember, "getCritStage");
- // Giving Leek to party member and testing if it applies
- partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ game.move.select(Moves.TACKLE);
- expect(critLevel.value).toBe(2);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
- it("LEEK held by fused FARFETCHD line (part)", async () => {
+ it("should raise CRIT stage by 2 when held by Pokemon fused with FARFETCHD line", async () => {
// Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@@ -154,9 +120,7 @@ describe("Items - Leek", () => {
species[Utils.randInt(species.length)]
]);
- const party = game.scene.getParty();
- const partyMember = party[0];
- const ally = party[1];
+ const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@@ -167,36 +131,31 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
- // Making sure modifier is not applied without holding item
- const critLevel = new Utils.IntegerHolder(0);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
- expect(critLevel.value).toBe(0);
+ const enemyMember = game.scene.getEnemyPokemon()!;
- // Giving Leek to party member and testing if it applies
- partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ vi.spyOn(enemyMember, "getCritStage");
- expect(critLevel.value).toBe(2);
+ game.move.select(Moves.TACKLE);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
- it("LEEK not held by FARFETCHD line", async () => {
+ it("should not raise CRIT stage when held by a Pokemon outside of FARFETCHD line", async () => {
await game.startBattle([
Species.PIKACHU
]);
- const partyMember = game.scene.getPlayerPokemon()!;
+ const enemyMember = game.scene.getEnemyPokemon()!;
- // Making sure modifier is not applied without holding item
- const critLevel = new Utils.IntegerHolder(0);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ vi.spyOn(enemyMember, "getCritStage");
- expect(critLevel.value).toBe(0);
+ game.move.select(Moves.TACKLE);
- // Giving Leek to party member and testing if it applies
- partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
+ await game.phaseInterceptor.to(TurnEndPhase);
- expect(critLevel.value).toBe(0);
+ expect(enemyMember.getCritStage).toHaveReturnedWith(0);
}, 20000);
});
diff --git a/src/test/items/light_ball.test.ts b/src/test/items/light_ball.test.ts
index cf4f5c9e22f..673348e7b7a 100644
--- a/src/test/items/light_ball.test.ts
+++ b/src/test/items/light_ball.test.ts
@@ -1,4 +1,4 @@
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
@@ -37,29 +37,29 @@ describe("Items - Light Ball", () => {
const partyMember = game.scene.getParty()[0];
- // Checking consoe log to make sure Light Ball is applied when getBattleStat (with the appropriate stat) is called
- partyMember.getBattleStat(Stat.DEF);
+ // Checking console log to make sure Light Ball is applied when getEffectiveStat (with the appropriate stat) is called
+ partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log("");
- partyMember.getBattleStat(Stat.SPDEF);
+ partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.ATK);
+ partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPATK);
+ partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPD);
+ partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
});
diff --git a/src/test/items/metal_powder.test.ts b/src/test/items/metal_powder.test.ts
index a3a4936532f..0206fd1f471 100644
--- a/src/test/items/metal_powder.test.ts
+++ b/src/test/items/metal_powder.test.ts
@@ -1,4 +1,4 @@
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
@@ -37,29 +37,29 @@ describe("Items - Metal Powder", () => {
const partyMember = game.scene.getParty()[0];
- // Checking consoe log to make sure Metal Powder is applied when getBattleStat (with the appropriate stat) is called
- partyMember.getBattleStat(Stat.DEF);
+ // Checking console log to make sure Metal Powder is applied when getEffectiveStat (with the appropriate stat) is called
+ partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log("");
- partyMember.getBattleStat(Stat.SPDEF);
+ partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.ATK);
+ partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPATK);
+ partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPD);
+ partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
});
diff --git a/src/test/items/quick_powder.test.ts b/src/test/items/quick_powder.test.ts
index 53521ba78f1..344b772feb4 100644
--- a/src/test/items/quick_powder.test.ts
+++ b/src/test/items/quick_powder.test.ts
@@ -1,4 +1,4 @@
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
@@ -37,29 +37,29 @@ describe("Items - Quick Powder", () => {
const partyMember = game.scene.getParty()[0];
- // Checking consoe log to make sure Quick Powder is applied when getBattleStat (with the appropriate stat) is called
- partyMember.getBattleStat(Stat.DEF);
+ // Checking console log to make sure Quick Powder is applied when getEffectiveStat (with the appropriate stat) is called
+ partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log("");
- partyMember.getBattleStat(Stat.SPDEF);
+ partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.ATK);
+ partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPATK);
+ partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPD);
+ partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
});
diff --git a/src/test/items/scope_lens.test.ts b/src/test/items/scope_lens.test.ts
index 85673218762..c8629093ab5 100644
--- a/src/test/items/scope_lens.test.ts
+++ b/src/test/items/scope_lens.test.ts
@@ -1,13 +1,10 @@
-import { BattlerIndex } from "#app/battle";
-import { CritBoosterModifier } from "#app/modifier/modifier";
-import { modifierTypes } from "#app/modifier/modifier-type";
-import { MoveEffectPhase } from "#app/phases/move-effect-phase";
-import * as Utils from "#app/utils";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { SPLASH_ONLY } from "../utils/testUtils";
describe("Items - Scope Lens", () => {
let phaserGame: Phaser.Game;
@@ -26,47 +23,29 @@ describe("Items - Scope Lens", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
- game.override.enemySpecies(Species.MAGIKARP);
- game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
- game.override.disableCrits();
+ game.override
+ .enemySpecies(Species.MAGIKARP)
+ .enemyMoveset(SPLASH_ONLY)
+ .moveset([ Moves.POUND ])
+ .startingHeldItems([{ name: "SCOPE_LENS" }])
+ .battleType("single")
+ .disableCrits();
- game.override.battleType("single");
}, 20000);
- it("SCOPE_LENS activates in battle correctly", async () => {
- game.override.startingHeldItems([{ name: "SCOPE_LENS" }]);
- game.override.moveset([Moves.POUND]);
- const consoleSpy = vi.spyOn(console, "log");
+ it("should raise CRIT stage by 1", async () => {
await game.startBattle([
Species.GASTLY
]);
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ vi.spyOn(enemyPokemon, "getCritStage");
+
game.move.select(Moves.POUND);
- await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
+ await game.phaseInterceptor.to(TurnEndPhase);
- await game.phaseInterceptor.to(MoveEffectPhase);
-
- expect(consoleSpy).toHaveBeenCalledWith("Applied", "Scope Lens", "");
- }, 20000);
-
- it("SCOPE_LENS held by random pokemon", async () => {
- await game.startBattle([
- Species.GASTLY
- ]);
-
- const partyMember = game.scene.getPlayerPokemon()!;
-
- // Making sure modifier is not applied without holding item
- const critLevel = new Utils.IntegerHolder(0);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
-
- expect(critLevel.value).toBe(0);
-
- // Giving Scope Lens to party member and testing if it applies
- partyMember.scene.addModifier(modifierTypes.SCOPE_LENS().newModifier(partyMember), true);
- partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
-
- expect(critLevel.value).toBe(1);
+ expect(enemyPokemon.getCritStage).toHaveReturnedWith(1);
}, 20000);
});
diff --git a/src/test/items/temp_stat_stage_booster.test.ts b/src/test/items/temp_stat_stage_booster.test.ts
new file mode 100644
index 00000000000..e5b95c6c3b6
--- /dev/null
+++ b/src/test/items/temp_stat_stage_booster.test.ts
@@ -0,0 +1,174 @@
+import { BATTLE_STATS, Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
+import { Species } from "#enums/species";
+import Phase from "phaser";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { Moves } from "#app/enums/moves";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { SPLASH_ONLY } from "../utils/testUtils";
+import { Abilities } from "#app/enums/abilities";
+import { TempStatStageBoosterModifier } from "#app/modifier/modifier";
+import { Mode } from "#app/ui/ui";
+import { Button } from "#app/enums/buttons";
+import { CommandPhase } from "#app/phases/command-phase";
+import { NewBattlePhase } from "#app/phases/new-battle-phase";
+import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
+import { BattleEndPhase } from "#app/phases/battle-end-phase";
+import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
+
+
+describe("Items - Temporary Stat Stage Boosters", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phase.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+
+ game.override
+ .battleType("single")
+ .enemySpecies(Species.SHUCKLE)
+ .enemyMoveset(SPLASH_ONLY)
+ .enemyAbility(Abilities.BALL_FETCH)
+ .moveset([ Moves.TACKLE, Moves.SPLASH, Moves.HONE_CLAWS, Moves.BELLY_DRUM ])
+ .startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
+ });
+
+ it("should provide a x1.3 stat stage multiplier", async() => {
+ await game.startBattle([
+ Species.PIKACHU
+ ]);
+
+ const partyMember = game.scene.getPlayerPokemon()!;
+
+ vi.spyOn(partyMember, "getStatStageMultiplier");
+
+ game.move.select(Moves.TACKLE);
+
+ await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
+
+ expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.3);
+ }, 20000);
+
+ it("should increase existing ACC stat stage by 1 for X_ACCURACY only", async() => {
+ game.override
+ .startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }])
+ .ability(Abilities.SIMPLE);
+
+ await game.startBattle([
+ Species.PIKACHU
+ ]);
+
+ const partyMember = game.scene.getPlayerPokemon()!;
+
+ vi.spyOn(partyMember, "getAccuracyMultiplier");
+
+ // Raise ACC by +2 stat stages
+ game.move.select(Moves.HONE_CLAWS);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ game.move.select(Moves.TACKLE);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ // ACC at +3 stat stages yields a x2 multiplier
+ expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(2);
+ }, 20000);
+
+
+ it("should increase existing stat stage multiplier by 3/10 for the rest of the boosters", async() => {
+ await game.startBattle([
+ Species.PIKACHU
+ ]);
+
+ const partyMember = game.scene.getPlayerPokemon()!;
+
+ vi.spyOn(partyMember, "getStatStageMultiplier");
+
+ // Raise ATK by +1 stat stage
+ game.move.select(Moves.HONE_CLAWS);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ game.move.select(Moves.TACKLE);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ // ATK at +1 stat stage yields a x1.5 multiplier, add 0.3 from X_ATTACK
+ expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.8);
+ }, 20000);
+
+ it("should not increase past maximum stat stage multiplier", async() => {
+ game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }, { name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
+
+ await game.startBattle([
+ Species.PIKACHU
+ ]);
+
+ const partyMember = game.scene.getPlayerPokemon()!;
+
+ vi.spyOn(partyMember, "getStatStageMultiplier");
+ vi.spyOn(partyMember, "getAccuracyMultiplier");
+
+ // Set all stat stages to 6
+ vi.spyOn(partyMember.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(6));
+
+ game.move.select(Moves.TACKLE);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(3);
+ expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(4);
+ }, 20000);
+
+ it("should renew how many battles are left of existing booster when picking up new booster of same type", async() => {
+ game.override
+ .startingLevel(200)
+ .itemRewards([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
+
+ await game.startBattle([
+ Species.PIKACHU
+ ]);
+
+ game.move.select(Moves.SPLASH);
+
+ await game.doKillOpponents();
+
+ await game.phaseInterceptor.to(BattleEndPhase);
+
+ const modifier = game.scene.findModifier(m => m instanceof TempStatStageBoosterModifier) as TempStatStageBoosterModifier;
+ expect(modifier.getBattlesLeft()).toBe(4);
+
+ // Forced X_ATTACK to spawn in the first slot with override
+ game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
+ const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
+ // Traverse to first modifier slot
+ handler.processInput(Button.LEFT);
+ handler.processInput(Button.UP);
+ handler.processInput(Button.ACTION);
+ }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true);
+
+ await game.phaseInterceptor.to(TurnInitPhase);
+
+ // Making sure only one booster is in the modifier list even after picking up another
+ let count = 0;
+ for (const m of game.scene.modifiers) {
+ if (m instanceof TempStatStageBoosterModifier) {
+ count++;
+ expect((m as TempStatStageBoosterModifier).getBattlesLeft()).toBe(5);
+ }
+ }
+ expect(count).toBe(1);
+ }, 20000);
+});
diff --git a/src/test/items/thick_club.test.ts b/src/test/items/thick_club.test.ts
index 347921446e6..bcb6b371264 100644
--- a/src/test/items/thick_club.test.ts
+++ b/src/test/items/thick_club.test.ts
@@ -1,4 +1,4 @@
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
@@ -37,29 +37,29 @@ describe("Items - Thick Club", () => {
const partyMember = game.scene.getParty()[0];
- // Checking consoe log to make sure Thick Club is applied when getBattleStat (with the appropriate stat) is called
- partyMember.getBattleStat(Stat.DEF);
+ // Checking console log to make sure Thick Club is applied when getEffectiveStat (with the appropriate stat) is called
+ partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log("");
- partyMember.getBattleStat(Stat.SPDEF);
+ partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.ATK);
+ partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPATK);
+ partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
console.log("");
- partyMember.getBattleStat(Stat.SPD);
+ partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
});
diff --git a/src/test/localization/battle-stat.test.ts b/src/test/localization/battle-stat.test.ts
deleted file mode 100644
index b5ba698c4b6..00000000000
--- a/src/test/localization/battle-stat.test.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat";
-import deBattleStat from "#app/locales/de/battle.json";
-import dePokemonInfo from "#app/locales/de/pokemon-info.json";
-import enBattleStat from "#app/locales/en/battle.json";
-import enPokemonInfo from "#app/locales/en/pokemon-info.json";
-import esBattleStat from "#app/locales/es/battle.json";
-import esPokemonInfo from "#app/locales/es/pokemon-info.json";
-import frBattleStat from "#app/locales/fr/battle.json";
-import frPokemonInfo from "#app/locales/fr/pokemon-info.json";
-import itBattleStat from "#app/locales/it/battle.json";
-import itPokemonInfo from "#app/locales/it/pokemon-info.json";
-import koBattleStat from "#app/locales/ko/battle.json";
-import koPokemonInfo from "#app/locales/ko/pokemon-info.json";
-import ptBrBattleStat from "#app/locales/pt_BR/battle.json";
-import ptBrPokemonInfo from "#app/locales/pt_BR/pokemon-info.json";
-import zhCnBattleStat from "#app/locales/zh_CN/battle.json";
-import zhCnPokemonInfo from "#app/locales/zh_CN/pokemon-info.json";
-import zhTwBattleStat from "#app/locales/zh_TW/battle.json";
-import zhTwPokemonInfo from "#app/locales/zh_TW/pokemon-info.json";
-import i18next, { initI18n } from "#app/plugins/i18n";
-import { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor";
-import { beforeAll, describe, expect, it } from "vitest";
-
-interface BattleStatTestUnit {
- stat: BattleStat,
- key: string
-}
-
-interface BattleStatLevelTestUnit {
- levels: integer,
- up: boolean,
- key: string
- changedStats: integer
-}
-
-function testBattleStatName(stat: BattleStat, expectMessage: string) {
- if (!expectMessage) {
- return;
- } // not translated yet!
- const message = getBattleStatName(stat);
- console.log(`message ${message}, expected ${expectMessage}`);
- expect(message).toBe(expectMessage);
-}
-
-function testBattleStatLevelChangeDescription(levels: integer, up: boolean, expectMessage: string, changedStats: integer) {
- if (!expectMessage) {
- return;
- } // not translated yet!
- const message = getBattleStatLevelChangeDescription("{{pokemonNameWithAffix}}", "{{stats}}", levels, up, changedStats);
- console.log(`message ${message}, expected ${expectMessage}`);
- expect(message).toBe(expectMessage);
-}
-
-describe("Test for BattleStat Localization", () => {
- const battleStatUnits: BattleStatTestUnit[] = [];
- const battleStatLevelUnits: BattleStatLevelTestUnit[] = [];
-
- beforeAll(() => {
- initI18n();
-
- battleStatUnits.push({stat: BattleStat.ATK, key: "Stat.ATK"});
- battleStatUnits.push({stat: BattleStat.DEF, key: "Stat.DEF"});
- battleStatUnits.push({stat: BattleStat.SPATK, key: "Stat.SPATK"});
- battleStatUnits.push({stat: BattleStat.SPDEF, key: "Stat.SPDEF"});
- battleStatUnits.push({stat: BattleStat.SPD, key: "Stat.SPD"});
- battleStatUnits.push({stat: BattleStat.ACC, key: "Stat.ACC"});
- battleStatUnits.push({stat: BattleStat.EVA, key: "Stat.EVA"});
-
- battleStatLevelUnits.push({levels: 1, up: true, key: "statRose_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 2, up: true, key: "statSharplyRose_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 3, up: true, key: "statRoseDrastically_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 4, up: true, key: "statRoseDrastically_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 5, up: true, key: "statRoseDrastically_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 6, up: true, key: "statRoseDrastically_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 7, up: true, key: "statWontGoAnyHigher_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 1, up: false, key: "statFell_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 2, up: false, key: "statHarshlyFell_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 3, up: false, key: "statSeverelyFell_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 4, up: false, key: "statSeverelyFell_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 5, up: false, key: "statSeverelyFell_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 6, up: false, key: "statSeverelyFell_one", changedStats: 1});
- battleStatLevelUnits.push({levels: 7, up: false, key: "statWontGoAnyLower_one", changedStats: 1});
- });
-
- it("Test getBattleStatName() in English", async () => {
- i18next.changeLanguage("en");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, enPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in English", async () => {
- i18next.changeLanguage("en");
- battleStatLevelUnits.forEach(unit => {
- testBattleStatLevelChangeDescription(unit.levels, unit.up, enBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in Español", async () => {
- i18next.changeLanguage("es");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, esPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in Español", async () => {
- i18next.changeLanguage("es");
- battleStatLevelUnits.forEach(unit => {
- testBattleStatLevelChangeDescription(unit.levels, unit.up, esBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in Italiano", async () => {
- i18next.changeLanguage("it");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, itPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in Italiano", async () => {
- i18next.changeLanguage("it");
- battleStatLevelUnits.forEach(unit => {
- testBattleStatLevelChangeDescription(unit.levels, unit.up, itBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in Français", async () => {
- i18next.changeLanguage("fr");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, frPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in Français", async () => {
- i18next.changeLanguage("fr");
- battleStatLevelUnits.forEach(unit => {
- testBattleStatLevelChangeDescription(unit.levels, unit.up, frBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in Deutsch", async () => {
- i18next.changeLanguage("de");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, dePokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in Deutsch", async () => {
- i18next.changeLanguage("de");
- battleStatLevelUnits.forEach(unit => {
- testBattleStatLevelChangeDescription(unit.levels, unit.up, deBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in Português (BR)", async () => {
- i18next.changeLanguage("pt-BR");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, ptBrPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in Português (BR)", async () => {
- i18next.changeLanguage("pt-BR");
- battleStatLevelUnits.forEach(unit => {
- testBattleStatLevelChangeDescription(unit.levels, unit.up, ptBrBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in 简体中文", async () => {
- i18next.changeLanguage("zh-CN");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, zhCnPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in 简体中文", async () => {
- i18next.changeLanguage("zh-CN");
- battleStatLevelUnits.forEach(unit => {
- // In i18next, the pluralization rules are language-specific, and Chinese only supports the _other suffix.
- unit.key = unit.key.replace("one", "other");
- testBattleStatLevelChangeDescription(unit.levels, unit.up, zhCnBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in 繁體中文", async () => {
- i18next.changeLanguage("zh-TW");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, zhTwPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in 繁體中文", async () => {
- i18next.changeLanguage("zh-TW");
- battleStatLevelUnits.forEach(unit => {
- // In i18next, the pluralization rules are language-specific, and Chinese only supports the _other suffix.
- unit.key = unit.key.replace("one", "other");
- testBattleStatLevelChangeDescription(unit.levels, unit.up, zhTwBattleStat[unit.key], unit.changedStats);
- });
- });
-
- it("Test getBattleStatName() in 한국어", async () => {
- await i18next.changeLanguage("ko");
- battleStatUnits.forEach(unit => {
- testBattleStatName(unit.stat, koPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
- });
- });
-
- it("Test getBattleStatLevelChangeDescription() in 한국어", async () => {
- i18next.changeLanguage("ko", () => {
- battleStatLevelUnits.forEach(unit => {
- const processor = new KoreanPostpositionProcessor();
- const message = processor.process(koBattleStat[unit.key]);
- testBattleStatLevelChangeDescription(unit.levels, unit.up, message, unit.changedStats);
- });
- });
- });
-});
diff --git a/src/test/moves/alluring_voice.test.ts b/src/test/moves/alluring_voice.test.ts
index e6ece39524a..9807d1bce85 100644
--- a/src/test/moves/alluring_voice.test.ts
+++ b/src/test/moves/alluring_voice.test.ts
@@ -40,7 +40,7 @@ describe("Moves - Alluring Voice", () => {
});
- it("should confuse the opponent if their stats were raised", async () => {
+ it("should confuse the opponent if their stat stages were raised", async () => {
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
diff --git a/src/test/moves/baton_pass.test.ts b/src/test/moves/baton_pass.test.ts
index 602da9e37f8..0643b73e481 100644
--- a/src/test/moves/baton_pass.test.ts
+++ b/src/test/moves/baton_pass.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import GameManager from "#app/test/utils/gameManager";
@@ -35,7 +35,7 @@ describe("Moves - Baton Pass", () => {
.disableCrits();
});
- it("passes stat stage buffs when player uses it", async () => {
+ it("transfers all stat stages when player uses it", async() => {
// arrange
await game.startBattle([
Species.RAICHU,
@@ -45,7 +45,10 @@ describe("Moves - Baton Pass", () => {
// round 1 - buff
game.move.select(Moves.NASTY_PLOT);
await game.toNextTurn();
- expect(game.scene.getPlayerPokemon()!.summonData.battleStats[BattleStat.SPATK]).toEqual(2);
+
+ let playerPokemon = game.scene.getPlayerPokemon()!;
+
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toEqual(2);
// round 2 - baton pass
game.move.select(Moves.BATON_PASS);
@@ -53,9 +56,9 @@ describe("Moves - Baton Pass", () => {
await game.phaseInterceptor.to(TurnEndPhase);
// assert
- const playerPkm = game.scene.getPlayerPokemon()!;
- expect(playerPkm.species.speciesId).toEqual(Species.SHUCKLE);
- expect(playerPkm.summonData.battleStats[BattleStat.SPATK]).toEqual(2);
+ playerPokemon = game.scene.getPlayerPokemon()!;
+ expect(playerPokemon.species.speciesId).toEqual(Species.SHUCKLE);
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toEqual(2);
}, 20000);
it("passes stat stage buffs when AI uses it", async () => {
@@ -80,7 +83,7 @@ describe("Moves - Baton Pass", () => {
// assert
// check buffs are still there
- expect(game.scene.getEnemyPokemon()!.summonData.battleStats[BattleStat.SPATK]).toEqual(2);
+ expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPATK)).toEqual(2);
// confirm that a switch actually happened. can't use species because I
// can't find a way to override trainer parties with more than 1 pokemon species
expect(game.scene.getEnemyPokemon()!.hp).not.toEqual(100);
diff --git a/src/test/moves/belly_drum.test.ts b/src/test/moves/belly_drum.test.ts
index e4956c6e83a..7024deb3f18 100644
--- a/src/test/moves/belly_drum.test.ts
+++ b/src/test/moves/belly_drum.test.ts
@@ -1,8 +1,8 @@
-import { BattleStat } from "#app/data/battle-stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { toDmgValue } from "#app/utils";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
+import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
@@ -43,8 +43,8 @@ describe("Moves - BELLY DRUM", () => {
// Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Belly_Drum_(move)
- test("Belly Drum raises the user's Attack to its max, at the cost of 1/2 of its maximum HP",
- async () => {
+ test("raises the user's ATK stat stage to its max, at the cost of 1/2 of its maximum HP",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
@@ -54,48 +54,48 @@ describe("Moves - BELLY DRUM", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6);
}, TIMEOUT
);
- test("Belly Drum will still take effect if an uninvolved stat is at max",
- async () => {
+ test("will still take effect if an uninvolved stat stage is at max",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO);
- // Here - BattleStat.ATK -> -3 and BattleStat.SPATK -> 6
- leadPokemon.summonData.battleStats[BattleStat.ATK] = -3;
- leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6;
+ // Here - Stat.ATK -> -3 and Stat.SPATK -> 6
+ leadPokemon.setStatStage(Stat.ATK, -3);
+ leadPokemon.setStatStage(Stat.SPATK, 6);
game.move.select(Moves.BELLY_DRUM);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6);
}, TIMEOUT
);
- test("Belly Drum fails if the pokemon's attack stat is at its maximum",
- async () => {
+ test("fails if the pokemon's ATK stat stage is at its maximum",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
- leadPokemon.summonData.battleStats[BattleStat.ATK] = 6;
+ leadPokemon.setStatStage(Stat.ATK, 6);
game.move.select(Moves.BELLY_DRUM);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6);
}, TIMEOUT
);
- test("Belly Drum fails if the user's health is less than 1/2",
- async () => {
+ test("fails if the user's health is less than 1/2",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
@@ -106,7 +106,7 @@ describe("Moves - BELLY DRUM", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
}, TIMEOUT
);
});
diff --git a/src/test/moves/burning_jealousy.test.ts b/src/test/moves/burning_jealousy.test.ts
index 2281fe74acb..2cb6a0bc52a 100644
--- a/src/test/moves/burning_jealousy.test.ts
+++ b/src/test/moves/burning_jealousy.test.ts
@@ -41,7 +41,7 @@ describe("Moves - Burning Jealousy", () => {
});
- it("should burn the opponent if their stats were raised", async () => {
+ it("should burn the opponent if their stat stages were raised", async () => {
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
@@ -53,7 +53,7 @@ describe("Moves - Burning Jealousy", () => {
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
}, TIMEOUT);
- it("should still burn the opponent if their stats were both raised and lowered in the same turn", async () => {
+ it("should still burn the opponent if their stat stages were both raised and lowered in the same turn", async () => {
game.override
.starterSpecies(0)
.battleType("double");
@@ -69,7 +69,7 @@ describe("Moves - Burning Jealousy", () => {
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
}, TIMEOUT);
- it("should ignore stats raised by imposter", async () => {
+ it("should ignore stat stages raised by IMPOSTER", async () => {
game.override
.enemySpecies(Species.DITTO)
.enemyAbility(Abilities.IMPOSTER)
@@ -88,7 +88,7 @@ describe("Moves - Burning Jealousy", () => {
await game.classicMode.startBattle();
}, TIMEOUT);
- it("should be boosted by Sheer Force even if opponent didn't raise stats", async () => {
+ it("should be boosted by Sheer Force even if opponent didn't raise stat stages", async () => {
game.override
.ability(Abilities.SHEER_FORCE)
.enemyMoveset(SPLASH_ONLY);
diff --git a/src/test/moves/clangorous_soul.test.ts b/src/test/moves/clangorous_soul.test.ts
index 9ea6da91595..9bd3bc2379e 100644
--- a/src/test/moves/clangorous_soul.test.ts
+++ b/src/test/moves/clangorous_soul.test.ts
@@ -1,12 +1,11 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#test/utils/gameManager";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
-import { toDmgValue } from "#app/utils";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
+import { Stat } from "#enums/stat";
import { SPLASH_ONLY } from "#test/utils/testUtils";
-import Phaser from "phaser";
-import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
const TIMEOUT = 20 * 1000;
/** HP Cost of Move */
@@ -14,7 +13,7 @@ const RATIO = 3;
/** Amount of extra HP lost */
const PREDAMAGE = 15;
-describe("Moves - CLANGOROUS_SOUL", () => {
+describe("Moves - Clangorous Soul", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@@ -40,91 +39,91 @@ describe("Moves - CLANGOROUS_SOUL", () => {
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Clangorous_Soul_(move)
- test("Clangorous Soul raises the user's Attack, Defense, Special Attack, Special Defense and Speed by one stage each, at the cost of 1/3 of its maximum HP",
- async () => {
+ it("raises the user's ATK, DEF, SPATK, SPDEF, and SPD stat stages by 1 each at the cost of 1/3 of its maximum HP",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
- const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO);
+ const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO);
game.move.select(Moves.CLANGOROUS_SOUL);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(1);
- expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(1);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(1);
+ expect(leadPokemon.getStatStage(Stat.DEF)).toBe(1);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(1);
+ expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(1);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(1);
}, TIMEOUT
);
- test("Clangorous Soul will still take effect if one or more of the involved stats are not at max",
- async () => {
+ it("will still take effect if one or more of the involved stat stages are not at max",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
- const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO);
+ const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO);
- //Here - BattleStat.SPD -> 0 and BattleStat.SPDEF -> 4
- leadPokemon.summonData.battleStats[BattleStat.ATK] = 6;
- leadPokemon.summonData.battleStats[BattleStat.DEF] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPDEF] = 4;
+ //Here - Stat.SPD -> 0 and Stat.SPDEF -> 4
+ leadPokemon.setStatStage(Stat.ATK, 6);
+ leadPokemon.setStatStage(Stat.DEF, 6);
+ leadPokemon.setStatStage(Stat.SPATK, 6);
+ leadPokemon.setStatStage(Stat.SPDEF, 4);
game.move.select(Moves.CLANGOROUS_SOUL);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(5);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(1);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.DEF)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(5);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(1);
}, TIMEOUT
);
- test("Clangorous Soul fails if all stats involved are at max",
- async () => {
+ it("fails if all stat stages involved are at max",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
- leadPokemon.summonData.battleStats[BattleStat.ATK] = 6;
- leadPokemon.summonData.battleStats[BattleStat.DEF] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPDEF] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPD] = 6;
+ leadPokemon.setStatStage(Stat.ATK, 6);
+ leadPokemon.setStatStage(Stat.DEF, 6);
+ leadPokemon.setStatStage(Stat.SPATK, 6);
+ leadPokemon.setStatStage(Stat.SPDEF, 6);
+ leadPokemon.setStatStage(Stat.SPD, 6);
game.move.select(Moves.CLANGOROUS_SOUL);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.DEF)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(6);
}, TIMEOUT
);
- test("Clangorous Soul fails if the user's health is less than 1/3",
- async () => {
+ it("fails if the user's health is less than 1/3",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
- const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO);
+ const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO);
leadPokemon.hp = hpLost - PREDAMAGE;
game.move.select(Moves.CLANGOROUS_SOUL);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.DEF)).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(0);
}, TIMEOUT
);
});
diff --git a/src/test/moves/crafty_shield.test.ts b/src/test/moves/crafty_shield.test.ts
index a341a50b0b9..e73a1fd256d 100644
--- a/src/test/moves/crafty_shield.test.ts
+++ b/src/test/moves/crafty_shield.test.ts
@@ -1,13 +1,13 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { BattlerTagType } from "#app/enums/battler-tag-type";
-import { BerryPhase } from "#app/phases/berry-phase";
-import { CommandPhase } from "#app/phases/command-phase";
-import { Abilities } from "#enums/abilities";
-import { Moves } from "#enums/moves";
-import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import GameManager from "../utils/gameManager";
+import { Species } from "#enums/species";
+import { Abilities } from "#enums/abilities";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { BattlerTagType } from "#app/enums/battler-tag-type";
+import { BerryPhase } from "#app/phases/berry-phase";
+import { CommandPhase } from "#app/phases/command-phase";
const TIMEOUT = 20 * 1000;
@@ -55,7 +55,7 @@ describe("Moves - Crafty Shield", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0));
+ leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
}, TIMEOUT
);
@@ -117,8 +117,8 @@ describe("Moves - Crafty Shield", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- expect(leadPokemon[0].summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(leadPokemon[1].summonData.battleStats[BattleStat.ATK]).toBe(2);
+ expect(leadPokemon[0].getStatStage(Stat.ATK)).toBe(0);
+ expect(leadPokemon[1].getStatStage(Stat.ATK)).toBe(2);
}
);
});
diff --git a/src/test/moves/double_team.test.ts b/src/test/moves/double_team.test.ts
index c45c8bd8516..fa224c8df9e 100644
--- a/src/test/moves/double_team.test.ts
+++ b/src/test/moves/double_team.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { Abilities } from "#app/enums/abilities";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
@@ -32,20 +32,20 @@ describe("Moves - Double Team", () => {
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
});
- it("increases the user's evasion by one stage.", async () => {
+ it("raises the user's EVA stat stage by 1", async () => {
await game.startBattle([Species.MAGIKARP]);
const ally = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "getAccuracyMultiplier");
- expect(ally.summonData.battleStats[BattleStat.EVA]).toBe(0);
+ expect(ally.getStatStage(Stat.EVA)).toBe(0);
game.move.select(Moves.DOUBLE_TEAM);
await game.phaseInterceptor.to(TurnEndPhase);
await game.toNextTurn();
- expect(ally.summonData.battleStats[BattleStat.EVA]).toBe(1);
+ expect(ally.getStatStage(Stat.EVA)).toBe(1);
expect(enemy.getAccuracyMultiplier).toHaveReturnedWith(.75);
});
});
diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts
index 223635575ab..5da6e082ce5 100644
--- a/src/test/moves/dragon_rage.test.ts
+++ b/src/test/moves/dragon_rage.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { Type } from "#app/data/type";
import { Species } from "#app/enums/species";
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
@@ -63,9 +63,8 @@ describe("Moves - Dragon Rage", () => {
game.move.select(Moves.DRAGON_RAGE);
await game.phaseInterceptor.to(TurnEndPhase);
- const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
- expect(damageDealt).toBe(dragonRageDamage);
+ expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage);
});
it("ignores resistances", async () => {
@@ -74,20 +73,18 @@ describe("Moves - Dragon Rage", () => {
game.move.select(Moves.DRAGON_RAGE);
await game.phaseInterceptor.to(TurnEndPhase);
- const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
- expect(damageDealt).toBe(dragonRageDamage);
+ expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage);
});
- it("ignores stat changes", async () => {
+ it("ignores SPATK stat stages", async () => {
game.override.disableCrits();
- partyPokemon.summonData.battleStats[BattleStat.SPATK] = 2;
+ partyPokemon.setStatStage(Stat.SPATK, 2);
game.move.select(Moves.DRAGON_RAGE);
await game.phaseInterceptor.to(TurnEndPhase);
- const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
- expect(damageDealt).toBe(dragonRageDamage);
+ expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage);
});
it("ignores stab", async () => {
@@ -96,9 +93,8 @@ describe("Moves - Dragon Rage", () => {
game.move.select(Moves.DRAGON_RAGE);
await game.phaseInterceptor.to(TurnEndPhase);
- const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
- expect(damageDealt).toBe(dragonRageDamage);
+ expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage);
});
it("ignores criticals", async () => {
@@ -106,20 +102,18 @@ describe("Moves - Dragon Rage", () => {
game.move.select(Moves.DRAGON_RAGE);
await game.phaseInterceptor.to(TurnEndPhase);
- const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
- expect(damageDealt).toBe(dragonRageDamage);
+ expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage);
});
- it("ignores damage modification from abilities such as ice scales", async () => {
+ it("ignores damage modification from abilities, for example ICE_SCALES", async () => {
game.override.disableCrits();
game.override.enemyAbility(Abilities.ICE_SCALES);
game.move.select(Moves.DRAGON_RAGE);
await game.phaseInterceptor.to(TurnEndPhase);
- const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
- expect(damageDealt).toBe(dragonRageDamage);
+ expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage);
});
it("ignores multi hit", async () => {
@@ -128,8 +122,7 @@ describe("Moves - Dragon Rage", () => {
game.move.select(Moves.DRAGON_RAGE);
await game.phaseInterceptor.to(TurnEndPhase);
- const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
- expect(damageDealt).toBe(dragonRageDamage);
+ expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage);
});
});
diff --git a/src/test/moves/fillet_away.test.ts b/src/test/moves/fillet_away.test.ts
index b2ff9e25dba..a639a86c5c1 100644
--- a/src/test/moves/fillet_away.test.ts
+++ b/src/test/moves/fillet_away.test.ts
@@ -1,8 +1,8 @@
-import { BattleStat } from "#app/data/battle-stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { toDmgValue } from "#app/utils";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
+import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
@@ -40,8 +40,8 @@ describe("Moves - FILLET AWAY", () => {
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/fillet_away_(move)
- test("Fillet Away raises the user's Attack, Special Attack, and Speed by two stages each, at the cost of 1/2 of its maximum HP",
- async () => {
+ test("raises the user's ATK, SPATK, and SPD stat stages by 2 each, at the cost of 1/2 of its maximum HP",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
@@ -51,55 +51,55 @@ describe("Moves - FILLET AWAY", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(2);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(2);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(2);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(2);
}, TIMEOUT
);
- test("Fillet Away will still take effect if one or more of the involved stats are not at max",
- async () => {
+ test("still takes effect if one or more of the involved stat stages are not at max",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO);
- //Here - BattleStat.SPD -> 0 and BattleStat.SPATK -> 3
- leadPokemon.summonData.battleStats[BattleStat.ATK] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPATK] = 3;
+ //Here - Stat.SPD -> 0 and Stat.SPATK -> 3
+ leadPokemon.setStatStage(Stat.ATK, 6);
+ leadPokemon.setStatStage(Stat.SPATK, 3);
game.move.select(Moves.FILLET_AWAY);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(5);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(2);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(5);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(2);
}, TIMEOUT
);
- test("Fillet Away fails if all stats involved are at max",
- async () => {
+ test("fails if all stat stages involved are at max",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
- leadPokemon.summonData.battleStats[BattleStat.ATK] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6;
- leadPokemon.summonData.battleStats[BattleStat.SPD] = 6;
+ leadPokemon.setStatStage(Stat.ATK, 6);
+ leadPokemon.setStatStage(Stat.SPATK, 6);
+ leadPokemon.setStatStage(Stat.SPD, 6);
game.move.select(Moves.FILLET_AWAY);
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(6);
}, TIMEOUT
);
- test("Fillet Away fails if the user's health is less than 1/2",
- async () => {
+ test("fails if the user's health is less than 1/2",
+ async() => {
await game.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
@@ -110,9 +110,9 @@ describe("Moves - FILLET AWAY", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0);
- expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.SPD)).toBe(0);
}, TIMEOUT
);
});
diff --git a/src/test/moves/fissure.test.ts b/src/test/moves/fissure.test.ts
index 51122b269b8..34612d1fb18 100644
--- a/src/test/moves/fissure.test.ts
+++ b/src/test/moves/fissure.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { Species } from "#app/enums/species";
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import { DamagePhase } from "#app/phases/damage-phase";
@@ -52,7 +52,7 @@ describe("Moves - Fissure", () => {
game.scene.clearEnemyHeldItemModifiers();
});
- it("ignores damage modification from abilities such as fur coat", async () => {
+ it("ignores damage modification from abilities, for example FUR_COAT", async () => {
game.override.ability(Abilities.NO_GUARD);
game.override.enemyAbility(Abilities.FUR_COAT);
@@ -62,10 +62,10 @@ describe("Moves - Fissure", () => {
expect(enemyPokemon.isFainted()).toBe(true);
});
- it("ignores accuracy stat", async () => {
+ it("ignores user's ACC stat stage", async () => {
vi.spyOn(partyPokemon, "getAccuracyMultiplier");
- enemyPokemon.summonData.battleStats[BattleStat.ACC] = -6;
+ partyPokemon.setStatStage(Stat.ACC, -6);
game.move.select(Moves.FISSURE);
@@ -75,10 +75,10 @@ describe("Moves - Fissure", () => {
expect(partyPokemon.getAccuracyMultiplier).toHaveReturnedWith(1);
});
- it("ignores evasion stat", async () => {
+ it("ignores target's EVA stat stage", async () => {
vi.spyOn(partyPokemon, "getAccuracyMultiplier");
- enemyPokemon.summonData.battleStats[BattleStat.EVA] = 6;
+ enemyPokemon.setStatStage(Stat.EVA, 6);
game.move.select(Moves.FISSURE);
diff --git a/src/test/moves/flower_shield.test.ts b/src/test/moves/flower_shield.test.ts
index b3e50219aec..ffe8ae995d3 100644
--- a/src/test/moves/flower_shield.test.ts
+++ b/src/test/moves/flower_shield.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { SemiInvulnerableTag } from "#app/data/battler-tags";
import { Type } from "#app/data/type";
import { Biome } from "#app/enums/biome";
@@ -34,24 +34,24 @@ describe("Moves - Flower Shield", () => {
game.override.enemyMoveset(SPLASH_ONLY);
});
- it("increases defense of all Grass-type Pokemon on the field by one stage - single battle", async () => {
+ it("raises DEF stat stage by 1 for all Grass-type Pokemon on the field by one stage - single battle", async () => {
game.override.enemySpecies(Species.CHERRIM);
await game.startBattle([Species.MAGIKARP]);
const cherrim = game.scene.getEnemyPokemon()!;
const magikarp = game.scene.getPlayerPokemon()!;
- expect(magikarp.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(0);
+ expect(magikarp.getStatStage(Stat.DEF)).toBe(0);
+ expect(cherrim.getStatStage(Stat.DEF)).toBe(0);
game.move.select(Moves.FLOWER_SHIELD);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(magikarp.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(1);
+ expect(magikarp.getStatStage(Stat.DEF)).toBe(0);
+ expect(cherrim.getStatStage(Stat.DEF)).toBe(1);
});
- it("increases defense of all Grass-type Pokemon on the field by one stage - double battle", async () => {
+ it("raises DEF stat stage by 1 for all Grass-type Pokemon on the field by one stage - double battle", async () => {
game.override.enemySpecies(Species.MAGIKARP).startingBiome(Biome.GRASS).battleType("double");
await game.startBattle([Species.CHERRIM, Species.MAGIKARP]);
@@ -60,21 +60,21 @@ describe("Moves - Flower Shield", () => {
const grassPokemons = field.filter(p => p.getTypes().includes(Type.GRASS));
const nonGrassPokemons = field.filter(pokemon => !grassPokemons.includes(pokemon));
- grassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0));
- nonGrassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0));
+ grassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(0));
+ nonGrassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(0));
game.move.select(Moves.FLOWER_SHIELD);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(TurnEndPhase);
- grassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(1));
- nonGrassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0));
+ grassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(1));
+ nonGrassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(0));
});
/**
* See semi-vulnerable state tags. {@linkcode SemiInvulnerableTag}
*/
- it("does not increase defense of a pokemon in semi-vulnerable state", async () => {
+ it("does not raise DEF stat stage for a Pokemon in semi-vulnerable state", async () => {
game.override.enemySpecies(Species.PARAS);
game.override.enemyMoveset([Moves.DIG, Moves.DIG, Moves.DIG, Moves.DIG]);
game.override.enemyLevel(50);
@@ -83,32 +83,32 @@ describe("Moves - Flower Shield", () => {
const paras = game.scene.getEnemyPokemon()!;
const cherrim = game.scene.getPlayerPokemon()!;
- expect(paras.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(0);
+ expect(paras.getStatStage(Stat.DEF)).toBe(0);
+ expect(cherrim.getStatStage(Stat.DEF)).toBe(0);
expect(paras.getTag(SemiInvulnerableTag)).toBeUndefined;
game.move.select(Moves.FLOWER_SHIELD);
await game.phaseInterceptor.to(TurnEndPhase);
expect(paras.getTag(SemiInvulnerableTag)).toBeDefined();
- expect(paras.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(1);
+ expect(paras.getStatStage(Stat.DEF)).toBe(0);
+ expect(cherrim.getStatStage(Stat.DEF)).toBe(1);
});
- it("does nothing if there are no Grass-type pokemon on the field", async () => {
+ it("does nothing if there are no Grass-type Pokemon on the field", async () => {
game.override.enemySpecies(Species.MAGIKARP);
await game.startBattle([Species.MAGIKARP]);
const enemy = game.scene.getEnemyPokemon()!;
const ally = game.scene.getPlayerPokemon()!;
- expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(ally.summonData.battleStats[BattleStat.DEF]).toBe(0);
+ expect(enemy.getStatStage(Stat.DEF)).toBe(0);
+ expect(ally.getStatStage(Stat.DEF)).toBe(0);
game.move.select(Moves.FLOWER_SHIELD);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(ally.summonData.battleStats[BattleStat.DEF]).toBe(0);
+ expect(enemy.getStatStage(Stat.DEF)).toBe(0);
+ expect(ally.getStatStage(Stat.DEF)).toBe(0);
});
});
diff --git a/src/test/moves/follow_me.test.ts b/src/test/moves/follow_me.test.ts
index d7ef199df3e..64fc9c16256 100644
--- a/src/test/moves/follow_me.test.ts
+++ b/src/test/moves/follow_me.test.ts
@@ -1,5 +1,5 @@
+import { Stat } from "#enums/stat";
import { BattlerIndex } from "#app/battle";
-import { Stat } from "#app/data/pokemon-stat";
import { Abilities } from "#app/enums/abilities";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
@@ -66,7 +66,7 @@ describe("Moves - Follow Me", () => {
game.move.select(Moves.FOLLOW_ME, 1);
await game.phaseInterceptor.to(TurnEndPhase, false);
- playerPokemon.sort((a, b) => a.getBattleStat(Stat.SPD) - b.getBattleStat(Stat.SPD));
+ playerPokemon.sort((a, b) => a.getEffectiveStat(Stat.SPD) - b.getEffectiveStat(Stat.SPD));
expect(playerPokemon[1].hp).toBeLessThan(playerStartingHp[1]);
expect(playerPokemon[0].hp).toBe(playerStartingHp[0]);
diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts
new file mode 100644
index 00000000000..445a432a812
--- /dev/null
+++ b/src/test/moves/freeze_dry.test.ts
@@ -0,0 +1,107 @@
+import { BattlerIndex } from "#app/battle";
+import { Abilities } from "#app/enums/abilities";
+import { Moves } from "#app/enums/moves";
+import { Species } from "#app/enums/species";
+import GameManager from "#test/utils/gameManager";
+import { SPLASH_ONLY } from "#test/utils/testUtils";
+import Phaser from "phaser";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+
+describe("Moves - Freeze-Dry", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+ const TIMEOUT = 20 * 1000;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemySpecies(Species.MAGIKARP)
+ .enemyAbility(Abilities.BALL_FETCH)
+ .enemyMoveset(SPLASH_ONLY)
+ .starterSpecies(Species.FEEBAS)
+ .ability(Abilities.BALL_FETCH)
+ .moveset([Moves.FREEZE_DRY]);
+ });
+
+ it("should deal 2x damage to pure water types", async () => {
+ await game.classicMode.startBattle();
+
+ const enemy = game.scene.getEnemyPokemon()!;
+ vi.spyOn(enemy, "getMoveEffectiveness");
+
+ game.move.select(Moves.FREEZE_DRY);
+ await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
+ await game.phaseInterceptor.to("MoveEffectPhase");
+
+ expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
+ }, TIMEOUT);
+
+ it("should deal 4x damage to water/flying types", async () => {
+ game.override.enemySpecies(Species.WINGULL);
+ await game.classicMode.startBattle();
+
+ const enemy = game.scene.getEnemyPokemon()!;
+ vi.spyOn(enemy, "getMoveEffectiveness");
+
+ game.move.select(Moves.FREEZE_DRY);
+ await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
+ await game.phaseInterceptor.to("MoveEffectPhase");
+
+ expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4);
+ }, TIMEOUT);
+
+ it("should deal 1x damage to water/fire types", async () => {
+ game.override.enemySpecies(Species.VOLCANION);
+ await game.classicMode.startBattle();
+
+ const enemy = game.scene.getEnemyPokemon()!;
+ vi.spyOn(enemy, "getMoveEffectiveness");
+
+ game.move.select(Moves.FREEZE_DRY);
+ await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
+ await game.phaseInterceptor.to("MoveEffectPhase");
+
+ expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1);
+ }, TIMEOUT);
+
+ // enable if this is ever fixed (lol)
+ it.todo("should deal 2x damage to water types under Normalize", async () => {
+ game.override.ability(Abilities.NORMALIZE);
+ await game.classicMode.startBattle();
+
+ const enemy = game.scene.getEnemyPokemon()!;
+ vi.spyOn(enemy, "getMoveEffectiveness");
+
+ game.move.select(Moves.FREEZE_DRY);
+ await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
+ await game.phaseInterceptor.to("MoveEffectPhase");
+
+ expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
+ }, TIMEOUT);
+
+ // enable once Electrify is implemented (and the interaction is fixed, as above)
+ it.todo("should deal 2x damage to water types under Electrify", async () => {
+ game.override.enemyMoveset(Array(4).fill(Moves.ELECTRIFY));
+ await game.classicMode.startBattle();
+
+ const enemy = game.scene.getEnemyPokemon()!;
+ vi.spyOn(enemy, "getMoveEffectiveness");
+
+ game.move.select(Moves.FREEZE_DRY);
+ await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
+ await game.phaseInterceptor.to("BerryPhase");
+
+ expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
+ }, TIMEOUT);
+});
diff --git a/src/test/moves/freezy_frost.test.ts b/src/test/moves/freezy_frost.test.ts
index 00d7104d373..ae42d5b6dc6 100644
--- a/src/test/moves/freezy_frost.test.ts
+++ b/src/test/moves/freezy_frost.test.ts
@@ -1,82 +1,61 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { allMoves } from "#app/data/move";
-import { MoveEndPhase } from "#app/phases/move-end-phase";
-import { TurnInitPhase } from "#app/phases/turn-init-phase";
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
-import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { SPLASH_ONLY } from "#test/utils/testUtils";
+import { allMoves } from "#app/data/move";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
describe("Moves - Freezy Frost", () => {
- describe("integration tests", () => {
- let phaserGame: Phaser.Game;
- let game: GameManager;
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
- beforeAll(() => {
- phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
- });
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
+ });
- afterEach(() => {
- game.phaseInterceptor.restoreOg();
- });
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
- beforeEach(() => {
- game = new GameManager(phaserGame);
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
- game.override.battleType("single");
+ game.override.battleType("single");
- game.override.enemySpecies(Species.RATTATA);
- game.override.enemyLevel(100);
- game.override.enemyMoveset(SPLASH_ONLY);
- game.override.enemyAbility(Abilities.NONE);
+ game.override.enemySpecies(Species.RATTATA);
+ game.override.enemyLevel(100);
+ game.override.enemyMoveset(SPLASH_ONLY);
+ game.override.enemyAbility(Abilities.NONE);
- game.override.startingLevel(100);
- game.override.moveset([Moves.FREEZY_FROST, Moves.SWORDS_DANCE, Moves.CHARM, Moves.SPLASH]);
- vi.spyOn(allMoves[Moves.FREEZY_FROST], "accuracy", "get").mockReturnValue(100);
- game.override.ability(Abilities.NONE);
- });
+ game.override.startingLevel(100);
+ game.override.moveset([Moves.FREEZY_FROST, Moves.SWORDS_DANCE, Moves.CHARM, Moves.SPLASH]);
+ vi.spyOn(allMoves[Moves.FREEZY_FROST], "accuracy", "get").mockReturnValue(100);
+ game.override.ability(Abilities.NONE);
+ });
- it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, player uses Freezy Frost to clear all stat changes", { timeout: 10000 }, async () => {
- await game.startBattle([Species.RATTATA]);
- const user = game.scene.getPlayerPokemon()!;
- const enemy = game.scene.getEnemyPokemon()!;
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ it("should clear all stat stage changes", { timeout: 10000 }, async () => {
+ await game.startBattle([Species.RATTATA]);
+ const user = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
- game.move.select(Moves.SWORDS_DANCE);
- await game.phaseInterceptor.to(TurnInitPhase);
+ expect(user.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(0);
- game.move.select(Moves.CHARM);
- await game.phaseInterceptor.to(TurnInitPhase);
- const userAtkBefore = user.summonData.battleStats[BattleStat.ATK];
- const enemyAtkBefore = enemy.summonData.battleStats[BattleStat.ATK];
- expect(userAtkBefore).toBe(2);
- expect(enemyAtkBefore).toBe(-2);
+ game.move.select(Moves.SWORDS_DANCE);
+ await game.phaseInterceptor.to(TurnInitPhase);
- game.move.select(Moves.FREEZY_FROST);
- await game.phaseInterceptor.to(TurnInitPhase);
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0);
- });
+ game.move.select(Moves.CHARM);
+ await game.phaseInterceptor.to(TurnInitPhase);
+ expect(user.getStatStage(Stat.ATK)).toBe(2);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(-2);
- it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, enemy uses Freezy Frost to clear all stat changes", { timeout: 10000 }, async () => {
- game.override.enemyMoveset([Moves.FREEZY_FROST, Moves.FREEZY_FROST, Moves.FREEZY_FROST, Moves.FREEZY_FROST]);
- await game.startBattle([Species.SHUCKLE]); // Shuckle for slower Swords Dance on first turn so Freezy Frost doesn't affect it.
- const user = game.scene.getPlayerPokemon()!;
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
-
- game.move.select(Moves.SWORDS_DANCE);
- await game.phaseInterceptor.to(TurnInitPhase);
-
- const userAtkBefore = user.summonData.battleStats[BattleStat.ATK];
- expect(userAtkBefore).toBe(2);
-
- game.move.select(Moves.SPLASH);
- await game.phaseInterceptor.to(MoveEndPhase);
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
- });
+ game.move.select(Moves.FREEZY_FROST);
+ await game.phaseInterceptor.to(TurnInitPhase);
+ expect(user.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(0);
});
});
diff --git a/src/test/moves/fusion_flare.test.ts b/src/test/moves/fusion_flare.test.ts
index 471f6a2ac7b..0a8f6f9115d 100644
--- a/src/test/moves/fusion_flare.test.ts
+++ b/src/test/moves/fusion_flare.test.ts
@@ -27,7 +27,7 @@ describe("Moves - Fusion Flare", () => {
game.override.moveset([fusionFlare]);
game.override.startingLevel(1);
- game.override.enemySpecies(Species.RESHIRAM);
+ game.override.enemySpecies(Species.RATTATA);
game.override.enemyMoveset([Moves.REST, Moves.REST, Moves.REST, Moves.REST]);
game.override.battleType("single");
diff --git a/src/test/moves/fusion_flare_bolt.test.ts b/src/test/moves/fusion_flare_bolt.test.ts
index ebef5148778..a8372fcaaab 100644
--- a/src/test/moves/fusion_flare_bolt.test.ts
+++ b/src/test/moves/fusion_flare_bolt.test.ts
@@ -1,6 +1,6 @@
+import { Stat } from "#enums/stat";
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
-import { Stat } from "#app/data/pokemon-stat";
import { DamagePhase } from "#app/phases/damage-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
diff --git a/src/test/moves/growth.test.ts b/src/test/moves/growth.test.ts
index dfbf5406351..defe5e26f41 100644
--- a/src/test/moves/growth.test.ts
+++ b/src/test/moves/growth.test.ts
@@ -1,14 +1,13 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { Stat } from "#app/data/pokemon-stat";
-import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
-import { TurnInitPhase } from "#app/phases/turn-init-phase";
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
-
+import { SPLASH_ONLY } from "../utils/testUtils";
+import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
describe("Moves - Growth", () => {
let phaserGame: Phaser.Game;
@@ -26,31 +25,25 @@ describe("Moves - Growth", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
- const moveToUse = Moves.GROWTH;
game.override.battleType("single");
- game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(Abilities.MOXIE);
game.override.ability(Abilities.INSOMNIA);
- game.override.startingLevel(2000);
- game.override.moveset([moveToUse]);
- game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
+ game.override.moveset([ Moves.GROWTH ]);
+ game.override.enemyMoveset(SPLASH_ONLY);
});
- it("GROWTH", async () => {
- const moveToUse = Moves.GROWTH;
+ it("should raise SPATK stat stage by 1", async() => {
await game.startBattle([
- Species.MIGHTYENA,
- Species.MIGHTYENA,
+ Species.MIGHTYENA
]);
- let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[Stat.SPATK]).toBe(0);
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0);
+ const playerPokemon = game.scene.getPlayerPokemon()!;
- game.move.select(moveToUse);
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0);
+
+ game.move.select(Moves.GROWTH);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase);
- battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
- expect(battleStatsPokemon[BattleStat.SPATK]).toBe(1);
+
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, 20000);
});
diff --git a/src/test/moves/guard_split.test.ts b/src/test/moves/guard_split.test.ts
new file mode 100644
index 00000000000..f95d09f726c
--- /dev/null
+++ b/src/test/moves/guard_split.test.ts
@@ -0,0 +1,82 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#app/test/utils/gameManager";
+import { Species } from "#enums/species";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { Abilities } from "#enums/abilities";
+import { SPLASH_ONLY } from "../utils/testUtils";
+
+describe("Moves - Guard Split", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemyAbility(Abilities.NONE)
+ .enemySpecies(Species.MEW)
+ .enemyLevel(200)
+ .moveset([ Moves.GUARD_SPLIT ])
+ .ability(Abilities.NONE);
+ });
+
+ it("should average the user's DEF and SPDEF stats with those of the target", async () => {
+ game.override.enemyMoveset(SPLASH_ONLY);
+ await game.startBattle([
+ Species.INDEEDEE
+ ]);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ const avgDef = Math.floor((player.getStat(Stat.DEF, false) + enemy.getStat(Stat.DEF, false)) / 2);
+ const avgSpDef = Math.floor((player.getStat(Stat.SPDEF, false) + enemy.getStat(Stat.SPDEF, false)) / 2);
+
+ game.move.select(Moves.GUARD_SPLIT);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStat(Stat.DEF, false)).toBe(avgDef);
+ expect(enemy.getStat(Stat.DEF, false)).toBe(avgDef);
+
+ expect(player.getStat(Stat.SPDEF, false)).toBe(avgSpDef);
+ expect(enemy.getStat(Stat.SPDEF, false)).toBe(avgSpDef);
+ }, 20000);
+
+ it("should be idempotent", async () => {
+ game.override.enemyMoveset(new Array(4).fill(Moves.GUARD_SPLIT));
+ await game.startBattle([
+ Species.INDEEDEE
+ ]);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ const avgDef = Math.floor((player.getStat(Stat.DEF, false) + enemy.getStat(Stat.DEF, false)) / 2);
+ const avgSpDef = Math.floor((player.getStat(Stat.SPDEF, false) + enemy.getStat(Stat.SPDEF, false)) / 2);
+
+ game.move.select(Moves.GUARD_SPLIT);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ game.move.select(Moves.GUARD_SPLIT);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStat(Stat.DEF, false)).toBe(avgDef);
+ expect(enemy.getStat(Stat.DEF, false)).toBe(avgDef);
+
+ expect(player.getStat(Stat.SPDEF, false)).toBe(avgSpDef);
+ expect(enemy.getStat(Stat.SPDEF, false)).toBe(avgSpDef);
+ }, 20000);
+});
diff --git a/src/test/moves/guard_swap.test.ts b/src/test/moves/guard_swap.test.ts
new file mode 100644
index 00000000000..407d475de09
--- /dev/null
+++ b/src/test/moves/guard_swap.test.ts
@@ -0,0 +1,63 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#app/test/utils/gameManager";
+import { Species } from "#enums/species";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { Abilities } from "#enums/abilities";
+import { MoveEndPhase } from "#app/phases/move-end-phase";
+
+describe("Moves - Guard Swap", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemyAbility(Abilities.BALL_FETCH)
+ .enemyMoveset(new Array(4).fill(Moves.SHELL_SMASH))
+ .enemySpecies(Species.MEW)
+ .enemyLevel(200)
+ .moveset([ Moves.GUARD_SWAP ])
+ .ability(Abilities.NONE);
+ });
+
+ it("should swap the user's DEF AND SPDEF stat stages with the target's", async () => {
+ await game.startBattle([
+ Species.INDEEDEE
+ ]);
+
+ // Should start with no stat stages
+ const player = game.scene.getPlayerPokemon()!;
+ // After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ game.move.select(Moves.GUARD_SWAP);
+
+ await game.phaseInterceptor.to(MoveEndPhase);
+
+ expect(player.getStatStage(Stat.DEF)).toBe(0);
+ expect(player.getStatStage(Stat.SPDEF)).toBe(0);
+ expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
+ expect(enemy.getStatStage(Stat.SPDEF)).toBe(-1);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStatStage(Stat.DEF)).toBe(-1);
+ expect(player.getStatStage(Stat.SPDEF)).toBe(-1);
+ expect(enemy.getStatStage(Stat.DEF)).toBe(0);
+ expect(enemy.getStatStage(Stat.SPDEF)).toBe(0);
+ }, 20000);
+});
diff --git a/src/test/moves/haze.test.ts b/src/test/moves/haze.test.ts
index 8a32a40cb32..42081ce74e8 100644
--- a/src/test/moves/haze.test.ts
+++ b/src/test/moves/haze.test.ts
@@ -1,13 +1,12 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { MoveEndPhase } from "#app/phases/move-end-phase";
-import { TurnInitPhase } from "#app/phases/turn-init-phase";
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
-import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { SPLASH_ONLY } from "#test/utils/testUtils";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
describe("Moves - Haze", () => {
describe("integration tests", () => {
@@ -37,44 +36,28 @@ describe("Moves - Haze", () => {
game.override.ability(Abilities.NONE);
});
- it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, player uses Haze to clear all stat changes", { timeout: 10000 }, async () => {
+ it("should reset all stat changes of all Pokemon on field", { timeout: 10000 }, async () => {
await game.startBattle([Species.RATTATA]);
const user = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0);
+
+ expect(user.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to(TurnInitPhase);
game.move.select(Moves.CHARM);
await game.phaseInterceptor.to(TurnInitPhase);
- const userAtkBefore = user.summonData.battleStats[BattleStat.ATK];
- const enemyAtkBefore = enemy.summonData.battleStats[BattleStat.ATK];
- expect(userAtkBefore).toBe(2);
- expect(enemyAtkBefore).toBe(-2);
+
+ expect(user.getStatStage(Stat.ATK)).toBe(2);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(-2);
game.move.select(Moves.HAZE);
await game.phaseInterceptor.to(TurnInitPhase);
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
- expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0);
- });
- it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, enemy uses Haze to clear all stat changes", { timeout: 10000 }, async () => {
- game.override.enemyMoveset([Moves.HAZE, Moves.HAZE, Moves.HAZE, Moves.HAZE]);
- await game.startBattle([Species.SHUCKLE]); // Shuckle for slower Swords Dance on first turn so Haze doesn't affect it.
- const user = game.scene.getPlayerPokemon()!;
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
-
- game.move.select(Moves.SWORDS_DANCE);
- await game.phaseInterceptor.to(TurnInitPhase);
-
- const userAtkBefore = user.summonData.battleStats[BattleStat.ATK];
- expect(userAtkBefore).toBe(2);
-
- game.move.select(Moves.SPLASH);
- await game.phaseInterceptor.to(MoveEndPhase);
- expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(user.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(0);
});
});
});
diff --git a/src/test/moves/lash_out.test.ts b/src/test/moves/lash_out.test.ts
index 78f4b712cf0..74d9fcd66c0 100644
--- a/src/test/moves/lash_out.test.ts
+++ b/src/test/moves/lash_out.test.ts
@@ -39,7 +39,7 @@ describe("Moves - Lash Out", () => {
});
- it("should deal double damage if the user's stats were lowered this turn", async () => {
+ it("should deal double damage if the user's stat stages were lowered this turn", async () => {
vi.spyOn(allMoves[Moves.LASH_OUT], "calculateBattlePower");
await game.classicMode.startBattle();
diff --git a/src/test/moves/make_it_rain.test.ts b/src/test/moves/make_it_rain.test.ts
index 0af7763f175..e41472d7561 100644
--- a/src/test/moves/make_it_rain.test.ts
+++ b/src/test/moves/make_it_rain.test.ts
@@ -1,13 +1,13 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { MoveEndPhase } from "#app/phases/move-end-phase";
-import { StatChangePhase } from "#app/phases/stat-change-phase";
+import { Stat } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
-import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { SPLASH_ONLY } from "#test/utils/testUtils";
+import { MoveEndPhase } from "#app/phases/move-end-phase";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
const TIMEOUT = 20 * 1000;
@@ -36,17 +36,17 @@ describe("Moves - Make It Rain", () => {
game.override.enemyLevel(100);
});
- it("should only reduce Sp. Atk. once in a double battle", async () => {
+ it("should only lower SPATK stat stage by 1 once in a double battle", async () => {
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
- const playerPokemon = game.scene.getPlayerField();
+ const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.MAKE_IT_RAIN);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(MoveEndPhase);
- expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);
}, TIMEOUT);
it("should apply effects even if the target faints", async () => {
@@ -60,10 +60,10 @@ describe("Moves - Make It Rain", () => {
game.move.select(Moves.MAKE_IT_RAIN);
- await game.phaseInterceptor.to(StatChangePhase);
+ await game.phaseInterceptor.to(StatStageChangePhase);
expect(enemyPokemon.isFainted()).toBe(true);
- expect(playerPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);
}, TIMEOUT);
it("should reduce Sp. Atk. once after KOing two enemies", async () => {
@@ -71,22 +71,22 @@ describe("Moves - Make It Rain", () => {
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
- const playerPokemon = game.scene.getPlayerField();
+ const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyField();
game.move.select(Moves.MAKE_IT_RAIN);
game.move.select(Moves.SPLASH, 1);
- await game.phaseInterceptor.to(StatChangePhase);
+ await game.phaseInterceptor.to(StatStageChangePhase);
enemyPokemon.forEach(p => expect(p.isFainted()).toBe(true));
- expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);
}, TIMEOUT);
- it("should reduce Sp. Atk if it only hits the second target", async () => {
+ it("should lower SPATK stat stage by 1 if it only hits the second target", async () => {
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
- const playerPokemon = game.scene.getPlayerField();
+ const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.MAKE_IT_RAIN);
game.move.select(Moves.SPLASH, 1);
@@ -96,6 +96,6 @@ describe("Moves - Make It Rain", () => {
await game.phaseInterceptor.to(MoveEndPhase);
- expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);
}, TIMEOUT);
});
diff --git a/src/test/moves/mat_block.test.ts b/src/test/moves/mat_block.test.ts
index 29a97806242..4a95985eb92 100644
--- a/src/test/moves/mat_block.test.ts
+++ b/src/test/moves/mat_block.test.ts
@@ -1,13 +1,13 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { BerryPhase } from "#app/phases/berry-phase";
-import { CommandPhase } from "#app/phases/command-phase";
-import { TurnEndPhase } from "#app/phases/turn-end-phase";
-import { Abilities } from "#enums/abilities";
-import { Moves } from "#enums/moves";
-import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import GameManager from "../utils/gameManager";
+import { Species } from "#enums/species";
+import { Abilities } from "#enums/abilities";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { BerryPhase } from "#app/phases/berry-phase";
+import { CommandPhase } from "#app/phases/command-phase";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
const TIMEOUT = 20 * 1000;
@@ -76,7 +76,7 @@ describe("Moves - Mat Block", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(-2));
+ leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(-2));
}, TIMEOUT
);
diff --git a/src/test/moves/octolock.test.ts b/src/test/moves/octolock.test.ts
index 34dad13b0d9..c86906ea240 100644
--- a/src/test/moves/octolock.test.ts
+++ b/src/test/moves/octolock.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { TrappedTag } from "#app/data/battler-tags";
import { CommandPhase } from "#app/phases/command-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
@@ -12,110 +12,106 @@ import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Octolock", () => {
- describe("integration tests", () => {
- let phaserGame: Phaser.Game;
- let game: GameManager;
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
- beforeAll(() => {
- phaserGame = new Phaser.Game({
- type: Phaser.HEADLESS,
- });
- });
-
- afterEach(() => {
- game.phaseInterceptor.restoreOg();
- });
-
- beforeEach(() => {
- game = new GameManager(phaserGame);
-
- game.override.battleType("single");
-
- game.override.enemySpecies(Species.RATTATA);
- game.override.enemyMoveset(SPLASH_ONLY);
- game.override.enemyAbility(Abilities.BALL_FETCH);
-
- game.override.startingLevel(2000);
- game.override.moveset([Moves.OCTOLOCK, Moves.SPLASH]);
- game.override.ability(Abilities.BALL_FETCH);
- });
-
- it("Reduces DEf and SPDEF by 1 each turn", { timeout: 10000 }, async () => {
- await game.startBattle([Species.GRAPPLOCT]);
-
- const enemyPokemon = game.scene.getEnemyField();
-
- // use Octolock and advance to init phase of next turn to check for stat changes
- game.move.select(Moves.OCTOLOCK);
- await game.phaseInterceptor.to(TurnInitPhase);
-
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(-1);
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-1);
-
- // take a second turn to make sure stat changes occur again
- await game.phaseInterceptor.to(CommandPhase);
- game.move.select(Moves.SPLASH);
-
- await game.phaseInterceptor.to(TurnInitPhase);
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(-2);
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-2);
- });
-
- it("If target pokemon has Big Pecks, Octolock should only reduce spdef by 1", { timeout: 10000 }, async () => {
- game.override.enemyAbility(Abilities.BIG_PECKS);
- await game.startBattle([Species.GRAPPLOCT]);
-
- const enemyPokemon = game.scene.getEnemyField();
-
- // use Octolock and advance to init phase of next turn to check for stat changes
- game.move.select(Moves.OCTOLOCK);
- await game.phaseInterceptor.to(TurnInitPhase);
-
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-1);
- });
-
- it("If target pokemon has White Smoke, Octolock should not reduce any stats", { timeout: 10000 }, async () => {
- game.override.enemyAbility(Abilities.WHITE_SMOKE);
- await game.startBattle([Species.GRAPPLOCT]);
-
- const enemyPokemon = game.scene.getEnemyField();
-
- // use Octolock and advance to init phase of next turn to check for stat changes
- game.move.select(Moves.OCTOLOCK);
- await game.phaseInterceptor.to(TurnInitPhase);
-
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(0);
- });
-
- it("If target pokemon has Clear Body, Octolock should not reduce any stats", { timeout: 10000 }, async () => {
- game.override.enemyAbility(Abilities.CLEAR_BODY);
- await game.startBattle([Species.GRAPPLOCT]);
-
- const enemyPokemon = game.scene.getEnemyField();
-
- // use Octolock and advance to init phase of next turn to check for stat changes
- game.move.select(Moves.OCTOLOCK);
- await game.phaseInterceptor.to(TurnInitPhase);
-
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(0);
- });
-
- it("Traps the target pokemon", { timeout: 10000 }, async () => {
- await game.startBattle([Species.GRAPPLOCT]);
-
- const enemyPokemon = game.scene.getEnemyField();
-
- // before Octolock - enemy should not be trapped
- expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeUndefined();
-
- game.move.select(Moves.OCTOLOCK);
-
- // after Octolock - enemy should be trapped
- await game.phaseInterceptor.to(MoveEndPhase);
- expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeDefined();
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
});
});
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+
+ game.override.battleType("single")
+ .enemySpecies(Species.RATTATA)
+ .enemyMoveset(SPLASH_ONLY)
+ .enemyAbility(Abilities.BALL_FETCH)
+ .startingLevel(2000)
+ .moveset([ Moves.OCTOLOCK, Moves.SPLASH ])
+ .ability(Abilities.BALL_FETCH);
+ });
+
+ it("lowers DEF and SPDEF stat stages of the target Pokemon by 1 each turn", { timeout: 10000 }, async () => {
+ await game.classicMode.startBattle([ Species.GRAPPLOCT ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ // use Octolock and advance to init phase of next turn to check for stat changes
+ game.move.select(Moves.OCTOLOCK);
+ await game.phaseInterceptor.to(TurnInitPhase);
+
+ expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
+ expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
+
+ // take a second turn to make sure stat changes occur again
+ await game.phaseInterceptor.to(CommandPhase);
+ game.move.select(Moves.SPLASH);
+
+ await game.phaseInterceptor.to(TurnInitPhase);
+ expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2);
+ expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-2);
+ });
+
+ it("if target pokemon has BIG_PECKS, should only lower SPDEF stat stage by 1", { timeout: 10000 }, async () => {
+ game.override.enemyAbility(Abilities.BIG_PECKS);
+ await game.classicMode.startBattle([ Species.GRAPPLOCT ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ // use Octolock and advance to init phase of next turn to check for stat changes
+ game.move.select(Moves.OCTOLOCK);
+ await game.phaseInterceptor.to(TurnInitPhase);
+
+ expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
+ });
+
+ it("if target pokemon has WHITE_SMOKE, should not reduce any stat stages", { timeout: 10000 }, async () => {
+ game.override.enemyAbility(Abilities.WHITE_SMOKE);
+ await game.classicMode.startBattle([ Species.GRAPPLOCT ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ // use Octolock and advance to init phase of next turn to check for stat changes
+ game.move.select(Moves.OCTOLOCK);
+ await game.phaseInterceptor.to(TurnInitPhase);
+
+ expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
+ });
+
+ it("if target pokemon has CLEAR_BODY, should not reduce any stat stages", { timeout: 10000 }, async () => {
+ game.override.enemyAbility(Abilities.CLEAR_BODY);
+ await game.classicMode.startBattle([ Species.GRAPPLOCT ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ // use Octolock and advance to init phase of next turn to check for stat changes
+ game.move.select(Moves.OCTOLOCK);
+ await game.phaseInterceptor.to(TurnInitPhase);
+
+ expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
+ });
+
+ it("traps the target pokemon", { timeout: 10000 }, async () => {
+ await game.classicMode.startBattle([ Species.GRAPPLOCT ]);
+
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+
+ // before Octolock - enemy should not be trapped
+ expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined();
+
+ game.move.select(Moves.OCTOLOCK);
+
+ // after Octolock - enemy should be trapped
+ await game.phaseInterceptor.to(MoveEndPhase);
+ expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined();
+ });
});
diff --git a/src/test/moves/parting_shot.test.ts b/src/test/moves/parting_shot.test.ts
index 7c2ca3f334c..d9535ca6482 100644
--- a/src/test/moves/parting_shot.test.ts
+++ b/src/test/moves/parting_shot.test.ts
@@ -1,14 +1,14 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { BerryPhase } from "#app/phases/berry-phase";
-import { FaintPhase } from "#app/phases/faint-phase";
-import { MessagePhase } from "#app/phases/message-phase";
-import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, test } from "vitest";
import GameManager from "../utils/gameManager";
+import { Stat } from "#enums/stat";
+import { BerryPhase } from "#app/phases/berry-phase";
+import { FaintPhase } from "#app/phases/faint-phase";
+import { MessagePhase } from "#app/phases/message-phase";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { SPLASH_ONLY } from "../utils/testUtils";
const TIMEOUT = 20 * 1000;
@@ -51,9 +51,8 @@ describe("Moves - Parting Shot", () => {
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
- const battleStatsOpponent = enemyPokemon.summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW);
}, TIMEOUT
);
@@ -72,9 +71,8 @@ describe("Moves - Parting Shot", () => {
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
- const battleStatsOpponent = enemyPokemon.summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW);
}, TIMEOUT
);
@@ -108,16 +106,15 @@ describe("Moves - Parting Shot", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).toBeDefined();
- const battleStatsOpponent = enemyPokemon.summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-6);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(-6);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-6);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-6);
// now parting shot should fail
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-6);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(-6);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-6);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-6);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW);
}, TIMEOUT
);
@@ -137,9 +134,8 @@ describe("Moves - Parting Shot", () => {
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
- const battleStatsOpponent = enemyPokemon.summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW);
}, TIMEOUT
);
@@ -158,9 +154,8 @@ describe("Moves - Parting Shot", () => {
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
- const battleStatsOpponent = enemyPokemon.summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW);
}, TIMEOUT
);
@@ -176,9 +171,8 @@ describe("Moves - Parting Shot", () => {
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
- const battleStatsOpponent = enemyPokemon.summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(-1);
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW);
}, TIMEOUT
);
@@ -199,9 +193,9 @@ describe("Moves - Parting Shot", () => {
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
- const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
- expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0);
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+ expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH);
}, TIMEOUT
);
diff --git a/src/test/moves/power_split.test.ts b/src/test/moves/power_split.test.ts
new file mode 100644
index 00000000000..a532a90a54d
--- /dev/null
+++ b/src/test/moves/power_split.test.ts
@@ -0,0 +1,82 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#app/test/utils/gameManager";
+import { Species } from "#enums/species";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { Abilities } from "#enums/abilities";
+import { SPLASH_ONLY } from "../utils/testUtils";
+
+describe("Moves - Power Split", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemyAbility(Abilities.NONE)
+ .enemySpecies(Species.MEW)
+ .enemyLevel(200)
+ .moveset([ Moves.POWER_SPLIT ])
+ .ability(Abilities.NONE);
+ });
+
+ it("should average the user's ATK and SPATK stats with those of the target", async () => {
+ game.override.enemyMoveset(SPLASH_ONLY);
+ await game.startBattle([
+ Species.INDEEDEE
+ ]);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2);
+ const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2);
+
+ game.move.select(Moves.POWER_SPLIT);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStat(Stat.ATK, false)).toBe(avgAtk);
+ expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk);
+
+ expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ }, 20000);
+
+ it("should be idempotent", async () => {
+ game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT));
+ await game.startBattle([
+ Species.INDEEDEE
+ ]);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2);
+ const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2);
+
+ game.move.select(Moves.POWER_SPLIT);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ game.move.select(Moves.POWER_SPLIT);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStat(Stat.ATK, false)).toBe(avgAtk);
+ expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk);
+
+ expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ }, 20000);
+});
diff --git a/src/test/moves/power_swap.test.ts b/src/test/moves/power_swap.test.ts
new file mode 100644
index 00000000000..f1efeaa3af3
--- /dev/null
+++ b/src/test/moves/power_swap.test.ts
@@ -0,0 +1,62 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#app/test/utils/gameManager";
+import { Species } from "#enums/species";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { Abilities } from "#enums/abilities";
+import { MoveEndPhase } from "#app/phases/move-end-phase";
+
+describe("Moves - Power Swap", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemyAbility(Abilities.BALL_FETCH)
+ .enemyMoveset(new Array(4).fill(Moves.SHELL_SMASH))
+ .enemySpecies(Species.MEW)
+ .enemyLevel(200)
+ .moveset([ Moves.POWER_SWAP ])
+ .ability(Abilities.NONE);
+ });
+
+ it("should swap the user's ATK AND SPATK stat stages with the target's", async () => {
+ await game.startBattle([
+ Species.INDEEDEE
+ ]);
+
+ // Should start with no stat stages
+ const player = game.scene.getPlayerPokemon()!;
+ // After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF
+ const enemy = game.scene.getEnemyPokemon()!;
+ game.move.select(Moves.POWER_SWAP);
+
+ await game.phaseInterceptor.to(MoveEndPhase);
+
+ expect(player.getStatStage(Stat.ATK)).toBe(0);
+ expect(player.getStatStage(Stat.SPATK)).toBe(0);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(2);
+ expect(enemy.getStatStage(Stat.SPATK)).toBe(2);
+
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStatStage(Stat.ATK)).toBe(2);
+ expect(player.getStatStage(Stat.SPATK)).toBe(2);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(0);
+ expect(enemy.getStatStage(Stat.SPATK)).toBe(0);
+ }, 20000);
+});
diff --git a/src/test/moves/protect.test.ts b/src/test/moves/protect.test.ts
index 3fd51f4bc93..d792f586a37 100644
--- a/src/test/moves/protect.test.ts
+++ b/src/test/moves/protect.test.ts
@@ -1,13 +1,13 @@
-import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
-import { BattleStat } from "#app/data/battle-stat";
-import { allMoves } from "#app/data/move";
-import { BerryPhase } from "#app/phases/berry-phase";
-import { Abilities } from "#enums/abilities";
-import { Moves } from "#enums/moves";
-import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import GameManager from "../utils/gameManager";
+import { Species } from "#enums/species";
+import { Abilities } from "#enums/abilities";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { allMoves } from "#app/data/move";
+import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
+import { BerryPhase } from "#app/phases/berry-phase";
const TIMEOUT = 20 * 1000;
@@ -87,7 +87,7 @@ describe("Moves - Protect", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0);
+ expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
}, TIMEOUT
);
diff --git a/src/test/moves/quick_guard.test.ts b/src/test/moves/quick_guard.test.ts
index 26d9a74e9fd..25f98f8fa61 100644
--- a/src/test/moves/quick_guard.test.ts
+++ b/src/test/moves/quick_guard.test.ts
@@ -1,12 +1,12 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { BerryPhase } from "#app/phases/berry-phase";
-import { CommandPhase } from "#app/phases/command-phase";
-import { Abilities } from "#enums/abilities";
-import { Moves } from "#enums/moves";
-import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import GameManager from "../utils/gameManager";
+import { Species } from "#enums/species";
+import { Abilities } from "#enums/abilities";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { BerryPhase } from "#app/phases/berry-phase";
+import { CommandPhase } from "#app/phases/command-phase";
const TIMEOUT = 20 * 1000;
@@ -76,7 +76,7 @@ describe("Moves - Quick Guard", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0));
+ leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
}, TIMEOUT
);
diff --git a/src/test/moves/speed_swap.test.ts b/src/test/moves/speed_swap.test.ts
new file mode 100644
index 00000000000..131d506792b
--- /dev/null
+++ b/src/test/moves/speed_swap.test.ts
@@ -0,0 +1,54 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#app/test/utils/gameManager";
+import { Species } from "#enums/species";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { Abilities } from "#enums/abilities";
+import { SPLASH_ONLY } from "../utils/testUtils";
+
+describe("Moves - Speed Swap", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemyAbility(Abilities.NONE)
+ .enemyMoveset(SPLASH_ONLY)
+ .enemySpecies(Species.MEW)
+ .enemyLevel(200)
+ .moveset([ Moves.SPEED_SWAP ])
+ .ability(Abilities.NONE);
+ });
+
+ it("should swap the user's SPD and the target's SPD stats", async () => {
+ await game.startBattle([
+ Species.INDEEDEE
+ ]);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ const playerSpd = player.getStat(Stat.SPD, false);
+ const enemySpd = enemy.getStat(Stat.SPD, false);
+
+ game.move.select(Moves.SPEED_SWAP);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStat(Stat.SPD, false)).toBe(enemySpd);
+ expect(enemy.getStat(Stat.SPD, false)).toBe(playerSpd);
+ }, 20000);
+});
diff --git a/src/test/moves/spit_up.test.ts b/src/test/moves/spit_up.test.ts
index ab47e65d653..f88791efb74 100644
--- a/src/test/moves/spit_up.test.ts
+++ b/src/test/moves/spit_up.test.ts
@@ -1,22 +1,24 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { StockpilingTag } from "#app/data/battler-tags";
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { MoveResult, TurnMove } from "#app/field/pokemon";
-import { MovePhase } from "#app/phases/move-phase";
-import { TurnInitPhase } from "#app/phases/turn-init-phase";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
-import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { SPLASH_ONLY } from "#test/utils/testUtils";
+import { MovePhase } from "#app/phases/move-phase";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
describe("Moves - Spit Up", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
+ const spitUp = allMoves[Moves.SPIT_UP];
+
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
@@ -35,8 +37,10 @@ describe("Moves - Spit Up", () => {
game.override.enemyAbility(Abilities.NONE);
game.override.enemyLevel(2000);
- game.override.moveset([Moves.SPIT_UP, Moves.SPIT_UP, Moves.SPIT_UP, Moves.SPIT_UP]);
+ game.override.moveset(new Array(4).fill(spitUp.id));
game.override.ability(Abilities.NONE);
+
+ vi.spyOn(spitUp, "calculateBattlePower");
});
describe("consumes all stockpile stacks to deal damage (scaling with stacks)", () => {
@@ -53,13 +57,11 @@ describe("Moves - Spit Up", () => {
expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
- vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
-
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase);
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower);
+ expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();
+ expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower);
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
});
@@ -78,13 +80,11 @@ describe("Moves - Spit Up", () => {
expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
- vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
-
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase);
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower);
+ expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();
+ expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower);
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
});
@@ -104,13 +104,11 @@ describe("Moves - Spit Up", () => {
expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
- vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
-
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase);
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower);
+ expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();
+ expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower);
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
});
@@ -124,14 +122,12 @@ describe("Moves - Spit Up", () => {
const stockpilingTag = pokemon.getTag(StockpilingTag)!;
expect(stockpilingTag).toBeUndefined();
- vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
-
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.FAIL });
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).not.toHaveBeenCalled();
+ expect(spitUp.calculateBattlePower).not.toHaveBeenCalled();
});
describe("restores stat boosts granted by stacks", () => {
@@ -144,22 +140,20 @@ describe("Moves - Spit Up", () => {
const stockpilingTag = pokemon.getTag(StockpilingTag)!;
expect(stockpilingTag).toBeDefined();
- vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
-
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(MovePhase);
- expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
- expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1);
+ expect(pokemon.getStatStage(Stat.DEF)).toBe(1);
+ expect(pokemon.getStatStage(Stat.SPDEF)).toBe(1);
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS });
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
+ expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();
- expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0);
+ expect(pokemon.getStatStage(Stat.DEF)).toBe(0);
+ expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0);
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
});
@@ -175,26 +169,19 @@ describe("Moves - Spit Up", () => {
// for the sake of simplicity (and because other tests cover the setup), set boost amounts directly
stockpilingTag.statChangeCounts = {
- [BattleStat.DEF]: -1,
- [BattleStat.SPDEF]: 2,
+ [Stat.DEF]: -1,
+ [Stat.SPDEF]: 2,
};
- expect(stockpilingTag.statChangeCounts).toMatchObject({
- [BattleStat.DEF]: -1,
- [BattleStat.SPDEF]: 2,
- });
-
- vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
-
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS });
- expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
+ expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();
- expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
- expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(-2);
+ expect(pokemon.getStatStage(Stat.DEF)).toBe(1);
+ expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2);
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
});
diff --git a/src/test/moves/spotlight.test.ts b/src/test/moves/spotlight.test.ts
index e5f4719d1d3..e4dc8815f6d 100644
--- a/src/test/moves/spotlight.test.ts
+++ b/src/test/moves/spotlight.test.ts
@@ -1,5 +1,5 @@
import { BattlerIndex } from "#app/battle";
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@@ -65,7 +65,7 @@ describe("Moves - Spotlight", () => {
* Spotlight will target the slower enemy. In this situation without Spotlight being used,
* the faster enemy would normally end up with the Center of Attention tag.
*/
- enemyPokemon.sort((a, b) => b.getBattleStat(Stat.SPD) - a.getBattleStat(Stat.SPD));
+ enemyPokemon.sort((a, b) => b.getEffectiveStat(Stat.SPD) - a.getEffectiveStat(Stat.SPD));
const spotTarget = enemyPokemon[1].getBattlerIndex();
const attackTarget = enemyPokemon[0].getBattlerIndex();
diff --git a/src/test/moves/stockpile.test.ts b/src/test/moves/stockpile.test.ts
index b1941b9f9b3..d57768d0ffd 100644
--- a/src/test/moves/stockpile.test.ts
+++ b/src/test/moves/stockpile.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { StockpilingTag } from "#app/data/battler-tags";
import { MoveResult, TurnMove } from "#app/field/pokemon";
import { CommandPhase } from "#app/phases/command-phase";
@@ -38,7 +38,7 @@ describe("Moves - Stockpile", () => {
game.override.ability(Abilities.NONE);
});
- it("Gains a stockpile stack and increases DEF and SPDEF by 1 on each use, fails at max stacks (3)", { timeout: 10000 }, async () => {
+ it("gains a stockpile stack and raises user's DEF and SPDEF stat stages by 1 on each use, fails at max stacks (3)", { timeout: 10000 }, async () => {
await game.startBattle([Species.ABOMASNOW]);
const user = game.scene.getPlayerPokemon()!;
@@ -47,8 +47,8 @@ describe("Moves - Stockpile", () => {
// we just have to know that they're implemented as a BattlerTag.
expect(user.getTag(StockpilingTag)).toBeUndefined();
- expect(user.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(0);
+ expect(user.getStatStage(Stat.DEF)).toBe(0);
+ expect(user.getStatStage(Stat.SPDEF)).toBe(0);
// use Stockpile four times
for (let i = 0; i < 4; i++) {
@@ -60,18 +60,16 @@ describe("Moves - Stockpile", () => {
await game.phaseInterceptor.to(TurnInitPhase);
const stockpilingTag = user.getTag(StockpilingTag)!;
- const def = user.summonData.battleStats[BattleStat.DEF];
- const spdef = user.summonData.battleStats[BattleStat.SPDEF];
if (i < 3) { // first three uses should behave normally
- expect(def).toBe(i + 1);
- expect(spdef).toBe(i + 1);
+ expect(user.getStatStage(Stat.DEF)).toBe(i + 1);
+ expect(user.getStatStage(Stat.SPDEF)).toBe(i + 1);
expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(i + 1);
} else { // fourth should have failed
- expect(def).toBe(3);
- expect(spdef).toBe(3);
+ expect(user.getStatStage(Stat.DEF)).toBe(3);
+ expect(user.getStatStage(Stat.SPDEF)).toBe(3);
expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(3);
expect(user.getMoveHistory().at(-1)).toMatchObject({ result: MoveResult.FAIL, move: Moves.STOCKPILE });
@@ -79,17 +77,17 @@ describe("Moves - Stockpile", () => {
}
});
- it("Gains a stockpile stack even if DEF and SPDEF are at +6", { timeout: 10000 }, async () => {
+ it("gains a stockpile stack even if user's DEF and SPDEF stat stages are at +6", { timeout: 10000 }, async () => {
await game.startBattle([Species.ABOMASNOW]);
const user = game.scene.getPlayerPokemon()!;
- user.summonData.battleStats[BattleStat.DEF] = 6;
- user.summonData.battleStats[BattleStat.SPDEF] = 6;
+ user.setStatStage(Stat.DEF, 6);
+ user.setStatStage(Stat.SPDEF, 6);
expect(user.getTag(StockpilingTag)).toBeUndefined();
- expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6);
- expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6);
+ expect(user.getStatStage(Stat.DEF)).toBe(6);
+ expect(user.getStatStage(Stat.SPDEF)).toBe(6);
game.move.select(Moves.STOCKPILE);
await game.phaseInterceptor.to(TurnInitPhase);
@@ -97,8 +95,8 @@ describe("Moves - Stockpile", () => {
const stockpilingTag = user.getTag(StockpilingTag)!;
expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(1);
- expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6);
- expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6);
+ expect(user.getStatStage(Stat.DEF)).toBe(6);
+ expect(user.getStatStage(Stat.SPDEF)).toBe(6);
// do it again, just for good measure
await game.phaseInterceptor.to(CommandPhase);
@@ -109,8 +107,8 @@ describe("Moves - Stockpile", () => {
const stockpilingTagAgain = user.getTag(StockpilingTag)!;
expect(stockpilingTagAgain).toBeDefined();
expect(stockpilingTagAgain.stockpiledCount).toBe(2);
- expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6);
- expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6);
+ expect(user.getStatStage(Stat.DEF)).toBe(6);
+ expect(user.getStatStage(Stat.SPDEF)).toBe(6);
});
});
});
diff --git a/src/test/moves/swallow.test.ts b/src/test/moves/swallow.test.ts
index 202f25fee74..9cea7ae8dc9 100644
--- a/src/test/moves/swallow.test.ts
+++ b/src/test/moves/swallow.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { StockpilingTag } from "#app/data/battler-tags";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { MoveResult, TurnMove } from "#app/field/pokemon";
@@ -138,7 +138,7 @@ describe("Moves - Swallow", () => {
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.FAIL });
});
- describe("restores stat boosts granted by stacks", () => {
+ describe("restores stat stage boosts granted by stacks", () => {
it("decreases stats based on stored values (both boosts equal)", { timeout: 10000 }, async () => {
await game.startBattle([Species.ABOMASNOW]);
@@ -151,20 +151,20 @@ describe("Moves - Swallow", () => {
game.move.select(Moves.SWALLOW);
await game.phaseInterceptor.to(MovePhase);
- expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
- expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1);
+ expect(pokemon.getStatStage(Stat.DEF)).toBe(1);
+ expect(pokemon.getStatStage(Stat.SPDEF)).toBe(1);
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS });
- expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(0);
- expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0);
+ expect(pokemon.getStatStage(Stat.DEF)).toBe(0);
+ expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0);
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
});
- it("decreases stats based on stored values (different boosts)", { timeout: 10000 }, async () => {
+ it("lower stat stages based on stored values (different boosts)", { timeout: 10000 }, async () => {
await game.startBattle([Species.ABOMASNOW]);
const pokemon = game.scene.getPlayerPokemon()!;
@@ -175,22 +175,18 @@ describe("Moves - Swallow", () => {
// for the sake of simplicity (and because other tests cover the setup), set boost amounts directly
stockpilingTag.statChangeCounts = {
- [BattleStat.DEF]: -1,
- [BattleStat.SPDEF]: 2,
+ [Stat.DEF]: -1,
+ [Stat.SPDEF]: 2,
};
- expect(stockpilingTag.statChangeCounts).toMatchObject({
- [BattleStat.DEF]: -1,
- [BattleStat.SPDEF]: 2,
- });
-
game.move.select(Moves.SWALLOW);
+
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS });
- expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
- expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(-2);
+ expect(pokemon.getStatStage(Stat.DEF)).toBe(1);
+ expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2);
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
});
diff --git a/src/test/moves/tackle.test.ts b/src/test/moves/tackle.test.ts
index 5eca9e344c8..b25c7524a1a 100644
--- a/src/test/moves/tackle.test.ts
+++ b/src/test/moves/tackle.test.ts
@@ -1,4 +1,4 @@
-import { Stat } from "#app/data/pokemon-stat";
+import { Stat } from "#enums/stat";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
diff --git a/src/test/moves/tail_whip.test.ts b/src/test/moves/tail_whip.test.ts
index 0a999fe1920..04730a04f7a 100644
--- a/src/test/moves/tail_whip.test.ts
+++ b/src/test/moves/tail_whip.test.ts
@@ -1,12 +1,13 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
-import { TurnInitPhase } from "#app/phases/turn-init-phase";
+import { Stat } from "#enums/stat";
+import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
-import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import { SPLASH_ONLY } from "../utils/testUtils";
+import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
describe("Moves - Tail whip", () => {
@@ -31,23 +32,23 @@ describe("Moves - Tail whip", () => {
game.override.enemyAbility(Abilities.INSOMNIA);
game.override.ability(Abilities.INSOMNIA);
game.override.startingLevel(2000);
- game.override.moveset([moveToUse]);
- game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
+ game.override.moveset([ moveToUse ]);
+ game.override.enemyMoveset(SPLASH_ONLY);
});
- it("TAIL_WHIP", async () => {
+ it("should lower DEF stat stage by 1", async() => {
const moveToUse = Moves.TAIL_WHIP;
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
- let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.DEF]).toBe(0);
+ const enemyPokemon = game.scene.getEnemyPokemon()!;
+ expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
game.move.select(moveToUse);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase);
- battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
- expect(battleStatsOpponent[BattleStat.DEF]).toBe(-1);
+
+ expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
}, 20000);
});
diff --git a/src/test/moves/tailwind.test.ts b/src/test/moves/tailwind.test.ts
index 6b70122d08d..d158a9cce86 100644
--- a/src/test/moves/tailwind.test.ts
+++ b/src/test/moves/tailwind.test.ts
@@ -1,5 +1,5 @@
+import { Stat } from "#enums/stat";
import { ArenaTagSide } from "#app/data/arena-tag";
-import { Stat } from "#app/data/pokemon-stat";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
@@ -38,16 +38,16 @@ describe("Moves - Tailwind", () => {
const magikarpSpd = magikarp.getStat(Stat.SPD);
const meowthSpd = meowth.getStat(Stat.SPD);
- expect(magikarp.getBattleStat(Stat.SPD)).equal(magikarpSpd);
- expect(meowth.getBattleStat(Stat.SPD)).equal(meowthSpd);
+ expect(magikarp.getEffectiveStat(Stat.SPD)).equal(magikarpSpd);
+ expect(meowth.getEffectiveStat(Stat.SPD)).equal(meowthSpd);
game.move.select(Moves.TAILWIND);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(magikarp.getBattleStat(Stat.SPD)).toBe(magikarpSpd * 2);
- expect(meowth.getBattleStat(Stat.SPD)).toBe(meowthSpd * 2);
+ expect(magikarp.getEffectiveStat(Stat.SPD)).toBe(magikarpSpd * 2);
+ expect(meowth.getEffectiveStat(Stat.SPD)).toBe(meowthSpd * 2);
expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER)).toBeDefined();
});
@@ -86,8 +86,8 @@ describe("Moves - Tailwind", () => {
const enemySpd = enemy.getStat(Stat.SPD);
- expect(ally.getBattleStat(Stat.SPD)).equal(allySpd);
- expect(enemy.getBattleStat(Stat.SPD)).equal(enemySpd);
+ expect(ally.getEffectiveStat(Stat.SPD)).equal(allySpd);
+ expect(enemy.getEffectiveStat(Stat.SPD)).equal(enemySpd);
expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER)).toBeUndefined();
expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY)).toBeUndefined();
@@ -95,8 +95,8 @@ describe("Moves - Tailwind", () => {
await game.phaseInterceptor.to(TurnEndPhase);
- expect(ally.getBattleStat(Stat.SPD)).toBe(allySpd * 2);
- expect(enemy.getBattleStat(Stat.SPD)).equal(enemySpd);
+ expect(ally.getEffectiveStat(Stat.SPD)).toBe(allySpd * 2);
+ expect(enemy.getEffectiveStat(Stat.SPD)).equal(enemySpd);
expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER)).toBeDefined();
expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY)).toBeUndefined();
});
diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts
index bd7df8403d1..fa7a99adc14 100644
--- a/src/test/moves/tera_blast.test.ts
+++ b/src/test/moves/tera_blast.test.ts
@@ -1,9 +1,8 @@
import { BattlerIndex } from "#app/battle";
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { allMoves } from "#app/data/move";
import { Type } from "#app/data/type";
import { Abilities } from "#app/enums/abilities";
-import { Stat } from "#app/enums/stat";
import { HitResult } from "#app/field/pokemon";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@@ -112,7 +111,7 @@ describe("Moves - Tera Blast", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase");
- expect(playerPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(-1);
- expect(playerPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, 20000);
});
diff --git a/src/test/moves/tidy_up.test.ts b/src/test/moves/tidy_up.test.ts
index 1ef7933c114..5204b06106b 100644
--- a/src/test/moves/tidy_up.test.ts
+++ b/src/test/moves/tidy_up.test.ts
@@ -1,4 +1,4 @@
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
@@ -60,7 +60,6 @@ describe("Moves - Tidy Up", () => {
game.move.select(Moves.TIDY_UP);
await game.phaseInterceptor.to(MoveEndPhase);
expect(game.scene.arena.getTag(ArenaTagType.STEALTH_ROCK)).toBeUndefined();
-
}, 20000);
it("toxic spikes are cleared", async () => {
@@ -73,7 +72,6 @@ describe("Moves - Tidy Up", () => {
game.move.select(Moves.TIDY_UP);
await game.phaseInterceptor.to(MoveEndPhase);
expect(game.scene.arena.getTag(ArenaTagType.TOXIC_SPIKES)).toBeUndefined();
-
}, 20000);
it("sticky webs are cleared", async () => {
@@ -87,7 +85,6 @@ describe("Moves - Tidy Up", () => {
game.move.select(Moves.TIDY_UP);
await game.phaseInterceptor.to(MoveEndPhase);
expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined();
-
}, 20000);
it.skip("substitutes are cleared", async () => {
@@ -101,22 +98,20 @@ describe("Moves - Tidy Up", () => {
game.move.select(Moves.TIDY_UP);
await game.phaseInterceptor.to(MoveEndPhase);
// TODO: check for subs here once the move is implemented
-
}, 20000);
it("user's stats are raised with no traps set", async () => {
await game.startBattle();
- const player = game.scene.getPlayerPokemon()!.summonData.battleStats;
- expect(player[BattleStat.ATK]).toBe(0);
- expect(player[BattleStat.SPD]).toBe(0);
+ const playerPokemon = game.scene.getPlayerPokemon()!;
+
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
+ expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.TIDY_UP);
await game.phaseInterceptor.to(TurnEndPhase);
- expect(player[BattleStat.ATK]).toBe(+1);
- expect(player[BattleStat.SPD]).toBe(+1);
-
+ expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
+ expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1);
}, 20000);
-
});
diff --git a/src/test/moves/transform.test.ts b/src/test/moves/transform.test.ts
new file mode 100644
index 00000000000..45769447e4d
--- /dev/null
+++ b/src/test/moves/transform.test.ts
@@ -0,0 +1,101 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+import Phaser from "phaser";
+import GameManager from "#app/test/utils/gameManager";
+import { Species } from "#enums/species";
+import { TurnEndPhase } from "#app/phases/turn-end-phase";
+import { Moves } from "#enums/moves";
+import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
+import { Abilities } from "#enums/abilities";
+import { SPLASH_ONLY } from "../utils/testUtils";
+
+// TODO: Add more tests once Transform is fully implemented
+describe("Moves - Transform", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override
+ .battleType("single")
+ .enemySpecies(Species.MEW)
+ .enemyLevel(200)
+ .enemyAbility(Abilities.BEAST_BOOST)
+ .enemyPassiveAbility(Abilities.BALL_FETCH)
+ .enemyMoveset(SPLASH_ONLY)
+ .ability(Abilities.INTIMIDATE)
+ .moveset([ Moves.TRANSFORM ]);
+ });
+
+ it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
+ await game.startBattle([
+ Species.DITTO
+ ]);
+
+ game.move.select(Moves.TRANSFORM);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
+ expect(player.getAbility()).toBe(enemy.getAbility());
+ expect(player.getGender()).toBe(enemy.getGender());
+
+ expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP));
+ for (const s of EFFECTIVE_STATS) {
+ expect(player.getStat(s, false)).toBe(enemy.getStat(s, false));
+ }
+
+ for (const s of BATTLE_STATS) {
+ expect(player.getStatStage(s)).toBe(enemy.getStatStage(s));
+ }
+
+ const playerMoveset = player.getMoveset();
+ const enemyMoveset = player.getMoveset();
+
+ for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) {
+ // TODO: Checks for 5 PP should be done here when that gets addressed
+ expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
+ }
+
+ const playerTypes = player.getTypes();
+ const enemyTypes = enemy.getTypes();
+
+ for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
+ expect(playerTypes[i]).toBe(enemyTypes[i]);
+ }
+ }, 20000);
+
+ it("should copy in-battle overridden stats", async () => {
+ game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT));
+
+ await game.startBattle([
+ Species.DITTO
+ ]);
+
+ const player = game.scene.getPlayerPokemon()!;
+ const enemy = game.scene.getEnemyPokemon()!;
+
+ const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2);
+ const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2);
+
+ game.move.select(Moves.TRANSFORM);
+ await game.phaseInterceptor.to(TurnEndPhase);
+
+ expect(player.getStat(Stat.ATK, false)).toBe(avgAtk);
+ expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk);
+
+ expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
+ });
+});
diff --git a/src/test/moves/wide_guard.test.ts b/src/test/moves/wide_guard.test.ts
index 616972de01b..6feeff815b5 100644
--- a/src/test/moves/wide_guard.test.ts
+++ b/src/test/moves/wide_guard.test.ts
@@ -1,12 +1,12 @@
-import { BattleStat } from "#app/data/battle-stat";
-import { BerryPhase } from "#app/phases/berry-phase";
-import { CommandPhase } from "#app/phases/command-phase";
-import { Abilities } from "#enums/abilities";
-import { Moves } from "#enums/moves";
-import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import GameManager from "../utils/gameManager";
+import { Species } from "#enums/species";
+import { Abilities } from "#enums/abilities";
+import { Moves } from "#enums/moves";
+import { Stat } from "#enums/stat";
+import { BerryPhase } from "#app/phases/berry-phase";
+import { CommandPhase } from "#app/phases/command-phase";
const TIMEOUT = 20 * 1000;
@@ -75,7 +75,7 @@ describe("Moves - Wide Guard", () => {
await game.phaseInterceptor.to(BerryPhase, false);
- leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0));
+ leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
}, TIMEOUT
);
diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts
index d5eaee003db..cc5f9018325 100644
--- a/src/test/utils/helpers/overridesHelper.ts
+++ b/src/test/utils/helpers/overridesHelper.ts
@@ -281,6 +281,31 @@ export class OverridesHelper extends GameManagerHelper {
return this;
}
+ /**
+ * Override the items rolled at the end of a battle
+ * @param items the items to be rolled
+ * @returns this
+ */
+ itemRewards(items: ModifierOverride[]) {
+ vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items);
+ this.log("Item rewards set to:", items);
+ return this;
+ }
+
+ /**
+ * Override the enemy (Pokemon) to have the given amount of health segments
+ * @param healthSegments the number of segments to give
+ * default: 0, the health segments will be handled like in the game based on wave, level and species
+ * 1: the Pokemon will not be a boss
+ * 2+: the Pokemon will be a boss with the given number of health segments
+ * @returns this
+ */
+ enemyHealthSegments(healthSegments: number) {
+ vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
+ this.log("Enemy Pokemon health segments set to:", healthSegments);
+ return this;
+ }
+
private log(...params: any[]) {
console.log("Overrides:", ...params);
}
diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts
index de65405abff..389ae36635a 100644
--- a/src/test/utils/phaseInterceptor.ts
+++ b/src/test/utils/phaseInterceptor.ts
@@ -1,4 +1,5 @@
import { Phase } from "#app/phase";
+import ErrorInterceptor from "#app/test/utils/errorInterceptor";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { BerryPhase } from "#app/phases/berry-phase";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
@@ -17,7 +18,6 @@ import { MoveEndPhase } from "#app/phases/move-end-phase";
import { MovePhase } from "#app/phases/move-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
-import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
import { SelectGenderPhase } from "#app/phases/select-gender-phase";
@@ -26,7 +26,7 @@ import { SelectStarterPhase } from "#app/phases/select-starter-phase";
import { SelectTargetPhase } from "#app/phases/select-target-phase";
import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
-import { StatChangePhase } from "#app/phases/stat-change-phase";
+import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
@@ -37,7 +37,7 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import { UnavailablePhase } from "#app/phases/unavailable-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
-import ErrorInterceptor from "#app/test/utils/errorInterceptor";
+import { PartyHealPhase } from "#app/phases/party-heal-phase";
import UI, { Mode } from "#app/ui/ui";
export default class PhaseInterceptor {
@@ -86,7 +86,7 @@ export default class PhaseInterceptor {
[NewBattlePhase, this.startPhase],
[VictoryPhase, this.startPhase],
[MoveEndPhase, this.startPhase],
- [StatChangePhase, this.startPhase],
+ [StatStageChangePhase, this.startPhase],
[ShinySparklePhase, this.startPhase],
[SelectTargetPhase, this.startPhase],
[UnavailablePhase, this.startPhase],
diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts
index eb4316dc24b..605b8c538a9 100644
--- a/src/ui/achvs-ui-handler.ts
+++ b/src/ui/achvs-ui-handler.ts
@@ -74,7 +74,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.headerText = addTextObject(this.scene, 0, 0, "", TextStyle.SETTINGS_LABEL);
this.headerText.setOrigin(0, 0);
this.headerText.setPositionRelative(this.headerBg, 8, 4);
- this.headerActionButton = new Phaser.GameObjects.Sprite(this.scene, 0, 0, "keyboard", "SPACE.png");
+ this.headerActionButton = new Phaser.GameObjects.Sprite(this.scene, 0, 0, "keyboard", "ACTION.png");
this.headerActionButton.setOrigin(0, 0);
this.headerActionButton.setPositionRelative(this.headerBg, 236, 6);
this.headerActionText = addTextObject(this.scene, 0, 0, "", TextStyle.WINDOW, {fontSize:"60px"});
diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts
index 11b807e8ab7..05c634609f8 100644
--- a/src/ui/battle-info.ts
+++ b/src/ui/battle-info.ts
@@ -7,7 +7,7 @@ import { StatusEffect } from "../data/status-effect";
import BattleScene from "../battle-scene";
import { Type, getTypeRgb } from "../data/type";
import { getVariantTint } from "#app/data/variant";
-import { BattleStat } from "#app/data/battle-stat";
+import { Stat } from "#enums/stat";
import BattleFlyout from "./battle-flyout";
import { WindowVariant, addWindow } from "./ui-theme";
import i18next from "i18next";
@@ -30,7 +30,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
private lastLevelExp: integer;
private lastLevel: integer;
private lastLevelCapped: boolean;
- private lastBattleStats: string;
+ private lastStats: string;
private box: Phaser.GameObjects.Sprite;
private nameText: Phaser.GameObjects.Text;
@@ -68,9 +68,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
public flyoutMenu?: BattleFlyout;
- private battleStatOrder: BattleStat[];
- private battleStatOrderPlayer = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD];
- private battleStatOrderEnemy = [BattleStat.HP, BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD];
+ private statOrder: Stat[];
+ private readonly statOrderPlayer = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD ];
+ private readonly statOrderEnemy = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD ];
constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) {
super(scene, x, y);
@@ -229,9 +229,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
const startingX = this.player ? -this.statsBox.width + 8 : -this.statsBox.width + 5;
const paddingX = this.player ? 4 : 2;
const statOverflow = this.player ? 1 : 0;
- this.battleStatOrder = this.player ? this.battleStatOrderPlayer : this.battleStatOrderEnemy; // this tells us whether or not to use the player or enemy battle stat order
+ this.statOrder = this.player ? this.statOrderPlayer : this.statOrderEnemy; // this tells us whether or not to use the player or enemy battle stat order
- this.battleStatOrder.map((s, i) => {
+ this.statOrder.map((s, i) => {
// we do a check for i > statOverflow to see when the stat labels go onto the next column
// For enemies, we have HP (i=0) by itself then a new column, so we check for i > 0
// For players, we don't have HP, so we start with i = 0 and i = 1 for our first column, and so need to check for i > 1
@@ -239,25 +239,25 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
const baseY = -this.statsBox.height / 2 + 4; // this is the baseline for the y-axis
let statY: number; // this will be the y-axis placement for the labels
- if (this.battleStatOrder[i] === BattleStat.SPD || this.battleStatOrder[i] === BattleStat.HP) {
+ if (this.statOrder[i] === Stat.SPD || this.statOrder[i] === Stat.HP) {
statY = baseY + 5;
} else {
statY = baseY + (!!(i % 2) === this.player ? 10 : 0); // we compare i % 2 against this.player to tell us where to place the label; because this.battleStatOrder for enemies has HP, this.battleStatOrder[1]=ATK, but for players this.battleStatOrder[0]=ATK, so this comparing i % 2 to this.player fixes this issue for us
}
- const statLabel = this.scene.add.sprite(statX, statY, "pbinfo_stat", BattleStat[s]);
+ const statLabel = this.scene.add.sprite(statX, statY, "pbinfo_stat", Stat[s]);
statLabel.setName("icon_stat_label_" + i.toString());
statLabel.setOrigin(0, 0);
statLabels.push(statLabel);
this.statValuesContainer.add(statLabel);
- const statNumber = this.scene.add.sprite(statX + statLabel.width, statY, "pbinfo_stat_numbers", this.battleStatOrder[i] !== BattleStat.HP ? "3" : "empty");
+ const statNumber = this.scene.add.sprite(statX + statLabel.width, statY, "pbinfo_stat_numbers", this.statOrder[i] !== Stat.HP ? "3" : "empty");
statNumber.setName("icon_stat_number_" + i.toString());
statNumber.setOrigin(0, 0);
this.statNumbers.push(statNumber);
this.statValuesContainer.add(statNumber);
- if (this.battleStatOrder[i] === BattleStat.HP) {
+ if (this.statOrder[i] === Stat.HP) {
statLabel.setVisible(false);
statNumber.setVisible(false);
}
@@ -433,10 +433,10 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.statValuesContainer.setPosition(8, 7);
}
- const battleStats = this.battleStatOrder.map(() => 0);
+ const stats = this.statOrder.map(() => 0);
- this.lastBattleStats = battleStats.join("");
- this.updateBattleStats(battleStats);
+ this.lastStats = stats.join("");
+ this.updateStats(stats);
}
getTextureName(): string {
@@ -650,14 +650,12 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.lastLevel = pokemon.level;
}
- const battleStats = pokemon.summonData
- ? pokemon.summonData.battleStats
- : this.battleStatOrder.map(() => 0);
- const battleStatsStr = battleStats.join("");
+ const stats = pokemon.getStatStages();
+ const statsStr = stats.join("");
- if (this.lastBattleStats !== battleStatsStr) {
- this.updateBattleStats(battleStats);
- this.lastBattleStats = battleStatsStr;
+ if (this.lastStats !== statsStr) {
+ this.updateStats(stats);
+ this.lastStats = statsStr;
}
this.shinyIcon.setVisible(pokemon.isShiny());
@@ -769,10 +767,10 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
}
}
- updateBattleStats(battleStats: integer[]): void {
- this.battleStatOrder.map((s, i) => {
- if (s !== BattleStat.HP) {
- this.statNumbers[i].setFrame(battleStats[s].toString());
+ updateStats(stats: integer[]): void {
+ this.statOrder.map((s, i) => {
+ if (s !== Stat.HP) {
+ this.statNumbers[i].setFrame(stats[s - 1].toString());
}
});
}
diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts
index 86f8d9e01a8..4c2b798558a 100644
--- a/src/ui/battle-message-ui-handler.ts
+++ b/src/ui/battle-message-ui-handler.ts
@@ -1,13 +1,12 @@
import BattleScene from "../battle-scene";
import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "./text";
import { Mode } from "./ui";
-import * as Utils from "../utils";
import MessageUiHandler from "./message-ui-handler";
-import { getStatName, Stat } from "../data/pokemon-stat";
import { addWindow } from "./ui-theme";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
import {Button} from "#enums/buttons";
import i18next from "i18next";
+import { Stat, PERMANENT_STATS, getStatKey } from "#app/enums/stat";
export default class BattleMessageUiHandler extends MessageUiHandler {
private levelUpStatsContainer: Phaser.GameObjects.Container;
@@ -100,9 +99,8 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
const levelUpStatsLabelsContent = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 73, -94, "", TextStyle.WINDOW, { maxLines: 6 });
let levelUpStatsLabelText = "";
- const stats = Utils.getEnumValues(Stat);
- for (const s of stats) {
- levelUpStatsLabelText += `${getStatName(s)}\n`;
+ for (const s of PERMANENT_STATS) {
+ levelUpStatsLabelText += `${i18next.t(getStatKey(s))}\n`;
}
levelUpStatsLabelsContent.text = levelUpStatsLabelText;
levelUpStatsLabelsContent.x -= levelUpStatsLabelsContent.displayWidth;
@@ -176,8 +174,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
}
const newStats = (this.scene as BattleScene).getParty()[partyMemberIndex].stats;
let levelUpStatsValuesText = "";
- const stats = Utils.getEnumValues(Stat);
- for (const s of stats) {
+ for (const s of PERMANENT_STATS) {
levelUpStatsValuesText += `${showTotals ? newStats[s] : newStats[s] - prevStats[s]}\n`;
}
this.levelUpStatsValuesContent.text = levelUpStatsValuesText;
@@ -199,10 +196,9 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
return new Promise(resolve => {
this.scene.executeWithSeedOffset(() => {
let levelUpStatsValuesText = "";
- const stats = Utils.getEnumValues(Stat);
const shownStats = this.getTopIvs(ivs, shownIvsCount);
- for (const s of stats) {
- levelUpStatsValuesText += `${shownStats.indexOf(s) > -1 ? this.getIvDescriptor(ivs[s], s, pokemonId) : "???"}\n`;
+ for (const s of PERMANENT_STATS) {
+ levelUpStatsValuesText += `${shownStats.includes(s) ? this.getIvDescriptor(ivs[s], s, pokemonId) : "???"}\n`;
}
this.levelUpStatsValuesContent.text = levelUpStatsValuesText;
this.levelUpStatsIncrContent.setVisible(false);
@@ -217,26 +213,17 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
}
getTopIvs(ivs: integer[], shownIvsCount: integer): Stat[] {
- const stats = Utils.getEnumValues(Stat);
let shownStats: Stat[] = [];
if (shownIvsCount < 6) {
- const statsPool = stats.slice(0);
+ let highestIv = -1;
for (let i = 0; i < shownIvsCount; i++) {
- let shownStat: Stat | null = null;
- let highestIv = -1;
- statsPool.map(s => {
- if (ivs[s] > highestIv) {
- shownStat = s as Stat;
- highestIv = ivs[s];
- }
- });
- if (shownStat !== null && shownStat !== undefined) {
- shownStats.push(shownStat);
- statsPool.splice(statsPool.indexOf(shownStat), 1);
+ if (ivs[i] > highestIv) {
+ shownStats.push(PERMANENT_STATS[i]);
+ highestIv = ivs[i];
}
}
} else {
- shownStats = stats;
+ shownStats = PERMANENT_STATS.slice();
}
return shownStats;
}
diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts
index 6349d219827..adbb3089e5c 100644
--- a/src/ui/menu-ui-handler.ts
+++ b/src/ui/menu-ui-handler.ts
@@ -31,6 +31,7 @@ let wikiUrl = "https://wiki.pokerogue.net/start";
const discordUrl = "https://discord.gg/uWpTfdKG49";
const githubUrl = "https://github.com/pagefaultgames/pokerogue";
const redditUrl = "https://www.reddit.com/r/pokerogue";
+const donateUrl = "https://github.com/sponsors/patapancakes";
export default class MenuUiHandler extends MessageUiHandler {
private readonly textPadding = 8;
@@ -369,7 +370,16 @@ export default class MenuUiHandler extends MessageUiHandler {
return true;
},
keepOpen: true
- }];
+ },
+ {
+ label: i18next.t("menuUiHandler:donate"),
+ handler: () => {
+ window.open(donateUrl, "_blank")?.focus();
+ return true;
+ },
+ keepOpen: true
+ }
+ ];
if (!bypassLogin && loggedInUser?.hasAdminRole) {
communityOptions.push({
label: "Admin",
diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts
index a61037548e6..176a7098347 100644
--- a/src/ui/party-ui-handler.ts
+++ b/src/ui/party-ui-handler.ts
@@ -1321,16 +1321,13 @@ class PartySlot extends Phaser.GameObjects.Container {
this.slotHpOverlay.setVisible(false);
this.slotHpText.setVisible(false);
let slotTmText: string;
- switch (true) {
- case (this.pokemon.compatibleTms.indexOf(tmMoveId) === -1):
- slotTmText = i18next.t("partyUiHandler:notAble");
- break;
- case (this.pokemon.getMoveset().filter(m => m?.moveId === tmMoveId).length > 0):
+
+ if (this.pokemon.getMoveset().filter(m => m?.moveId === tmMoveId).length > 0) {
slotTmText = i18next.t("partyUiHandler:learned");
- break;
- default:
+ } else if (this.pokemon.compatibleTms.indexOf(tmMoveId) === -1) {
+ slotTmText = i18next.t("partyUiHandler:notAble");
+ } else {
slotTmText = i18next.t("partyUiHandler:able");
- break;
}
this.slotDescriptionLabel.setText(slotTmText);
diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts
index 25210277edc..7a183a11d29 100644
--- a/src/ui/run-info-ui-handler.ts
+++ b/src/ui/run-info-ui-handler.ts
@@ -13,8 +13,9 @@ import { BattleType } from "../battle";
import { TrainerVariant } from "../field/trainer";
import { Challenges } from "#enums/challenges";
import { getLuckString, getLuckTextTint } from "../modifier/modifier-type";
-import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle.js";
+import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle";
import { Type, getTypeRgb } from "../data/type";
+import { TypeColor, TypeShadow } from "#app/enums/color";
import { getNatureStatMultiplier, getNatureName } from "../data/nature";
import { getVariantTint } from "#app/data/variant";
import { PokemonHeldItemModifier, TerastallizeModifier } from "../modifier/modifier";
@@ -373,15 +374,16 @@ export default class RunInfoUiHandler extends UiHandler {
break;
case GameModes.CHALLENGE:
modeText.appendText(`${i18next.t("gameMode:challenge")}`, false);
- modeText.appendText(`\t\t${i18next.t("runHistory:challengeRules")}: `);
+ modeText.appendText(`${i18next.t("runHistory:challengeRules")}: `);
+ modeText.setWrapMode(1); // wrap by word
+ modeText.setWrapWidth(500);
const rules: string[] = this.challengeParser();
if (rules) {
for (let i = 0; i < rules.length; i++) {
- const newline = i > 0 && i%2 === 0;
if (i > 0) {
- modeText.appendText(" + ", newline);
+ modeText.appendText(" + ", false);
}
- modeText.appendText(rules[i], newline);
+ modeText.appendText(rules[i], false);
}
}
break;
@@ -470,14 +472,18 @@ export default class RunInfoUiHandler extends UiHandler {
rules.push(i18next.t(`runHistory:challengeMonoGen${this.runInfo.challenges[i].value}`));
break;
case Challenges.SINGLE_TYPE:
- rules.push(i18next.t(`pokemonInfo:Type.${Type[this.runInfo.challenges[i].value-1]}` as const));
+ const typeRule = Type[this.runInfo.challenges[i].value-1];
+ const typeTextColor = `[color=${TypeColor[typeRule]}]`;
+ const typeShadowColor = `[shadow=${TypeShadow[typeRule]}]`;
+ const typeText = typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)!+"[/color]"+"[/shadow]";
+ rules.push(typeText);
break;
case Challenges.FRESH_START:
rules.push(i18next.t("challenges:freshStart.name"));
break;
case Challenges.INVERSE_BATTLE:
//
- rules.push(i18next.t("challenges:inverseBattle.shortName").split("").reverse().join(""));
+ rules.push(i18next.t("challenges:inverseBattle.shortName"));
break;
}
}
@@ -628,7 +634,7 @@ export default class RunInfoUiHandler extends UiHandler {
// Pokemon Held Items - not displayed by default
// Endless/Endless Spliced have a different scale because Pokemon tend to accumulate more items in these runs.
const heldItemsScale = (this.runInfo.gameMode === GameModes.SPLICED_ENDLESS || this.runInfo.gameMode === GameModes.ENDLESS) ? 0.25 : 0.5;
- const heldItemsContainer = this.scene.add.container(-82, 6);
+ const heldItemsContainer = this.scene.add.container(-82, 2);
const heldItemsList : PokemonHeldItemModifier[] = [];
if (this.runInfo.modifiers.length) {
for (const m of this.runInfo.modifiers) {
@@ -648,6 +654,9 @@ export default class RunInfoUiHandler extends UiHandler {
break;
}
const itemIcon = item?.getIcon(this.scene, true);
+ if (item?.stackCount < item?.getMaxHeldItemCount(pokemon) && itemIcon.list[1] instanceof Phaser.GameObjects.BitmapText) {
+ itemIcon.list[1].clearTint();
+ }
itemIcon.setScale(heldItemsScale);
itemIcon.setPosition((index%19) * 10, row * 10);
heldItemsContainer.add(itemIcon);
diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts
index ff76a467d25..4906503c803 100644
--- a/src/ui/starter-select-ui-handler.ts
+++ b/src/ui/starter-select-ui-handler.ts
@@ -32,19 +32,19 @@ import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
import {Passive as PassiveAttr} from "#enums/passive";
import * as Challenge from "../data/challenge";
import MoveInfoOverlay from "./move-info-overlay";
-import { getEggTierForSpecies } from "#app/data/egg.js";
+import { getEggTierForSpecies } from "#app/data/egg";
import { Device } from "#enums/devices";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import {Button} from "#enums/buttons";
-import { EggSourceType } from "#app/enums/egg-source-types.js";
+import { EggSourceType } from "#app/enums/egg-source-types";
import AwaitableUiHandler from "./awaitable-ui-handler";
import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "./dropdown";
import { StarterContainer } from "./starter-container";
import { DropDownColumn, FilterBar } from "./filter-bar";
import { ScrollBar } from "./scroll-bar";
-import { SelectChallengePhase } from "#app/phases/select-challenge-phase.js";
-import { TitlePhase } from "#app/phases/title-phase.js";
+import { SelectChallengePhase } from "#app/phases/select-challenge-phase";
+import { TitlePhase } from "#app/phases/title-phase";
import { Abilities } from "#app/enums/abilities";
export type StarterSelectCallback = (starters: Starter[]) => void;
@@ -263,6 +263,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private pokemonHatchedIcon : Phaser.GameObjects.Sprite;
private pokemonHatchedCountText: Phaser.GameObjects.Text;
private pokemonShinyIcon: Phaser.GameObjects.Sprite;
+ private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite;
+ private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite;
private instructionsContainer: Phaser.GameObjects.Container;
private filterInstructionsContainer: Phaser.GameObjects.Container;
@@ -574,6 +576,18 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonPassiveText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonPassiveText);
+ this.pokemonPassiveDisabledIcon = this.scene.add.sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_stop");
+ this.pokemonPassiveDisabledIcon.setOrigin(0, 0.5);
+ this.pokemonPassiveDisabledIcon.setScale(0.35);
+ this.pokemonPassiveDisabledIcon.setVisible(false);
+ this.starterSelectContainer.add(this.pokemonPassiveDisabledIcon);
+
+ this.pokemonPassiveLockedIcon = this.scene.add.sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_lock");
+ this.pokemonPassiveLockedIcon.setOrigin(0, 0.5);
+ this.pokemonPassiveLockedIcon.setScale(0.42, 0.38);
+ this.pokemonPassiveLockedIcon.setVisible(false);
+ this.starterSelectContainer.add(this.pokemonPassiveLockedIcon);
+
this.pokemonNatureLabelText = addTextObject(this.scene, 6, 145 + starterInfoYOffset, i18next.t("starterSelectUiHandler:nature"), TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
this.pokemonNatureLabelText.setOrigin(0, 0);
this.pokemonNatureLabelText.setVisible(false);
@@ -734,7 +748,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonShinyIcon = this.scene.add.sprite(14, 76, "shiny_icons");
this.pokemonShinyIcon.setOrigin(0.15, 0.2);
this.pokemonShinyIcon.setScale(1);
- this.pokemonCaughtHatchedContainer.add ((this.pokemonShinyIcon));
+ this.pokemonCaughtHatchedContainer.add(this.pokemonShinyIcon);
this.pokemonHatchedCountText = addTextObject(this.scene, 24, 19, "0", TextStyle.SUMMARY_ALT);
this.pokemonHatchedCountText.setOrigin(0, 0);
@@ -2935,6 +2949,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
this.pokemonSprite.setVisible(false);
+ this.pokemonPassiveLabelText.setVisible(false);
+ this.pokemonPassiveText.setVisible(false);
+ this.pokemonPassiveDisabledIcon.setVisible(false);
+ this.pokemonPassiveLockedIcon.setVisible(false);
if (this.assetLoadCancelled) {
this.assetLoadCancelled.value = true;
@@ -3066,9 +3084,34 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonAbilityText.setShadowColor(this.getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD, true));
const passiveAttr = this.scene.gameData.starterData[species.speciesId].passiveAttr;
- this.pokemonPassiveText.setText(passiveAttr & PassiveAttr.UNLOCKED ? passiveAttr & PassiveAttr.ENABLED ? allAbilities[starterPassiveAbilities[this.lastSpecies.speciesId]].name : i18next.t("starterSelectUiHandler:disabled") : i18next.t("starterSelectUiHandler:locked"));
- this.pokemonPassiveText.setColor(this.getTextColor(passiveAttr === (PassiveAttr.UNLOCKED | PassiveAttr.ENABLED) ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GRAY));
- this.pokemonPassiveText.setShadowColor(this.getTextColor(passiveAttr === (PassiveAttr.UNLOCKED | PassiveAttr.ENABLED) ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GRAY, true));
+ const passiveAbility = allAbilities[starterPassiveAbilities[this.lastSpecies.speciesId]];
+
+ if (passiveAbility) {
+ const isUnlocked = !!(passiveAttr & PassiveAttr.UNLOCKED);
+ const isEnabled = !!(passiveAttr & PassiveAttr.ENABLED);
+
+ const textStyle = isUnlocked && isEnabled ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GRAY;
+ const textAlpha = isUnlocked && isEnabled ? 1 : 0.5;
+
+ this.pokemonPassiveLabelText.setVisible(true);
+ this.pokemonPassiveLabelText.setColor(this.getTextColor(TextStyle.SUMMARY_ALT));
+ this.pokemonPassiveLabelText.setShadowColor(this.getTextColor(TextStyle.SUMMARY_ALT, true));
+ this.pokemonPassiveText.setVisible(true);
+ this.pokemonPassiveText.setText(passiveAbility.name);
+ this.pokemonPassiveText.setColor(this.getTextColor(textStyle));
+ this.pokemonPassiveText.setAlpha(textAlpha);
+ this.pokemonPassiveText.setShadowColor(this.getTextColor(textStyle, true));
+
+ const iconPosition = {
+ x: this.pokemonPassiveText.x + this.pokemonPassiveText.displayWidth + 1,
+ y: this.pokemonPassiveText.y + this.pokemonPassiveText.displayHeight / 2
+ };
+ this.pokemonPassiveDisabledIcon.setVisible(isUnlocked && !isEnabled);
+ this.pokemonPassiveDisabledIcon.setPosition(iconPosition.x, iconPosition.y);
+ this.pokemonPassiveLockedIcon.setVisible(!isUnlocked);
+ this.pokemonPassiveLockedIcon.setPosition(iconPosition.x, iconPosition.y);
+
+ }
this.pokemonNatureText.setText(getNatureName(natureIndex as unknown as Nature, true, true, false, this.scene.uiTheme));
diff --git a/src/ui/stats-container.ts b/src/ui/stats-container.ts
index 2bd7099a2c5..c6e0ea3a71c 100644
--- a/src/ui/stats-container.ts
+++ b/src/ui/stats-container.ts
@@ -1,7 +1,8 @@
import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
import BattleScene from "../battle-scene";
-import { Stat, getStatName } from "../data/pokemon-stat";
import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./text";
+import { PERMANENT_STATS, getStatKey } from "#app/enums/stat";
+import i18next from "i18next";
const ivChartSize = 24;
const ivChartStatCoordMultipliers = [[0, -1], [0.825, -0.5], [0.825, 0.5], [-0.825, -0.5], [-0.825, 0.5], [0, 1]];
@@ -53,16 +54,16 @@ export class StatsContainer extends Phaser.GameObjects.Container {
this.ivStatValueTexts = [];
- new Array(6).fill(null).map((_, i: integer) => {
- const statLabel = addTextObject(this.scene, ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[i][0] * 1.325, ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[i][1] * 1.325 - 4 + ivLabelOffset[i], getStatName(i as Stat), TextStyle.TOOLTIP_CONTENT);
+ for (const s of PERMANENT_STATS) {
+ const statLabel = addTextObject(this.scene, ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[s][0] * 1.325, ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[s][1] * 1.325 - 4 + ivLabelOffset[s], i18next.t(getStatKey(s)), TextStyle.TOOLTIP_CONTENT);
statLabel.setOrigin(0.5);
- this.ivStatValueTexts[i] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT);
- this.ivStatValueTexts[i].setOrigin(0.5);
+ this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT);
+ this.ivStatValueTexts[s].setOrigin(0.5);
this.add(statLabel);
- this.add(this.ivStatValueTexts[i]);
- });
+ this.add(this.ivStatValueTexts[s]);
+ }
}
updateIvs(ivs: integer[], originalIvs?: integer[]): void {
diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts
index ea7b798f2bf..8ae72f08edd 100644
--- a/src/ui/summary-ui-handler.ts
+++ b/src/ui/summary-ui-handler.ts
@@ -11,7 +11,6 @@ import Move, { MoveCategory } from "../data/move";
import { getPokeballAtlasKey } from "../data/pokeball";
import { getGenderColor, getGenderSymbol } from "../data/gender";
import { getLevelRelExp, getLevelTotalExp } from "../data/exp";
-import { Stat, getStatName } from "../data/pokemon-stat";
import { PokemonHeldItemModifier } from "../modifier/modifier";
import { StatusEffect } from "../data/status-effect";
import { getBiomeName } from "../data/biomes";
@@ -19,10 +18,11 @@ import { Nature, getNatureName, getNatureStatMultiplier } from "../data/nature";
import { loggedInUser } from "../account";
import { Variant, getVariantTint } from "#app/data/variant";
import {Button} from "#enums/buttons";
-import { Ability } from "../data/ability.js";
+import { Ability } from "../data/ability";
import i18next from "i18next";
import {modifierSortFunc} from "../modifier/modifier";
import { PlayerGender } from "#enums/player-gender";
+import { Stat, PERMANENT_STATS, getStatKey } from "#app/enums/stat";
enum Page {
PROFILE,
@@ -836,10 +836,8 @@ export default class SummaryUiHandler extends UiHandler {
const statsContainer = this.scene.add.container(0, -pageBg.height);
pageContainer.add(statsContainer);
- const stats = Utils.getEnumValues(Stat) as Stat[];
-
- stats.forEach((stat, s) => {
- const statName = getStatName(stat);
+ PERMANENT_STATS.forEach((stat, s) => {
+ const statName = i18next.t(getStatKey(stat));
const rowIndex = s % 3;
const colIndex = Math.floor(s / 3);
@@ -850,7 +848,7 @@ export default class SummaryUiHandler extends UiHandler {
statsContainer.add(statLabel);
const statValueText = stat !== Stat.HP
- ? Utils.formatStat(this.pokemon?.stats[s]!) // TODO: is this bang correct?
+ ? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct?
: `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct?
const statValue = addTextObject(this.scene, 120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT);