Merge branch 'beta' into moveTelekinesis

This commit is contained in:
Mumble 2024-09-29 19:39:28 -07:00 committed by GitHub
commit 2d6da143e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 563 additions and 213 deletions

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
github: patapancakes
github: pagefaultgames

43
package-lock.json generated
View File

@ -37,7 +37,7 @@
"typedoc": "^0.26.4",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0-alpha.54",
"vite": "^5.3.5",
"vite": "^5.4.8",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.0.4",
"vitest-canvas-mock": "^0.3.3"
@ -3650,7 +3650,6 @@
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -5042,7 +5041,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -5441,9 +5439,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@ -5459,16 +5457,21 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss/node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -5865,11 +5868,10 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -6469,15 +6471,14 @@
}
},
"node_modules/vite": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.39",
"rollup": "^4.13.0"
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
@ -6496,6 +6497,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@ -6513,6 +6515,9 @@
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},

View File

@ -41,7 +41,7 @@
"typedoc": "^0.26.4",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0-alpha.54",
"vite": "^5.3.5",
"vite": "^5.4.8",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.0.4",
"vitest-canvas-mock": "^0.3.3"

View File

@ -1040,10 +1040,6 @@ export default class BattleScene extends SceneBase {
this.gameMode = getGameMode(GameModes.CLASSIC);
this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24));
console.log("Seed:", this.seed);
this.resetSeed(); // Properly resets RNG after saving and quitting a session
this.disableMenu = false;
this.score = 0;
@ -1078,6 +1074,12 @@ export default class BattleScene extends SceneBase {
//@ts-ignore - allowing `null` for currentBattle causes a lot of trouble
this.currentBattle = null; // TODO: resolve ts-ignore
// Reset RNG after end of game or save & quit.
// This needs to happen after clearing this.currentBattle or the seed will be affected by the last wave played
this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24));
console.log("Seed:", this.seed);
this.resetSeed();
this.biomeWaveText.setText(startingWave.toString());
this.biomeWaveText.setVisible(false);
@ -3091,11 +3093,6 @@ export default class BattleScene extends SceneBase {
private isWaveMysteryEncounter(newBattleType: BattleType, waveIndex: number, sessionDataEncounterType?: MysteryEncounterType): boolean {
const [lowestMysteryEncounterWave, highestMysteryEncounterWave] = this.gameMode.getMysteryEncounterLegalWaves();
if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(waveIndex) && waveIndex < highestMysteryEncounterWave && waveIndex > lowestMysteryEncounterWave) {
// If ME type is already defined in session data, no need to roll RNG check
if (!isNullOrUndefined(sessionDataEncounterType)) {
return true;
}
// Base spawn weight is BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT/256, and increases by WEIGHT_INCREMENT_ON_SPAWN_MISS/256 for each missed attempt at spawning an encounter on a valid floor
const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance;
const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents;

View File

@ -497,7 +497,7 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[], rand
}
/* 1/3 chance for evil team grunts to be double battles */
const evilTeamGrunts = [TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT];
const evilTeamGrunts = [TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT];
const isEvilTeamGrunt = evilTeamGrunts.includes(trainerTypes[rand]);
if (trainerConfigs[trainerTypes[rand]].hasDouble && isEvilTeamGrunt) {

View File

@ -4,7 +4,7 @@ import { Constructor } from "#app/utils";
import * as Utils from "../utils";
import { getPokemonNameWithAffix } from "../messages";
import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
import { BattlerTag, GroundedTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
@ -536,53 +536,6 @@ export class PostDefendAbAttr extends AbAttr {
}
}
/**
* Applies the effects of Gulp Missile when the user is hit by an attack.
* @extends PostDefendAbAttr
*/
export class PostDefendGulpMissileAbAttr extends PostDefendAbAttr {
constructor() {
super(true);
}
/**
* Damages the attacker and triggers the secondary effect based on the form or the BattlerTagType.
* @param {Pokemon} pokemon - The defending Pokemon.
* @param passive - n/a
* @param {Pokemon} attacker - The attacking Pokemon.
* @param {Move} move - The move being used.
* @param {HitResult} hitResult - n/a
* @param {any[]} args - n/a
* @returns Whether the effects of the ability are applied.
*/
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise<boolean> {
const battlerTag = pokemon.getTag(GulpMissileTag);
if (!battlerTag || move.category === MoveCategory.STATUS || pokemon.getTag(SemiInvulnerableTag)) {
return false;
}
if (simulated) {
return true;
}
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
if (!cancelled.value) {
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER);
}
if (battlerTag.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1));
} else {
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
}
pokemon.removeTag(battlerTag.tagType);
return true;
}
}
export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const attackPriority = new Utils.IntegerHolder(move.priority);
@ -5210,8 +5163,7 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonMoldBreaker", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(MoveAbilityBypassAbAttr),
new Ability(Abilities.SUPER_LUCK, 4)
.attr(BonusCritAbAttr)
.partial(),
.attr(BonusCritAbAttr),
new Ability(Abilities.AFTERMATH, 4)
.attr(PostFaintContactDamageAbAttr, 4)
.bypassFaint(),
@ -5669,13 +5621,19 @@ export function initAbilities() {
new Ability(Abilities.MIRROR_ARMOR, 8)
.ignorable()
.unimplemented(),
/**
* Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an
* ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case
* where Cramorant is fainted.
* @see {@linkcode GulpMissileTagAttr} and {@linkcode GulpMissileTag} for Gulp Missile implementation
*/
new Ability(Abilities.GULP_MISSILE, 8)
.attr(UnsuppressableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.attr(NoFusionAbilityAbAttr)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(PostDefendGulpMissileAbAttr),
.bypassFaint(),
new Ability(Abilities.STALWART, 8)
.attr(BlockRedirectAbAttr),
new Ability(Abilities.STEAM_ENGINE, 8)

View File

@ -511,6 +511,39 @@ class WaterSportTag extends WeakenMoveTypeTag {
}
}
/**
* Arena Tag class for the secondary effect of {@link https://bulbapedia.bulbagarden.net/wiki/Plasma_Fists_(move) | Plasma Fists}.
* Converts Normal-type moves to Electric type for the rest of the turn.
*/
export class PlasmaFistsTag extends ArenaTag {
constructor() {
super(ArenaTagType.PLASMA_FISTS, 1, Moves.PLASMA_FISTS);
}
/** Queues Plasma Fists' on-add message */
onAdd(arena: Arena): void {
arena.scene.queueMessage(i18next.t("arenaTag:plasmaFistsOnAdd"));
}
onRemove(arena: Arena): void { } // Removes default on-remove message
/**
* Converts Normal-type moves to Electric type
* @param arena n/a
* @param args
* - `[0]` {@linkcode Utils.NumberHolder} A container with a move's {@linkcode Type}
* @returns `true` if the given move type changed; `false` otherwise.
*/
apply(arena: Arena, args: any[]): boolean {
const moveType = args[0];
if (moveType instanceof Utils.NumberHolder && moveType.value === Type.NORMAL) {
moveType.value = Type.ELECTRIC;
return true;
}
return false;
}
}
/**
* Abstract class to implement arena traps.
*/
@ -1010,6 +1043,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
return new MudSportTag(turnCount, sourceId);
case ArenaTagType.WATER_SPORT:
return new WaterSportTag(turnCount, sourceId);
case ArenaTagType.PLASMA_FISTS:
return new PlasmaFistsTag();
case ArenaTagType.SPIKES:
return new SpikesTag(sourceId, side);
case ArenaTagType.TOXIC_SPIKES:

View File

@ -2123,7 +2123,36 @@ export class StockpilingTag extends BattlerTag {
*/
export class GulpMissileTag extends BattlerTag {
constructor(tagType: BattlerTagType, sourceMove: Moves) {
super(tagType, BattlerTagLapseType.CUSTOM, 0, sourceMove);
super(tagType, BattlerTagLapseType.HIT, 0, sourceMove);
}
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (pokemon.getTag(BattlerTagType.UNDERWATER)) {
return true;
}
const moveEffectPhase = pokemon.scene.getCurrentPhase();
if (moveEffectPhase instanceof MoveEffectPhase) {
const attacker = moveEffectPhase.getUserPokemon();
if (!attacker) {
return false;
}
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
if (!cancelled.value) {
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER);
}
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1));
} else {
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
}
}
return false;
}
/**
@ -2589,6 +2618,43 @@ export class ImprisonTag extends MoveRestrictionBattlerTag {
}
}
/**
* Battler Tag that applies the effects of Syrup Bomb to the target Pokemon.
* For three turns, starting from the turn of hit, at the end of each turn, the target Pokemon's speed will decrease by 1.
* The tag can also expire by taking the target Pokemon off the field.
*/
export class SyrupBombTag extends BattlerTag {
constructor() {
super(BattlerTagType.SYRUP_BOMB, BattlerTagLapseType.TURN_END, 3, Moves.SYRUP_BOMB);
}
/**
* Adds the Syrup Bomb battler tag to the target Pokemon.
* @param {Pokemon} pokemon the target Pokemon
*/
override onAdd(pokemon: Pokemon) {
super.onAdd(pokemon);
pokemon.scene.queueMessage(i18next.t("battlerTags:syrupBombOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
/**
* Applies the single-stage speed down to the target Pokemon and decrements the tag's turn count
* @param {Pokemon} pokemon the target Pokemon
* @param {BattlerTagLapseType} _lapseType
* @returns `true` if the turnCount is still greater than 0 | `false` if the turnCount is 0 or the target Pokemon has been removed from the field
*/
override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
if (!pokemon.isActive(true)) {
return false;
}
pokemon.scene.queueMessage(i18next.t("battlerTags:syrupBombLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); // Custom message in lieu of an animation in mainline
pokemon.scene.unshiftPhase(new StatStageChangePhase(
pokemon.scene, pokemon.getBattlerIndex(), true,
[Stat.SPD], -1, true, false, true
));
return --this.turnCount > 0;
}
}
/**
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
@ -2763,6 +2829,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new TauntTag();
case BattlerTagType.IMPRISON:
return new ImprisonTag(sourceId);
case BattlerTagType.SYRUP_BOMB:
return new SyrupBombTag();
case BattlerTagType.NONE:
default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -81,7 +81,7 @@ export const speciesEggMoves = {
[Species.LEDYBA]: [ Moves.POLLEN_PUFF, Moves.THIEF, Moves.PARTING_SHOT, Moves.SPORE ],
[Species.SPINARAK]: [ Moves.PARTING_SHOT, Moves.ATTACK_ORDER, Moves.GASTRO_ACID, Moves.STRENGTH_SAP ],
[Species.CHINCHOU]: [ Moves.THUNDERCLAP, Moves.BOUNCY_BUBBLE, Moves.THUNDER_CAGE, Moves.TAIL_GLOW ],
[Species.PICHU]: [ Moves.MOONBLAST, Moves.WAVE_CRASH, Moves.AIR_SLASH, Moves.AURA_WHEEL ],
[Species.PICHU]: [ Moves.MOONBLAST, Moves.TRIPLE_AXEL, Moves.FIERY_DANCE, Moves.AURA_WHEEL ],
[Species.CLEFFA]: [ Moves.CALM_MIND, Moves.EARTH_POWER, Moves.WISH, Moves.LIGHT_OF_RUIN ],
[Species.IGGLYBUFF]: [ Moves.DRAIN_PUNCH, Moves.GRAV_APPLE, Moves.SOFT_BOILED, Moves.EXTREME_SPEED ],
[Species.TOGEPI]: [ Moves.SCORCHING_SANDS, Moves.ROOST, Moves.RELIC_SONG, Moves.FIERY_DANCE ],
@ -195,7 +195,7 @@ export const speciesEggMoves = {
[Species.REGISTEEL]: [ Moves.BODY_PRESS, Moves.SIZZLY_SLIDE, Moves.RECOVER, Moves.GIGATON_HAMMER ],
[Species.LATIAS]: [ Moves.CORE_ENFORCER, Moves.FUSION_FLARE, Moves.SPARKLY_SWIRL, Moves.MYSTICAL_POWER ],
[Species.LATIOS]: [ Moves.CORE_ENFORCER, Moves.BLUE_FLARE, Moves.NASTY_PLOT, Moves.TACHYON_CUTTER ],
[Species.KYOGRE]: [ Moves.RECOVER, Moves.HURRICANE, Moves.FLIP_TURN, Moves.WILDBOLT_STORM ],
[Species.KYOGRE]: [ Moves.RECOVER, Moves.HURRICANE, Moves.FREEZY_FROST, Moves.WILDBOLT_STORM ],
[Species.GROUDON]: [ Moves.STONE_AXE, Moves.SOLAR_BLADE, Moves.MORNING_SUN, Moves.SACRED_FIRE ],
[Species.RAYQUAZA]: [ Moves.V_CREATE, Moves.DRAGON_DARTS, Moves.CORE_ENFORCER, Moves.OBLIVION_WING ],
[Species.JIRACHI]: [ Moves.TACHYON_CUTTER, Moves.TRIPLE_ARROWS, Moves.ROCK_SLIDE, Moves.SHELL_SMASH ],

View File

@ -5226,6 +5226,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
*/
const switchOutTarget = this.selfSwitch ? user : target;
if (switchOutTarget instanceof PlayerPokemon) {
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
if (switchOutTarget.hp > 0) {
@ -5234,6 +5237,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
}
return false;
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
// Switch out logic for trainer battles
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
@ -5244,6 +5250,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
false, false), MoveEndPhase);
}
} else {
if (user.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
// Switch out logic for everything else (eg: WILD battles)
switchOutTarget.leaveField(false);
@ -8900,8 +8909,8 @@ export function initMoves() {
.attr(HalfSacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.PLASMA_FISTS, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7)
.punchingMove()
.partial(),
.attr(AddArenaTagAttr, ArenaTagType.PLASMA_FISTS, 1)
.punchingMove(),
new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
.attr(PhotonGeyserCategoryAttr)
.ignoresAbilities()
@ -8931,7 +8940,7 @@ export function initMoves() {
.partial()
.ignoresVirtual(),
/* End Unused */
new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, 100, 2, 7) //LGPE Implementation
new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) //LGPE Implementation
.attr(CritOnlyAttr),
new AttackMove(Moves.SPLISHY_SPLASH, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 30, 0, 7)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
@ -9616,9 +9625,8 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES)
.triageMove(),
new AttackMove(Moves.SYRUP_BOMB, Type.GRASS, MoveCategory.SPECIAL, 60, 85, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) //Temporary
.ballBombMove()
.partial(),
.attr(AddBattlerTagAttr, BattlerTagType.SYRUP_BOMB, false, false, 3)
.ballBombMove(),
new AttackMove(Moves.IVY_CUDGEL, Type.GRASS, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 9)
.attr(IvyCudgelTypeAttr)
.attr(HighCritAttr)

View File

@ -159,7 +159,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
return true;
},
onHover: () => {
const formName = tradePokemon.species.forms && tradePokemon.species.forms.length > tradePokemon.formIndex ? tradePokemon.species.forms[pokemon.formIndex].formName : null;
const formName = tradePokemon.species.forms && tradePokemon.species.forms.length > tradePokemon.formIndex ? tradePokemon.species.forms[tradePokemon.formIndex].formName : null;
const line1 = i18next.t("pokemonInfoContainer:ability") + " " + tradePokemon.getAbility().name + (tradePokemon.getGender() !== Gender.GENDERLESS ? " | " + i18next.t("pokemonInfoContainer:gender") + " " + getGenderSymbol(tradePokemon.getGender()) : "");
const line2 = i18next.t("pokemonInfoContainer:nature") + " " + getNatureName(tradePokemon.getNature()) + (formName ? " | " + i18next.t("pokemonInfoContainer:form") + " " + formName : "");
showEncounterText(scene, `${line1}\n${line2}`, 0, 0, false);

View File

@ -558,6 +558,10 @@ function onGameOver(scene: BattleScene) {
// Revert BGM
scene.playBgm(scene.arena.bgm);
// Clear any leftover battle phases
scene.clearPhaseQueue();
scene.clearPhaseQueueSplice();
// Return enemy Pokemon
const pokemon = scene.getEnemyPokemon();
if (pokemon) {

View File

@ -224,7 +224,7 @@ export function getRandomSpeciesByStarterTier(starterTiers: number | [number, nu
// If no filtered mons exist at specified starter tiers, will expand starter search range until there are
// Starts by decrementing starter tier min until it is 0, then increments tier max up to 10
let tryFilterStarterTiers: [PokemonSpecies, number][] = filteredSpecies.filter(s => (s[1] >= min && s[1] <= max));
while (tryFilterStarterTiers.length === 0 && (min !== 0 && max !== 10)) {
while (tryFilterStarterTiers.length === 0 && !(min === 0 && max === 10)) {
if (min > 0) {
min--;
} else {

View File

@ -11,6 +11,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day";
import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier } from "#app/modifier/modifier";
export enum SpeciesWildEvolutionDelay {
NONE,
@ -1647,8 +1648,14 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.FROSMOTH, 1, null, new SpeciesFriendshipEvolutionCondition(90, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.MEDIUM)
],
[Species.GIMMIGHOUL]: [
new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG)
new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG)
]
};

View File

@ -25,4 +25,5 @@ export enum ArenaTagType {
SAFEGUARD = "SAFEGUARD",
NO_CRIT = "NO_CRIT",
IMPRISON = "IMPRISON",
PLASMA_FISTS = "PLASMA_FISTS",
}

View File

@ -85,5 +85,6 @@ export enum BattlerTagType {
TORMENT = "TORMENT",
TAUNT = "TAUNT",
IMPRISON = "IMPRISON",
SYRUP_BOMB = "SYRUP_BOMB",
TELEKINESIS = "TELEKINESIS"
}

View File

@ -63,6 +63,12 @@ import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { SwitchType } from "#enums/switch-type";
/** `64/65536 -> 1/1024` */
const BASE_SHINY_CHANCE = 64;
/** `1/256` */
const BASE_HIDDEN_ABILITY_CHANCE = 256;
export enum FieldPosition {
CENTER,
LEFT,
@ -139,7 +145,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`;
}
const hiddenAbilityChance = new Utils.IntegerHolder(256);
const hiddenAbilityChance = new Utils.IntegerHolder(BASE_HIDDEN_ABILITY_CHANCE);
if (!this.hasTrainer()) {
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
}
@ -1512,6 +1518,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder);
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder);
this.scene.arena.applyTags(ArenaTagType.PLASMA_FISTS, moveTypeHolder);
return moveTypeHolder.value as Type;
}
@ -1822,7 +1830,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID.
* F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits.
* The XOR of E and F are then compared to the {@linkcode shinyThreshold} (or {@linkcode thresholdOverride} if set) to see whether or not to generate a shiny.
* The base shiny odds are {@linkcode baseShinyChance} / 65536
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536
* @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* @returns true if the Pokemon has been set as a shiny, false otherwise
*/
@ -1838,9 +1846,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const E = this.scene.gameData.trainerId ^ this.scene.gameData.secretId;
const F = rand1 ^ rand2;
/** `64/65536 -> 1/1024` */
const baseShinyChance = 64;
const shinyThreshold = new Utils.IntegerHolder(baseShinyChance);
const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined) {
if (this.scene.eventManager.isEventActive()) {
shinyThreshold.value *= this.scene.eventManager.getShinyMultiplier();
@ -1865,15 +1871,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Function that tries to set a Pokemon shiny based on seed.
* For manual use only, usually to roll a Pokemon's shiny chance a second time.
*
* The base shiny odds are {@linkcode baseShinyChance} / 65536
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536
* @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
* @returns true if the Pokemon has been set as a shiny, false otherwise
*/
trySetShinySeed(thresholdOverride?: integer, applyModifiersToOverride?: boolean): boolean {
/** `64/65536 -> 1/1024` */
const baseShinyChance = 64;
const shinyThreshold = new Utils.IntegerHolder(baseShinyChance);
const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined || applyModifiersToOverride) {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
shinyThreshold.value = thresholdOverride;
@ -1931,7 +1935,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
generateFusionSpecies(forStarter?: boolean): void {
const hiddenAbilityChance = new Utils.IntegerHolder(256);
const hiddenAbilityChance = new Utils.IntegerHolder(BASE_HIDDEN_ABILITY_CHANCE);
if (!this.hasTrainer()) {
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
}
@ -3979,7 +3983,8 @@ export class PlayerPokemon extends Pokemon {
let compatible = false;
for (const p of tmSpecies[tm]) {
if (Array.isArray(p)) {
if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) {
const [pkm, form] = p;
if ((pkm === this.species.speciesId || this.fusionSpecies && pkm === this.fusionSpecies.speciesId) && form === this.getFormKey()) {
compatible = true;
break;
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "Lehmsuhler hört auf zu wirken!",
"waterSportOnAdd": "Die Stärke aller Feuer-Attacken wurde reduziert!",
"waterSportOnRemove": "Nassmacher hört auf zu wirken!",
"plasmaFistsOnAdd": "Ein elektrisch geladener Niederschlag regnet auf das Kampffeld herab!",
"spikesOnAdd": "Die {{opponentDesc}} sind von Stacheln umgeben!",
"spikesActivateTrap": "Die {{pokemonNameWithAffix}} wurde durch Stachler verletzt!!",
"toxicSpikesOnAdd": "Die {{opponentDesc}} sind überall von giftigen Stacheln umgeben",
@ -54,4 +55,4 @@
"safeguardOnRemove": "Der mystische Schleier, der das ganze Feld umgab, hat sich gelüftet!",
"safeguardOnRemovePlayer": "Der mystische Schleier, der dein Team umgab, hat sich gelüftet!",
"safeguardOnRemoveEnemy": "Der mystische Schleier, der das gegnerische Team umgab, hat sich gelüftet!"
}
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "The effects of Mud Sport\nhave faded.",
"waterSportOnAdd": "Fire's power was weakened!",
"waterSportOnRemove": "The effects of Water Sport\nhave faded.",
"plasmaFistsOnAdd": "A deluge of ions showers the battlefield!",
"spikesOnAdd": "{{moveName}} were scattered\nall around {{opponentDesc}}'s feet!",
"spikesActivateTrap": "{{pokemonNameWithAffix}} is hurt\nby the spikes!",
"toxicSpikesOnAdd": "{{moveName}} were scattered\nall around {{opponentDesc}}'s feet!",

View File

@ -78,5 +78,7 @@
"tormentOnAdd": "{{pokemonNameWithAffix}} was subjected to torment!",
"tauntOnAdd": "{{pokemonNameWithAffix}} fell for the taunt!",
"imprisonOnAdd": "{{pokemonNameWithAffix}} sealed the opponents move(s)!",
"autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!"
"autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!",
"syrupBombOnAdd": "{{pokemonNameWithAffix}} got covered in sticky, candy syrup!",
"syrupBombLapse": "The sticky syrup slowed down {{pokemonNameWithAffix}}!"
}

View File

@ -71,5 +71,5 @@
"safeguard": "{{targetName}} is protected by Safeguard!",
"substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!",
"substituteNotEnoughHp": "But it does not have enough HP\nleft to make a substitute!",
"afterYou": "{{pokemonName}} took the kind offer!"
"afterYou": "{{targetName}} took the kind offer!"
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "Chapoteo Lodo ha dejado de surtir efecto.",
"waterSportOnAdd": "¡Se han debilitado los ataques\nde tipo Fuego!",
"waterSportOnRemove": "Hidrochorro ha dejado de surtir efecto.",
"plasmaFistsOnAdd": "¡Una lluvia de electrones cae sobre\nel terreno de combate!",
"spikesOnAdd": "¡El equipo de {{opponentDesc}} ha sido rodeado por {{moveName}}!",
"spikesActivateTrap": "¡Las púas han herido a {{pokemonNameWithAffix}}!",
"toxicSpikesOnAdd": "¡El equipo de {{opponentDesc}} ha sido rodeado por {{moveName}}!",
@ -54,4 +55,4 @@
"safeguardOnRemove": "¡Velo Sagrado dejó de hacer efecto!",
"safeguardOnRemovePlayer": "El efecto de Velo Sagrado en tu equipo se ha disipado.",
"safeguardOnRemoveEnemy": "El efecto de Velo Sagrado en el equipo enemigo se ha disipado."
}
}

View File

@ -67,5 +67,5 @@
"swapArenaTags": "¡{{pokemonName}} ha intercambiado los efectos del terreno de combate!",
"exposedMove": "¡{{pokemonName}} ha identificado\n{{targetPokemonName}}!",
"safeguard": "¡{{targetName}} está protegido por Velo Sagrado!",
"afterYou": "¡{{pokemonName}} ha decidido aprovechar la oportunidad!"
"afterYou": "¡{{targetName}} ha decidido aprovechar la oportunidad!"
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "Leffet de Lance-Boue se dissipe !",
"waterSportOnAdd": "La puissance des capacités\nde type Feu diminue !",
"waterSportOnRemove": "Leffet de Tourniquet se dissipe !",
"plasmaFistsOnAdd": "Un déluge de plasma sabat sur le terrain !",
"spikesOnAdd": "Des {{moveName}} séparpillent autour de {{opponentDesc}} !",
"spikesActivateTrap": "{{pokemonNameWithAffix}} est blessé\npar les picots !",
"toxicSpikesOnAdd": "Des {{moveName}} séparpillent autour de {{opponentDesc}} !",

View File

@ -71,5 +71,5 @@
"safeguard": "{{targetName}} est protégé\npar la capacité Rune Protect !",
"substituteOnOverlap": "{{pokemonName}} a déjà\nun clone !",
"substituteNotEnoughHp": "Mais il est trop faible\npour créer un clone !",
"afterYou": "{{pokemonName}} accepte\navec joie !"
"afterYou": "{{targetName}} accepte\navec joie !"
}

View File

@ -28,7 +28,7 @@
"select_prompt": "Choisissez un objet à donner.",
"invalid_selection": "Ce Pokémon ne porte pas ce genre dobjet.",
"selected": "Vous remettez lobjet {{selectedItem}} au Dresseur.",
"selected_dialogue": "Sérieux ? {{selectedItem}}, comme ça en cadeau ?\nCest très générieux !$En guise de ma reconnaissance, laisse-moi\nte faire un cadeau un peu spécial !$Il est dans ma famille depusi des générations, et ça me ferait plaisir de te loffrir !"
"selected_dialogue": "Sérieux ? {{selectedItem}}, comme ça en cadeau ?\nCest très générieux !$En guise de ma reconnaissance, laisse-moi\nte faire un cadeau un peu spécial !$Il est dans ma famille depuis des générations, et ça me ferait plaisir de te loffrir !"
}
},
"battle_won": "Tes connaissances et tes capacités ont totalement exploité nos faiblesses !$En remerciement de cette leçon, permets-moi\ndapprendre une capacité Insecte à un de tes Pokémon !",

View File

@ -1,7 +1,7 @@
{
"intro": "Vous tombez sur du matériel dentrainement.",
"title": "Session dentrainement",
"description": "Ce matériel semble pouvoir être utilisé pour entrainer un membre de votre équipe ! Il existe plusieurs moyens avec lesquels vous pourriez entrainer un Pokémon, comme @[TOOLTIP_TITLE]{en le faisant combattre et vaincre le reste de votre équipe}.",
"description": "Ce matériel semble pouvoir être utilisé pour entrainer un membre de votre équipe ! Il existe plusieurs moyens avec lesquels vous pourriez entrainer un Pokémon, comme @[TOOLTIP_TITLE]{en le combattant et le vainquant avec le reste de votre équipe}.",
"query": "Quel entrainement choisir ?",
"invalid_selection": "Le Pokémon doit être en bonne santé.",
"option": {

View File

@ -14,13 +14,13 @@
"label": "Le nourrir",
"disabled_tooltip": "Vous avez besoin de 4 Baies pour choisir cette option",
"tooltip": "(-) Donner 4 Baies\n(+) Le {{enemyPokemon}} vous apprécie",
"selected": "Vous lancer quelques Baies\nau {{enemyPokemon}} !$Il les engloutit avec joie !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !"
"selected": "Vous lancez quelques Baies\nau {{enemyPokemon}} !$Il les engloutit avec joie !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !"
},
"3": {
"label": "Devenir amis",
"disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir cette option",
"tooltip": "(+) {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Le {{enemyPokemon}} vous apprécie",
"selected": "Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}} pour charmer le {{enemyPokemon}} !$The {{enemyPokemon}} veut se joindre\nà votre équipe !"
"selected": "Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}} pour charmer le {{enemyPokemon}} !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !"
}
}
}

View File

@ -11,7 +11,7 @@
"expGainsSpeed": "Vit. barre dExp",
"expPartyDisplay": "Afficher Exp équipe",
"skipSeenDialogues": "Passer dialogues connus",
"eggSkip": "Animation déclosion",
"eggSkip": "Passer les éclosions",
"never": "Jamais",
"always": "Toujours",
"ask": "Demander",

View File

@ -1,8 +1,9 @@
{
"plasmaFistsOnAdd": "Una pioggia di elettroni si rovescia sui Pokémon!",
"safeguardOnAdd": "Un velo mistico ricopre il campo!",
"safeguardOnAddPlayer": "Un velo mistico ricopre la tua squadra!",
"safeguardOnAddEnemy": "Un velo mistico ricopre la squadra avversaria!",
"safeguardOnRemove": "Il campo non è più protetto da Salvaguardia!",
"safeguardOnRemovePlayer": "La tua squadra non è più protetta da Salvaguardia!",
"safeguardOnRemoveEnemy": "La squadra avversaria non è più protetta da Salvaguardia!"
}
}

View File

@ -68,5 +68,5 @@
"exposedMove": "{{pokemonName}} ha identificato\n{{targetPokemonName}}!",
"chillyReception": "{{pokemonName}} sta per fare una battuta!",
"safeguard": "Salvaguardia protegge {{targetName}}!",
"afterYou": "{{pokemonName}} approfitta della cortesia!"
"afterYou": "{{targetName}} approfitta della cortesia!"
}

View File

@ -2,11 +2,11 @@
"blockRecoilDamage": "{{pokemonName}}は {{abilityName}}で 反動ダメージを 受けない!",
"badDreams": "{{pokemonName}}は ナイトメアに うなされている!",
"costar": "{{pokemonName}}は {{allyName}}の\n能力変化を コピーした",
"iceFaceAvoidedDamage": "{{pokemonNameWithAffix}}は\n{{abilityName}}で ダメージを 受けない!",
"iceFaceAvoidedDamage": "{{pokemonNameWithAffix}} は\n{{abilityName}}で ダメージを 受けない!",
"perishBody": "{{pokemonName}}の {{abilityName}}で\nおたがいは 3ターン後に ほろびいてしまう",
"poisonHeal": "{{pokemonName}}は {{abilityName}}で 回復した!",
"trace": "{{pokemonName}}は 相手の {{targetName}}の\n{{abilityName}}を トレースした!",
"windPowerCharged": "{{pokemonNameWithAffix}}は\n{{moveName}}を 受けて じゅうでんした!",
"windPowerCharged": "{{pokemonName}} は\n{{moveName}}を 受けて じゅうでんした!",
"quickDraw": "{{pokemonName}}は クイックドロウで\n行動が はやくなった",
"disguiseAvoidedDamage": "{{pokemonNameWithAffix}}の\nばけのかわが はがれた",
"blockItemTheft": "{{pokemonNameWithAffix}}の {{abilityName}}で\n道具を うばわれない",
@ -48,8 +48,8 @@
"weatherEffectDisappeared": "天候の影響が なくなった!",
"postSummonMoldBreaker": "{{pokemonNameWithAffix}}は\nかたやぶりだ",
"postSummonAnticipation": "{{pokemonNameWithAffix}}は\nみぶるいした",
"postSummonTurboblaze": "{{pokemonNameWithAffix}}は\n燃え盛(もえさか)る オーラを 放っている!",
"postSummonTeravolt": "{{pokemonNameWithAffix}}は\n弾(はじ)ける オーラを 放っている!",
"postSummonTurboblaze": "{{pokemonNameWithAffix}}は\n燃え盛る オーラを 放っている!",
"postSummonTeravolt": "{{pokemonNameWithAffix}}は\n弾ける オーラを 放っている!",
"postSummonDarkAura": "{{pokemonNameWithAffix}}は\nダークオーラを 放っている",
"postSummonFairyAura": "{{pokemonNameWithAffix}}は\nフェアリーオーラを 放っている",
"postSummonAuraBreak": "{{pokemonNameWithAffix}}は\nすべての オーラを 制圧する",

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "どろあそびの 効果が なくなった!",
"waterSportOnAdd": "炎の威力が 弱まった!",
"waterSportOnRemove": "みずあそびの 効果が なくなった!",
"plasmaFistsOnAdd": "電子のシャワーが 降りそそいだ!",
"spikesOnAdd": "{{opponentDesc}}の 足下に\n{{moveName}}が 散らばった!",
"spikesActivateTrap": "{{pokemonNameWithAffix}}は\nまきびしの ダメージを 受けた",
"toxicSpikesOnAdd": "{{opponentDesc}}の 足下に\n{{moveName}}が 散らばった!",

View File

@ -68,5 +68,5 @@
"chillyReception": "{{pokemonName}}は\n寒い ギャグを かました",
"swapArenaTags": "{{pokemonName}}は\nお互いの 場の 効果を 入れ替えた",
"exposedMove": "{{pokemonName}}は {{targetPokemonName}}の\n正体を 見破った",
"afterYou": "{{pokemonName}}は\nお言葉に 甘えることにした"
"afterYou": "{{targetName}}は\nお言葉に 甘えることにした"
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "흙놀이의 효과가\n없어졌다!",
"waterSportOnAdd": "불꽃의 위력이 약해졌다!",
"waterSportOnRemove": "물놀이의 효과가\n없어졌다!",
"plasmaFistsOnAdd": "전기 입자가 쏟아졌다!",
"spikesOnAdd": "{{opponentDesc}}의 발밑에\n압정이 뿌려졌다!",
"spikesActivateTrap": "{{pokemonNameWithAffix}}[[는]]\n압정뿌리기의 데미지를 입었다!",
"toxicSpikesOnAdd": "{{opponentDesc}}의 발밑에\n독압정이 뿌려졌다!",
@ -54,4 +55,4 @@
"safeguardOnRemove": "필드를 감싸던 신비의 베일이 없어졌다!",
"safeguardOnRemovePlayer": "우리 편을 감싸던 신비의 베일이 없어졌다!",
"safeguardOnRemoveEnemy": "상대 편을 감싸던 신비의 베일이 없어졌다!"
}
}

View File

@ -69,5 +69,5 @@
"chillyReception": "{{pokemonName}}[[는]] 썰렁한 개그를 선보였다!",
"exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!",
"safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!",
"afterYou": "{{pokemonName}}[[는]]\n배려를 받아들이기로 했다!"
"afterYou": "{{targetName}}[[는]]\n배려를 받아들이기로 했다!"
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "Os efeitos de Mud Sport\nsumiram.",
"waterSportOnAdd": "O poder de movimentos de fogo foi enfraquecido!",
"waterSportOnRemove": "Os efeitos de Water Sport\nsumiram.",
"plasmaFistsOnAdd": "Um dilúvio de íons chove sobre o campo de batalha!",
"spikesOnAdd": "{{moveName}} foram espalhados\nno chão ao redor de {{opponentDesc}}!",
"spikesActivateTrap": "{{pokemonNameWithAffix}} foi ferido\npelos espinhos!",
"toxicSpikesOnAdd": "{{moveName}} foram espalhados\nno chão ao redor de {{opponentDesc}}!",

View File

@ -64,5 +64,5 @@
"chillyReception": "{{pokemonName}} está prestes a contar uma piada gelada!",
"exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!",
"safeguard": "{{targetName}} está protegido por Safeguard!",
"afterYou": "{{pokemonName}} aceitou a gentil oferta!"
"afterYou": "{{targetName}} aceitou a gentil oferta!"
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "玩泥巴的效果消失了!",
"waterSportOnAdd": "火焰的威力减弱了!",
"waterSportOnRemove": "玩水的效果消失了!",
"plasmaFistsOnAdd": "等离子雨倾盆而下!",
"spikesOnAdd": "{{opponentDesc}}脚下\n散落着{{moveName}}",
"spikesActivateTrap": "{{pokemonNameWithAffix}}\n受到了撒菱的伤害",
"toxicSpikesOnAdd": "{{opponentDesc}}脚下\n散落着{{moveName}}",
@ -54,4 +55,4 @@
"safeguardOnRemove": "包围整个场地的\n神秘之幕消失了",
"safeguardOnRemovePlayer": "包围我方的\n神秘之幕消失了",
"safeguardOnRemoveEnemy": "包围对手的\n神秘之幕消失了"
}
}

View File

@ -360,7 +360,7 @@
},
"EVOLUTION_TRACKER_GIMMIGHOUL": {
"name": "金子宝物",
"description": "这个小精靈爱金子! 继续挑金子然后谁知道什么会发生!"
"description": "这个宝可梦最爱金币!多收集点金币的话会发生什么呢?"
},
"BATON": {
"name": "接力棒",

View File

@ -68,5 +68,5 @@
"chillyReception": "{{pokemonName}}\n说出了冷笑话",
"exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!",
"safeguard": "{{targetName}}\n正受到神秘之幕的保护",
"afterYou": "{{pokemonName}}\n接受了对手的好意"
"afterYou": "{{targetName}}\n接受了对手的好意"
}

View File

@ -28,6 +28,7 @@
"mudSportOnRemove": "玩泥巴的效果消失了!",
"waterSportOnAdd": "火焰的威力減弱了!",
"waterSportOnRemove": "玩水的效果消失了!",
"plasmaFistsOnAdd": "等離子雨傾盆而下!",
"spikesOnAdd": "{{opponentDesc}}腳下\n散落著{{moveName}}",
"spikesActivateTrap": "{{pokemonNameWithAffix}}\n受到了撒菱的傷害",
"toxicSpikesOnAdd": "{{opponentDesc}}腳下\n散落著{{moveName}}",

View File

@ -68,5 +68,5 @@
"chillyReception": "{{pokemonName}}\n說了冷笑話",
"exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!",
"safeguard": "{{targetName}}\n正受到神秘之幕的保護",
"afterYou": "{{pokemonName}}\n接受了對手的好意"
"afterYou": "{{targetName}}\n接受了對手的好意"
}

View File

@ -1387,7 +1387,8 @@ export const modifierTypes = {
FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(false),
RARE_FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(true),
EVOLUTION_TRACKER_GIMMIGHOUL: () => new PokemonHeldItemModifierType("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "relic_gold", (type, _args) => new Modifiers.EvoTrackerModifier(type, (_args[0] as Pokemon).id, Species.GIMMIGHOUL, 10)),
EVOLUTION_TRACKER_GIMMIGHOUL: () => new PokemonHeldItemModifierType("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "relic_gold",
(type, args) => new Modifiers.EvoTrackerModifier(type, (args[0] as Pokemon).id, Species.GIMMIGHOUL, 10)),
MEGA_BRACELET: () => new ModifierType("modifierType:ModifierType.MEGA_BRACELET", "mega_bracelet", (type, _args) => new Modifiers.MegaEvolutionAccessModifier(type)),
DYNAMAX_BAND: () => new ModifierType("modifierType:ModifierType.DYNAMAX_BAND", "dynamax_band", (type, _args) => new Modifiers.GigantamaxAccessModifier(type)),

View File

@ -1,5 +1,5 @@
import * as ModifierTypes from "./modifier-type";
import { ModifierType, modifierTypes } from "./modifier-type";
import { getModifierType, ModifierType, modifierTypes } from "./modifier-type";
import BattleScene from "../battle-scene";
import { getLevelTotalExp } from "../data/exp";
import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball";
@ -852,26 +852,47 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
}
matchType(modifier: Modifier): boolean {
if (modifier instanceof EvoTrackerModifier) {
return (modifier as EvoTrackerModifier).species === this.species;
}
return false;
return modifier instanceof EvoTrackerModifier && modifier.species === this.species && modifier.required === this.required;
}
clone(): PersistentModifier {
return new EvoTrackerModifier(this.type, this.pokemonId, this.species, this.stackCount);
return new EvoTrackerModifier(this.type, this.pokemonId, this.species, this.required, this.stackCount);
}
getArgs(): any[] {
return super.getArgs().concat(this.species);
return super.getArgs().concat([this.species, this.required]);
}
apply(args: any[]): boolean {
return true;
}
getMaxHeldItemCount(_pokemon: Pokemon): integer {
return this.required;
getIconStackText(scene: BattleScene, virtual?: boolean): Phaser.GameObjects.BitmapText | null {
if (this.getMaxStackCount(scene) === 1 || (virtual && !this.virtualStackCount)) {
return null;
}
const pokemon = scene.getPokemonById(this.pokemonId);
this.stackCount = pokemon
? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length
: this.stackCount;
const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11);
text.letterSpacing = -0.5;
if (this.getStackCount() >= this.required) {
text.setTint(0xf89890);
}
text.setOrigin(0, 0);
return text;
}
getMaxHeldItemCount(pokemon: Pokemon): integer {
this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length;
return 999;
}
}
@ -2414,9 +2435,8 @@ export class MoneyRewardModifier extends ConsumableModifier {
scene.getParty().map(p => {
if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) {
p.evoCounter++;
const modifierType: ModifierType = modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL();
const modifier = modifierType!.newModifier(p);
p.evoCounter ? p.evoCounter++ : p.evoCounter = 1;
const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier;
scene.addModifier(modifier);
}
});
@ -2586,7 +2606,7 @@ export class HealShopCostModifier extends PersistentModifier {
constructor(type: ModifierType, shopMultiplier: number, stackCount?: integer) {
super(type, stackCount);
this.shopMultiplier = shopMultiplier;
this.shopMultiplier = shopMultiplier ?? 2.5;
}
match(modifier: Modifier): boolean {
@ -2598,11 +2618,16 @@ export class HealShopCostModifier extends PersistentModifier {
}
apply(args: any[]): boolean {
(args[0] as Utils.IntegerHolder).value *= this.shopMultiplier;
const moneyCost = args[0] as Utils.NumberHolder;
moneyCost.value = Math.floor(moneyCost.value * this.shopMultiplier);
return true;
}
getArgs(): any[] {
return super.getArgs().concat(this.shopMultiplier);
}
getMaxStackCount(scene: BattleScene): integer {
return 1;
}

View File

@ -238,7 +238,7 @@ export class GameOverPhase extends BattlePhase {
enemyModifiers: this.scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)),
arena: new ArenaData(this.scene.arena),
pokeballCounts: this.scene.pokeballCounts,
money: this.scene.money,
money: Math.floor(this.scene.money),
score: this.scene.score,
waveIndex: this.scene.currentBattle.waveIndex,
battleType: this.scene.currentBattle.battleType,

View File

@ -25,6 +25,7 @@ import { GameOverPhase } from "#app/phases/game-over-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { SwitchType } from "#enums/switch-type";
import { BattlerTagType } from "#enums/battler-tag-type";
/**
* Will handle (in order):
@ -218,9 +219,17 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
start() {
super.start();
// Lapse any residual flinches/endures but ignore all other turn-end battle tags
const includedLapseTags = [BattlerTagType.FLINCHED, BattlerTagType.ENDURING];
const field = this.scene.getField(true).filter(p => p.summonData);
field.forEach(pokemon => {
pokemon.lapseTags(BattlerTagLapseType.TURN_END);
const tags = pokemon.summonData.tags;
tags.filter(t => includedLapseTags.includes(t.tagType)
&& t.lapseTypes.includes(BattlerTagLapseType.TURN_END)
&& !(t.lapse(pokemon, BattlerTagLapseType.TURN_END))).forEach(t => {
t.onRemove(pokemon);
tags.splice(tags.indexOf(t), 1);
});
});
// Remove any status tick phases

View File

@ -46,8 +46,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
const pokemon = this.getPokemon();
if (!pokemon.isOnField() || (!this.revive && !pokemon.isActive())) {
super.end();
return;
return super.end();
}
const hasMessage = !!this.message;
@ -58,7 +57,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
if (healBlock && this.hpHealed > 0) {
this.scene.queueMessage(healBlock.onActivation(pokemon));
this.message = null;
super.end();
return super.end();
} else if (healOrDamage) {
const hpRestoreMultiplier = new Utils.IntegerHolder(1);
if (!this.revive) {

View File

@ -43,20 +43,17 @@ export class TurnStartPhase extends FieldPhase {
orderedTargets = Utils.randSeedShuffle(orderedTargets);
}, this.scene.currentBattle.turn, this.scene.waveSeed);
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getEffectiveStat(Stat.SPD) || 0;
const bSpeed = b?.getEffectiveStat(Stat.SPD) || 0;
return bSpeed - aSpeed;
});
// Next, a check for Trick Room is applied. If Trick Room is present, the order is reversed.
// Next, a check for Trick Room is applied to determine sort order.
const speedReversed = new Utils.BooleanHolder(false);
this.scene.arena.applyTags(TrickRoomTag, speedReversed);
if (speedReversed.value) {
orderedTargets = orderedTargets.reverse();
}
// Adjust the sort function based on whether Trick Room is active.
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed;
});
return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER));
}

View File

@ -957,7 +957,7 @@ export class GameData {
enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)),
arena: new ArenaData(scene.arena),
pokeballCounts: scene.pokeballCounts,
money: scene.money,
money: Math.floor(scene.money),
score: scene.score,
waveIndex: scene.currentBattle.waveIndex,
battleType: scene.currentBattle.battleType,
@ -1047,7 +1047,7 @@ export class GameData {
scene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
}
scene.money = sessionData.money || 0;
scene.money = Math.floor(sessionData.money || 0);
scene.updateMoneyText();
if (scene.money > this.gameStats.highestMoney) {

View File

@ -6,6 +6,10 @@ const LATEST_VERSION = "1.0.5";
export function applySessionDataPatches(data: SessionSaveData) {
const curVersion = data.gameVersion;
// Always sanitize money as a safeguard
data.money = Math.floor(data.money);
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":

View File

@ -1,11 +1,7 @@
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { StatusEffect } from "#app/enums/status-effect";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
import Pokemon from "#app/field/pokemon";
import { BerryPhase } from "#app/phases/berry-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import GameManager from "#app/test/utils/gameManager";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -53,13 +49,13 @@ describe("Abilities - Gulp Missile", () => {
});
it("changes to Gulping Form if HP is over half when Surf or Dive is used", async () => {
await game.startBattle([Species.CRAMORANT]);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DIVE);
await game.toNextTurn();
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getHpRatio()).toBeGreaterThanOrEqual(.5);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
@ -67,21 +63,21 @@ describe("Abilities - Gulp Missile", () => {
});
it("changes to Gorging Form if HP is under half when Surf or Dive is used", async () => {
await game.startBattle([Species.CRAMORANT]);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.49);
expect(cramorant.getHpRatio()).toBe(.49);
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined();
expect(cramorant.formIndex).toBe(GORGING_FORM);
});
it("changes to base form when switched out after Surf or Dive is used", async () => {
await game.startBattle([Species.CRAMORANT, Species.MAGIKARP]);
await game.classicMode.startBattle([Species.CRAMORANT, Species.MAGIKARP]);
const cramorant = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SURF);
@ -96,51 +92,51 @@ describe("Abilities - Gulp Missile", () => {
});
it("changes form during Dive's charge turn", async () => {
await game.startBattle([Species.CRAMORANT]);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
it("deals 1/4 of the attacker's maximum HP when hit by a damaging attack", async () => {
game.override.enemyMoveset([Moves.TACKLE]);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.TACKLE);
await game.classicMode.startBattle([Species.CRAMORANT]);
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "damageAndUpdate");
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
});
it("does not have any effect when hit by non-damaging attack", async () => {
game.override.enemyMoveset([Moves.TAIL_WHIP]);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.TAIL_WHIP);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => {
game.override.enemyMoveset([Moves.TACKLE]);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.TACKLE);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
@ -149,12 +145,12 @@ describe("Abilities - Gulp Missile", () => {
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
@ -163,8 +159,8 @@ describe("Abilities - Gulp Missile", () => {
});
it("paralyzes the enemy when hit in Gorging form", async () => {
game.override.enemyMoveset([Moves.TACKLE]);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.TACKLE);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
@ -173,12 +169,12 @@ describe("Abilities - Gulp Missile", () => {
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.45);
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined();
expect(cramorant.formIndex).toBe(GORGING_FORM);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
expect(enemy.status?.effect).toBe(StatusEffect.PARALYSIS);
@ -187,21 +183,21 @@ describe("Abilities - Gulp Missile", () => {
});
it("does not activate the ability when underwater", async () => {
game.override.enemyMoveset([Moves.SURF]);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.SURF);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to(BerryPhase, false);
await game.phaseInterceptor.to("BerryPhase", false);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
it("prevents effect damage but inflicts secondary effect on attacker with Magic Guard", async () => {
game.override.enemyMoveset([Moves.TACKLE]).enemyAbility(Abilities.MAGIC_GUARD);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.MAGIC_GUARD);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
@ -209,13 +205,13 @@ describe("Abilities - Gulp Missile", () => {
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
const enemyHpPreEffect = enemy.hp;
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.hp).toBe(enemyHpPreEffect);
expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
@ -223,20 +219,36 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(NORMAL_FORM);
});
it("activates on faint", async () => {
game.override.enemyMoveset(Moves.THUNDERBOLT);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SURF);
await game.phaseInterceptor.to("FaintPhase");
expect(cramorant.hp).toBe(0);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
expect(cramorant.formIndex).toBe(NORMAL_FORM);
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.DEF)).toBe(-1);
});
it("cannot be suppressed", async () => {
game.override.enemyMoveset([Moves.GASTRO_ACID]);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.GASTRO_ACID);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
@ -244,19 +256,19 @@ describe("Abilities - Gulp Missile", () => {
});
it("cannot be swapped with another ability", async () => {
game.override.enemyMoveset([Moves.SKILL_SWAP]);
await game.startBattle([Species.CRAMORANT]);
game.override.enemyMoveset(Moves.SKILL_SWAP);
await game.classicMode.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
game.move.select(Moves.SURF);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
@ -266,9 +278,9 @@ describe("Abilities - Gulp Missile", () => {
it("cannot be copied", async () => {
game.override.enemyAbility(Abilities.TRACE);
await game.startBattle([Species.CRAMORANT]);
await game.classicMode.startBattle([Species.CRAMORANT]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnStartPhase);
await game.phaseInterceptor.to("TurnStartPhase");
expect(game.scene.getEnemyPokemon()?.hasAbility(Abilities.GULP_MISSILE)).toBe(false);
});

View File

@ -3,6 +3,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "../utils/gameManager";
import { PokeballType } from "#app/enums/pokeball";
import BattleScene from "#app/battle-scene";
import { Moves } from "#app/enums/moves";
describe("Spec - Pokemon", () => {
let phaserGame: Phaser.Game;
@ -63,4 +64,15 @@ describe("Spec - Pokemon", () => {
});
});
});
it("should not share tms between different forms", async () => {
game.override.starterForms({ [Species.ROTOM]: 4 });
await game.classicMode.startBattle([Species.ROTOM]);
const fanRotom = game.scene.getPlayerPokemon()!;
expect(fanRotom.compatibleTms).not.toContain(Moves.BLIZZARD);
expect(fanRotom.compatibleTms).toContain(Moves.AIR_SLASH);
});
});

View File

@ -0,0 +1,98 @@
import { BattlerIndex } from "#app/battle";
import { Type } from "#app/data/type";
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, it, expect, vi } from "vitest";
describe("Moves - Plasma Fists", () => {
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.PLASMA_FISTS, Moves.TACKLE])
.battleType("double")
.startingLevel(100)
.enemySpecies(Species.DUSCLOPS)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE)
.enemyLevel(100);
});
it("should convert all subsequent Normal-type attacks to Electric-type", async () => {
await game.classicMode.startBattle([Species.DUSCLOPS, Species.BLASTOISE]);
const field = game.scene.getField(true);
field.forEach(p => vi.spyOn(p, "getMoveType"));
game.move.select(Moves.PLASMA_FISTS, 0, BattlerIndex.ENEMY);
game.move.select(Moves.TACKLE, 1, BattlerIndex.ENEMY_2);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to("BerryPhase", false);
field.forEach(p => {
expect(p.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
expect(p.hp).toBeLessThan(p.getMaxHp());
});
});
it("should not affect Normal-type attacks boosted by Pixilate", async () => {
game.override
.battleType("single")
.enemyAbility(Abilities.PIXILATE);
await game.classicMode.startBattle([Species.ONIX]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "getMoveType");
game.move.select(Moves.PLASMA_FISTS);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.getMoveType).toHaveLastReturnedWith(Type.FAIRY);
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
});
it("should affect moves that become Normal type due to Normalize", async () => {
game.override
.battleType("single")
.enemyAbility(Abilities.NORMALIZE)
.enemyMoveset(Moves.WATER_GUN);
await game.classicMode.startBattle([Species.DUSCLOPS]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "getMoveType");
game.move.select(Moves.PLASMA_FISTS);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
});
});

View File

@ -0,0 +1,82 @@
import { allMoves } from "#app/data/move";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { BattlerIndex } from "#app/battle";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - SYRUP BOMB", () => {
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
.starterSpecies(Species.MAGIKARP)
.enemySpecies(Species.SNORLAX)
.startingLevel(30)
.enemyLevel(100)
.moveset([Moves.SYRUP_BOMB, Moves.SPLASH])
.enemyMoveset(Moves.SPLASH);
vi.spyOn(allMoves[Moves.SYRUP_BOMB], "accuracy", "get").mockReturnValue(100);
});
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/syrup_bomb_(move)
it("decreases the target Pokemon's speed stat once per turn for 3 turns",
async () => {
await game.startBattle([Species.MAGIKARP]);
const targetPokemon = game.scene.getEnemyPokemon()!;
expect(targetPokemon.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.SYRUP_BOMB);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.move.forceHit();
await game.toNextTurn();
expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeDefined();
expect(targetPokemon.getStatStage(Stat.SPD)).toBe(-1);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeDefined();
expect(targetPokemon.getStatStage(Stat.SPD)).toBe(-2);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeUndefined();
expect(targetPokemon.getStatStage(Stat.SPD)).toBe(-3);
}
);
it("does not affect Pokemon with the ability Bulletproof",
async () => {
game.override.enemyAbility(Abilities.BULLETPROOF);
await game.startBattle([Species.MAGIKARP]);
const targetPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SYRUP_BOMB);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.move.forceHit();
await game.toNextTurn();
expect(targetPokemon.isFullHp()).toBe(true);
expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeUndefined();
expect(targetPokemon.getStatStage(Stat.SPD)).toBe(0);
}
);
});

View File

@ -303,7 +303,7 @@ export default class GameManager {
vi.spyOn(enemy, "getNextMove").mockReturnValueOnce({
move: moveId,
targets: (target && !legalTargets.multiple && legalTargets.targets.includes(target))
targets: (target !== undefined && !legalTargets.multiple && legalTargets.targets.includes(target))
? [target]
: enemy.getNextTargets(moveId)
});

View File

@ -99,8 +99,9 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
clear() {
super.clear();
this.cursor = -1;
this.scrollGridHandler.reset();
this.cursor = -1;
this.summaryContainer.setVisible(false);
this.pokemonIconsContainer.removeAll(true);
this.pokemonContainers = [];
@ -164,8 +165,8 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
this.scrollGridHandler.setTotalElements(this.eggHatchData.length);
this.updatePokemonIcons();
this.setCursor(0);
this.scene.playSoundWithoutBgm("evolution_fanfare");
return true;
}

View File

@ -6,6 +6,7 @@ import { TextStyle, addTextObject, getTextStyleOptions } from "./text";
import { getSplashMessages } from "../data/splash-messages";
import i18next from "i18next";
import { TimedEventDisplay } from "#app/timed-event-manager";
import { version } from "../../package.json";
export default class TitleUiHandler extends OptionSelectUiHandler {
/** If the stats can not be retrieved, use this fallback value */
@ -16,6 +17,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
private splashMessage: string;
private splashMessageText: Phaser.GameObjects.Text;
private eventDisplay: TimedEventDisplay;
private appVersionText: Phaser.GameObjects.Text;
private titleStatsTimer: NodeJS.Timeout | null;
@ -68,6 +70,11 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
loop: -1,
yoyo: true,
});
this.appVersionText = addTextObject(this.scene, logo.x - 60, logo.y + logo.displayHeight + 4, "", TextStyle.MONEY, { fontSize: "54px" });
this.appVersionText.setOrigin(0.5, 0.5);
this.appVersionText.setAngle(0);
this.titleContainer.add(this.appVersionText);
}
updateTitleStats(): void {
@ -91,6 +98,8 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
this.splashMessage = Utils.randItem(getSplashMessages());
this.splashMessageText.setText(i18next.t(this.splashMessage, { count: TitleUiHandler.BATTLES_WON_FALLBACK }));
this.appVersionText.setText("v"+version);
const ui = this.getUi();
if (this.scene.eventManager.isEventActive()) {