Merge branch 'beta' into danish-workspace

This commit is contained in:
Lugiad 2024-11-04 02:14:37 +01:00 committed by GitHub
commit 871472af71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 3945 additions and 1001 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.1.0", "version": "1.1.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.1.0", "version": "1.1.6",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.2.7", "@material/material-color-utilities": "^0.2.7",

View File

@ -1,7 +1,7 @@
{ {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"private": true, "private": true,
"version": "1.1.0", "version": "1.1.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658-ash.png",
"format": "RGBA8888",
"size": {
"w": 79,
"h": 79
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 79, "h": 74 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 74 },
"w": 79, "sourceSize": { "w": 79, "h": 74 },
"h": 74 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 79,
"h": 74
},
"frame": {
"x": 0,
"y": 0,
"w": 79,
"h": 74
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:936f62fa49ba4d6e402bb2e2eaf2afd0:ed00ba047a44b4bf1309bc147dd000e3:bfbf521a5c7bd80bcd95a96d9789c0dd$" "format": "I8",
"size": { "w": 79, "h": 74 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658.png",
"format": "RGBA8888",
"size": {
"w": 75,
"h": 75
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 85, "h": 67 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 85, "h": 67 },
"w": 75, "sourceSize": { "w": 85, "h": 67 },
"h": 65 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 65
},
"frame": {
"x": 0,
"y": 0,
"w": 75,
"h": 65
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:e0b10df331bd4ce6760edab61dee144b:061561c45beff89a92bf0158d065204f:5affcab976148657d36bf4ff3410f92d$" "format": "I8",
"size": { "w": 85, "h": 67 },
"scale": "1"
} }
} }

View File

@ -5,8 +5,7 @@
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 63 }, "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 63 },
"sourceSize": { "w": 64, "h": 63 }, "sourceSize": { "w": 64, "h": 63 }
"duration": 100
} }
], ],
"meta": { "meta": {

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658-ash.png",
"format": "RGBA8888",
"size": {
"w": 73,
"h": 73
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 73, "h": 73 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 73, "h": 73 },
"w": 73, "sourceSize": { "w": 73, "h": 73 },
"h": 69 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 73,
"h": 69
},
"frame": {
"x": 0,
"y": 0,
"w": 73,
"h": 69
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:4f38801bb3afeda5faff04bdcf6a666f:0c78ce2715e7510bf55da0a92b42661c:bfbf521a5c7bd80bcd95a96d9789c0dd$" "format": "I8",
"size": { "w": 73, "h": 73 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658.png",
"format": "RGBA8888",
"size": {
"w": 77,
"h": 77
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 77, "h": 77 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 },
"w": 77, "sourceSize": { "w": 77, "h": 77 },
"h": 65 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 77,
"h": 65
},
"frame": {
"x": 0,
"y": 0,
"w": 77,
"h": 65
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:acdb9925f3f23b947504eec7cc28c92d:1a13d9d418f6c107bb9e5d621d9154bb:5affcab976148657d36bf4ff3410f92d$" "format": "I8",
"size": { "w": 77, "h": 77 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "688.png",
"format": "RGBA8888",
"size": {
"w": 52,
"h": 52
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 51, "h": 65 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 51, "h": 65 },
"w": 41, "sourceSize": { "w": 51, "h": 65 }
"h": 52
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 41,
"h": 52
},
"frame": {
"x": 0,
"y": 0,
"w": 41,
"h": 52
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-dev",
"smartupdate": "$TexturePacker:SmartUpdate:ea462f2b1b46327e3b8fcb7ec5e44f08:2d2598cc03dec73182dbea237ad83b34:176060351d0044923af938ba7932a6ef$" "image": "688.png",
"format": "I8",
"size": { "w": 51, "h": 65 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658-ash.png",
"format": "RGBA8888",
"size": {
"w": 73,
"h": 73
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 73, "h": 73 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 73, "h": 73 },
"w": 73, "sourceSize": { "w": 73, "h": 73 },
"h": 69 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 73,
"h": 69
},
"frame": {
"x": 0,
"y": 0,
"w": 73,
"h": 69
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:d474b821316a87dfe09b397bdc2db5ef:497de0c2ec59ceba163e870b3226c76c:bfbf521a5c7bd80bcd95a96d9789c0dd$" "format": "I8",
"size": { "w": 73, "h": 73 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658.png",
"format": "RGBA8888",
"size": {
"w": 77,
"h": 77
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 77, "h": 77 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 },
"w": 77, "sourceSize": { "w": 77, "h": 77 },
"h": 65 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 77,
"h": 65
},
"frame": {
"x": 0,
"y": 0,
"w": 77,
"h": 65
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:5891f87a78022cde3402e7d9714cc7bf:756360084290e39c139e3fef91c81759:5affcab976148657d36bf4ff3410f92d$" "format": "I8",
"size": { "w": 77, "h": 77 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "688.png",
"format": "RGBA8888",
"size": {
"w": 52,
"h": 52
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 51, "h": 65 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 51, "h": 65 },
"w": 41, "sourceSize": { "w": 51, "h": 65 }
"h": 52
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 41,
"h": 52
},
"frame": {
"x": 0,
"y": 0,
"w": 41,
"h": 52
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-dev",
"smartupdate": "$TexturePacker:SmartUpdate:0261b6c9242bba728fcfbfc515875b27:de0d9ddceed9311b33ae50ba86e969d1:176060351d0044923af938ba7932a6ef$" "image": "688.png",
"format": "I8",
"size": { "w": 51, "h": 65 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658-ash.png",
"format": "RGBA8888",
"size": {
"w": 79,
"h": 79
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 79, "h": 74 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 74 },
"w": 79, "sourceSize": { "w": 79, "h": 74 },
"h": 74 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 79,
"h": 74
},
"frame": {
"x": 0,
"y": 0,
"w": 79,
"h": 74
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:3dd081ba5490f090a73de8423aac2f6b:f088fafaea755476f2abf488e7340cab:bfbf521a5c7bd80bcd95a96d9789c0dd$" "format": "I8",
"size": { "w": 79, "h": 74 },
"scale": "1"
} }
} }

View File

@ -1,41 +1,19 @@
{ { "frames": [
"textures": [
{
"image": "658.png",
"format": "RGBA8888",
"size": {
"w": 75,
"h": 75
},
"scale": 1,
"frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 85, "h": 67 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"sourceSize": { "spriteSourceSize": { "x": 0, "y": 0, "w": 85, "h": 67 },
"w": 75, "sourceSize": { "w": 85, "h": 67 },
"h": 65 "duration": 100
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 65
},
"frame": {
"x": 0,
"y": 0,
"w": 75,
"h": 65
}
}
]
} }
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.aseprite.org/",
"version": "3.0", "version": "1.3.7-x64",
"smartupdate": "$TexturePacker:SmartUpdate:be07c062265a19e890f1e2d2d1b5527d:ad4583a5a0498c496e9a93574c55ee03:5affcab976148657d36bf4ff3410f92d$" "format": "I8",
"size": { "w": 85, "h": 67 },
"scale": "1"
} }
} }

View File

@ -5,8 +5,7 @@
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 63 }, "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 63 },
"sourceSize": { "w": 64, "h": 63 }, "sourceSize": { "w": 64, "h": 63 }
"duration": 100
} }
], ],
"meta": { "meta": {

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "statuses_es.png", "image": "statuses_es-ES.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 22, "w": 22,

View File

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 441 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "types_es.png", "image": "types_es-ES.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 32, "w": 32,

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -1 +1 @@
Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef

View File

@ -323,6 +323,7 @@ export default class BattleScene extends SceneBase {
this.conditionalQueue = []; this.conditionalQueue = [];
this.phaseQueuePrependSpliceIndex = -1; this.phaseQueuePrependSpliceIndex = -1;
this.nextCommandPhaseQueue = []; this.nextCommandPhaseQueue = [];
this.eventManager = new TimedEventManager();
this.updateGameInfo(); this.updateGameInfo();
} }
@ -378,7 +379,6 @@ export default class BattleScene extends SceneBase {
this.fieldSpritePipeline = new FieldSpritePipeline(this.game); this.fieldSpritePipeline = new FieldSpritePipeline(this.game);
(this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline); (this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline);
this.eventManager = new TimedEventManager();
this.launchBattle(); this.launchBattle();
} }

View File

@ -1,4 +1,4 @@
import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { Type } from "./type"; import { Type } from "./type";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -9,7 +9,7 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g
import { Gender } from "./gender"; 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, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
import i18next from "i18next"; import i18next from "i18next";
@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales";
import { Command } from "../ui/command-ui-handler"; import { Command } from "../ui/command-ui-handler";
import { BerryModifierType } from "#app/modifier/modifier-type"; import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "./pokeball"; import { getPokeballName } from "./pokeball";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex, BattleType } from "#app/battle";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
@ -29,6 +29,12 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { SwitchType } from "#app/enums/switch-type";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
export class Ability implements Localizable { export class Ability implements Localizable {
public id: Abilities; public id: Abilities;
@ -330,6 +336,30 @@ export class ReceivedMoveDamageMultiplierAbAttr extends PreDefendAbAttr {
} }
} }
/**
* Reduces the damage dealt to an allied Pokemon. Used by Friend Guard.
* @see {@linkcode applyPreDefend}
*/
export class AlliedFieldDamageReductionAbAttr extends PreDefendAbAttr {
private damageMultiplier: number;
constructor(damageMultiplier: number) {
super();
this.damageMultiplier = damageMultiplier;
}
/**
* Handles the damage reduction
* @param args
* - `[0]` {@linkcode Utils.NumberHolder} - The damage being dealt
*/
override applyPreDefend(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _attacker: Pokemon, _move: Move, _cancelled: Utils.BooleanHolder, args: any[]): boolean {
const damage = args[0] as Utils.NumberHolder;
damage.value = Utils.toDmgValue(damage.value * this.damageMultiplier);
return true;
}
}
export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultiplierAbAttr { export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
constructor(moveType: Type, damageMultiplier: number) { constructor(moveType: Type, damageMultiplier: number) {
super((target, user, move) => user.getMoveType(move) === moveType, damageMultiplier); super((target, user, move) => user.getMoveType(move) === moveType, damageMultiplier);
@ -3068,7 +3098,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
/** /**
* Condition function to applied to abilities related to Sheer Force. * Condition function to applied to abilities related to Sheer Force.
* Checks if last move used against target was affected by a Sheer Force user and: * Checks if last move used against target was affected by a Sheer Force user and:
* Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell * Disables: Color Change, Pickpocket, Berserk, Anger Shell
* @returns {AbAttrCondition} If false disables the ability which the condition is applied to. * @returns {AbAttrCondition} If false disables the ability which the condition is applied to.
*/ */
function getSheerForceHitDisableAbCondition(): AbAttrCondition { function getSheerForceHitDisableAbCondition(): AbAttrCondition {
@ -3590,22 +3620,19 @@ export class MoodyAbAttr extends PostTurnAbAttr {
} }
} }
export class PostTurnStatStageChangeAbAttr extends PostTurnAbAttr { export class SpeedBoostAbAttr extends PostTurnAbAttr {
private stats: BattleStat[];
private stages: number;
constructor(stats: BattleStat[], stages: number) { constructor() {
super(true); super(true);
this.stats = Array.isArray(stats)
? stats
: [ stats ];
this.stages = stages;
} }
applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
if (!simulated) { if (!simulated) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); if (!pokemon.turnData.switchedInThisTurn && !pokemon.turnData.failedRunAway) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPD ], 1));
} else {
return false;
}
} }
return true; return true;
} }
@ -4679,6 +4706,84 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
} }
} }
/**
* This applies a terrain-based type change to the Pokemon.
* Used by Mimicry.
*/
export class TerrainEventTypeChangeAbAttr extends PostSummonAbAttr {
constructor() {
super(true);
}
override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, _args: any[]): boolean {
if (pokemon.isTerastallized()) {
return false;
}
const currentTerrain = pokemon.scene.arena.getTerrainType();
const typeChange: Type[] = this.determineTypeChange(pokemon, currentTerrain);
if (typeChange.length !== 0) {
if (pokemon.summonData.addedType && typeChange.includes(pokemon.summonData.addedType)) {
pokemon.summonData.addedType = null;
}
pokemon.summonData.types = typeChange;
pokemon.updateInfo();
}
return true;
}
/**
* Retrieves the type(s) the Pokemon should change to in response to a terrain
* @param pokemon
* @param currentTerrain {@linkcode TerrainType}
* @returns a list of type(s)
*/
private determineTypeChange(pokemon: Pokemon, currentTerrain: TerrainType): Type[] {
const typeChange: Type[] = [];
switch (currentTerrain) {
case TerrainType.ELECTRIC:
typeChange.push(Type.ELECTRIC);
break;
case TerrainType.MISTY:
typeChange.push(Type.FAIRY);
break;
case TerrainType.GRASSY:
typeChange.push(Type.GRASS);
break;
case TerrainType.PSYCHIC:
typeChange.push(Type.PSYCHIC);
break;
default:
pokemon.getTypes(false, false, true).forEach(t => {
typeChange.push(t);
});
break;
}
return typeChange;
}
/**
* Checks if the Pokemon should change types if summoned into an active terrain
* @returns `true` if there is an active terrain requiring a type change | `false` if not
*/
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
if (pokemon.scene.arena.getTerrainType() !== TerrainType.NONE) {
return this.apply(pokemon, passive, simulated, new Utils.BooleanHolder(false), []);
}
return false;
}
override getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]) {
const currentTerrain = pokemon.scene.arena.getTerrainType();
const pokemonNameWithAffix = getPokemonNameWithAffix(pokemon);
if (currentTerrain === TerrainType.NONE) {
return i18next.t("abilityTriggers:pokemonTypeChangeRevert", { pokemonNameWithAffix });
} else {
const moveType = i18next.t(`pokemonInfo:Type.${Type[this.determineTypeChange(pokemon, currentTerrain)[0]]}`);
return i18next.t("abilityTriggers:pokemonTypeChange", { pokemonNameWithAffix, moveType });
}
}
}
async function applyAbAttrsInternal<TAttr extends AbAttr>( async function applyAbAttrsInternal<TAttr extends AbAttr>(
attrType: Constructor<TAttr>, attrType: Constructor<TAttr>,
pokemon: Pokemon | null, pokemon: Pokemon | null,
@ -4734,6 +4839,239 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
} }
} }
class ForceSwitchOutHelper {
constructor(private switchType: SwitchType) {}
/**
* Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type.
*
* @param pokemon The {@linkcode Pokemon} attempting to switch out.
* @returns `true` if the switch is successful
*/
public switchOutLogic(pokemon: Pokemon): boolean {
const switchOutTarget = pokemon;
/**
* If the switch-out target is a player-controlled Pokémon, the function checks:
* - Whether there are available party members to switch in.
* - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated.
*/
if (switchOutTarget instanceof PlayerPokemon) {
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
return true;
}
/**
* For non-wild battles, it checks if the opposing party has any available Pokémon to switch in.
* If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated.
*/
} else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) {
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(),
(pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false, false), MoveEndPhase);
return true;
}
/**
* For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles).
*/
} else {
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false);
pokemon.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
if (switchOutTarget.scene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon);
}
}
if (!switchOutTarget.getAlly()?.isActive(true)) {
pokemon.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene));
pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene));
}
}
}
return false;
}
/**
* Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions.
*
* @param pokemon The Pokémon attempting to switch out.
* @param opponent The opponent Pokémon.
* @returns `true` if the switch-out condition is met
*/
public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean {
const switchOutTarget = pokemon;
const player = switchOutTarget instanceof PlayerPokemon;
if (player) {
const blockedByAbility = new Utils.BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility);
return !blockedByAbility.value;
}
if (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) {
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
}
if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) {
return false;
}
const party = player ? pokemon.scene.getParty() : pokemon.scene.getEnemyParty();
return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD)
|| party.filter(p => p.isAllowedInBattle()
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount();
}
/**
* Returns a message if the switch-out attempt fails due to ability effects.
*
* @param target The target Pokémon.
* @returns The failure message, or `null` if no failure.
*/
public getFailedText(target: Pokemon): string | null {
const blockedByAbility = new Utils.BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null;
}
}
/**
* Calculates the amount of recovery from the Shell Bell item.
*
* If the Pokémon is holding a Shell Bell, this function computes the amount of health
* recovered based on the damage dealt in the current turn. The recovery is multiplied by the
* Shell Bell's modifier (if any).
*
* @param pokemon - The Pokémon whose Shell Bell recovery is being calculated.
* @returns The amount of health recovered by Shell Bell.
*/
function calculateShellBellRecovery(pokemon: Pokemon): number {
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier);
if (shellBellModifier) {
return Utils.toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount;
}
return 0;
}
/**
* Triggers after the Pokemon takes any damage
* @extends AbAttr
*/
export class PostDamageAbAttr extends AbAttr {
public applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
return false;
}
}
/**
* Ability attribute for forcing a Pokémon to switch out after its health drops below half.
* This attribute checks various conditions related to the damage received, the moves used by the Pokémon
* and its opponents, and determines whether a forced switch-out should occur.
*
* Used by Wimp Out and Emergency Exit
*
* @extends PostDamageAbAttr
* @see {@linkcode applyPostDamage}
*/
export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH);
private hpRatio: number;
constructor(hpRatio: number = 0.5) {
super();
this.hpRatio = hpRatio;
}
/**
* Applies the switch-out logic after the Pokémon takes damage.
* Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and
* the Pokémon's health after damage to determine whether the switch-out should occur.
*
* @param pokemon The Pokémon that took damage.
* @param damage The amount of damage taken by the Pokémon.
* @param passive N/A
* @param simulated Whether the ability is being simulated.
* @param args N/A
* @param source The Pokemon that dealt damage
* @returns `true` if the switch-out logic was successfully applied
*/
public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
const moveHistory = pokemon.getMoveHistory();
// Will not activate when the Pokémon's HP is lowered by cutting its own HP
const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ];
if (moveHistory.length > 0) {
const lastMoveUsed = moveHistory[moveHistory.length - 1];
if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) {
return false;
}
}
// Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.
const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ];
if (source) {
const enemyMoveHistory = source.getMoveHistory();
if (enemyMoveHistory.length > 0) {
const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1];
// Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop.
if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) {
return false;
// Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force.
} else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) {
return false;
// Activate only after the last hit of multistrike moves
} else if (source.turnData.hitsLeft > 1) {
return false;
}
if (source.turnData.hitCount > 1) {
damage = pokemon.turnData.damageTaken;
}
}
}
if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) {
// Activates if it falls below half and recovers back above half from a Shell Bell
const shellBellHeal = calculateShellBellRecovery(pokemon);
if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) {
for (const opponent of pokemon.getOpponents()) {
if (!this.helper.getSwitchOutCondition(pokemon, opponent)) {
return false;
}
}
return this.helper.switchOutLogic(pokemon);
} else {
return false;
}
} else {
return false;
}
}
public getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
return this.helper.getFailedText(target);
}
}
export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> { export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated); return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated);
} }
@ -4767,6 +5105,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbA
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated);
} }
export function applyPostDamageAbAttrs(attrType: Constructor<PostDamageAbAttr>,
pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[], source?: Pokemon): Promise<void> {
return applyAbAttrsInternal<PostDamageAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), args);
}
/** /**
* Applies a field Stat multiplier attribute * Applies a field Stat multiplier attribute
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
@ -4909,7 +5252,7 @@ export function initAbilities() {
.attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN),
new Ability(Abilities.SPEED_BOOST, 3) new Ability(Abilities.SPEED_BOOST, 3)
.attr(PostTurnStatStageChangeAbAttr, [ Stat.SPD ], 1), .attr(SpeedBoostAbAttr),
new Ability(Abilities.BATTLE_ARMOR, 3) new Ability(Abilities.BATTLE_ARMOR, 3)
.attr(BlockCritAbAttr) .attr(BlockCritAbAttr)
.ignorable(), .ignorable(),
@ -4963,7 +5306,8 @@ export function initAbilities() {
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1) .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
.ignorable(), .ignorable(),
new Ability(Abilities.SHIELD_DUST, 3) new Ability(Abilities.SHIELD_DUST, 3)
.attr(IgnoreMoveEffectsAbAttr), .attr(IgnoreMoveEffectsAbAttr)
.ignorable(),
new Ability(Abilities.OWN_TEMPO, 3) new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
@ -5081,11 +5425,9 @@ export function initAbilities() {
new Ability(Abilities.CUTE_CHARM, 3) new Ability(Abilities.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
new Ability(Abilities.PLUS, 3) new Ability(Abilities.PLUS, 3)
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.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) new Ability(Abilities.MINUS, 3)
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.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) new Ability(Abilities.FORECAST, 3)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
@ -5310,8 +5652,8 @@ export function initAbilities() {
new Ability(Abilities.HEALER, 5) new Ability(Abilities.HEALER, 5)
.conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), .conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true),
new Ability(Abilities.FRIEND_GUARD, 5) new Ability(Abilities.FRIEND_GUARD, 5)
.ignorable() .attr(AlliedFieldDamageReductionAbAttr, 0.75)
.unimplemented(), .ignorable(),
new Ability(Abilities.WEAK_ARMOR, 5) new Ability(Abilities.WEAK_ARMOR, 5)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2), .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2),
@ -5423,7 +5765,8 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(MoveAbilityBypassAbAttr), .attr(MoveAbilityBypassAbAttr),
new Ability(Abilities.AROMA_VEIL, 6) new Ability(Abilities.AROMA_VEIL, 6)
.attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]), .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ])
.ignorable(),
new Ability(Abilities.FLOWER_VEIL, 6) new Ability(Abilities.FLOWER_VEIL, 6)
.ignorable() .ignorable()
.unimplemented(), .unimplemented(),
@ -5508,11 +5851,11 @@ export function initAbilities() {
new Ability(Abilities.STAMINA, 7) new Ability(Abilities.STAMINA, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7) new Ability(Abilities.WIMP_OUT, 7)
.condition(getSheerForceHitDisableAbCondition()) .attr(PostDamageForceSwitchAbAttr)
.unimplemented(), .edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.EMERGENCY_EXIT, 7) new Ability(Abilities.EMERGENCY_EXIT, 7)
.condition(getSheerForceHitDisableAbCondition()) .attr(PostDamageForceSwitchAbAttr)
.unimplemented(), .edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.WATER_COMPACTION, 7) new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7) new Ability(Abilities.MERCILESS, 7)
@ -5743,7 +6086,7 @@ export function initAbilities() {
new Ability(Abilities.POWER_SPOT, 8) new Ability(Abilities.POWER_SPOT, 8)
.attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3), .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3),
new Ability(Abilities.MIMICRY, 8) new Ability(Abilities.MIMICRY, 8)
.unimplemented(), .attr(TerrainEventTypeChangeAbAttr),
new Ability(Abilities.SCREEN_CLEANER, 8) new Ability(Abilities.SCREEN_CLEANER, 8)
.attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]), .attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]),
new Ability(Abilities.STEELY_SPIRIT, 8) new Ability(Abilities.STEELY_SPIRIT, 8)
@ -5874,16 +6217,14 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.SWORD_OF_RUIN, 9) new Ability(Abilities.SWORD_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })),
.ignorable(),
new Ability(Abilities.TABLETS_OF_RUIN, 9) new Ability(Abilities.TABLETS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
.ignorable(), .ignorable(),
new Ability(Abilities.BEADS_OF_RUIN, 9) new Ability(Abilities.BEADS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })),
.ignorable(),
new Ability(Abilities.ORICHALCUM_PULSE, 9) new Ability(Abilities.ORICHALCUM_PULSE, 9)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)
@ -5900,7 +6241,7 @@ export function initAbilities() {
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5),
new Ability(Abilities.SUPREME_OVERLORD, 9) new Ability(Abilities.SUPREME_OVERLORD, 9)
.attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 5)) .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 5))
.partial(), // Counter resets every wave .partial(), // Counter resets every wave instead of on arena reset
new Ability(Abilities.COSTAR, 9) new Ability(Abilities.COSTAR, 9)
.attr(PostSummonCopyAllyStatsAbAttr), .attr(PostSummonCopyAllyStatsAbAttr),
new Ability(Abilities.TOXIC_DEBRIS, 9) new Ability(Abilities.TOXIC_DEBRIS, 9)

View File

@ -126,6 +126,7 @@ export class MistTag extends ArenaTag {
* Cancels the lowering of stats * Cancels the lowering of stats
* @param arena the {@linkcode Arena} containing this effect * @param arena the {@linkcode Arena} containing this effect
* @param simulated `true` if the effect should be applied quietly * @param simulated `true` if the effect should be applied quietly
* @param attacker the {@linkcode Pokemon} using a move into this effect.
* @param cancelled a {@linkcode BooleanHolder} whose value is set to `true` * @param cancelled a {@linkcode BooleanHolder} whose value is set to `true`
* to flag the stat reduction as cancelled * to flag the stat reduction as cancelled
* @returns `true` if a stat reduction was cancelled; `false` otherwise * @returns `true` if a stat reduction was cancelled; `false` otherwise

View File

@ -1443,7 +1443,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
], ],
[Species.ROCKRUFF]: [ [Species.ROCKRUFF]: [
new SpeciesFormEvolution(Species.LYCANROC, "", "midday", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY) && (p.formIndex === 0))), new SpeciesFormEvolution(Species.LYCANROC, "", "midday", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY) && (p.formIndex === 0))),
new SpeciesFormEvolution(Species.LYCANROC, "", "dusk", 25, null, new SpeciesEvolutionCondition(p => p.formIndex === 1)), new SpeciesFormEvolution(Species.LYCANROC, "own-tempo", "dusk", 25, null, new SpeciesEvolutionCondition(p => p.formIndex === 1)),
new SpeciesFormEvolution(Species.LYCANROC, "", "midnight", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT) && (p.formIndex === 0))) new SpeciesFormEvolution(Species.LYCANROC, "", "midnight", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT) && (p.formIndex === 0)))
], ],
[Species.STEENEE]: [ [Species.STEENEE]: [

View File

@ -497,7 +497,7 @@ export const speciesEggTiers = {
[Species.DREEPY]: EggTier.RARE, [Species.DREEPY]: EggTier.RARE,
[Species.ZACIAN]: EggTier.LEGENDARY, [Species.ZACIAN]: EggTier.LEGENDARY,
[Species.ZAMAZENTA]: EggTier.LEGENDARY, [Species.ZAMAZENTA]: EggTier.LEGENDARY,
[Species.ETERNATUS]: EggTier.COMMON, [Species.ETERNATUS]: EggTier.LEGENDARY,
[Species.KUBFU]: EggTier.EPIC, [Species.KUBFU]: EggTier.EPIC,
[Species.ZARUDE]: EggTier.EPIC, [Species.ZARUDE]: EggTier.EPIC,
[Species.REGIELEKI]: EggTier.EPIC, [Species.REGIELEKI]: EggTier.EPIC,

File diff suppressed because it is too large Load Diff

View File

@ -544,11 +544,15 @@ export class Egg {
//// ////
} }
export function getLegendaryGachaSpeciesForTimestamp(scene: BattleScene, timestamp: number): Species { export function getValidLegendaryGachaSpecies() : Species[] {
const legendarySpecies = Object.entries(speciesEggTiers) return Object.entries(speciesEggTiers)
.filter(s => s[1] === EggTier.LEGENDARY) .filter(s => s[1] === EggTier.LEGENDARY)
.map(s => parseInt(s[0])) .map(s => parseInt(s[0]))
.filter(s => getPokemonSpecies(s).isObtainable()); .filter(s => getPokemonSpecies(s).isObtainable() && s !== Species.ETERNATUS);
}
export function getLegendaryGachaSpeciesForTimestamp(scene: BattleScene, timestamp: number): Species {
const legendarySpecies = getValidLegendaryGachaSpecies();
let ret: Species; let ret: Species;

View File

@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { WeatherType } from "./weather"; import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle"; import { BattlerIndex, BattleType } from "../battle";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
@ -1049,31 +1049,80 @@ export enum MoveEffectTrigger {
POST_TARGET, POST_TARGET,
} }
interface MoveEffectAttrOptions {
/**
* Defines when this effect should trigger in the move's effect order
* @see {@linkcode MoveEffectPhase}
*/
trigger?: MoveEffectTrigger;
/** Should this effect only apply on the first hit? */
firstHitOnly?: boolean;
/** Should this effect only apply on the last hit? */
lastHitOnly?: boolean;
/** Should this effect only apply on the first target hit? */
firstTargetOnly?: boolean;
/** Overrides the secondary effect chance for this attr if set. */
effectChanceOverride?: number;
}
/** Base class defining all Move Effect Attributes /** Base class defining all Move Effect Attributes
* @extends MoveAttr * @extends MoveAttr
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class MoveEffectAttr extends MoveAttr { export class MoveEffectAttr extends MoveAttr {
/** Defines when this effect should trigger in the move's effect order /**
* @see {@linkcode phases.MoveEffectPhase.start} * A container for this attribute's optional parameters
* @see {@linkcode MoveEffectAttrOptions} for supported params.
*/ */
public trigger: MoveEffectTrigger; protected options?: MoveEffectAttrOptions;
/** Should this effect only apply on the first hit? */
public firstHitOnly: boolean;
/** Should this effect only apply on the last hit? */
public lastHitOnly: boolean;
/** Should this effect only apply on the first target hit? */
public firstTargetOnly: boolean;
/** Overrides the secondary effect chance for this attr if set. */
public effectChanceOverride?: number;
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) { constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) {
super(selfTarget); super(selfTarget);
this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY; this.options = options;
this.firstHitOnly = firstHitOnly; }
this.lastHitOnly = lastHitOnly;
this.firstTargetOnly = firstTargetOnly; /**
this.effectChanceOverride = effectChanceOverride; * Defines when this effect should trigger in the move's effect order.
* @default MoveEffectTrigger.POST_APPLY
* @see {@linkcode MoveEffectTrigger}
*/
public get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY;
}
/**
* `true` if this effect should only trigger on the first hit of
* multi-hit moves.
* @default false
*/
public get firstHitOnly () {
return this.options?.firstHitOnly ?? false;
}
/**
* `true` if this effect should only trigger on the last hit of
* multi-hit moves.
* @default false
*/
public get lastHitOnly () {
return this.options?.lastHitOnly ?? false;
}
/**
* `true` if this effect should apply only upon hitting a target
* for the first time when targeting multiple {@linkcode Pokemon}.
* @default false
*/
public get firstTargetOnly () {
return this.options?.firstTargetOnly ?? false;
}
/**
* If defined, overrides the move's base chance for this
* secondary effect to trigger.
*/
public get effectChanceOverride () {
return this.options?.effectChanceOverride;
} }
/** /**
@ -1398,7 +1447,7 @@ export class RecoilAttr extends MoveEffectAttr {
private unblockable: boolean; private unblockable: boolean;
constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) { constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) {
super(true, MoveEffectTrigger.POST_APPLY, false, true); super(true, { lastHitOnly: true });
this.useHp = useHp; this.useHp = useHp;
this.damageRatio = damageRatio; this.damageRatio = damageRatio;
@ -1420,8 +1469,13 @@ export class RecoilAttr extends MoveEffectAttr {
return false; return false;
} }
const damageValue = (!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio; // Chloroblast and Struggle should not deal recoil damage if the move was not successful
const minValue = user.turnData.damageDealt ? 1 : 0; if (this.useHp && [ MoveResult.FAIL, MoveResult.MISS ].includes(user.getLastXMoves(1)[0]?.result)) {
return false;
}
const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio;
const minValue = user.turnData.totalDamageDealt ? 1 : 0;
const recoilDamage = Utils.toDmgValue(damageValue, minValue); const recoilDamage = Utils.toDmgValue(damageValue, minValue);
if (!recoilDamage) { if (!recoilDamage) {
return false; return false;
@ -1451,7 +1505,7 @@ export class RecoilAttr extends MoveEffectAttr {
**/ **/
export class SacrificialAttr extends MoveEffectAttr { export class SacrificialAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.POST_TARGET); super(true, { trigger: MoveEffectTrigger.POST_TARGET });
} }
/** /**
@ -1484,7 +1538,7 @@ export class SacrificialAttr extends MoveEffectAttr {
**/ **/
export class SacrificialAttrOnHit extends MoveEffectAttr { export class SacrificialAttrOnHit extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
} }
/** /**
@ -1523,7 +1577,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr {
*/ */
export class HalfSacrificialAttr extends MoveEffectAttr { export class HalfSacrificialAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.POST_TARGET); super(true, { trigger: MoveEffectTrigger.POST_TARGET });
} }
/** /**
@ -1927,7 +1981,7 @@ export class HitHealAttr extends MoveEffectAttr {
private healStat: EffectiveStat | null; private healStat: EffectiveStat | null;
constructor(healRatio?: number | null, healStat?: EffectiveStat) { constructor(healRatio?: number | null, healStat?: EffectiveStat) {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
this.healRatio = healRatio ?? 0.5; this.healRatio = healRatio ?? 0.5;
this.healStat = healStat ?? null; this.healStat = healStat ?? null;
@ -1952,7 +2006,7 @@ export class HitHealAttr extends MoveEffectAttr {
message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) }); message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) });
} else { } else {
// Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc. // Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc.
healAmount = Utils.toDmgValue(user.turnData.currDamageDealt * this.healRatio); healAmount = Utils.toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio);
message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) }); message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) });
} }
if (reverseDrain) { if (reverseDrain) {
@ -2136,7 +2190,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
public overrideStatus: boolean = false; public overrideStatus: boolean = false;
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
super(selfTarget, MoveEffectTrigger.HIT); super(selfTarget, { trigger: MoveEffectTrigger.HIT });
this.effect = effect; this.effect = effect;
this.turnsRemaining = turnsRemaining; this.turnsRemaining = turnsRemaining;
@ -2177,7 +2231,10 @@ export class StatusEffectAttr extends MoveEffectAttr {
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false);
return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1);
const pokemon = this.selfTarget ? user : target;
return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
} }
} }
@ -2197,13 +2254,16 @@ export class MultiStatusEffectAttr extends StatusEffectAttr {
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false);
return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1);
const pokemon = this.selfTarget ? user : target;
return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
} }
} }
export class PsychoShiftEffectAttr extends MoveEffectAttr { export class PsychoShiftEffectAttr extends MoveEffectAttr {
constructor() { constructor() {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -2228,7 +2288,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(user.status?.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0;
} }
} }
/** /**
@ -2240,7 +2300,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
private chance: number; private chance: number;
constructor(chance: number) { constructor(chance: number) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.chance = chance; this.chance = chance;
} }
@ -2301,7 +2361,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
private berriesOnly: boolean; private berriesOnly: boolean;
constructor(berriesOnly: boolean) { constructor(berriesOnly: boolean) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.berriesOnly = berriesOnly; this.berriesOnly = berriesOnly;
} }
@ -2375,7 +2435,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
export class EatBerryAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr {
protected chosenBerry: BerryModifier | undefined; protected chosenBerry: BerryModifier | undefined;
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
} }
/** /**
* Causes the target to eat a berry. * Causes the target to eat a berry.
@ -2478,7 +2538,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
* @param ...effects - List of status effects to cure * @param ...effects - List of status effects to cure
*/ */
constructor(selfTarget: boolean, ...effects: StatusEffect[]) { constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true); super(selfTarget, { lastHitOnly: true });
this.effects = effects; this.effects = effects;
} }
@ -2808,35 +2868,67 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
} }
} }
/**
* Set of optional parameters that may be applied to stat stage changing effects
* @extends MoveEffectAttrOptions
* @see {@linkcode StatStageChangeAttr}
*/
interface StatStageChangeAttrOptions extends MoveEffectAttrOptions {
/** If defined, needs to be met in order for the stat change to apply */
condition?: MoveConditionFunc,
/** `true` to display a message */
showMessage?: boolean
}
/** /**
* Attribute used for moves that change stat stages * Attribute used for moves that change stat stages
* *
* @param stats {@linkcode BattleStat} Array of stat(s) to change * @param stats {@linkcode BattleStat} Array of stat(s) to change
* @param stages How many stages to change the stat(s) by, [-6, 6] * @param stages How many stages to change the stat(s) by, [-6, 6]
* @param selfTarget `true` if the move is self-targetting * @param selfTarget `true` if the move is self-targetting
* @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes * @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute.
* @param showMessage `true` to display a message; default `true`
* @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false`
* @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT}
* @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false`
* @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false`
* @param effectChanceOverride Will override the move's normal secondary effect chance if specified
* *
* @extends MoveEffectAttr * @extends MoveEffectAttr
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class StatStageChangeAttr extends MoveEffectAttr { export class StatStageChangeAttr extends MoveEffectAttr {
public stats: BattleStat[]; public stats: BattleStat[];
public stages: integer; public stages: number;
private condition?: MoveConditionFunc | null; /**
private showMessage: boolean; * Container for optional parameters to this attribute.
* @see {@linkcode StatStageChangeAttrOptions} for available optional params
*/
protected override options?: StatStageChangeAttrOptions;
constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) { constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, options?: StatStageChangeAttrOptions) {
super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride); super(selfTarget, options);
this.stats = stats; this.stats = stats;
this.stages = stages; this.stages = stages;
this.condition = condition; this.options = options;
this.showMessage = showMessage; }
/**
* The condition required for the stat stage change to apply.
* Defaults to `null` (i.e. no condition required).
*/
private get condition () {
return this.options?.condition ?? null;
}
/**
* `true` to display a message for the stat change.
* @default true
*/
private get showMessage () {
return this.options?.showMessage ?? true;
}
/**
* Indicates when the stat change should trigger
* @default MoveEffectTrigger.HIT
*/
public override get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.HIT;
} }
/** /**
@ -2921,20 +3013,6 @@ export class SecretPowerAttr extends MoveEffectAttr {
super(false); super(false);
} }
/**
* Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance
* @returns `true` if the move's secondary effect should apply
*/
override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
this.effectChanceOverride = move.chance;
const moveChance = this.getMoveChance(user, target, move, this.selfTarget);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
return true;
} else {
return false;
}
}
/** /**
* Used to apply the secondary effect to the target Pokemon * Used to apply the secondary effect to the target Pokemon
* @returns `true` if a secondary effect is successfully applied * @returns `true` if a secondary effect is successfully applied
@ -2951,8 +3029,6 @@ export class SecretPowerAttr extends MoveEffectAttr {
const biome = user.scene.arena.biomeType; const biome = user.scene.arena.biomeType;
secondaryEffect = this.determineBiomeEffect(biome); secondaryEffect = this.determineBiomeEffect(biome);
} }
// effectChanceOverride used in the application of the actual secondary effect
secondaryEffect.effectChanceOverride = 100;
return secondaryEffect.apply(user, target, move, []); return secondaryEffect.apply(user, target, move, []);
} }
@ -3128,7 +3204,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
private messageCallback: ((user: Pokemon) => void) | undefined; private messageCallback: ((user: Pokemon) => void) | undefined;
constructor(stat: 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); super(stat, levels, true);
this.cutRatio = cutRatio; this.cutRatio = cutRatio;
this.messageCallback = messageCallback; this.messageCallback = messageCallback;
@ -4389,7 +4465,7 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
} }
if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) { if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) {
const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct? const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!;
moveType.value = Type[Type[form]]; moveType.value = Type[Type[form]];
return true; return true;
@ -4878,7 +4954,7 @@ export class BypassRedirectAttr extends MoveAttr {
export class FrenzyAttr extends MoveEffectAttr { export class FrenzyAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT, false, true); super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true });
} }
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
@ -4951,7 +5027,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
private failOnOverlap: boolean; private failOnOverlap: boolean;
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) { constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) {
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly); super(selfTarget, { lastHitOnly: lastHitOnly });
this.tagType = tagType; this.tagType = tagType;
this.turnCountMin = turnCountMin; this.turnCountMin = turnCountMin;
@ -5386,7 +5462,7 @@ export class AddArenaTagAttr extends MoveEffectAttr {
public selfSideTarget: boolean; public selfSideTarget: boolean;
constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) {
super(true, MoveEffectTrigger.POST_APPLY); super(true);
this.tagType = tagType; this.tagType = tagType;
this.turnCount = turnCount!; // TODO: is the bang correct? this.turnCount = turnCount!; // TODO: is the bang correct?
@ -5424,7 +5500,7 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr {
public selfSideTarget: boolean; public selfSideTarget: boolean;
constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) { constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) {
super(true, MoveEffectTrigger.POST_APPLY); super(true);
this.tagTypes = tagTypes; this.tagTypes = tagTypes;
this.selfSideTarget = selfSideTarget; this.selfSideTarget = selfSideTarget;
@ -5490,7 +5566,7 @@ export class RemoveArenaTrapAttr extends MoveEffectAttr {
private targetBothSides: boolean; private targetBothSides: boolean;
constructor(targetBothSides: boolean = false) { constructor(targetBothSides: boolean = false) {
super(true, MoveEffectTrigger.PRE_APPLY); super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
this.targetBothSides = targetBothSides; this.targetBothSides = targetBothSides;
} }
@ -5526,7 +5602,7 @@ export class RemoveScreensAttr extends MoveEffectAttr {
private targetBothSides: boolean; private targetBothSides: boolean;
constructor(targetBothSides: boolean = false) { constructor(targetBothSides: boolean = false) {
super(true, MoveEffectTrigger.PRE_APPLY); super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
this.targetBothSides = targetBothSides; this.targetBothSides = targetBothSides;
} }
@ -5564,7 +5640,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr {
constructor(SwapTags: ArenaTagType[]) { constructor(SwapTags: ArenaTagType[]) {
super(true, MoveEffectTrigger.POST_APPLY); super(true);
this.SwapTags = SwapTags; this.SwapTags = SwapTags;
} }
@ -5685,12 +5761,13 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
} }
} }
export class ForceSwitchOutAttr extends MoveEffectAttr { export class ForceSwitchOutAttr extends MoveEffectAttr {
constructor( constructor(
private selfSwitch: boolean = false, private selfSwitch: boolean = false,
private switchType: SwitchType = SwitchType.SWITCH private switchType: SwitchType = SwitchType.SWITCH
) { ) {
super(false, MoveEffectTrigger.POST_APPLY, false, true); super(false, { lastHitOnly: true });
} }
isBatonPass() { isBatonPass() {
@ -5703,12 +5780,19 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return false; return false;
} }
/**
* Move the switch out logic inside the conditional block
* This ensures that the switch out only happens when the conditions are met
*/
const switchOutTarget = this.selfSwitch ? user : target; const switchOutTarget = this.selfSwitch ? user : target;
if (switchOutTarget instanceof PlayerPokemon) { if (switchOutTarget instanceof PlayerPokemon) {
/**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN)
) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for the player's Pokemon // Switch out logic for the player's Pokemon
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false; return false;
@ -5734,11 +5818,27 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
false, false), MoveEndPhase); false, false), MoveEndPhase);
} }
} else { } else {
/**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for everything else (eg: WILD battles) // Switch out logic for everything else (eg: WILD battles)
if (user.scene.currentBattle.waveIndex % 10 === 0) { if (user.scene.currentBattle.waveIndex % 10 === 0) {
return false; return false;
} }
// Don't allow wild mons to flee with U-turn et al
if (this.selfSwitch && !user.isPlayer() && move.category !== MoveCategory.STATUS) {
return false;
}
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false); switchOutTarget.leaveField(false);
user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
@ -5821,8 +5921,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
return ret; return ret;
} }
}
/**
* Helper function to check if the Pokémon's health is below half after taking damage.
* Used for an edge case interaction with Wimp Out/Emergency Exit.
* If the Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.
*/
hpDroppedBelowHalf(target: Pokemon): boolean {
const pokemonHealth = target.hp;
const maxPokemonHealth = target.getMaxHp();
const damageTaken = target.turnData.damageTaken;
const initialHealth = pokemonHealth + damageTaken;
// Check if the Pokémon's health has dropped below half after the damage
return initialHealth >= maxPokemonHealth / 2 && pokemonHealth < maxPokemonHealth / 2;
}
}
export class ChillyReceptionAttr extends ForceSwitchOutAttr { export class ChillyReceptionAttr extends ForceSwitchOutAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -5841,7 +5955,7 @@ export class RemoveTypeAttr extends MoveEffectAttr {
private messageCallback: ((user: Pokemon) => void) | undefined; private messageCallback: ((user: Pokemon) => void) | undefined;
constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) { constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) {
super(true, MoveEffectTrigger.POST_TARGET); super(true, { trigger: MoveEffectTrigger.POST_TARGET });
this.removedType = removedType; this.removedType = removedType;
this.messageCallback = messageCallback; this.messageCallback = messageCallback;
@ -5858,6 +5972,9 @@ export class RemoveTypeAttr extends MoveEffectAttr {
const userTypes = user.getTypes(true); const userTypes = user.getTypes(true);
const modifiedTypes = userTypes.filter(type => type !== this.removedType); const modifiedTypes = userTypes.filter(type => type !== this.removedType);
if (modifiedTypes.length === 0) {
modifiedTypes.push(Type.UNKNOWN);
}
user.summonData.types = modifiedTypes; user.summonData.types = modifiedTypes;
user.updateInfo(); user.updateInfo();
@ -5880,7 +5997,11 @@ export class CopyTypeAttr extends MoveEffectAttr {
return false; return false;
} }
user.summonData.types = target.getTypes(true); const targetTypes = target.getTypes(true);
if (targetTypes.includes(Type.UNKNOWN) && targetTypes.indexOf(Type.UNKNOWN) > -1) {
targetTypes[targetTypes.indexOf(Type.UNKNOWN)] = Type.NORMAL;
}
user.summonData.types = targetTypes;
user.updateInfo(); user.updateInfo();
user.scene.queueMessage(i18next.t("moveTriggers:copyType", { pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target) })); user.scene.queueMessage(i18next.t("moveTriggers:copyType", { pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target) }));
@ -5889,7 +6010,7 @@ export class CopyTypeAttr extends MoveEffectAttr {
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN; return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN || target.summonData.addedType !== null;
} }
} }
@ -5903,22 +6024,114 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr {
return false; return false;
} }
const biomeType = user.scene.arena.getTypeForBiome(); const terrainType = user.scene.arena.getTerrainType();
let typeChange: Type;
if (terrainType !== TerrainType.NONE) {
typeChange = this.getTypeForTerrain(user.scene.arena.getTerrainType());
} else {
typeChange = this.getTypeForBiome(user.scene.arena.biomeType);
}
user.summonData.types = [ biomeType ]; user.summonData.types = [ typeChange ];
user.updateInfo(); user.updateInfo();
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[biomeType]}`) })); user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[typeChange]}`) }));
return true; return true;
} }
/**
* Retrieves a type from the current terrain
* @param terrainType {@linkcode TerrainType}
* @returns {@linkcode Type}
*/
private getTypeForTerrain(terrainType: TerrainType): Type {
switch (terrainType) {
case TerrainType.ELECTRIC:
return Type.ELECTRIC;
case TerrainType.MISTY:
return Type.FAIRY;
case TerrainType.GRASSY:
return Type.GRASS;
case TerrainType.PSYCHIC:
return Type.PSYCHIC;
case TerrainType.NONE:
default:
return Type.UNKNOWN;
}
}
/**
* Retrieves a type from the current biome
* @param biomeType {@linkcode Biome}
* @returns {@linkcode Type}
*/
private getTypeForBiome(biomeType: Biome): Type {
switch (biomeType) {
case Biome.TOWN:
case Biome.PLAINS:
case Biome.METROPOLIS:
return Type.NORMAL;
case Biome.GRASS:
case Biome.TALL_GRASS:
return Type.GRASS;
case Biome.FOREST:
case Biome.JUNGLE:
return Type.BUG;
case Biome.SLUM:
case Biome.SWAMP:
return Type.POISON;
case Biome.SEA:
case Biome.BEACH:
case Biome.LAKE:
case Biome.SEABED:
return Type.WATER;
case Biome.MOUNTAIN:
return Type.FLYING;
case Biome.BADLANDS:
return Type.GROUND;
case Biome.CAVE:
case Biome.DESERT:
return Type.ROCK;
case Biome.ICE_CAVE:
case Biome.SNOWY_FOREST:
return Type.ICE;
case Biome.MEADOW:
case Biome.FAIRY_CAVE:
case Biome.ISLAND:
return Type.FAIRY;
case Biome.POWER_PLANT:
return Type.ELECTRIC;
case Biome.VOLCANO:
return Type.FIRE;
case Biome.GRAVEYARD:
case Biome.TEMPLE:
return Type.GHOST;
case Biome.DOJO:
case Biome.CONSTRUCTION_SITE:
return Type.FIGHTING;
case Biome.FACTORY:
case Biome.LABORATORY:
return Type.STEEL;
case Biome.RUINS:
case Biome.SPACE:
return Type.PSYCHIC;
case Biome.WASTELAND:
case Biome.END:
return Type.DRAGON;
case Biome.ABYSS:
return Type.DARK;
default:
return Type.UNKNOWN;
}
}
} }
export class ChangeTypeAttr extends MoveEffectAttr { export class ChangeTypeAttr extends MoveEffectAttr {
private type: Type; private type: Type;
constructor(type: Type) { constructor(type: Type) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.type = type; this.type = type;
} }
@ -5941,17 +6154,13 @@ export class AddTypeAttr extends MoveEffectAttr {
private type: Type; private type: Type;
constructor(type: Type) { constructor(type: Type) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.type = type; this.type = type;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const types = target.getTypes().slice(0, 2).filter(t => t !== Type.UNKNOWN); // TODO: Figure out some way to actually check if another version of this effect is already applied target.summonData.addedType = this.type;
if (this.type !== Type.UNKNOWN) {
types.push(this.type);
}
target.summonData.types = types;
target.updateInfo(); target.updateInfo();
user.scene.queueMessage(i18next.t("moveTriggers:addType", { typeName: i18next.t(`pokemonInfo:Type.${Type[this.type]}`), pokemonName: getPokemonNameWithAffix(target) })); user.scene.queueMessage(i18next.t("moveTriggers:addType", { typeName: i18next.t(`pokemonInfo:Type.${Type[this.type]}`), pokemonName: getPokemonNameWithAffix(target) }));
@ -6472,7 +6681,7 @@ export class AbilityChangeAttr extends MoveEffectAttr {
public ability: Abilities; public ability: Abilities;
constructor(ability: Abilities, selfTarget?: boolean) { constructor(ability: Abilities, selfTarget?: boolean) {
super(selfTarget, MoveEffectTrigger.HIT); super(selfTarget, { trigger: MoveEffectTrigger.HIT });
this.ability = ability; this.ability = ability;
} }
@ -6501,7 +6710,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
public copyToPartner: boolean; public copyToPartner: boolean;
constructor(copyToPartner: boolean = false) { constructor(copyToPartner: boolean = false) {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
this.copyToPartner = copyToPartner; this.copyToPartner = copyToPartner;
} }
@ -6540,7 +6749,7 @@ export class AbilityGiveAttr extends MoveEffectAttr {
public copyToPartner: boolean; public copyToPartner: boolean;
constructor() { constructor() {
super(false, MoveEffectTrigger.HIT); super(false, { trigger: MoveEffectTrigger.HIT });
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -6852,7 +7061,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
export class MoneyAttr extends MoveEffectAttr { export class MoneyAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT, true); super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true });
} }
apply(user: Pokemon, target: Pokemon, move: Move): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
@ -6869,7 +7078,7 @@ export class MoneyAttr extends MoveEffectAttr {
*/ */
export class DestinyBondAttr extends MoveEffectAttr { export class DestinyBondAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.PRE_APPLY); super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
} }
/** /**
@ -6919,7 +7128,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
public effect: StatusEffect; public effect: StatusEffect;
constructor(effect: StatusEffect) { constructor(effect: StatusEffect) {
super(true, MoveEffectTrigger.HIT); super(true, { trigger: MoveEffectTrigger.HIT });
this.effect = effect; this.effect = effect;
} }
@ -7044,6 +7253,11 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined;
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const party: Pokemon[] = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty();
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField());
};
export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> { function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
@ -7953,6 +8167,7 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2) new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2)
.attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS) .attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS)
.condition(failIfLastInPartyCondition)
.hidesUser(), .hidesUser(),
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
@ -8971,7 +9186,7 @@ export function initMoves() {
// If any fielded pokémon is grass-type and grounded. // 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()); return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded());
}) })
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }),
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
.attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE),
@ -8983,8 +9198,7 @@ export function initMoves() {
.ignoresProtect() .ignoresProtect()
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GHOST) .attr(AddTypeAttr, Type.GHOST),
.edgeCase(), // Weird interaction with Forest's Curse, reflect type, burn up
new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
.soundBased(), .soundBased(),
@ -8996,8 +9210,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.triageMove(), .triageMove(),
new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GRASS) .attr(AddTypeAttr, Type.GRASS),
.edgeCase(), // Weird interaction with Trick or Treat, reflect type, burn up
new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6)
.windMove() .windMove()
.makesContact(false) .makesContact(false)
@ -9010,7 +9223,7 @@ export function initMoves() {
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY })
.attr(ForceSwitchOutAttr, true) .attr(ForceSwitchOutAttr, true)
.soundBased(), .soundBased(),
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
@ -9025,7 +9238,7 @@ export function initMoves() {
.condition(failIfLastCondition), .condition(failIfLastCondition),
new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6)
.target(MoveTarget.ALL) .target(MoveTarget.ALL)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag) }),
new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6)
.attr(TerrainChangeAttr, TerrainType.GRASSY) .attr(TerrainChangeAttr, TerrainType.GRASSY)
.target(MoveTarget.BOTH_SIDES), .target(MoveTarget.BOTH_SIDES),
@ -9057,7 +9270,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.soundBased(), .soundBased(),
new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, undefined, undefined, undefined, undefined, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true })
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6) new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6)
@ -9083,7 +9296,7 @@ export function initMoves() {
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2), .attr(StatStageChangeAttr, [ Stat.SPATK ], -2),
new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.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, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC })
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
.ignoresSubstitute() .ignoresSubstitute()
@ -9094,7 +9307,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true)
.ignoresVirtual(), .ignoresVirtual(),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES) .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)))), .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
@ -9314,7 +9527,7 @@ export function initMoves() {
new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES) .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)))), .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
@ -9371,7 +9584,7 @@ export function initMoves() {
.ballBombMove() .ballBombMove()
.makesContact(false), .makesContact(false),
new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { firstTargetOnly: true })
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7),
@ -9485,13 +9698,13 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true })
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES) .target(MoveTarget.ALL_NEAR_ENEMIES)
.edgeCase() // I assume it needs clanging scales and Kommo-O .edgeCase() // I assume it needs clanging scales and Kommo-O
.ignoresVirtual(), .ignoresVirtual(),
/* End Unused */ /* End Unused */
new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) //LGPE Implementation new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) // LGPE Implementation
.attr(CritOnlyAttr), .attr(CritOnlyAttr),
new AttackMove(Moves.SPLISHY_SPLASH, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 30, 0, 7) new AttackMove(Moves.SPLISHY_SPLASH, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 30, 0, 7)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
@ -9501,7 +9714,7 @@ export function initMoves() {
new AttackMove(Moves.PIKA_PAPOW, Type.ELECTRIC, MoveCategory.SPECIAL, -1, -1, 20, -1, 0, 7) new AttackMove(Moves.PIKA_PAPOW, Type.ELECTRIC, MoveCategory.SPECIAL, -1, -1, 20, -1, 0, 7)
.attr(FriendshipPowerAttr), .attr(FriendshipPowerAttr),
new AttackMove(Moves.BOUNCY_BUBBLE, Type.WATER, MoveCategory.SPECIAL, 60, 100, 20, -1, 0, 7) new AttackMove(Moves.BOUNCY_BUBBLE, Type.WATER, MoveCategory.SPECIAL, 60, 100, 20, -1, 0, 7)
.attr(HitHealAttr, 1.0) .attr(HitHealAttr) // Custom
.triageMove() .triageMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.BUZZY_BUZZ, Type.ELECTRIC, MoveCategory.SPECIAL, 60, 100, 20, 100, 0, 7) new AttackMove(Moves.BUZZY_BUZZ, Type.ELECTRIC, MoveCategory.SPECIAL, 60, 100, 20, 100, 0, 7)
@ -9723,8 +9936,8 @@ export function initMoves() {
.attr(ClearTerrainAttr) .attr(ClearTerrainAttr)
.condition((user, target, move) => !!user.scene.arena.terrain), .condition((user, target, move) => !!user.scene.arena.terrain),
new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true) .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true })
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true })
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8)
@ -9858,7 +10071,7 @@ export function initMoves() {
new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8)
.makesContact(false) .makesContact(false)
.attr(HighCritAttr) .attr(HighCritAttr)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, false, { effectChanceOverride: 50 })
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8)
.attr(StatusEffectAttr, StatusEffect.BURN) .attr(StatusEffectAttr, StatusEffect.BURN)
@ -9994,7 +10207,7 @@ export function initMoves() {
.attr(TeraMoveCategoryAttr) .attr(TeraMoveCategoryAttr)
.attr(TeraBlastTypeAttr) .attr(TeraBlastTypeAttr)
.attr(TeraBlastPowerAttr) .attr(TeraBlastPowerAttr)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) })
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */ .partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP) .attr(ProtectAttr, BattlerTagType.SILK_TRAP)
@ -10005,6 +10218,7 @@ export function initMoves() {
.attr(ConfuseAttr) .attr(ConfuseAttr)
.recklessMove(), .recklessMove(),
new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9)
.partial() // Counter resets every wave instead of on arena reset
.attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 100)) .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 100))
.makesContact(false), .makesContact(false),
new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
@ -10077,7 +10291,7 @@ export function initMoves() {
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(MoneyAttr) .attr(MoneyAttr)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, { firstTargetOnly: true })
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) 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) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1)
@ -10094,12 +10308,13 @@ export function initMoves() {
.makesContact(), .makesContact(),
new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9)
.attr(AddSubstituteAttr, 0.5) .attr(AddSubstituteAttr, 0.5)
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL), .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
.condition(failIfLastInPartyCondition),
new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9)
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(ChillyReceptionAttr, true), .attr(ChillyReceptionAttr, true),
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)
.attr(RemoveArenaTrapAttr, true) .attr(RemoveArenaTrapAttr, true)
.attr(RemoveAllSubstitutesAttr), .attr(RemoveAllSubstitutesAttr),
new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9)

View File

@ -1,7 +1,7 @@
import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
@ -280,7 +280,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
let numRogue = 0; let numRogue = 0;
items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) items.filter(m => m.isTransferable && !(m instanceof BerryModifier))
.forEach(m => { .forEach(m => {
const type = m.type.withTierFromPool(); const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party);
const tier = type.tier ?? ModifierTier.ULTRA; const tier = type.tier ?? ModifierTier.ULTRA;
if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) {
numRogue += m.stackCount; numRogue += m.stackCount;

View File

@ -418,7 +418,7 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie
// Populates item id and tier (order matters) // Populates item id and tier (order matters)
result = result result = result
.withIdFromFunc(modifierTypes[modifierId]) .withIdFromFunc(modifierTypes[modifierId])
.withTierFromPool(); .withTierFromPool(ModifierPoolType.PLAYER, scene.getParty());
return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result;
} }

View File

@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok
return allSpecies[species - 1]; return allSpecies[species - 1];
} }
export function getPokemonSpeciesForm(species: Species, formIndex: integer): PokemonSpeciesForm { export function getPokemonSpeciesForm(species: Species, formIndex: number): PokemonSpeciesForm {
const retSpecies: PokemonSpecies = species >= 2000 const retSpecies: PokemonSpecies = species >= 2000
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct? ? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
: allSpecies[species - 1]; : allSpecies[species - 1];
@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean;
export abstract class PokemonSpeciesForm { export abstract class PokemonSpeciesForm {
public speciesId: Species; public speciesId: Species;
public formIndex: integer; protected _formIndex: number;
public generation: integer; protected _generation: number;
public type1: Type; readonly type1: Type;
public type2: Type | null; readonly type2: Type | null;
public height: number; readonly height: number;
public weight: number; readonly weight: number;
public ability1: Abilities; readonly ability1: Abilities;
public ability2: Abilities; readonly ability2: Abilities;
public abilityHidden: Abilities; readonly abilityHidden: Abilities;
public baseTotal: integer; readonly baseTotal: number;
public baseStats: integer[]; readonly baseStats: number[];
public catchRate: integer; readonly catchRate: number;
public baseFriendship: integer; readonly baseFriendship: number;
public baseExp: integer; readonly baseExp: number;
public genderDiffs: boolean; readonly genderDiffs: boolean;
public isStarterSelectable: boolean; readonly isStarterSelectable: boolean;
constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) { catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean
) {
this.type1 = type1; this.type1 = type1;
this.type2 = type2; this.type2 = type2;
this.height = height; this.height = height;
@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
isOfType(type: integer): boolean { get generation(): number {
return this._generation;
}
set generation(generation: number) {
this._generation = generation;
}
get formIndex(): number {
return this._formIndex;
}
set formIndex(formIndex: number) {
this._formIndex = formIndex;
}
isOfType(type: number): boolean {
return this.type1 === type || (this.type2 !== null && this.type2 === type); return this.type1 === type || (this.type2 !== null && this.type2 === type);
} }
@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm {
* Method to get the total number of abilities a Pokemon species has. * Method to get the total number of abilities a Pokemon species has.
* @returns Number of abilities * @returns Number of abilities
*/ */
getAbilityCount(): integer { getAbilityCount(): number {
return this.abilityHidden !== Abilities.NONE ? 3 : 2; return this.abilityHidden !== Abilities.NONE ? 3 : 2;
} }
@ -197,7 +214,7 @@ export abstract class PokemonSpeciesForm {
* @param abilityIndex Which ability to get (should only be 0-2) * @param abilityIndex Which ability to get (should only be 0-2)
* @returns The id of the Ability * @returns The id of the Ability
*/ */
getAbility(abilityIndex: integer): Abilities { getAbility(abilityIndex: number): Abilities {
let ret: Abilities; let ret: Abilities;
if (abilityIndex === 0) { if (abilityIndex === 0) {
ret = this.ability1; ret = this.ability1;
@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
getSpriteAtlasPath(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/"); const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
} }
getSpriteId(female: boolean, formIndex?: integer, shiny?: boolean, variant: integer = 0, back?: boolean): string { getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant: number = 0, back?: boolean): string {
if (formIndex === undefined || this instanceof PokemonForm) { if (formIndex === undefined || this instanceof PokemonForm) {
formIndex = this.formIndex; formIndex = this.formIndex;
} }
@ -299,11 +316,11 @@ export abstract class PokemonSpeciesForm {
return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`; return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`;
} }
getSpriteKey(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`; return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`;
} }
abstract getFormSpriteKey(formIndex?: integer): string; abstract getFormSpriteKey(formIndex?: number): string;
/** /**
@ -311,9 +328,9 @@ export abstract class PokemonSpeciesForm {
* @param formIndex optional form index for pokemon with different forms * @param formIndex optional form index for pokemon with different forms
* @returns species id if no additional forms, index with formkey if a pokemon with a form * @returns species id if no additional forms, index with formkey if a pokemon with a form
*/ */
getVariantDataIndex(formIndex?: integer) { getVariantDataIndex(formIndex?: number) {
let formkey: string | null = null; let formkey: string | null = null;
let variantDataIndex: integer | string = this.speciesId; let variantDataIndex: number | string = this.speciesId;
const species = getPokemonSpecies(this.speciesId); const species = getPokemonSpecies(this.speciesId);
if (species.forms.length > 0 && formIndex !== undefined) { if (species.forms.length > 0 && formIndex !== undefined) {
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex); formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm {
return variantDataIndex; return variantDataIndex;
} }
getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string { getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string {
const variantDataIndex = this.getVariantDataIndex(formIndex); const variantDataIndex = this.getVariantDataIndex(formIndex);
const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]); const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]);
return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`; return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`;
} }
getIconId(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
if (formIndex === undefined) { if (formIndex === undefined) {
formIndex = this.formIndex; formIndex = this.formIndex;
} }
@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
getCryKey(formIndex?: integer): string { getCryKey(formIndex?: number): string {
let speciesId = this.speciesId; let speciesId = this.speciesId;
if (this.speciesId > 2000) { if (this.speciesId > 2000) {
switch (this.speciesId) { switch (this.speciesId) {
@ -446,7 +463,7 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean { validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean {
const rootSpeciesId = this.getRootSpeciesId(); const rootSpeciesId = this.getRootSpeciesId();
for (const moveId of moveset) { for (const moveId of moveset) {
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) { if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
@ -467,7 +484,7 @@ export abstract class PokemonSpeciesForm {
return true; return true;
} }
loadAssets(scene: BattleScene, female: boolean, formIndex?: integer, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> { loadAssets(scene: BattleScene, female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant);
scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant));
@ -536,7 +553,7 @@ export abstract class PokemonSpeciesForm {
return cry; return cry;
} }
generateCandyColors(scene: BattleScene): integer[][] { generateCandyColors(scene: BattleScene): number[][] {
const sourceTexture = scene.textures.get(this.getSpriteKey(false)); const sourceTexture = scene.textures.get(this.getSpriteKey(false));
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame]; const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const spriteColors: integer[][] = []; const spriteColors: number[][] = [];
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
const frame = sourceFrame; const frame = sourceFrame;
@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm {
} }
for (let i = 0; i < pixelData.length; i += 4) { for (let i = 0; i < pixelData.length; i += 4) {
const total = pixelData.slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0);
if (!total) { if (!total) {
continue; continue;
} }
@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm {
Math.random = originalRandom; Math.random = originalRandom;
return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as integer[]); return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]);
} }
} }
export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable { export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
public name: string; public name: string;
public subLegendary: boolean; readonly subLegendary: boolean;
public legendary: boolean; readonly legendary: boolean;
public mythical: boolean; readonly mythical: boolean;
public species: string; readonly species: string;
public growthRate: GrowthRate; readonly growthRate: GrowthRate;
public malePercent: number | null; readonly malePercent: number | null;
public genderDiffs: boolean; readonly genderDiffs: boolean;
public canChangeForm: boolean; readonly canChangeForm: boolean;
public forms: PokemonForm[]; readonly forms: PokemonForm[];
constructor(id: Species, generation: integer, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, constructor(id: Species, generation: number, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string,
type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null, catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null,
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) { genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]
) {
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
catchRate, baseFriendship, baseExp, genderDiffs, false); catchRate, baseFriendship, baseExp, genderDiffs, false);
this.speciesId = id; this.speciesId = id;
@ -631,7 +649,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
}); });
} }
getName(formIndex?: integer): string { getName(formIndex?: number): string {
if (formIndex !== undefined && this.forms.length) { if (formIndex !== undefined && this.forms.length) {
const form = this.forms[formIndex]; const form = this.forms[formIndex];
let key: string | null; let key: string | null;
@ -662,11 +680,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`); this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`);
} }
getWildSpeciesForLevel(level: integer, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species {
return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0)); return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0));
} }
getTrainerSpeciesForLevel(level: integer, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { getTrainerSpeciesForLevel(level: number, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species {
return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave); return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave);
} }
@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
* @param strength {@linkcode PartyMemberStrength} The strength of the party member in question * @param strength {@linkcode PartyMemberStrength} The strength of the party member in question
* @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. * @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance.
*/ */
private getStrengthLevelDiff(strength: PartyMemberStrength): integer { private getStrengthLevelDiff(strength: PartyMemberStrength): number {
switch (Math.min(strength, PartyMemberStrength.STRONGER)) { switch (Math.min(strength, PartyMemberStrength.STRONGER)) {
case PartyMemberStrength.WEAKEST: case PartyMemberStrength.WEAKEST:
return 60; return 60;
@ -705,7 +723,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
} }
} }
getSpeciesForLevel(level: integer, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { getSpeciesForLevel(level: number, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species {
const prevolutionLevels = this.getPrevolutionLevels(); const prevolutionLevels = this.getPrevolutionLevels();
if (prevolutionLevels.length) { if (prevolutionLevels.length) {
@ -847,7 +865,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
} }
// This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon // This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon
getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { getSimulatedEvolutionChain(currentLevel: number, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] {
const ret: EvolutionLevel[] = []; const ret: EvolutionLevel[] = [];
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) { if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
const prevolutionLevels = this.getPrevolutionLevels().reverse(); const prevolutionLevels = this.getPrevolutionLevels().reverse();
@ -899,7 +917,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId); return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId);
} }
getFormSpriteKey(formIndex?: integer) { getFormSpriteKey(formIndex?: number) {
if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) { if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) {
console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`); console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`);
formIndex = Math.min(formIndex, this.forms.length - 1); formIndex = Math.min(formIndex, this.forms.length - 1);
@ -919,16 +937,17 @@ export class PokemonForm extends PokemonSpeciesForm {
private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ]; private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ];
constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) { catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean = false, formSpriteKey: string | null = null, isStarterSelectable: boolean = false
) {
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey)); catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey));
this.formName = formName; this.formName = formName;
this.formKey = formKey; this.formKey = formKey;
this.formSpriteKey = formSpriteKey !== undefined ? formSpriteKey : null; this.formSpriteKey = formSpriteKey;
} }
getFormSpriteKey(_formIndex?: integer) { getFormSpriteKey(_formIndex?: number) {
return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey; return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey;
} }
} }

View File

@ -3,6 +3,8 @@
* or {@linkcode SwitchSummonPhase} will carry out. * or {@linkcode SwitchSummonPhase} will carry out.
*/ */
export enum SwitchType { export enum SwitchType {
/** Switchout specifically for when combat starts and the player is prompted if they will switch Pokemon */
INITIAL_SWITCH,
/** Basic switchout where the Pokemon to switch in is selected */ /** Basic switchout where the Pokemon to switch in is selected */
SWITCH, SWITCH,
/** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */ /** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */

View File

@ -10,7 +10,14 @@ import Move from "#app/data/move";
import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Terrain, TerrainType } from "#app/data/terrain"; import { Terrain, TerrainType } from "#app/data/terrain";
import { applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, PostTerrainChangeAbAttr, PostWeatherChangeAbAttr } from "#app/data/ability"; import {
applyAbAttrs,
applyPostTerrainChangeAbAttrs,
applyPostWeatherChangeAbAttrs,
PostTerrainChangeAbAttr,
PostWeatherChangeAbAttr,
TerrainEventTypeChangeAbAttr
} from "#app/data/ability";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena";
@ -217,66 +224,6 @@ export class Arena {
return 0; return 0;
} }
getTypeForBiome() {
switch (this.biomeType) {
case Biome.TOWN:
case Biome.PLAINS:
case Biome.METROPOLIS:
return Type.NORMAL;
case Biome.GRASS:
case Biome.TALL_GRASS:
return Type.GRASS;
case Biome.FOREST:
case Biome.JUNGLE:
return Type.BUG;
case Biome.SLUM:
case Biome.SWAMP:
return Type.POISON;
case Biome.SEA:
case Biome.BEACH:
case Biome.LAKE:
case Biome.SEABED:
return Type.WATER;
case Biome.MOUNTAIN:
return Type.FLYING;
case Biome.BADLANDS:
return Type.GROUND;
case Biome.CAVE:
case Biome.DESERT:
return Type.ROCK;
case Biome.ICE_CAVE:
case Biome.SNOWY_FOREST:
return Type.ICE;
case Biome.MEADOW:
case Biome.FAIRY_CAVE:
case Biome.ISLAND:
return Type.FAIRY;
case Biome.POWER_PLANT:
return Type.ELECTRIC;
case Biome.VOLCANO:
return Type.FIRE;
case Biome.GRAVEYARD:
case Biome.TEMPLE:
return Type.GHOST;
case Biome.DOJO:
case Biome.CONSTRUCTION_SITE:
return Type.FIGHTING;
case Biome.FACTORY:
case Biome.LABORATORY:
return Type.STEEL;
case Biome.RUINS:
case Biome.SPACE:
return Type.PSYCHIC;
case Biome.WASTELAND:
case Biome.END:
return Type.DRAGON;
case Biome.ABYSS:
return Type.DARK;
default:
return Type.UNKNOWN;
}
}
getBgTerrainColorRatioForBiome(): number { getBgTerrainColorRatioForBiome(): number {
switch (this.biomeType) { switch (this.biomeType) {
case Biome.SPACE: case Biome.SPACE:
@ -387,6 +334,7 @@ export class Arena {
this.scene.getField(true).filter(p => p.isOnField()).map(pokemon => { this.scene.getField(true).filter(p => p.isOnField()).map(pokemon => {
pokemon.findAndRemoveTags(t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain)); pokemon.findAndRemoveTags(t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain));
applyPostTerrainChangeAbAttrs(PostTerrainChangeAbAttr, pokemon, terrain); applyPostTerrainChangeAbAttrs(PostTerrainChangeAbAttr, pokemon, terrain);
applyAbAttrs(TerrainEventTypeChangeAbAttr, pokemon, null, false);
}); });
return true; return true;
@ -786,7 +734,7 @@ export class Arena {
case Biome.VOLCANO: case Biome.VOLCANO:
return 17.637; return 17.637;
case Biome.GRAVEYARD: case Biome.GRAVEYARD:
return 3.232; return 13.711;
case Biome.DOJO: case Biome.DOJO:
return 6.205; return 6.205;
case Biome.FACTORY: case Biome.FACTORY:

View File

@ -12,7 +12,7 @@ import * as Utils from "#app/utils";
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type";
import { getLevelTotalExp } from "#app/data/exp"; import { getLevelTotalExp } from "#app/data/exp";
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; 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, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "#app/modifier/modifier"; import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#app/data/pokeball";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#app/data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, 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, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, 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, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability";
import PokemonData from "#app/system/pokemon-data"; import PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
@ -428,38 +428,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
resolve(); resolve();
}; };
if (this.shiny) { if (this.shiny) {
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => { const populateVariantColors = (isBackSprite: boolean = false): Promise<void> => {
return new Promise(resolve => { return new Promise(resolve => {
const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
let config = variantData; let config = variantData;
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride)); const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride));
battleSpritePath.split("/").map(p => config ? config = config[p] : null); battleSpritePath.split("/").map(p => config ? config = config[p] : null);
const variantSet: VariantSet = config as VariantSet; const variantSet: VariantSet = config as VariantSet;
if (variantSet && variantSet[this.variant] === 1) { if (variantSet && variantSet[this.variant] === 1) {
if (variantColorCache.hasOwnProperty(key)) { const cacheKey = this.getBattleSpriteKey(isBackSprite);
return resolve(); if (!variantColorCache.hasOwnProperty(cacheKey)) {
this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath);
} }
this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`).
then(res => {
// Prevent the JSON from processing if it failed to load
if (!res.ok) {
console.error(`Could not load ${res.url}!`);
return;
} }
return res.json();
}).then(c => {
variantColorCache[key] = c;
resolve(); resolve();
}); });
} else {
resolve();
}
});
}; };
if (this.isPlayer()) { if (this.isPlayer()) {
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve()); Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve());
} else { } else {
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve()); populateVariantColors(false).then(() => updateFusionPaletteAndResolve());
} }
} else { } else {
updateFusionPaletteAndResolve(); updateFusionPaletteAndResolve();
@ -472,6 +460,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}); });
} }
/**
* Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on
* non-experimental sprites before giving up.
*
* @param cacheKey the cache key for the variant color sprite
* @param attemptedSpritePath the sprite path that failed to load
* @param useExpSprite was the attempted sprite experimental
* @param battleSpritePath the filename of the sprite
* @param optionalParams any additional params to log
*/
fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) {
console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams);
if (useExpSprite) {
this.populateVariantColorCache(cacheKey, false, battleSpritePath);
}
}
/**
* Attempt to process variant sprite.
*
* @param cacheKey the cache key for the variant color sprite
* @param useExpSprite should the experimental sprite be used
* @param battleSpritePath the filename of the sprite
*/
populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) {
const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`;
this.scene.cachedFetch(spritePath).then(res => {
// Prevent the JSON from processing if it failed to load
if (!res.ok) {
return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText);
}
return res.json();
}).catch(error => {
this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
}).then(c => {
variantColorCache[cacheKey] = c;
});
}
getFormKey(): string { getFormKey(): string {
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) { if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
return ""; return "";
@ -1258,6 +1285,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
// the type added to Pokemon from moves like Forest's Curse or Trick Or Treat
if (!ignoreOverride && this.summonData && this.summonData.addedType && !types.includes(this.summonData.addedType)) {
types.push(this.summonData.addedType);
}
// If both types are the same (can happen in weird custom typing scenarios), reduce to single type // If both types are the same (can happen in weird custom typing scenarios), reduce to single type
if (types.length > 1 && types[0] === types[1]) { if (types.length > 1 && types[0] === types[1]) {
types.splice(0, 1); types.splice(0, 1);
@ -2667,9 +2699,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage);
} }
/** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */
if (!ignoreAbility) { if (!ignoreAbility) {
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage); applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage);
/** Additionally apply friend guard damage reduction if ally has it. */
if (this.scene.currentBattle.double && this.getAlly()?.isActive(true)) {
applyPreDefendAbAttrs(AlliedFieldDamageReductionAbAttr, this.getAlly(), source, move, cancelled, simulated, damage);
}
} }
// This attribute may modify damage arbitrarily, so be careful about changing its order of application. // This attribute may modify damage arbitrarily, so be careful about changing its order of application.
@ -2780,7 +2818,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* We explicitly require to ignore the faint phase here, as we want to show the messages * We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase. * about the critical hit and the super effective/not very effective messages before the faint phase.
*/ */
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source);
if (damage > 0) { if (damage > 0) {
if (source.isPlayer()) { if (source.isPlayer()) {
@ -2789,10 +2827,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.gameData.gameStats.highestDamage = damage; this.scene.gameData.gameStats.highestDamage = damage;
} }
} }
source.turnData.damageDealt += damage; source.turnData.totalDamageDealt += damage;
source.turnData.currDamageDealt = damage; source.turnData.singleHitDamageDealt = damage;
this.turnData.damageTaken += damage; this.turnData.damageTaken += damage;
this.battleData.hitCount++; this.battleData.hitCount++;
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
}
}
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
this.turnData.attacksReceived.unshift(attackResult); this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) { if (source.isPlayer() && !this.isPlayer()) {
@ -2880,7 +2927,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.destroySubstitute(); this.destroySubstitute();
this.resetSummonData(); this.resetSummonData();
} }
return damage; return damage;
} }
@ -2894,12 +2940,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
* @returns integer of damage done * @returns integer of damage done
*/ */
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer {
const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
this.scene.unshiftPhase(damagePhase); this.scene.unshiftPhase(damagePhase);
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
// Damage amount may have changed, but needed to be queued before calling damage function // Damage amount may have changed, but needed to be queued before calling damage function
damagePhase.updateAmount(damage); damagePhase.updateAmount(damage);
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
return damage; return damage;
} }
@ -5083,7 +5130,6 @@ export class PokemonSummonData {
public tags: BattlerTag[] = []; public tags: BattlerTag[] = [];
public abilitySuppressed: boolean = false; public abilitySuppressed: boolean = false;
public abilitiesApplied: Abilities[] = []; public abilitiesApplied: Abilities[] = [];
public speciesForm: PokemonSpeciesForm | null; public speciesForm: PokemonSpeciesForm | null;
public fusionSpeciesForm: PokemonSpeciesForm; public fusionSpeciesForm: PokemonSpeciesForm;
public ability: Abilities = Abilities.NONE; public ability: Abilities = Abilities.NONE;
@ -5094,6 +5140,7 @@ export class PokemonSummonData {
public moveset: (PokemonMove | null)[]; public moveset: (PokemonMove | null)[];
// If not initialized this value will not be populated from save data. // If not initialized this value will not be populated from save data.
public types: Type[] = []; public types: Type[] = [];
public addedType: Type | null = null;
} }
export class PokemonBattleData { export class PokemonBattleData {
@ -5122,8 +5169,8 @@ export class PokemonTurnData {
* - `0` = Move is finished * - `0` = Move is finished
*/ */
public hitsLeft: number = -1; public hitsLeft: number = -1;
public damageDealt: number = 0; public totalDamageDealt: number = 0;
public currDamageDealt: number = 0; public singleHitDamageDealt: number = 0;
public damageTaken: number = 0; public damageTaken: number = 0;
public attacksReceived: AttackMoveResult[] = []; public attacksReceived: AttackMoveResult[] = [];
public order: number; public order: number;
@ -5131,6 +5178,8 @@ export class PokemonTurnData {
public statStagesDecreased: boolean = false; public statStagesDecreased: boolean = false;
public moveEffectiveness: TypeDamageMultiplier | null = null; public moveEffectiveness: TypeDamageMultiplier | null = null;
public combiningPledge?: Moves; public combiningPledge?: Moves;
public switchedInThisTurn: boolean = false;
public failedRunAway: boolean = false;
} }
export enum AiType { export enum AiType {

View File

@ -244,7 +244,7 @@ export class LoadingScene extends SceneBase {
this.loadAtlas("statuses", ""); this.loadAtlas("statuses", "");
this.loadAtlas("types", ""); this.loadAtlas("types", "");
} }
const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ]; const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ];
if (lang && availableLangs.includes(lang)) { if (lang && availableLangs.includes(lang)) {
this.loadImage("halloween2024-event-" + lang, "events"); this.loadImage("halloween2024-event-" + lang, "events");
} else { } else {

View File

@ -44,7 +44,7 @@ document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems
let game; let game;
const startGame = async () => { const startGame = async (manifest?: any) => {
await initI18n(); await initI18n();
const LoadingScene = (await import("./loading-scene")).LoadingScene; const LoadingScene = (await import("./loading-scene")).LoadingScene;
const BattleScene = (await import("./battle-scene")).default; const BattleScene = (await import("./battle-scene")).default;
@ -94,13 +94,15 @@ const startGame = async () => {
version: version version: version
}); });
game.sound.pauseOnBlur = false; game.sound.pauseOnBlur = false;
if (manifest) {
game["manifest"] = manifest;
}
}; };
fetch("/manifest.json") fetch("/manifest.json")
.then(res => res.json()) .then(res => res.json())
.then(jsonResponse => { .then(jsonResponse => {
startGame(); startGame(jsonResponse.manifest);
game["manifest"] = jsonResponse.manifest;
}).catch(() => { }).catch(() => {
// Manifest not found (likely local build) // Manifest not found (likely local build)
startGame(); startGame();

View File

@ -19,7 +19,7 @@ import { Unlockables } from "#app/system/unlockables";
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler";
import { getModifierTierTextTint } from "#app/ui/text"; import { getModifierTierTextTint } from "#app/ui/text";
import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
@ -121,17 +121,40 @@ export class ModifierType {
* Populates item tier for ModifierType instance * Populates item tier for ModifierType instance
* Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use)
* To find the tier, this function performs a reverse lookup of the item type in modifier pools * To find the tier, this function performs a reverse lookup of the item type in modifier pools
* It checks the weight of the item and will use the first tier for which the weight is greater than 0
* This is to allow items to be in multiple item pools depending on the conditions, for example for events
* If all tiers have a weight of 0 for the item, the first tier where the item was found is used
* @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from
* @param party optional. Needed to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc})
* if not provided or empty, the weight check will be ignored
* @param rerollCount Default `0`. Used to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc})
*/ */
withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER, party?: PlayerPokemon[], rerollCount: number = 0): ModifierType {
let defaultTier: undefined | ModifierTier;
for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const tier of Object.values(getModifierPoolForType(poolType))) {
for (const modifier of tier) { for (const modifier of tier) {
if (this.id === modifier.modifierType.id) { if (this.id === modifier.modifierType.id) {
let weight: number;
if (modifier.weight instanceof Function) {
weight = party ? modifier.weight(party, rerollCount) : 0;
} else {
weight = modifier.weight;
}
if (weight > 0) {
this.tier = modifier.modifierType.tier; this.tier = modifier.modifierType.tier;
return this; return this;
} else if (isNullOrUndefined(defaultTier)) {
// If weight is 0, keep track of the first tier where the item was found
defaultTier = modifier.modifierType.tier;
} }
} }
} }
}
// Didn't find a pool with weight > 0, fallback to first tier where the item was found, if any
if (defaultTier) {
this.tier = defaultTier;
}
return this; return this;
} }
@ -502,45 +525,25 @@ export class BerryModifierType extends PokemonHeldItemModifierType implements Ge
} }
} }
function getAttackTypeBoosterItemName(type: Type) { enum AttackTypeBoosterItem {
switch (type) { SILK_SCARF,
case Type.NORMAL: BLACK_BELT,
return "Silk Scarf"; SHARP_BEAK,
case Type.FIGHTING: POISON_BARB,
return "Black Belt"; SOFT_SAND,
case Type.FLYING: HARD_STONE,
return "Sharp Beak"; SILVER_POWDER,
case Type.POISON: SPELL_TAG,
return "Poison Barb"; METAL_COAT,
case Type.GROUND: CHARCOAL,
return "Soft Sand"; MYSTIC_WATER,
case Type.ROCK: MIRACLE_SEED,
return "Hard Stone"; MAGNET,
case Type.BUG: TWISTED_SPOON,
return "Silver Powder"; NEVER_MELT_ICE,
case Type.GHOST: DRAGON_FANG,
return "Spell Tag"; BLACK_GLASSES,
case Type.STEEL: FAIRY_FEATHER
return "Metal Coat";
case Type.FIRE:
return "Charcoal";
case Type.WATER:
return "Mystic Water";
case Type.GRASS:
return "Miracle Seed";
case Type.ELECTRIC:
return "Magnet";
case Type.PSYCHIC:
return "Twisted Spoon";
case Type.ICE:
return "Never-Melt Ice";
case Type.DRAGON:
return "Dragon Fang";
case Type.DARK:
return "Black Glasses";
case Type.FAIRY:
return "Fairy Feather";
}
} }
export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
@ -548,7 +551,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i
public boostPercent: integer; public boostPercent: integer;
constructor(moveType: Type, boostPercent: integer) { constructor(moveType: Type, boostPercent: integer) {
super("", `${getAttackTypeBoosterItemName(moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`, super("", `${AttackTypeBoosterItem[moveType]?.toLowerCase()}`,
(_type, args) => new AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent)); (_type, args) => new AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent));
this.moveType = moveType; this.moveType = moveType;
@ -556,7 +559,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i
} }
get name(): string { get name(): string {
return i18next.t(`modifierType:AttackTypeBoosterItem.${getAttackTypeBoosterItemName(this.moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`); return i18next.t(`modifierType:AttackTypeBoosterItem.${AttackTypeBoosterItem[this.moveType]?.toLowerCase()}`);
} }
getDescription(scene: BattleScene): string { getDescription(scene: BattleScene): string {
@ -2137,7 +2140,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
// Populates item id and tier // Populates item id and tier
guaranteedMod = guaranteedMod guaranteedMod = guaranteedMod
.withIdFromFunc(modifierTypes[modifierId]) .withIdFromFunc(modifierTypes[modifierId])
.withTierFromPool(); .withTierFromPool(ModifierPoolType.PLAYER, party);
const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod;
if (modType) { if (modType) {
@ -2206,7 +2209,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[],
} }
if (modifierType) { if (modifierType) {
options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(); options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(ModifierPoolType.PLAYER, party);
} }
} }
} }

View File

@ -1767,10 +1767,10 @@ export class HitHealModifier extends PokemonHeldItemModifier {
* @returns `true` if the {@linkcode Pokemon} was healed * @returns `true` if the {@linkcode Pokemon} was healed
*/ */
override apply(pokemon: Pokemon): boolean { override apply(pokemon: Pokemon): boolean {
if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) { if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) {
const scene = pokemon.scene; const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
toDmgValue(pokemon.turnData.damageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true));
} }
return true; return true;

View File

@ -10,6 +10,10 @@ import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
export class AttemptRunPhase extends PokemonPhase { export class AttemptRunPhase extends PokemonPhase {
/** For testing purposes: this is to force the pokemon to fail and escape */
public forceFailEscape = false;
constructor(scene: BattleScene, fieldIndex: number) { constructor(scene: BattleScene, fieldIndex: number) {
super(scene, fieldIndex); super(scene, fieldIndex);
} }
@ -28,7 +32,7 @@ export class AttemptRunPhase extends PokemonPhase {
applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance); applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance);
if (playerPokemon.randSeedInt(100) < escapeChance.value) { if (playerPokemon.randSeedInt(100) < escapeChance.value && !this.forceFailEscape) {
this.scene.playSound("se/flee"); this.scene.playSound("se/flee");
this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);
@ -51,6 +55,7 @@ export class AttemptRunPhase extends PokemonPhase {
this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new BattleEndPhase(this.scene));
this.scene.pushPhase(new NewBattlePhase(this.scene)); this.scene.pushPhase(new NewBattlePhase(this.scene));
} else { } else {
playerPokemon.turnData.failedRunAway = true;
this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500); this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500);
} }

View File

@ -51,7 +51,7 @@ export class CheckSwitchPhase extends BattlePhase {
this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.setMode(Mode.MESSAGE);
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, false, true)); this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.INITIAL_SWITCH, this.fieldIndex, false, true));
this.end(); this.end();
}, () => { }, () => {
this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.setMode(Mode.MESSAGE);

View File

@ -65,6 +65,15 @@ export class FaintPhase extends PokemonPhase {
} }
} }
/** In case the current pokemon was just switched in, make sure it is counted as participating in the combat */
this.scene.getPlayerField().forEach((pokemon, i) => {
if (pokemon?.isActive(true)) {
if (pokemon.isPlayer()) {
this.scene.currentBattle.addParticipant(pokemon as PlayerPokemon);
}
}
});
if (!this.tryOverrideForBattleSpec()) { if (!this.tryOverrideForBattleSpec()) {
this.doFaint(); this.doFaint();
} }

View File

@ -1,20 +1,62 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability"; import BattleScene from "#app/battle-scene";
import {
AddSecondStrikeAbAttr,
AlwaysHitAbAttr,
applyPostAttackAbAttrs,
applyPostDefendAbAttrs,
applyPreAttackAbAttrs,
IgnoreMoveEffectsAbAttr,
MaxMultiHitAbAttr,
PostAttackAbAttr,
PostDefendAbAttr,
TypeImmunityAbAttr,
} from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import {
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; BattlerTagLapseType,
DamageProtectedTag,
ProtectedTag,
SemiInvulnerableTag,
SubstituteTag,
} from "#app/data/battler-tags";
import {
applyFilteredMoveAttrs,
applyMoveAttrs,
AttackMove,
FixedDamageAttr,
HitsTagAttr,
MissEffectAttr,
MoveAttr,
MoveCategory,
MoveEffectAttr,
MoveEffectTrigger,
MoveFlags,
MoveTarget,
MultiHitAttr,
NoEffectAttr,
OneHitKOAttr,
OverrideMoveEffectAttr,
ToxicAccuracyAttr,
VariableTargetAttr,
} from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves";
import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { PokemonPhase } from "./pokemon-phase";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import {
ContactHeldItemTransferChanceModifier,
EnemyAttackStatusEffectChanceModifier,
FlinchChanceModifier,
HitHealModifier,
PokemonMultiHitModifier,
} from "#app/modifier/modifier";
import { BooleanHolder, executeIf, NumberHolder } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
export class MoveEffectPhase extends PokemonPhase { export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove; public move: PokemonMove;
@ -35,7 +77,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.targets = targets; this.targets = targets;
} }
start() { public override start(): void {
super.start(); super.start();
/** The Pokemon using this phase's invoked move */ /** The Pokemon using this phase's invoked move */
@ -52,12 +94,12 @@ export class MoveEffectPhase extends PokemonPhase {
* Does an effect from this move override other effects on this turn? * Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use. * e.g. Charging moves (Fly, etc.) on their first turn of use.
*/ */
const overridden = new Utils.BooleanHolder(false); const overridden = new BooleanHolder(false);
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
const move = this.move.getMove(); const move = this.move.getMove();
// Assume single target for override // Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => {
// If other effects were overriden, stop this phase before they can be applied // If other effects were overriden, stop this phase before they can be applied
if (overridden.value) { if (overridden.value) {
return this.end(); return this.end();
@ -71,14 +113,14 @@ export class MoveEffectPhase extends PokemonPhase {
* effects of the move itself, Parental Bond, and Multi-Lens to do so. * effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/ */
if (user.turnData.hitsLeft === -1) { if (user.turnData.hitsLeft === -1) {
const hitCount = new Utils.IntegerHolder(1); const hitCount = new NumberHolder(1);
// Assume single target for multi hit // Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, double the hit count // If Parental Bond is applicable, double the hit count
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0)); applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0));
// If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0));
} }
// Set the user's relevant turnData fields to reflect the final hit count // Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value; user.turnData.hitCount = hitCount.value;
@ -100,7 +142,8 @@ export class MoveEffectPhase extends PokemonPhase {
const hasActiveTargets = targets.some(t => t.isActive(true)); const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0].getTag(SemiInvulnerableTag); && !targets[0].getTag(SemiInvulnerableTag);
/** /**
@ -111,7 +154,7 @@ export class MoveEffectPhase extends PokemonPhase {
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" })); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS; moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, move); applyMoveAttrs(MissEffectAttr, user, null, move);
} else { } else {
@ -127,29 +170,39 @@ export class MoveEffectPhase extends PokemonPhase {
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target // Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; let hasHit: boolean = false;
for (const target of targets) { for (const target of targets) {
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
continue;
}
/** The {@linkcode ArenaTagSide} to which the target belongs */ /** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
const hasConditionalProtectApplied = new Utils.BooleanHolder(false); const hasConditionalProtectApplied = new BooleanHolder(false);
/** Does the applied conditional protection bypass Protect-ignoring effects? */ /** Does the applied conditional protection bypass Protect-ignoring effects? */
const bypassIgnoreProtect = new Utils.BooleanHolder(false); const bypassIgnoreProtect = new BooleanHolder(false);
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
if (!this.move.getMove().isAllyTarget()) { if (!this.move.getMove().isAllyTarget()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
} }
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) const isProtected = (
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) bypassIgnoreProtect.value
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value
|| (!target.findTags(t => t instanceof DamageProtectedTag).length
&& target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !target.getTag(SemiInvulnerableTag); && !target.getTag(SemiInvulnerableTag);
/** /**
@ -218,7 +271,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Does this phase represent the invoked move's last strike? */ /** Does this phase represent the invoked move's last strike? */
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive());
/** /**
* If the user can change forms by using the invoked move, * If the user can change forms by using the invoked move,
@ -234,85 +287,48 @@ export class MoveEffectPhase extends PokemonPhase {
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result. * type requires different conditions to be met with respect to the move's hit result.
*/ */
applyAttrs.push(new Promise(resolve => { const k = new Promise<void>((resolve) => {
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) //Start promise chain and apply PRE_APPLY move attributes
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) =>
user, target, move).then(() => { attr instanceof MoveEffectAttr
// All other effects require the move to not have failed or have been cancelled to trigger && attr.trigger === MoveEffectTrigger.PRE_APPLY
if (hitResult !== HitResult.FAIL) { && (!attr.firstHitOnly || firstHit)
/** && (!attr.lastHitOnly || lastHit)
* If the invoked move's effects are meant to trigger during the move's "charge turn," && hitResult !== HitResult.NO_EFFECT, user, target, move);
* ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects. /** Don't complete if the move failed */
*/ if (hitResult === HitResult.FAIL) {
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
// All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
/**
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
// If the move was not protected against, apply all HIT effects
Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
// Only apply the following effects if the move was not deflected by a substitute
if (move.hitsSubstitute(user, target)) {
return resolve(); return resolve();
} }
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens /** Apply Move/Ability Effects in correct order */
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { promiseChain = promiseChain
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); .then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
})).then(() => { if (hitResult !== HitResult.NO_EFFECT) {
// Apply the user's post-attack ability effects promiseChain
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { .then(this.applyPostApplyEffects(user, target, firstHit, lastHit))
/** .then(this.applyHeldItemFlinchCheck(user, target, dealsDamage))
* If the invoked move is an attack, apply the user's chance to .then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
* steal an item from the target granted by Grip Claw .then(() => resolve());
*/
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
resolve();
});
});
})
).then(() => resolve());
});
} else { } else {
applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); promiseChain
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
.then(resolve);
} }
}); });
} else {
resolve(); applyAttrs.push(k);
}
});
}));
} }
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ? const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
null; null;
if (!!postTarget) { if (postTarget) {
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); applyAttrs[applyAttrs.length - 1].then(() => postTarget);
} else { // Otherwise, push a new asynchronous move effect } else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget); applyAttrs.push(postTarget);
} }
@ -327,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase {
*/ */
targets.forEach(target => { targets.forEach(target => {
const substitute = target.getTag(SubstituteTag); const substitute = target.getTag(SubstituteTag);
if (!!substitute && substitute.hp <= 0) { if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE); target.lapseTag(BattlerTagType.SUBSTITUTE);
} }
}); });
@ -337,7 +353,7 @@ export class MoveEffectPhase extends PokemonPhase {
}); });
} }
end() { public override end(): void {
const user = this.getUserPokemon(); const user = this.getUserPokemon();
/** /**
* If this phase isn't for the invoked move's last strike, * If this phase isn't for the invoked move's last strike,
@ -347,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase {
* to the user. * to the user.
*/ */
if (user) { if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase()); this.scene.unshiftPhase(this.getNewHitPhase());
} else { } else {
// Queue message for number of hits made by multi-move // Queue message for number of hits made by multi-move
@ -367,11 +383,135 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** /**
* Resolves whether this phase's invoked move hits or misses the given target * Apply self-targeted effects that trigger `POST_APPLY`
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move *
* @returns `true` if the move does not miss the target; `false` otherwise * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/ */
hitCheck(target: Pokemon): boolean { protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies non-self-targeted effects that trigger `POST_APPLY`
* (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& !attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies effects that trigger on HIT
* (i.e. Final Gambit, Power-Up Punch, Drain Punch)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> {
return applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit)
&& (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove());
}
/**
* Applies reactive effects that occur when a Pokémon is hit.
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @returns a `Promise` intended to be passed into a `then()` call.
*/
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> {
return executeIf(!target.isFainted() || target.canApplyAbility(), () =>
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult)
.then(() => {
if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
}
})
);
}
/**
* Applies all effects and attributes that require a move to connect with a target,
* namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param isProtected - `true` if the target is protected by effects such as Protect
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> {
return () => executeIf(!isProtected, () =>
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() =>
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() =>
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
})
);
}
/**
* Handles checking for and applying Flinches
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param dealsDamage - `true` if the attempted move successfully dealt damage
* @returns a function intended to be passed into a `then()` call.
*/
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
return () => {
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
};
}
/**
* Resolves whether this phase's invoked move hits the given target
* @param target - The {@linkcode Pokemon} targeted by the invoked move
* @returns `true` if the move hits the target
*/
public hitCheck(target: Pokemon): boolean {
// Moves targeting the user and entry hazards can't miss // Moves targeting the user and entry hazards can't miss
if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) { if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) {
return true; return true;
@ -425,29 +565,29 @@ export class MoveEffectPhase extends PokemonPhase {
return rand < (moveAccuracy * accuracyMultiplier); return rand < (moveAccuracy * accuracyMultiplier);
} }
/** Returns the {@linkcode Pokemon} using this phase's invoked move */ /** @returns The {@linkcode Pokemon} using this phase's invoked move */
getUserPokemon(): Pokemon | undefined { public getUserPokemon(): Pokemon | undefined {
if (this.battlerIndex > BattlerIndex.ENEMY_2) { if (this.battlerIndex > BattlerIndex.ENEMY_2) {
return this.scene.getPokemonById(this.battlerIndex) ?? undefined; return this.scene.getPokemonById(this.battlerIndex) ?? undefined;
} }
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
} }
/** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ /** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */
getTargets(): Pokemon[] { public getTargets(): Pokemon[] {
return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
} }
/** Returns the first target of this phase's invoked move */ /** @returns The first target of this phase's invoked move */
getTarget(): Pokemon | undefined { public getFirstTarget(): Pokemon | undefined {
return this.getTargets()[0]; return this.getTargets()[0];
} }
/** /**
* Removes the given {@linkcode Pokemon} from this phase's target list * Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed * @param target - The {@linkcode Pokemon} to be removed
*/ */
removeTarget(target: Pokemon): void { protected removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
if (targetIndex !== -1) { if (targetIndex !== -1) {
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
@ -459,23 +599,25 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target {@linkcode Pokemon} if defined, only stop subsequent * @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon * strikes against this Pokemon
*/ */
stopMultiHit(target?: Pokemon): void { public stopMultiHit(target?: Pokemon): void {
/** If given a specific target, remove the target from subsequent strikes */ // If given a specific target, remove the target from subsequent strikes
if (target) { if (target) {
this.removeTarget(target); this.removeTarget(target);
} }
/** const user = this.getUserPokemon();
* If no target specified, or the specified target was the last of this move's if (!user) {
* targets, completely cancel all subsequent strikes. return;
*/ }
// If no target specified, or the specified target was the last of this move's
// targets, completely cancel all subsequent strikes.
if (!target || this.targets.length === 0 ) { if (!target || this.targets.length === 0 ) {
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? user.turnData.hitCount = 1;
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? user.turnData.hitsLeft = 1;
} }
} }
/** Returns a new MoveEffectPhase with the same properties as this phase */ /** @returns A new `MoveEffectPhase` with the same properties as this phase */
getNewHitPhase() { protected getNewHitPhase(): MoveEffectPhase {
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
} }
} }

View File

@ -3,7 +3,7 @@ import BattleScene from "#app/battle-scene";
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability"; import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability";
import { CommonAnim } from "#app/data/battle-anims"; import { CommonAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, frenzyMissFunc, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
@ -470,6 +470,10 @@ export class MovePhase extends BattlePhase {
this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
} }
if (this.cancelled && this.pokemon.summonData?.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) {
frenzyMissFunc(this.pokemon, this.move.getMove());
}
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);

View File

@ -1,6 +1,6 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability";
import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims";
import { getStatusEffectActivationText } from "#app/data/status-effect"; import { getStatusEffectActivationText } from "#app/data/status-effect";
import { BattleSpec } from "#app/enums/battle-spec"; import { BattleSpec } from "#app/enums/battle-spec";
@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
pokemon.updateInfo(); pokemon.updateInfo();
applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []);
} }
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end()); new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end());
} else { } else {

View File

@ -65,7 +65,7 @@ export class StatStageChangePhase extends PokemonPhase {
if (!this.selfTarget && stages.value < 0) { if (!this.selfTarget && stages.value < 0) {
// TODO: add a reference to the source of the stat change to fix Infiltrator interaction // TODO: add a reference to the source of the stat change to fix Infiltrator interaction
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, null, false, cancelled); this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, null, cancelled);
} }
if (!cancelled.value && !this.selfTarget && stages.value < 0) { if (!cancelled.value && !this.selfTarget && stages.value < 0) {

View File

@ -64,10 +64,8 @@ export class SwitchSummonPhase extends SummonPhase {
} }
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) {
if (this.switchType === SwitchType.SWITCH) {
const substitute = pokemon.getTag(SubstituteTag); const substitute = pokemon.getTag(SubstituteTag);
if (substitute) { if (substitute) {
this.scene.tweens.add({ this.scene.tweens.add({
@ -186,6 +184,11 @@ export class SwitchSummonPhase extends SummonPhase {
} }
} }
if (this.switchType !== SwitchType.INITIAL_SWITCH) {
pokemon.resetTurnData();
pokemon.turnData.switchedInThisTurn = true;
}
this.lastPokemon?.resetSummonData(); this.lastPokemon?.resetSummonData();
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);

View File

@ -205,11 +205,11 @@ export class TurnStartPhase extends FieldPhase {
} }
this.scene.pushPhase(new WeatherEffectPhase(this.scene)); this.scene.pushPhase(new WeatherEffectPhase(this.scene));
this.scene.pushPhase(new BerryPhase(this.scene));
/** Add a new phase to check who should be taking status damage */ /** Add a new phase to check who should be taking status damage */
this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, moveOrder)); this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, moveOrder));
this.scene.pushPhase(new BerryPhase(this.scene));
this.scene.pushPhase(new TurnEndPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene));
/** /**

View File

@ -153,7 +153,7 @@ export async function initI18n(): Promise<void> {
i18next.use(new KoreanPostpositionProcessor()); i18next.use(new KoreanPostpositionProcessor());
await i18next.init({ await i18next.init({
fallbackLng: "en", fallbackLng: "en",
supportedLngs: [ "en", "es", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca-ES", "da" ], supportedLngs: [ "en", "es-ES", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca-ES", "da" ],
backend: { backend: {
loadPath(lng: string, [ ns ]: string[]) { loadPath(lng: string, [ ns ]: string[]) {
let fileName: string; let fileName: string;
@ -164,7 +164,7 @@ export async function initI18n(): Promise<void> {
} else { } else {
fileName = camelCaseToKebabCase(ns); fileName = camelCaseToKebabCase(ns);
} }
return `/locales/${lng}/${fileName}.json?v=${pkg.version}`; return `./locales/${lng}/${fileName}.json?v=${pkg.version}`;
}, },
}, },
defaultNS: "menu", defaultNS: "menu",

View File

@ -866,8 +866,8 @@ export function setSetting(scene: BattleScene, setting: string, value: integer):
handler: () => changeLocaleHandler("en") handler: () => changeLocaleHandler("en")
}, },
{ {
label: "Español", label: "Español (ES)",
handler: () => changeLocaleHandler("es") handler: () => changeLocaleHandler("es-ES")
}, },
{ {
label: "Italiano", label: "Italiano",

View File

@ -0,0 +1,120 @@
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BattlerIndex } from "#app/battle";
import { allAbilities } from "#app/data/ability";
import { allMoves, MoveCategory } from "#app/data/move";
describe("Moves - Friend Guard", () => {
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("double")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([ Moves.TACKLE, Moves.SPLASH, Moves.DRAGON_RAGE ])
.enemySpecies(Species.SHUCKLE)
.moveset([ Moves.SPLASH ])
.startingLevel(100);
});
it("should reduce damage that other allied Pokémon receive from attacks (from any Pokémon) by 25%", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
const [ player1, player2 ] = game.scene.getPlayerField();
const spy = vi.spyOn(player1, "getAttackDamage");
const enemy1 = game.scene.getEnemyField()[0];
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Get the last return value from `getAttackDamage`
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding)
expect(turn1Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL)));
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Get the last return value from `getAttackDamage`
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// With the ally's Friend Guard, damage should have been reduced from base damage by 25%
expect(turn2Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL) * 0.75));
});
it("should NOT reduce damage to pokemon with friend guard", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
const player2 = game.scene.getPlayerField()[1];
const spy = vi.spyOn(player2, "getAttackDamage");
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
expect(turn2Damage).toBe(turn1Damage);
});
it("should NOT reduce damage from fixed damage attacks", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
const [ player1, player2 ] = game.scene.getPlayerField();
const spy = vi.spyOn(player1, "getAttackDamage");
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
expect(turn1Damage).toBe(40);
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
expect(turn2Damage).toBe(40);
});
});

View File

@ -0,0 +1,91 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Type } from "#app/data/type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Mimicry", () => {
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
.moveset([ Moves.SPLASH ])
.ability(Abilities.MIMICRY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH);
});
it("Mimicry activates after the Pokémon with Mimicry is switched in while terrain is present, or whenever there is a change in terrain", async () => {
game.override.enemyAbility(Abilities.MISTY_SURGE);
await game.classicMode.startBattle([ Species.FEEBAS, Species.ABRA ]);
const [ playerPokemon1, playerPokemon2 ] = game.scene.getParty();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon1.getTypes().includes(Type.FAIRY)).toBe(true);
game.doSwitchPokemon(1);
await game.toNextTurn();
expect(playerPokemon2.getTypes().includes(Type.FAIRY)).toBe(true);
});
it("Pokemon should revert back to its original, root type once terrain ends", async () => {
game.override
.moveset([ Moves.SPLASH, Moves.TRANSFORM ])
.enemyAbility(Abilities.MIMICRY)
.enemyMoveset([ Moves.SPLASH, Moves.PSYCHIC_TERRAIN ]);
await game.classicMode.startBattle([ Species.REGIELEKI ]);
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(Moves.TRANSFORM);
await game.forceEnemyMove(Moves.PSYCHIC_TERRAIN);
await game.toNextTurn();
expect(playerPokemon?.getTypes().includes(Type.PSYCHIC)).toBe(true);
if (game.scene.arena.terrain) {
game.scene.arena.terrain.turnsLeft = 1;
}
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon?.getTypes().includes(Type.ELECTRIC)).toBe(true);
});
it("If the Pokemon is under the effect of a type-adding move and an equivalent terrain activates, the move's effect disappears", async () => {
game.override
.enemyMoveset([ Moves.FORESTS_CURSE, Moves.GRASSY_TERRAIN ]);
await game.classicMode.startBattle([ Species.FEEBAS ]);
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.FORESTS_CURSE);
await game.toNextTurn();
expect(playerPokemon?.summonData.addedType).toBe(Type.GRASS);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.GRASSY_TERRAIN);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon?.summonData.addedType).toBeNull();
expect(playerPokemon?.getTypes().includes(Type.GRASS)).toBe(true);
});
});

View File

@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => {
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(30); expect(chance.value).toBe(30);
}, 20000); }, 20000);
@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => {
expect(move.id).toBe(Moves.AIR_SLASH); expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(60); expect(chance.value).toBe(60);
}, 20000); }, 20000);

View File

@ -60,8 +60,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
expect(power.value).toBe(move.power * 5461 / 4096); expect(power.value).toBe(move.power * 5461 / 4096);
@ -93,8 +93,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1); expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power); expect(power.value).toBe(move.power);
@ -126,8 +126,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1); expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power); expect(power.value).toBe(move.power);
@ -161,7 +161,7 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
const user = phase.getUserPokemon()!; const user = phase.getUserPokemon()!;
const target = phase.getTarget()!; const target = phase.getFirstTarget()!;
const opponentType = target.getTypes()[0]; const opponentType = target.getTypes()[0];
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);

View File

@ -57,8 +57,8 @@ describe("Abilities - Shield Dust", () => {
expect(move.id).toBe(Moves.AIR_SLASH); expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance); applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance);
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
}, 20000); }, 20000);

View File

@ -0,0 +1,125 @@
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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#app/ui/command-ui-handler";
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
describe("Abilities - Speed 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.DRAGAPULT)
.ability(Abilities.SPEED_BOOST)
.enemyMoveset(Moves.SPLASH)
.moveset([ Moves.SPLASH, Moves.U_TURN ]);
});
it("should increase speed by 1 stage at end of turn",
async () => {
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1);
});
it("should not trigger this turn if pokemon was switched into combat via attack, but the turn after",
async () => {
await game.classicMode.startBattle([
Species.SHUCKLE,
Species.NINJASK
]);
game.move.select(Moves.U_TURN);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
const playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1);
});
it("checking back to back swtiches",
async () => {
await game.classicMode.startBattle([
Species.SHUCKLE,
Species.NINJASK
]);
game.move.select(Moves.U_TURN);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
let playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.U_TURN);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1);
});
it("should not trigger this turn if pokemon was switched into combat via normal switch, but the turn after",
async () => {
await game.classicMode.startBattle([
Species.SHUCKLE,
Species.NINJASK
]);
game.doSwitchPokemon(1);
await game.toNextTurn();
const playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1);
});
it("should not trigger if pokemon fails to escape",
async () => {
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
commandPhase.handleCommand(Command.RUN, 0);
const runPhase = game.scene.getCurrentPhase() as AttemptRunPhase;
runPhase.forceFailEscape = true;
await game.phaseInterceptor.to(AttemptRunPhase);
await game.toNextTurn();
const playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1);
});
});

View File

@ -0,0 +1,614 @@
import { BattlerIndex } from "#app/battle";
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import GameManager from "#app/test/utils/gameManager";
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 { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Wimp Out", () => {
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")
.ability(Abilities.WIMP_OUT)
.enemySpecies(Species.NINJASK)
.enemyPassiveAbility(Abilities.NO_GUARD)
.startingLevel(90)
.enemyLevel(70)
.moveset([ Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE ])
.enemyMoveset(Moves.FALSE_SWIPE)
.disableCrits();
});
function confirmSwitch(): void {
const [ pokemon1, pokemon2 ] = game.scene.getParty();
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD);
expect(pokemon2.species.speciesId).toBe(Species.WIMPOD);
expect(pokemon2.isFainted()).toBe(false);
expect(pokemon2.getHpRatio()).toBeLessThan(0.5);
}
function confirmNoSwitch(): void {
const [ pokemon1, pokemon2 ] = game.scene.getParty();
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD);
expect(pokemon1.species.speciesId).toBe(Species.WIMPOD);
expect(pokemon1.isFainted()).toBe(false);
expect(pokemon1.getHpRatio()).toBeLessThan(0.5);
}
it("triggers regenerator passive single time when switching out with wimp out", async () => {
game.override
.passiveAbility(Abilities.REGENERATOR)
.startingLevel(5)
.enemyLevel(100);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const wimpod = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(wimpod.hp).toEqual(Math.floor(wimpod.getMaxHp() * 0.33 + 1));
confirmSwitch();
});
it("It makes wild pokemon flee if triggered", async () => {
game.override.enemyAbility(Abilities.WIMP_OUT);
await game.classicMode.startBattle([
Species.GOLISOPOD,
Species.TYRUNT
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
enemyPokemon.hp *= 0.52;
game.move.select(Moves.FALSE_SWIPE);
await game.phaseInterceptor.to("BerryPhase");
const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true);
});
it("Does not trigger when HP already below half", async () => {
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp = 5;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(wimpod.hp).toEqual(1);
confirmNoSwitch();
});
it("Trapping moves do not prevent Wimp Out from activating.", async () => {
game.override
.enemyMoveset([ Moves.SPIRIT_SHACKLE ])
.startingLevel(53)
.enemyLevel(45);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TRAPPED)).toBeUndefined();
expect(game.scene.getParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined();
confirmSwitch();
});
it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => {
game.override
.startingLevel(95)
.enemyMoveset([ Moves.U_TURN ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
const hasFled = enemyPokemon.switchOutStatus;
expect(hasFled).toBe(false);
confirmSwitch();
});
it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => {
game.override
.startingLevel(190)
.startingWave(8)
.enemyMoveset([ Moves.U_TURN ]);
await game.classicMode.startBattle([
Species.GOLISOPOD,
Species.TYRUNT
]);
const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1);
});
it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => {
game.override
.startingLevel(69)
.enemyMoveset([ Moves.DRAGON_TAIL ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const wimpod = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("SwitchSummonPhase", false);
expect(wimpod.summonData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
});
it("triggers when recoil damage is taken", async () => {
game.override
.moveset([ Moves.HEAD_SMASH ])
.enemyMoveset([ Moves.SPLASH ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.HEAD_SMASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("It does not activate when the Pokémon cuts its own HP", async () => {
game.override
.moveset([ Moves.SUBSTITUTE ])
.enemyMoveset([ Moves.SPLASH ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.52;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("Does not trigger when neutralized", async () => {
game.override
.enemyAbility(Abilities.NEUTRALIZING_GAS)
.startingLevel(5);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery", async () => {
game.override
.moveset([ Moves.DOUBLE_EDGE ])
.enemyMoveset([ Moves.SPLASH ])
.startingHeldItems([
{ name: "SHELL_BELL", count: 3 },
{ name: "HEALING_CHARM", count: 5 },
]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.75;
game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getParty()[1].getHpRatio()).toBeGreaterThan(0.5);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT);
});
it("Wimp Out will activate due to weather damage", async () => {
game.override
.weather(WeatherType.HAIL)
.enemyMoveset([ Moves.SPLASH ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("Does not trigger when enemy has sheer force", async () => {
game.override
.enemyAbility(Abilities.SHEER_FORCE)
.enemyMoveset(Moves.SLUDGE_BOMB)
.startingLevel(95);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("Wimp Out will activate due to post turn status damage", async () => {
game.override
.statusEffect(StatusEffect.POISON)
.enemyMoveset([ Moves.SPLASH ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to bad dreams", async () => {
game.override
.statusEffect(StatusEffect.SLEEP)
.enemyAbility(Abilities.BAD_DREAMS);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to leech seed", async () => {
game.override
.enemyMoveset([ Moves.LEECH_SEED ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to curse damage", async () => {
game.override
.enemySpecies(Species.DUSKNOIR)
.enemyMoveset([ Moves.CURSE ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to salt cure damage", async () => {
game.override
.enemySpecies(Species.NACLI)
.enemyMoveset([ Moves.SALT_CURE ])
.enemyLevel(1);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.70;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to damaging trap damage", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.enemyMoveset([ Moves.WHIRLPOOL ])
.enemyLevel(1);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.55;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override
.passiveAbility(Abilities.MAGIC_GUARD)
.enemyMoveset([ Moves.LEECH_SEED ])
.weather(WeatherType.HAIL)
.statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getParty()[0].getHpRatio()).toEqual(0.51);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
});
it("Wimp Out activating should not cancel a double battle", async () => {
game.override
.battleType("double")
.enemyAbility(Abilities.WIMP_OUT)
.enemyMoveset([ Moves.SPLASH ])
.enemyLevel(1);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const enemyLeadPokemon = game.scene.getEnemyParty()[0];
const enemySecPokemon = game.scene.getEnemyParty()[1];
game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
const isVisibleLead = enemyLeadPokemon.visible;
const hasFledLead = enemyLeadPokemon.switchOutStatus;
const isVisibleSec = enemySecPokemon.visible;
const hasFledSec = enemySecPokemon.switchOutStatus;
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
});
it("Wimp Out will activate due to aftermath", async () => {
game.override
.moveset([ Moves.THUNDER_PUNCH ])
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.AFTERMATH)
.enemyMoveset([ Moves.SPLASH ])
.enemyLevel(1);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.THUNDER_PUNCH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("Activates due to entry hazards", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override
.enemySpecies(Species.CENTISKORCH)
.enemyAbility(Abilities.WIMP_OUT)
.startingWave(4);
await game.classicMode.startBattle([
Species.TYRUNT
]);
expect(game.phaseInterceptor.log).not.toContain("MovePhase");
expect(game.phaseInterceptor.log).toContain("BattleEndPhase");
});
it("Wimp Out will activate due to Nightmare", async () => {
game.override
.enemyMoveset([ Moves.NIGHTMARE ])
.statusEffect(StatusEffect.SLEEP);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.65;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("triggers status on the wimp out user before a new pokemon is switched in", async () => {
game.override
.enemyMoveset(Moves.SLUDGE_BOMB)
.startingLevel(80);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getParty()[1].status?.effect).toEqual(StatusEffect.POISON);
confirmSwitch();
});
it("triggers after last hit of multi hit move", async () => {
game.override
.enemyMoveset(Moves.BULLET_SEED)
.enemyAbility(Abilities.SKILL_LINK);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
expect(enemyPokemon.turnData.hitCount).toBe(5);
confirmSwitch();
});
it("triggers after last hit of multi hit move (multi lens)", async () => {
game.override
.enemyMoveset(Moves.TACKLE)
.enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
expect(enemyPokemon.turnData.hitCount).toBe(2);
confirmSwitch();
});
it("triggers after last hit of Parental Bond", async () => {
game.override
.enemyMoveset(Moves.TACKLE)
.enemyAbility(Abilities.PARENTAL_BOND);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
expect(enemyPokemon.turnData.hitCount).toBe(2);
confirmSwitch();
});
// TODO: This interaction is not implemented yet
it.todo("Wimp Out will not activate if the Pokémon's HP falls below half due to hurting itself in confusion", async () => {
game.override
.moveset([ Moves.SWORDS_DANCE ])
.enemyMoveset([ Moves.SWAGGER ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.hp *= 0.51;
playerPokemon.setStatStage(Stat.ATK, 6);
playerPokemon.addTag(BattlerTagType.CONFUSED);
// TODO: add helper function to force confusion self-hits
while (playerPokemon.getHpRatio() > 0.49) {
game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to("TurnEndPhase");
}
confirmNoSwitch();
});
});

View File

@ -1,4 +1,7 @@
import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; import { speciesEggTiers } from "#app/data/balance/species-egg-tiers";
import { speciesStarterCosts } from "#app/data/balance/starters";
import { Egg, getLegendaryGachaSpeciesForTimestamp, getValidLegendaryGachaSpecies } from "#app/data/egg";
import { allSpecies } from "#app/data/pokemon-species";
import { EggSourceType } from "#app/enums/egg-source-types"; import { EggSourceType } from "#app/enums/egg-source-types";
import { EggTier } from "#app/enums/egg-type"; import { EggTier } from "#app/enums/egg-type";
import { VariantTier } from "#app/enums/variant-tier"; import { VariantTier } from "#app/enums/variant-tier";
@ -64,6 +67,12 @@ describe("Egg Generation Tests", () => {
expect(gachaSpeciesCount).toBeGreaterThan(0.4 * EGG_HATCH_COUNT); expect(gachaSpeciesCount).toBeGreaterThan(0.4 * EGG_HATCH_COUNT);
expect(gachaSpeciesCount).toBeLessThan(0.6 * EGG_HATCH_COUNT); expect(gachaSpeciesCount).toBeLessThan(0.6 * EGG_HATCH_COUNT);
}); });
it("should never be allowed to generate Eternatus via the legendary gacha", () => {
const validLegendaryGachaSpecies = getValidLegendaryGachaSpecies();
expect(validLegendaryGachaSpecies.every(s => speciesEggTiers[s] === EggTier.LEGENDARY)).toBe(true);
expect(validLegendaryGachaSpecies.every(s => allSpecies[s].isObtainable())).toBe(true);
expect(validLegendaryGachaSpecies.includes(Species.ETERNATUS)).toBe(false);
});
it("should hatch an Arceus. Set from species", () => { it("should hatch an Arceus. Set from species", () => {
const scene = game.scene; const scene = game.scene;
const expectedSpecies = Species.ARCEUS; const expectedSpecies = Species.ARCEUS;
@ -376,4 +385,23 @@ describe("Egg Generation Tests", () => {
expect(diffShiny).toBe(true); expect(diffShiny).toBe(true);
expect(diffAbility).toBe(true); expect(diffAbility).toBe(true);
}); });
// For now, we are using this test to detect oversights in egg tiers.
// Delete this test if the balance team rebalances species costs independently of egg tiers.
it("should have correct egg tiers based on species costs", () => {
const getExpectedEggTier = (starterCost) =>
starterCost <= 3 ? EggTier.COMMON
: starterCost <= 5 ? EggTier.RARE
: starterCost <= 7 ? EggTier.EPIC
: EggTier.LEGENDARY;
allSpecies.forEach(pokemonSpecies => {
const rootSpecies = pokemonSpecies.getRootSpeciesId();
const speciesCost = speciesStarterCosts[rootSpecies];
const expectedEggTier = getExpectedEggTier(speciesCost);
const actualEggTier = speciesEggTiers[rootSpecies];
expect(actualEggTier).toBe(expectedEggTier);
});
});
}); });

View File

@ -0,0 +1,49 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TerrainType } from "#app/data/terrain";
import { Type } from "#app/data/type";
import { BattlerIndex } from "#app/battle";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Camouflage", () => {
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
.moveset([ Moves.CAMOUFLAGE ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.REGIELEKI)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.PSYCHIC_TERRAIN);
});
it("Camouflage should look at terrain first when selecting a type to change into", async () => {
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.CAMOUFLAGE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTerrainType()).toBe(TerrainType.PSYCHIC);
const pokemonType = playerPokemon.getTypes()[0];
expect(pokemonType).toBe(Type.PSYCHIC);
});
});

View File

@ -0,0 +1,42 @@
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";
describe("Moves - Chloroblast", () => {
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
.moveset([ Moves.CHLOROBLAST ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.PROTECT);
});
it("should not deal recoil damage if the opponent uses protect", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.CHLOROBLAST);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBe(true);
});
});

View File

@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120);
}, 20000); }, 20000);
@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140);
}, 20000); }, 20000);
@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160);
}, 20000); }, 20000);
@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180);
}, 20000); }, 20000);
@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);

View File

@ -59,7 +59,7 @@ describe("Moves - Focus Punch", () => {
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
expect(leadPokemon.getMoveHistory().length).toBe(1); expect(leadPokemon.getMoveHistory().length).toBe(1);
expect(leadPokemon.turnData.damageDealt).toBe(enemyStartingHp - enemyPokemon.hp); expect(leadPokemon.turnData.totalDamageDealt).toBe(enemyStartingHp - enemyPokemon.hp);
} }
); );
@ -86,7 +86,7 @@ describe("Moves - Focus Punch", () => {
expect(enemyPokemon.hp).toBe(enemyStartingHp); expect(enemyPokemon.hp).toBe(enemyStartingHp);
expect(leadPokemon.getMoveHistory().length).toBe(1); expect(leadPokemon.getMoveHistory().length).toBe(1);
expect(leadPokemon.turnData.damageDealt).toBe(0); expect(leadPokemon.turnData.totalDamageDealt).toBe(0);
} }
); );

View File

@ -0,0 +1,47 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Type } from "#app/data/type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Forest's Curse", () => {
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
.moveset([ Moves.FORESTS_CURSE, Moves.TRICK_OR_TREAT ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("will replace the added type from Trick Or Treat", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const enemyPokemon = game.scene.getEnemyPokemon();
game.move.select(Moves.TRICK_OR_TREAT);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon!.summonData.addedType).toBe(Type.GHOST);
game.move.select(Moves.FORESTS_CURSE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon?.summonData.addedType).toBe(Type.GRASS);
});
});

View File

@ -0,0 +1,49 @@
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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Mist", () => {
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
.moveset([ Moves.MIST, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("double")
.disableCrits()
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.GROWL);
});
it("should prevent the user's side from having stats lowered", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const playerPokemon = game.scene.getPlayerField();
game.move.select(Moves.MIST, 0);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
playerPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
});
it.todo("should be ignored by opponents with Infiltrator");
});

View File

@ -0,0 +1,59 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Type } from "#app/data/type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Reflect Type", () => {
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
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemyAbility(Abilities.BALL_FETCH);
});
it("will make the user Normal/Grass if targetting a typeless Pokemon affected by Forest's Curse", async () => {
game.override
.moveset([ Moves.FORESTS_CURSE, Moves.REFLECT_TYPE ])
.startingLevel(60)
.enemySpecies(Species.CHARMANDER)
.enemyMoveset([ Moves.BURN_UP, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.FEEBAS ]);
const playerPokemon = game.scene.getPlayerPokemon();
const enemyPokemon = game.scene.getEnemyPokemon();
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.BURN_UP);
await game.toNextTurn();
game.move.select(Moves.FORESTS_CURSE);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
expect(enemyPokemon?.getTypes().includes(Type.UNKNOWN)).toBe(true);
expect(enemyPokemon?.getTypes().includes(Type.GRASS)).toBe(true);
game.move.select(Moves.REFLECT_TYPE);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon?.getTypes()[0]).toBe(Type.NORMAL);
expect(playerPokemon?.getTypes().includes(Type.GRASS)).toBe(true);
});
});

View File

@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { allMoves, SecretPowerAttr } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
@ -11,6 +11,7 @@ import { StatusEffect } from "#enums/status-effect";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagSide } from "#app/data/arena-tag";
import { allAbilities, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
describe("Moves - Secret Power", () => { describe("Moves - Secret Power", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -60,30 +61,38 @@ describe("Moves - Secret Power", () => {
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
}); });
it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect", it("Secret Power's effect chance is doubled by Serene Grace, but not by the 'rainbow' effect from Fire/Water Pledge",
async () => { async () => {
game.override game.override
.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ]) .moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ])
.ability(Abilities.SERENE_GRACE)
.enemyMoveset([ Moves.SPLASH ]) .enemyMoveset([ Moves.SPLASH ])
.battleType("double"); .battleType("double");
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]); await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0]; const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0];
vi.spyOn(secretPowerAttr, "getMoveChance"); vi.spyOn(sereneGraceAttr, "apply");
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY); game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2); game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined(); let rainbowEffect = game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER);
expect(rainbowEffect).toBeDefined();
rainbowEffect = rainbowEffect!;
vi.spyOn(rainbowEffect, "apply");
game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY); game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30); expect(sereneGraceAttr.apply).toHaveBeenCalledOnce();
expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true);
expect(rainbowEffect.apply).toHaveBeenCalledTimes(0);
} }
); );
}); });

View File

@ -1,4 +1,5 @@
import { SubstituteTag } from "#app/data/battler-tags"; import { SubstituteTag } from "#app/data/battler-tags";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -53,4 +54,18 @@ describe("Moves - Shed Tail", () => {
expect(substituteTag).toBeDefined(); expect(substituteTag).toBeDefined();
expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4)); expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4));
}); });
it("should fail if no ally is available to switch in", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const magikarp = game.scene.getPlayerPokemon()!;
expect(game.scene.getParty().length).toBe(1);
game.move.select(Moves.SHED_TAIL);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(magikarp.isOnField()).toBeTruthy();
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
}); });

View File

@ -0,0 +1,47 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Type } from "#app/data/type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Trick Or Treat", () => {
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
.moveset([ Moves.FORESTS_CURSE, Moves.TRICK_OR_TREAT ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("will replace added type from Forest's Curse", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const enemyPokemon = game.scene.getEnemyPokemon();
game.move.select(Moves.FORESTS_CURSE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon!.summonData.addedType).toBe(Type.GRASS);
game.move.select(Moves.TRICK_OR_TREAT);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon?.summonData.addedType).toBe(Type.GHOST);
});
});

View File

@ -0,0 +1,72 @@
import { BattlerIndex } from "#app/battle";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
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, it, expect } from "vitest";
describe("Frenzy Move Reset", () => {
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")
.disableCrits()
.starterSpecies(Species.MAGIKARP)
.moveset(Moves.THRASH)
.statusEffect(StatusEffect.PARALYSIS)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(100)
.enemySpecies(Species.SHUCKLE)
.enemyAbility(Abilities.BALL_FETCH);
});
/*
* Thrash (or frenzy moves in general) should not continue to run if attack fails due to paralysis
*
* This is a 3-turn Thrash test:
* 1. Thrash is selected and succeeds to hit the enemy -> Enemy Faints
*
* 2. Thrash is automatically selected but misses due to paralysis
* Note: After missing the Pokemon should stop automatically attacking
*
* 3. At the start of the 3rd turn the Player should be able to select a move/switch Pokemon/etc.
* Note: This means that BattlerTag.FRENZY is not anymore in pokemon.summonData.tags and pokemon.summonData.moveQueue is empty
*
*/
it("should cancel frenzy move if move fails turn 2", async () => {
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.THRASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceStatusActivation(false);
await game.toNextTurn();
expect(playerPokemon.summonData.moveQueue.length).toBe(2);
expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(true);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceStatusActivation(true);
await game.toNextTurn();
expect(playerPokemon.summonData.moveQueue.length).toBe(0);
expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(false);
});
});

View File

@ -24,6 +24,7 @@ import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
import EventEmitter = Phaser.Events.EventEmitter; import EventEmitter = Phaser.Events.EventEmitter;
import UpdateList = Phaser.GameObjects.UpdateList; import UpdateList = Phaser.GameObjects.UpdateList;
import { version } from "../../../package.json"; import { version } from "../../../package.json";
import { MockTimedEventManager } from "./mocks/mockTimedEventManager";
Object.defineProperty(window, "localStorage", { Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(), value: mockLocalStorage(),
@ -232,6 +233,7 @@ export default class GameWrapper {
this.scene.make = new MockGameObjectCreator(mockTextureManager); this.scene.make = new MockGameObjectCreator(mockTextureManager);
this.scene.time = new MockClock(this.scene); this.scene.time = new MockClock(this.scene);
this.scene.remove = vi.fn(); // TODO: this should be stubbed differently this.scene.remove = vi.fn(); // TODO: this should be stubbed differently
this.scene.eventManager = new MockTimedEventManager(); // Disable Timed Events
} }
} }

View File

@ -0,0 +1,17 @@
import { TimedEventManager } from "#app/timed-event-manager";
/** Mock TimedEventManager so that ongoing events don't impact tests */
export class MockTimedEventManager extends TimedEventManager {
override activeEvent() {
return undefined;
}
override isEventActive(): boolean {
return false;
}
override getFriendshipMultiplier(): number {
return 1;
}
override getShinyMultiplier(): number {
return 1;
}
}

View File

@ -35,7 +35,7 @@ const timedEvents: TimedEvent[] = [
endDate: new Date(Date.UTC(2024, 10, 4, 0)), endDate: new Date(Date.UTC(2024, 10, 4, 0)),
bannerKey: "halloween2024-event-", bannerKey: "halloween2024-event-",
scale: 0.21, scale: 0.21,
availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ] availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ]
} }
]; ];

View File

@ -107,7 +107,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
let pokemonIconX = -20; let pokemonIconX = -20;
let pokemonIconY = 6; let pokemonIconY = 6;
if ([ "de", "es", "fr", "ko", "pt-BR" ].includes(currentLanguage)) { if ([ "de", "es-ES", "fr", "ko", "pt-BR" ].includes(currentLanguage)) {
gachaTextStyle = TextStyle.SMALLER_WINDOW_ALT; gachaTextStyle = TextStyle.SMALLER_WINDOW_ALT;
gachaX = 2; gachaX = 2;
gachaY = 2; gachaY = 2;
@ -115,7 +115,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
let legendaryLabelX = gachaX; let legendaryLabelX = gachaX;
let legendaryLabelY = gachaY; let legendaryLabelY = gachaY;
if ([ "de", "es" ].includes(currentLanguage)) { if ([ "de", "es-ES" ].includes(currentLanguage)) {
pokemonIconX = -25; pokemonIconX = -25;
pokemonIconY = 10; pokemonIconY = 10;
legendaryLabelX = -6; legendaryLabelX = -6;
@ -128,7 +128,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
switch (gachaType as GachaType) { switch (gachaType as GachaType) {
case GachaType.LEGENDARY: case GachaType.LEGENDARY:
if ([ "de", "es" ].includes(currentLanguage)) { if ([ "de", "es-ES" ].includes(currentLanguage)) {
gachaUpLabel.setAlign("center"); gachaUpLabel.setAlign("center");
gachaUpLabel.setY(0); gachaUpLabel.setY(0);
} }
@ -149,7 +149,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
gachaInfoContainer.add(pokemonIcon); gachaInfoContainer.add(pokemonIcon);
break; break;
case GachaType.MOVE: case GachaType.MOVE:
if ([ "de", "es", "fr", "pt-BR" ].includes(currentLanguage)) { if ([ "de", "es-ES", "fr", "pt-BR" ].includes(currentLanguage)) {
gachaUpLabel.setAlign("center"); gachaUpLabel.setAlign("center");
gachaUpLabel.setY(0); gachaUpLabel.setY(0);
} }

View File

@ -32,7 +32,7 @@ let wikiUrl = "https://wiki.pokerogue.net/start";
const discordUrl = "https://discord.gg/uWpTfdKG49"; const discordUrl = "https://discord.gg/uWpTfdKG49";
const githubUrl = "https://github.com/pagefaultgames/pokerogue"; const githubUrl = "https://github.com/pagefaultgames/pokerogue";
const redditUrl = "https://www.reddit.com/r/pokerogue"; const redditUrl = "https://www.reddit.com/r/pokerogue";
const donateUrl = "https://github.com/sponsors/patapancakes"; const donateUrl = "https://github.com/sponsors/pagefaultgames";
export default class MenuUiHandler extends MessageUiHandler { export default class MenuUiHandler extends MessageUiHandler {
private readonly textPadding = 8; private readonly textPadding = 8;

View File

@ -21,24 +21,6 @@ interface LanguageSetting {
} }
const languageSettings: { [key: string]: LanguageSetting } = { const languageSettings: { [key: string]: LanguageSetting } = {
"en": {
infoContainerTextSize: "64px"
},
"de": {
infoContainerTextSize: "64px",
},
"es": {
infoContainerTextSize: "64px"
},
"fr": {
infoContainerTextSize: "64px"
},
"it": {
infoContainerTextSize: "64px"
},
"zh": {
infoContainerTextSize: "64px"
},
"pt": { "pt": {
infoContainerTextSize: "60px", infoContainerTextSize: "60px",
infoContainerLabelXPos: -15, infoContainerLabelXPos: -15,
@ -237,14 +219,20 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
const formKey = (pokemon.species?.forms?.[pokemon.formIndex!]?.formKey); const formKey = (pokemon.species?.forms?.[pokemon.formIndex!]?.formKey);
const formText = Utils.capitalizeString(formKey, "-", false, false) || ""; const formText = Utils.capitalizeString(formKey, "-", false, false) || "";
const speciesName = Utils.capitalizeString(Species[pokemon.species.getRootSpeciesId()], "_", true, false); const speciesName = Utils.capitalizeString(Species[pokemon.species.speciesId], "_", true, false);
let formName = ""; let formName = "";
if (pokemon.species.speciesId === Species.ARCEUS) { if (pokemon.species.speciesId === Species.ARCEUS) {
formName = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`); formName = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`);
} else { } else {
const i18key = `pokemonForm:${speciesName}${formText}`; const i18key = `pokemonForm:${speciesName}${formText}`;
formName = i18next.exists(i18key) ? i18next.t(i18key) : formText; if (i18next.exists(i18key)) {
formName = i18next.t(i18key);
} else {
const rootSpeciesName = Utils.capitalizeString(Species[pokemon.species.getRootSpeciesId()], "_", true, false);
const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`;
formName = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText;
}
} }
if (formName) { if (formName) {

View File

@ -13,7 +13,7 @@ interface LanguageSetting {
} }
const languageSettings: { [key: string]: LanguageSetting } = { const languageSettings: { [key: string]: LanguageSetting } = {
"es":{ "es-ES": {
inputFieldFontSize: "50px", inputFieldFontSize: "50px",
errorMessageFontSize: "40px", errorMessageFontSize: "40px",
} }

View File

@ -674,7 +674,7 @@ export default class RunInfoUiHandler extends UiHandler {
const def = i18next.t("pokemonInfo:Stat.DEFshortened") + ": " + pStats[2]; const def = i18next.t("pokemonInfo:Stat.DEFshortened") + ": " + pStats[2];
const spatk = i18next.t("pokemonInfo:Stat.SPATKshortened") + ": " + pStats[3]; const spatk = i18next.t("pokemonInfo:Stat.SPATKshortened") + ": " + pStats[3];
const spdef = i18next.t("pokemonInfo:Stat.SPDEFshortened") + ": " + pStats[4]; const spdef = i18next.t("pokemonInfo:Stat.SPDEFshortened") + ": " + pStats[4];
const speedLabel = (currentLanguage === "es" || currentLanguage === "pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened"); const speedLabel = (currentLanguage === "es-ES" || currentLanguage === "pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened");
const speed = speedLabel + ": " + pStats[5]; const speed = speedLabel + ": " + pStats[5];
// Column 1: HP Atk Def // Column 1: HP Atk Def
const pokeStatText1 = addBBCodeTextObject(this.scene, -5, 0, hp, TextStyle.SUMMARY, { fontSize: textContainerFontSize, lineSpacing: lineSpacing }); const pokeStatText1 = addBBCodeTextObject(this.scene, -5, 0, hp, TextStyle.SUMMARY, { fontSize: textContainerFontSize, lineSpacing: lineSpacing });

View File

@ -29,10 +29,10 @@ export default class SettingsDisplayUiHandler extends AbstractSettingsUiHandler
label: "English", label: "English",
}; };
break; break;
case "es": case "es-ES":
this.settings[languageIndex].options[0] = { this.settings[languageIndex].options[0] = {
value: "Español", value: "Español (ES)",
label: "Español", label: "Español (ES)",
}; };
break; break;
case "it": case "it":

View File

@ -81,7 +81,7 @@ const languageSettings: { [key: string]: LanguageSetting } = {
instructionTextSize: "35px", instructionTextSize: "35px",
starterInfoXPos: 33, starterInfoXPos: 33,
}, },
"es":{ "es-ES":{
starterInfoTextSize: "56px", starterInfoTextSize: "56px",
instructionTextSize: "35px", instructionTextSize: "35px",
}, },

View File

@ -184,6 +184,7 @@ export default class TargetSelectUiHandler extends UiHandler {
} }
clear() { clear() {
this.cursor = -1;
super.clear(); super.clear();
this.eraseCursor(); this.eraseCursor();
} }

View File

@ -487,7 +487,7 @@ export function verifyLang(lang?: string): boolean {
} }
switch (lang) { switch (lang) {
case "es": case "es-ES":
case "fr": case "fr":
case "da": case "da":
case "de": case "de":