Merge branch 'beta' into fusionevomovefix

This commit is contained in:
NightKev 2024-11-30 01:51:38 -08:00 committed by GitHub
commit 7ad957c619
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 3234 additions and 6432 deletions

View File

@ -123,6 +123,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to
- Involuntary-Twitch - Involuntary-Twitch
- selstar - selstar
- koda_want_to_sleep - koda_want_to_sleep
- thedreadedden
### 🎨 Move Animations ### 🎨 Move Animations
- Pokémon Reborn - Pokémon Reborn

87
package-lock.json generated
View File

@ -16,6 +16,7 @@
"i18next-http-backend": "^2.6.1", "i18next-http-backend": "^2.6.1",
"i18next-korean-postposition-processor": "^1.0.0", "i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.1.0", "json-stable-stringify": "^1.1.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0", "phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.1.84" "phaser3-rex-plugins": "^1.1.84"
}, },
@ -2723,6 +2724,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cross-fetch": { "node_modules/cross-fetch": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
@ -4045,6 +4051,11 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -4072,6 +4083,11 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": { "node_modules/ini": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
@ -4481,6 +4497,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -4648,6 +4675,14 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/linkify-it": { "node_modules/linkify-it": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
@ -5237,6 +5272,11 @@
"dev": true, "dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/papaparse": { "node_modules/papaparse": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
@ -5485,6 +5525,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/prompts": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -5551,6 +5596,25 @@
} }
] ]
}, },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/rechoir": { "node_modules/rechoir": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@ -5741,6 +5805,11 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safe-regex": { "node_modules/safe-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
@ -5800,6 +5869,11 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -5917,6 +5991,14 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -6473,6 +6555,11 @@
"requires-port": "^1.0.0" "requires-port": "^1.0.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.8", "version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",

View File

@ -55,6 +55,7 @@
"i18next-http-backend": "^2.6.1", "i18next-http-backend": "^2.6.1",
"i18next-korean-postposition-processor": "^1.0.0", "i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.1.0", "json-stable-stringify": "^1.1.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0", "phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.1.84" "phaser3-rex-plugins": "^1.1.84"
}, },

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -3008,7 +3008,8 @@ export default class BattleScene extends SceneBase {
} }
validateAchv(achv: Achv, args?: unknown[]): boolean { validateAchv(achv: Achv, args?: unknown[]): boolean {
if (!this.gameData.achvUnlocks.hasOwnProperty(achv.id) && achv.validate(this, args)) { if ((!this.gameData.achvUnlocks.hasOwnProperty(achv.id) || Overrides.ACHIEVEMENTS_REUNLOCK_OVERRIDE)
&& achv.validate(this, args)) {
this.gameData.achvUnlocks[achv.id] = new Date().getTime(); this.gameData.achvUnlocks[achv.id] = new Date().getTime();
this.ui.achvBar.showAchv(achv); this.ui.achvBar.showAchv(achv);
if (vouchers.hasOwnProperty(achv.id)) { if (vouchers.hasOwnProperty(achv.id)) {

View File

@ -4112,9 +4112,13 @@ export class PostBattleAbAttr extends AbAttr {
} }
export class PostBattleLootAbAttr extends PostBattleAbAttr { export class PostBattleLootAbAttr extends PostBattleAbAttr {
/**
* @param args - `[0]`: boolean for if the battle ended in a victory
* @returns `true` if successful
*/
applyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { applyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
const postBattleLoot = pokemon.scene.currentBattle.postBattleLoot; const postBattleLoot = pokemon.scene.currentBattle.postBattleLoot;
if (!simulated && postBattleLoot.length) { if (!simulated && postBattleLoot.length && args[0]) {
const randItem = Utils.randSeedItem(postBattleLoot); const randItem = Utils.randSeedItem(postBattleLoot);
//@ts-ignore - TODO see below //@ts-ignore - TODO see below
if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true, undefined, false)) { // TODO: fix. This is a promise!? if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true, undefined, false)) { // TODO: fix. This is a promise!?
@ -4575,14 +4579,15 @@ export class MoneyAbAttr extends PostBattleAbAttr {
/** /**
* @param pokemon {@linkcode Pokemon} that is the user of this ability. * @param pokemon {@linkcode Pokemon} that is the user of this ability.
* @param passive N/A * @param passive N/A
* @param args N/A * @param args - `[0]`: boolean for if the battle ended in a victory
* @returns true * @returns `true` if successful
*/ */
applyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { applyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
if (!simulated) { if (!simulated && args[0]) {
pokemon.scene.currentBattle.moneyScattered += pokemon.scene.getWaveMoneyAmount(0.2); pokemon.scene.currentBattle.moneyScattered += pokemon.scene.getWaveMoneyAmount(0.2);
return true;
} }
return true; return false;
} }
} }
@ -4590,13 +4595,12 @@ export class MoneyAbAttr extends PostBattleAbAttr {
* Applies a stat change after a Pokémon is summoned, * Applies a stat change after a Pokémon is summoned,
* conditioned on the presence of a specific arena tag. * conditioned on the presence of a specific arena tag.
* *
* @extends {PostSummonStatStageChangeAbAttr} * @extends PostSummonStatStageChangeAbAttr
*/ */
export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageChangeAbAttr { export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageChangeAbAttr {
/** /**
* The type of arena tag that conditions the stat change. * The type of arena tag that conditions the stat change.
* @private * @private
* @type {ArenaTagType}
*/ */
private tagType: ArenaTagType; private tagType: ArenaTagType;
@ -4972,7 +4976,7 @@ class ForceSwitchOutHelper {
pokemon.scene.clearEnemyHeldItemModifiers(); pokemon.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) { if (switchOutTarget.hp) {
pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene)); pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene, false));
pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene)); pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene));
} }
} }
@ -5779,9 +5783,10 @@ export function initAbilities() {
.attr(WonderSkinAbAttr) .attr(WonderSkinAbAttr)
.ignorable(), .ignorable(),
new Ability(Abilities.ANALYTIC, 5) new Ability(Abilities.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) => .attr(MovePowerBoostAbAttr, (user, target, move) => {
!!target?.getLastXMoves(1).find(m => m.turn === target?.scene.currentBattle.turn) const movePhase = user?.scene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user.id);
|| user?.scene.currentBattle.turnCommands[target?.getBattlerIndex() ?? BattlerIndex.ATTACKER]?.command !== Command.FIGHT, 1.3), return Utils.isNullOrUndefined(movePhase);
}, 1.3),
new Ability(Abilities.ILLUSION, 5) new Ability(Abilities.ILLUSION, 5)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)
@ -5929,10 +5934,10 @@ export function initAbilities() {
.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)
.attr(PostDamageForceSwitchAbAttr) .attr(PostDamageForceSwitchAbAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion .edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
new Ability(Abilities.EMERGENCY_EXIT, 7) new Ability(Abilities.EMERGENCY_EXIT, 7)
.attr(PostDamageForceSwitchAbAttr) .attr(PostDamageForceSwitchAbAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion .edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
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)
@ -6340,8 +6345,7 @@ export function initAbilities() {
.attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ]) .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ])
.ignorable(), .ignorable(),
new Ability(Abilities.SUPERSWEET_SYRUP, 9) new Ability(Abilities.SUPERSWEET_SYRUP, 9)
.attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1) .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1),
.condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)),
new Ability(Abilities.HOSPITALITY, 9) new Ability(Abilities.HOSPITALITY, 9)
.attr(PostSummonAllyHealAbAttr, 4, true), .attr(PostSummonAllyHealAbAttr, 4, true),
new Ability(Abilities.TOXIC_CHAIN, 9) new Ability(Abilities.TOXIC_CHAIN, 9)

View File

@ -0,0 +1,46 @@
import { Species } from "#enums/species";
/**
* A list of all {@link https://bulbapedia.bulbagarden.net/wiki/Paradox_Pok%C3%A9mon | Paradox Pokemon}, NOT including the legendaries Miraidon and Koraidon.
*/
export const NON_LEGEND_PARADOX_POKEMON = [
Species.GREAT_TUSK,
Species.SCREAM_TAIL,
Species.BRUTE_BONNET,
Species.FLUTTER_MANE,
Species.SLITHER_WING,
Species.SANDY_SHOCKS,
Species.ROARING_MOON,
Species.WALKING_WAKE,
Species.GOUGING_FIRE,
Species.RAGING_BOLT,
Species.IRON_TREADS,
Species.IRON_BUNDLE,
Species.IRON_HANDS,
Species.IRON_JUGULIS,
Species.IRON_MOTH,
Species.IRON_THORNS,
Species.IRON_VALIANT,
Species.IRON_LEAVES,
Species.IRON_BOULDER,
Species.IRON_CROWN,
];
/**
* A list of all {@link https://bulbapedia.bulbagarden.net/wiki/Ultra_Beast | Ultra Beasts}, NOT including legendaries such as Necrozma or the Cosmog line.
*
* Note that all of these Ultra Beasts are still considered Sub-Legendary.
*/
export const NON_LEGEND_ULTRA_BEASTS = [
Species.NIHILEGO,
Species.BUZZWOLE,
Species.PHEROMOSA,
Species.XURKITREE,
Species.CELESTEELA,
Species.KARTANA,
Species.GUZZLORD,
Species.POIPOLE,
Species.NAGANADEL,
Species.STAKATAKA,
Species.BLACEPHALON,
];

View File

@ -1385,14 +1385,38 @@ export class UserHpDamageAttr extends FixedDamageAttr {
} }
export class TargetHalfHpDamageAttr extends FixedDamageAttr { export class TargetHalfHpDamageAttr extends FixedDamageAttr {
// the initial amount of hp the target had before the first hit
// used for multi lens
private initialHp: number;
constructor() { constructor() {
super(0); super(0);
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
(args[0] as Utils.IntegerHolder).value = Utils.toDmgValue(target.hp / 2); // first, determine if the hit is coming from multi lens or not
const lensCount = user.getHeldItems().find(i => i instanceof PokemonMultiHitModifier)?.getStackCount() ?? 0;
if (lensCount <= 0) {
// no multi lenses; we can just halve the target's hp and call it a day
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(target.hp / 2);
return true;
}
return true; // figure out what hit # we're on
switch (user.turnData.hitCount - user.turnData.hitsLeft) {
case 0:
// first hit of move; update initialHp tracker
this.initialHp = target.hp;
default:
// multi lens added hit; use initialHp tracker to ensure correct damage
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(this.initialHp / 2);
return true;
break;
case lensCount + 1:
// parental bond added hit; calc damage as normal
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(target.hp / 2);
return true;
break;
}
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
@ -5967,50 +5991,113 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch * 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 it did, the user of U-turn or Volt Switch will not be switched out.
*/ */
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr)
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN) && [ Moves.U_TURN, Moves.VOLT_SWITCH, Moves.FLIP_TURN ].includes(move.id)
) { ) {
if (this.hpDroppedBelowHalf(target)) { if (this.hpDroppedBelowHalf(target)) {
return false; return false;
} }
} }
// Switch out logic for the player's Pokemon
if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { // Find indices of off-field Pokemon that are eligible to be switched into
const eligibleNewIndices: number[] = [];
switchOutTarget.scene.getPlayerParty().forEach((pokemon, index) => {
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
eligibleNewIndices.push(index);
}
});
if (eligibleNewIndices.length < 1) {
return false; return false;
} }
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); if (this.switchType === SwitchType.FORCE_SWITCH) {
user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); switchOutTarget.leaveField(true);
return true; const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
user.scene.prependToPhase(
new SwitchSummonPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
slotIndex,
false,
true
),
MoveEndPhase
);
} else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
user.scene.prependToPhase(
new SwitchPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
true,
true
),
MoveEndPhase
);
return true;
}
} }
return false; return false;
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
// Switch out logic for trainer battles // Find indices of off-field Pokemon that are eligible to be switched into
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { const eligibleNewIndices: number[] = [];
switchOutTarget.scene.getEnemyParty().forEach((pokemon, index) => {
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
eligibleNewIndices.push(index);
}
});
if (eligibleNewIndices.length < 1) {
return false; return false;
} }
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
// for opponent switching out if (this.switchType === SwitchType.FORCE_SWITCH) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); switchOutTarget.leaveField(true);
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), user.scene.prependToPhase(
false, false), MoveEndPhase); new SwitchSummonPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
slotIndex,
false,
false
),
MoveEndPhase
);
} else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
user.scene.prependToPhase(
new SwitchSummonPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false,
false
),
MoveEndPhase
);
}
} }
} else { } else { // Switch out logic for wild pokemon
/** /**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch * 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 it did, the user of U-turn or Volt Switch will not be switched out.
*/ */
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr)
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) { && [ Moves.U_TURN, Moves.VOLT_SWITCH, Moves.FLIP_TURN ].includes(move.id)
) {
if (this.hpDroppedBelowHalf(target)) { if (this.hpDroppedBelowHalf(target)) {
return false; return false;
} }
} }
// 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;
} }
@ -6035,7 +6122,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
user.scene.clearEnemyHeldItemModifiers(); user.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) { if (switchOutTarget.hp) {
user.scene.pushPhase(new BattleEndPhase(user.scene)); user.scene.pushPhase(new BattleEndPhase(user.scene, false));
user.scene.pushPhase(new NewBattlePhase(user.scene)); user.scene.pushPhase(new NewBattlePhase(user.scene));
} }
} }
@ -7777,11 +7864,10 @@ export function initMoves() {
.windMove(), .windMove(),
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1), new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.ignoresSubstitute() .ignoresSubstitute()
.hidesTarget() .hidesTarget()
.windMove() .windMove(),
.partial(), // Should force random switches
new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
@ -7857,10 +7943,9 @@ export function initMoves() {
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.soundBased() .soundBased()
.hidesTarget() .hidesTarget(),
.partial(), // Should force random switching
new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1) new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.soundBased(), .soundBased(),
@ -9222,8 +9307,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
new AttackMove(Moves.CIRCLE_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5) new AttackMove(Moves.CIRCLE_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
.attr(ForceSwitchOutAttr) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.partial(), // Should force random switches .hidesTarget(),
new AttackMove(Moves.INCINERATE, Type.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) new AttackMove(Moves.INCINERATE, Type.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_ENEMIES) .target(MoveTarget.ALL_NEAR_ENEMIES)
.attr(RemoveHeldItemAttr, true), .attr(RemoveHeldItemAttr, true),
@ -9291,9 +9376,8 @@ export function initMoves() {
new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5) new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5)
.attr(CritOnlyAttr), .attr(CritOnlyAttr),
new AttackMove(Moves.DRAGON_TAIL, Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5) new AttackMove(Moves.DRAGON_TAIL, Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
.attr(ForceSwitchOutAttr) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.hidesTarget() .hidesTarget(),
.partial(), // Should force random switches
new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5) new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, true),
new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
@ -10554,6 +10638,7 @@ export function initMoves() {
new AttackMove(Moves.TWIN_BEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9) new AttackMove(Moves.TWIN_BEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9)
.attr(MultiHitAttr, MultiHitType._2), .attr(MultiHitAttr, MultiHitType._2),
new AttackMove(Moves.RAGE_FIST, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) new AttackMove(Moves.RAGE_FIST, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9)
.partial() // Counter resets every wave instead of on arena reset
.attr(HitCountPowerAttr) .attr(HitCountPowerAttr)
.punchingMove(), .punchingMove(),
new AttackMove(Moves.ARMOR_CANNON, Type.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) new AttackMove(Moves.ARMOR_CANNON, Type.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)

View File

@ -8,7 +8,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils"; import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils";
import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
@ -174,7 +174,7 @@ export const DarkDealEncounter: MysteryEncounter =
const roll = randSeedInt(100); const roll = randSeedInt(100);
const starterTier: number | [number, number] = const starterTier: number | [number, number] =
roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [ 9, 10 ]; roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [ 9, 10 ];
const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes)); const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterCost(starterTier, excludedBosses, bossTypes));
const pokemonConfig: EnemyPokemonConfig = { const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies, species: bossSpecies,
isBoss: true, isBoss: true,

View File

@ -9,9 +9,9 @@ import { EnemyPokemon } from "#app/field/pokemon";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { IntegerHolder, randSeedInt } from "#app/utils"; import { IntegerHolder, randSeedInt } from "#app/utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterTier, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterCost, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -19,6 +19,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
import { SummonPhase } from "#app/phases/summon-phase"; import { SummonPhase } from "#app/phases/summon-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/safariZone"; const namespace = "mysteryEncounters/safariZone";
@ -261,7 +262,7 @@ async function summonSafariPokemon(scene: BattleScene) {
let enemySpecies; let enemySpecies;
let pokemon; let pokemon;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false)); enemySpecies = getSafariSpeciesSpawn();
const level = scene.currentBattle.getLevelForWave(); const level = scene.currentBattle.getLevelForWave();
enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode)); enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode));
pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false); pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false);
@ -526,3 +527,10 @@ async function doEndTurn(scene: BattleScene, cursorIndex: number) {
initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true });
} }
} }
/**
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
*/
export function getSafariSpeciesSpawn(): PokemonSpecies {
return getPokemonSpecies(getRandomSpeciesByStarterCost([ 0, 5 ], NON_LEGEND_PARADOX_POKEMON, undefined, false, false, false));
}

View File

@ -21,7 +21,6 @@ import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { achvs } from "#app/system/achv";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Type } from "#enums/type"; import { Type } from "#enums/type";
import { getPokeballTintColor } from "#app/data/pokeball"; import { getPokeballTintColor } from "#app/data/pokeball";
@ -520,12 +519,6 @@ function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter:
]; ];
} }
function checkAchievement(scene: BattleScene) {
if (scene.arena.biomeType === Biome.SPACE) {
scene.validateAchv(achvs.BREEDERS_IN_SPACE);
}
}
function restorePartyAndHeldItems(scene: BattleScene) { function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party // Restore original party
@ -617,8 +610,6 @@ function onGameOver(scene: BattleScene) {
function doPostEncounterCleanup(scene: BattleScene) { function doPostEncounterCleanup(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
if (!encounter.misc.encounterFailed) { if (!encounter.misc.encounterFailed) {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon // Give 20 friendship to the chosen pokemon
encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED); encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED);
restorePartyAndHeldItems(scene); restorePartyAndHeldItems(scene);

View File

@ -4,8 +4,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { catchPokemon, getRandomSpeciesByStarterCost, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
@ -17,6 +17,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/thePokemonSalesman"; const namespace = "mysteryEncounters/thePokemonSalesman";
@ -60,12 +61,12 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
.withOnInit((scene: BattleScene) => { .withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
let species = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false)); let species = getSalesmanSpeciesOffer();
let tries = 0; let tries = 0;
// Reroll any species that don't have HAs // Reroll any species that don't have HAs
while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) { while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) {
species = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false)); species = getSalesmanSpeciesOffer();
tries++; tries++;
} }
@ -164,3 +165,10 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
} }
) )
.build(); .build();
/**
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
*/
export function getSalesmanSpeciesOffer(): PokemonSpecies {
return getPokemonSpecies(getRandomSpeciesByStarterCost([ 0, 5 ], NON_LEGEND_PARADOX_POKEMON, undefined, false, false, false));
}

View File

@ -731,7 +731,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase:
scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase));
scene.pushPhase(new EggLapsePhase(scene)); scene.pushPhase(new EggLapsePhase(scene));
} else if (!scene.getEnemyParty().find(p => encounter.encounterMode !== MysteryEncounterMode.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) { } else if (!scene.getEnemyParty().find(p => encounter.encounterMode !== MysteryEncounterMode.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) {
scene.pushPhase(new BattleEndPhase(scene)); scene.pushPhase(new BattleEndPhase(scene, true));
if (encounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { if (encounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) {
scene.pushPhase(new TrainerVictoryPhase(scene)); scene.pushPhase(new TrainerVictoryPhase(scene));
} }

View File

@ -207,7 +207,7 @@ export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed:
* @param allowMythical * @param allowMythical
* @returns * @returns
*/ */
export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[], allowSubLegendary: boolean = true, allowLegendary: boolean = true, allowMythical: boolean = true): Species { export function getRandomSpeciesByStarterCost(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[], allowSubLegendary: boolean = true, allowLegendary: boolean = true, allowMythical: boolean = true): Species {
let min = Array.isArray(starterTiers) ? starterTiers[0] : starterTiers; let min = Array.isArray(starterTiers) ? starterTiers[0] : starterTiers;
let max = Array.isArray(starterTiers) ? starterTiers[1] : starterTiers; let max = Array.isArray(starterTiers) ? starterTiers[1] : starterTiers;

View File

@ -10,5 +10,7 @@ export enum SwitchType {
/** 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 */
BATON_PASS, BATON_PASS,
/** Transfers the returning Pokemon's Substitute to the switched in Pokemon */ /** Transfers the returning Pokemon's Substitute to the switched in Pokemon */
SHED_TAIL SHED_TAIL,
/** Force switchout to a random party member */
FORCE_SWITCH,
} }

View File

@ -707,7 +707,7 @@ export class Arena {
case Biome.METROPOLIS: case Biome.METROPOLIS:
return 141.470; return 141.470;
case Biome.FOREST: case Biome.FOREST:
return 4.294; return 0.341;
case Biome.SEA: case Biome.SEA:
return 0.024; return 0.024;
case Biome.SWAMP: case Biome.SWAMP:

View File

@ -51,7 +51,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { FaintPhase } from "#app/phases/faint-phase"; import { FaintPhase } from "#app/phases/faint-phase";
import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
@ -2631,8 +2631,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}; };
} }
// If the attack deals fixed damaged, return a result with that much damage // If the attack deals fixed damage, return a result with that much damage
const fixedDamage = new Utils.IntegerHolder(0); const fixedDamage = new Utils.NumberHolder(0);
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
if (fixedDamage.value) { if (fixedDamage.value) {
const multiLensMultiplier = new Utils.NumberHolder(1); const multiLensMultiplier = new Utils.NumberHolder(1);
@ -3018,7 +3018,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @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, source?: Pokemon): 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 DamageAnimPhase(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

View File

@ -86,6 +86,8 @@ class DefaultOverrides {
readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = []; readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = [];
/** Set to `true` to show all tutorials */ /** Set to `true` to show all tutorials */
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false; readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false;
/** Set to `true` to be able to re-earn already unlocked achievements */
readonly ACHIEVEMENTS_REUNLOCK_OVERRIDE: boolean = false;
/** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */ /** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */
readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null; readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null;

View File

@ -52,7 +52,7 @@ export class AttemptRunPhase extends PokemonPhase {
enemyPokemon.trySetStatus(StatusEffect.FAINT); enemyPokemon.trySetStatus(StatusEffect.FAINT);
}); });
this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new BattleEndPhase(this.scene, false));
this.scene.pushPhase(new NewBattlePhase(this.scene)); this.scene.pushPhase(new NewBattlePhase(this.scene));
} else { } else {
playerPokemon.turnData.failedRunAway = true; playerPokemon.turnData.failedRunAway = true;

View File

@ -8,7 +8,7 @@ export class BattleEndPhase extends BattlePhase {
/** If true, will increment battles won */ /** If true, will increment battles won */
isVictory: boolean; isVictory: boolean;
constructor(scene: BattleScene, isVictory: boolean = true) { constructor(scene: BattleScene, isVictory: boolean) {
super(scene); super(scene);
this.isVictory = isVictory; this.isVictory = isVictory;
@ -17,16 +17,17 @@ export class BattleEndPhase extends BattlePhase {
start() { start() {
super.start(); super.start();
this.scene.gameData.gameStats.battles++;
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) {
this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1;
}
if (this.isVictory) { if (this.isVictory) {
this.scene.currentBattle.addBattleScore(this.scene); this.scene.currentBattle.addBattleScore(this.scene);
this.scene.gameData.gameStats.battles++;
if (this.scene.currentBattle.trainer) { if (this.scene.currentBattle.trainer) {
this.scene.gameData.gameStats.trainersDefeated++; this.scene.gameData.gameStats.trainersDefeated++;
} }
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) {
this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1;
}
} }
// Endless graceful end // Endless graceful end
@ -42,7 +43,7 @@ export class BattleEndPhase extends BattlePhase {
} }
for (const pokemon of this.scene.getPokemonAllowedInBattle()) { for (const pokemon of this.scene.getPokemonAllowedInBattle()) {
applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); applyPostBattleAbAttrs(PostBattleAbAttr, pokemon, false, this.isVictory);
} }
if (this.scene.currentBattle.moneyScattered) { if (this.scene.currentBattle.moneyScattered) {

View File

@ -1,11 +1,11 @@
import BattleScene from "#app/battle-scene"; import type BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { type BattlerIndex } from "#app/battle";
import { BattleSpec } from "#app/enums/battle-spec"; import { BattleSpec } from "#enums/battle-spec";
import { DamageResult, HitResult } from "#app/field/pokemon"; import { type DamageResult, HitResult } from "#app/field/pokemon";
import * as Utils from "#app/utils"; import { fixedInt } from "#app/utils";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
export class DamagePhase extends PokemonPhase { export class DamageAnimPhase extends PokemonPhase {
private amount: integer; private amount: integer;
private damageResult: DamageResult; private damageResult: DamageResult;
private critical: boolean; private critical: boolean;
@ -25,7 +25,7 @@ export class DamagePhase extends PokemonPhase {
if (this.scene.moveAnimations) { if (this.scene.moveAnimations) {
this.scene.toggleInvert(true); this.scene.toggleInvert(true);
} }
this.scene.time.delayedCall(Utils.fixedInt(1000), () => { this.scene.time.delayedCall(fixedInt(1000), () => {
this.scene.toggleInvert(false); this.scene.toggleInvert(false);
this.applyDamage(); this.applyDamage();
}); });

View File

@ -34,7 +34,7 @@ export class EggLapsePhase extends Phase {
if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 1) { if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 1) {
this.scene.ui.showText(i18next.t("battle:eggHatching"), 0, () => { this.scene.ui.showText(i18next.t("battle:eggHatching"), 0, () => {
// show prompt for skip, blocking inputs for 1 second // show prompt for skip, blocking inputs for 1 second
this.scene.ui.showText(i18next.t("battle:eggSkipPrompt"), 0); this.scene.ui.showText(i18next.t("battle:eggSkipPrompt", { eggsToHatch: eggsToHatchCount }), 0);
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => { this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
this.hatchEggsSkipped(eggsToHatch); this.hatchEggsSkipped(eggsToHatch);
this.showSummary(); this.showSummary();

View File

@ -12,7 +12,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import i18next from "i18next"; import i18next from "i18next";
import { DamagePhase } from "./damage-phase"; import { DamageAnimPhase } from "./damage-anim-phase";
import { GameOverPhase } from "./game-over-phase"; import { GameOverPhase } from "./game-over-phase";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import { SwitchPhase } from "./switch-phase"; import { SwitchPhase } from "./switch-phase";
@ -206,7 +206,7 @@ export class FaintPhase extends PokemonPhase {
} else { } else {
// Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase // Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase
enemy.hp++; enemy.hp++;
this.scene.unshiftPhase(new DamagePhase(this.scene, enemy.getBattlerIndex(), 0, HitResult.OTHER)); this.scene.unshiftPhase(new DamageAnimPhase(this.scene, enemy.getBattlerIndex(), 0, HitResult.OTHER));
this.end(); this.end();
} }
return true; return true;

View File

@ -1,5 +1,5 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr } from "#app/data/ability"; import { applyPreSwitchOutAbAttrs, PostDamageForceSwitchAbAttr, PreSwitchOutAbAttr } from "#app/data/ability";
import { allMoves, ForceSwitchOutAttr } from "#app/data/move"; import { allMoves, ForceSwitchOutAttr } from "#app/data/move";
import { getPokeballTintColor } from "#app/data/pokeball"; import { getPokeballTintColor } from "#app/data/pokeball";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
@ -166,10 +166,11 @@ export class SwitchSummonPhase extends SummonPhase {
const currentCommand = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.command; const currentCommand = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.command;
const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted(); const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted();
const lastPokemonHasForceSwitchAbAttr = this.lastPokemon.hasAbilityWithAttr(PostDamageForceSwitchAbAttr) && !this.lastPokemon.isFainted();
// Compensate for turn spent summoning // Compensate for turn spent summoning
// Or compensate for force switch move if switched out pokemon is not fainted // Or compensate for force switch move if switched out pokemon is not fainted
if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted) { if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted || lastPokemonHasForceSwitchAbAttr) {
pokemon.battleSummonData.turnCount--; pokemon.battleSummonData.turnCount--;
pokemon.battleSummonData.waveTurnCount--; pokemon.battleSummonData.waveTurnCount--;
} }

View File

@ -9,6 +9,8 @@ import { BattlePhase } from "./battle-phase";
import { ModifierRewardPhase } from "./modifier-reward-phase"; import { ModifierRewardPhase } from "./modifier-reward-phase";
import { MoneyRewardPhase } from "./money-reward-phase"; import { MoneyRewardPhase } from "./money-reward-phase";
import { TrainerSlot } from "#app/data/trainer-config"; import { TrainerSlot } from "#app/data/trainer-config";
import { Biome } from "#app/enums/biome";
import { achvs } from "#app/system/achv";
export class TrainerVictoryPhase extends BattlePhase { export class TrainerVictoryPhase extends BattlePhase {
constructor(scene: BattleScene) { constructor(scene: BattleScene) {
@ -34,11 +36,17 @@ export class TrainerVictoryPhase extends BattlePhase {
} }
const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct? const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct?
// Validate Voucher for boss trainers
if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (vouchers.hasOwnProperty(TrainerType[trainerType])) {
if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) {
this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType]));
} }
} }
// Breeders in Space achievement
if (this.scene.arena.biomeType === Biome.SPACE
&& (trainerType === TrainerType.BREEDER || trainerType === TrainerType.EXPERT_POKEMON_BREEDER)) {
this.scene.validateAchv(achvs.BREEDERS_IN_SPACE);
}
this.scene.ui.showText(i18next.t("battle:trainerDefeated", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }), null, () => { this.scene.ui.showText(i18next.t("battle:trainerDefeated", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }), null, () => {
const victoryMessages = this.scene.currentBattle.trainer?.getVictoryMessages()!; // TODO: is this bang correct? const victoryMessages = this.scene.currentBattle.trainer?.getVictoryMessages()!; // TODO: is this bang correct?

View File

@ -41,7 +41,7 @@ export class VictoryPhase extends PokemonPhase {
} }
if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) { if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) {
this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new BattleEndPhase(this.scene, true));
if (this.scene.currentBattle.battleType === BattleType.TRAINER) { if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
this.scene.pushPhase(new TrainerVictoryPhase(this.scene)); this.scene.pushPhase(new TrainerVictoryPhase(this.scene));
} }

View File

@ -358,7 +358,7 @@ export const achvs = {
MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0), INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 100).setSecret(), BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
}; };
export function initAchievements() { export function initAchievements() {

View File

@ -1,9 +1,9 @@
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import i18next from "i18next"; import i18next from "i18next";
import BattleScene from "../../battle-scene"; import BattleScene from "#app/battle-scene";
import { hasTouchscreen } from "../../touch-controls"; import { hasTouchscreen } from "#app/touch-controls";
import { updateWindowType } from "../../ui/ui-theme"; import { updateWindowType } from "#app/ui/ui-theme";
import { CandyUpgradeNotificationChangedEvent } from "../../events/battle-scene"; import { CandyUpgradeNotificationChangedEvent } from "#app/events/battle-scene";
import SettingsUiHandler from "#app/ui/settings/settings-ui-handler"; import SettingsUiHandler from "#app/ui/settings/settings-ui-handler";
import { EaseType } from "#enums/ease-type"; import { EaseType } from "#enums/ease-type";
import { MoneyFormat } from "#enums/money-format"; import { MoneyFormat } from "#enums/money-format";
@ -44,6 +44,7 @@ const OFF_ON: SettingOption[] = [
label: i18next.t("settings:on") label: i18next.t("settings:on")
} }
]; ];
const AUTO_DISABLED: SettingOption[] = [ const AUTO_DISABLED: SettingOption[] = [
{ {
value: "Auto", value: "Auto",
@ -55,6 +56,19 @@ const AUTO_DISABLED: SettingOption[] = [
} }
]; ];
const TOUCH_CONTROLS_OPTIONS: SettingOption[] = [
{
value: "Auto",
label: i18next.t("settings:auto")
},
{
value: "Disabled",
label: i18next.t("settings:disabled"),
needConfirmation: true,
confirmationMessage: i18next.t("settings:confirmDisableTouch")
}
];
const SHOP_CURSOR_TARGET_OPTIONS: SettingOption[] = [ const SHOP_CURSOR_TARGET_OPTIONS: SettingOption[] = [
{ {
value: "Rewards", value: "Rewards",
@ -100,7 +114,9 @@ export enum SettingType {
type SettingOption = { type SettingOption = {
value: string, value: string,
label: string label: string,
needConfirmation?: boolean,
confirmationMessage?: string
}; };
export interface Setting { export interface Setting {
@ -344,13 +360,6 @@ export const Setting: Array<Setting> = [
default: 1, default: 1,
type: SettingType.GENERAL type: SettingType.GENERAL
}, },
{
key: SettingKeys.Touch_Controls,
label: i18next.t("settings:touchControls"),
options: AUTO_DISABLED,
default: 0,
type: SettingType.GENERAL
},
{ {
key: SettingKeys.Vibration, key: SettingKeys.Vibration,
label: i18next.t("settings:vibrations"), label: i18next.t("settings:vibrations"),
@ -358,6 +367,28 @@ export const Setting: Array<Setting> = [
default: 0, default: 0,
type: SettingType.GENERAL type: SettingType.GENERAL
}, },
{
key: SettingKeys.Touch_Controls,
label: i18next.t("settings:touchControls"),
options: TOUCH_CONTROLS_OPTIONS,
default: 0,
type: SettingType.GENERAL,
isHidden: () => !hasTouchscreen()
},
{
key: SettingKeys.Move_Touch_Controls,
label: i18next.t("settings:moveTouchControls"),
options: [
{
value: "Configure",
label: i18next.t("settings:change")
}
],
default: 0,
type: SettingType.GENERAL,
activatable: true,
isHidden: () => !hasTouchscreen()
},
{ {
key: SettingKeys.Language, key: SettingKeys.Language,
label: i18next.t("settings:language"), label: i18next.t("settings:language"),
@ -643,20 +674,6 @@ export const Setting: Array<Setting> = [
type: SettingType.AUDIO, type: SettingType.AUDIO,
requireReload: true requireReload: true
}, },
{
key: SettingKeys.Move_Touch_Controls,
label: i18next.t("settings:moveTouchControls"),
options: [
{
value: "Configure",
label: i18next.t("settings:change")
}
],
default: 0,
type: SettingType.GENERAL,
activatable: true,
isHidden: () => !hasTouchscreen()
},
{ {
key: SettingKeys.Shop_Cursor_Target, key: SettingKeys.Shop_Cursor_Target,
label: i18next.t("settings:shopCursorTarget"), label: i18next.t("settings:shopCursorTarget"),
@ -849,7 +866,7 @@ export function setSetting(scene: BattleScene, setting: string, value: integer):
if (scene.ui) { if (scene.ui) {
const cancelHandler = () => { const cancelHandler = () => {
scene.ui.revertMode(); scene.ui.revertMode();
(scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(0, 0, true); (scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(-1, 0, true);
}; };
const changeLocaleHandler = (locale: string): boolean => { const changeLocaleHandler = (locale: string): boolean => {
try { try {

View File

@ -0,0 +1,81 @@
import { BattlerIndex } from "#app/battle";
import { isBetween, toDmgValue } from "#app/utils";
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("Abilities - Analytic", () => {
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, Moves.TACKLE ])
.ability(Abilities.ANALYTIC)
.battleType("single")
.disableCrits()
.startingLevel(200)
.enemyLevel(200)
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should increase damage if the user moves last", async () => {
await game.classicMode.startBattle([ Species.ARCEUS ]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.toNextTurn();
const damage1 = enemy.getInverseHp();
enemy.hp = enemy.getMaxHp();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(isBetween(enemy.getInverseHp(), toDmgValue(damage1 * 1.3) - 3, toDmgValue(damage1 * 1.3) + 3)).toBe(true);
});
it("should increase damage only if the user moves last in doubles", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.GENGAR, Species.SHUCKLE ]);
const [ enemy, ] = game.scene.getEnemyField();
game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
await game.toNextTurn();
const damage1 = enemy.getInverseHp();
enemy.hp = enemy.getMaxHp();
game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(isBetween(enemy.getInverseHp(), toDmgValue(damage1 * 1.3) - 3, toDmgValue(damage1 * 1.3) + 3)).toBe(true);
enemy.hp = enemy.getMaxHp();
game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2 ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemy.getInverseHp()).toBe(damage1);
});
});

View File

@ -2,7 +2,7 @@ import { BattlerIndex } from "#app/battle";
import { allAbilities } from "#app/data/ability"; import { allAbilities } from "#app/data/ability";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { WeatherType } from "#app/enums/weather-type"; import { WeatherType } from "#app/enums/weather-type";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { PostSummonPhase } from "#app/phases/post-summon-phase"; import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
@ -273,7 +273,7 @@ describe("Abilities - Forecast", () => {
const castform = game.scene.getPlayerPokemon()!; const castform = game.scene.getPlayerPokemon()!;
// Damage phase should come first // Damage phase should come first
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to(DamageAnimPhase);
expect(castform.hp).toBeLessThan(castform.getMaxHp()); expect(castform.hp).toBeLessThan(castform.getMaxHp());
// Then change form // Then change form

View File

@ -0,0 +1,74 @@
import type { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#app/ui/command-ui-handler";
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, vi } from "vitest";
describe("Abilities - Honey Gather", () => {
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, Moves.ROAR, Moves.THUNDERBOLT ])
.startingLevel(100)
.ability(Abilities.HONEY_GATHER)
.passiveAbility(Abilities.RUN_AWAY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should give money when winning a battle", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
game.scene.money = 1000;
game.move.select(Moves.THUNDERBOLT);
await game.toNextWave();
expect(game.scene.money).toBeGreaterThan(1000);
});
it("should not give money when the enemy pokemon flees", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
game.scene.money = 1000;
game.move.select(Moves.ROAR);
await game.toNextTurn();
expect(game.scene.money).toBe(1000);
expect(game.scene.currentBattle.waveIndex).toBe(2);
});
it("should not give money when the player flees", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
game.scene.money = 1000;
// something weird is going on with the test framework, so this is required to prevent a crash
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "scene", "get").mockReturnValue(game.scene);
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
commandPhase.handleCommand(Command.RUN, 0);
await game.toNextTurn();
expect(game.scene.money).toBe(1000);
expect(game.scene.currentBattle.waveIndex).toBe(2);
});
});

View File

@ -42,7 +42,7 @@ describe("Abilities - Hustle", () => {
game.move.select(Moves.TACKLE); game.move.select(Moves.TACKLE);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(pikachu.getEffectiveStat).toHaveReturnedWith(Math.floor(atk * 1.5)); expect(pikachu.getEffectiveStat).toHaveReturnedWith(Math.floor(atk * 1.5));
}); });
@ -68,7 +68,7 @@ describe("Abilities - Hustle", () => {
vi.spyOn(pikachu, "getAccuracyMultiplier"); vi.spyOn(pikachu, "getAccuracyMultiplier");
game.move.select(Moves.GIGA_DRAIN); game.move.select(Moves.GIGA_DRAIN);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(pikachu.getEffectiveStat).toHaveReturnedWith(spatk); expect(pikachu.getEffectiveStat).toHaveReturnedWith(spatk);
expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1); expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1);
@ -86,7 +86,7 @@ describe("Abilities - Hustle", () => {
vi.spyOn(allMoves[Moves.FISSURE], "calculateBattleAccuracy"); vi.spyOn(allMoves[Moves.FISSURE], "calculateBattleAccuracy");
game.move.select(Moves.FISSURE); game.move.select(Moves.FISSURE);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(enemyPokemon.turnData.damageTaken).toBe(enemyPokemon.getMaxHp()); expect(enemyPokemon.turnData.damageTaken).toBe(enemyPokemon.getMaxHp());
expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1); expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1);

View File

@ -51,7 +51,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.TACKLE); game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
const firstStrikeDamage = enemyStartingHp - enemyPokemon.hp; const firstStrikeDamage = enemyStartingHp - enemyPokemon.hp;
enemyStartingHp = enemyPokemon.hp; enemyStartingHp = enemyPokemon.hp;
@ -129,7 +129,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.SELF_DESTRUCT); game.move.select(Moves.SELF_DESTRUCT);
await game.phaseInterceptor.to("DamagePhase", false); await game.phaseInterceptor.to("DamageAnimPhase", false);
expect(leadPokemon.turnData.hitCount).toBe(1); expect(leadPokemon.turnData.hitCount).toBe(1);
} }
@ -147,7 +147,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.ROLLOUT); game.move.select(Moves.ROLLOUT);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase", false); await game.phaseInterceptor.to("DamageAnimPhase", false);
expect(leadPokemon.turnData.hitCount).toBe(1); expect(leadPokemon.turnData.hitCount).toBe(1);
} }
@ -181,7 +181,7 @@ describe("Abilities - Parental Bond", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.COUNTER); game.move.select(Moves.COUNTER);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
const playerDamage = leadPokemon.getMaxHp() - leadPokemon.hp; const playerDamage = leadPokemon.getMaxHp() - leadPokemon.hp;
@ -221,7 +221,7 @@ describe("Abilities - Parental Bond", () => {
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.EARTHQUAKE); game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to("DamagePhase", false); await game.phaseInterceptor.to("DamageAnimPhase", false);
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
} }
@ -238,7 +238,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.MIND_BLOWN); game.move.select(Moves.MIND_BLOWN);
await game.phaseInterceptor.to("DamagePhase", false); await game.phaseInterceptor.to("DamageAnimPhase", false);
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
@ -285,7 +285,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.TACKLE); game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(leadPokemon.turnData.hitCount).toBe(3); expect(leadPokemon.turnData.hitCount).toBe(3);
} }
@ -307,7 +307,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.SEISMIC_TOSS); game.move.select(Moves.SEISMIC_TOSS);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(leadPokemon.turnData.hitCount).toBe(3); expect(leadPokemon.turnData.hitCount).toBe(3);
@ -329,7 +329,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.HYPER_BEAM); game.move.select(Moves.HYPER_BEAM);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeUndefined(); expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeUndefined();
@ -353,7 +353,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.ANCHOR_SHOT); game.move.select(Moves.ANCHOR_SHOT);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined();
@ -380,7 +380,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.SMACK_DOWN); game.move.select(Moves.SMACK_DOWN);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
@ -424,7 +424,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.WAKE_UP_SLAP); game.move.select(Moves.WAKE_UP_SLAP);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP); expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP);

View File

@ -28,7 +28,9 @@ describe("Abilities - Speed Boost", () => {
game.override game.override
.battleType("single") .battleType("single")
.enemySpecies(Species.DRAGAPULT) .enemySpecies(Species.SHUCKLE)
.enemyAbility(Abilities.BALL_FETCH)
.enemyLevel(100)
.ability(Abilities.SPEED_BOOST) .ability(Abilities.SPEED_BOOST)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.moveset([ Moves.SPLASH, Moves.U_TURN ]); .moveset([ Moves.SPLASH, Moves.U_TURN ]);
@ -70,21 +72,23 @@ describe("Abilities - Speed Boost", () => {
Species.NINJASK Species.NINJASK
]); ]);
game.move.select(Moves.U_TURN); const [ shuckle, ninjask ] = game.scene.getPlayerParty();
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.move.select(Moves.U_TURN);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.toNextTurn(); await game.toNextTurn();
playerPokemon = game.scene.getPlayerPokemon()!; expect(game.scene.getPlayerPokemon()!).toBe(ninjask);
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); expect(ninjask.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.U_TURN);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!).toBe(shuckle);
expect(shuckle.getStatStage(Stat.SPD)).toBe(0);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); expect(shuckle.getStatStage(Stat.SPD)).toBe(1);
}); });
it("should not trigger this turn if pokemon was switched into combat via normal switch, but the turn after", it("should not trigger this turn if pokemon was switched into combat via normal switch, but the turn after",

View File

@ -1,5 +1,5 @@
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -55,7 +55,7 @@ describe("Abilities - Sturdy", () => {
enemyPokemon.hp = enemyPokemon.getMaxHp() - 1; enemyPokemon.hp = enemyPokemon.getMaxHp() - 1;
game.move.select(Moves.CLOSE_COMBAT); game.move.select(Moves.CLOSE_COMBAT);
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to(DamageAnimPhase);
expect(enemyPokemon.hp).toBe(0); expect(enemyPokemon.hp).toBe(0);
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
@ -81,7 +81,7 @@ describe("Abilities - Sturdy", () => {
await game.startBattle(); await game.startBattle();
game.move.select(Moves.CLOSE_COMBAT); game.move.select(Moves.CLOSE_COMBAT);
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to(DamageAnimPhase);
const enemyPokemon: EnemyPokemon = game.scene.getEnemyParty()[0]; const enemyPokemon: EnemyPokemon = game.scene.getEnemyParty()[0];
expect(enemyPokemon.hp).toBe(0); expect(enemyPokemon.hp).toBe(0);

View File

@ -3,7 +3,7 @@ import { Stat } from "#enums/stat";
import { GameModes, getGameMode } from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode";
import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { EncounterPhase } from "#app/phases/encounter-phase"; import { EncounterPhase } from "#app/phases/encounter-phase";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { LoginPhase } from "#app/phases/login-phase"; import { LoginPhase } from "#app/phases/login-phase";
@ -267,7 +267,7 @@ describe("Test Battle Phase", () => {
]); ]);
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
await game.killPokemon(game.scene.currentBattle.enemyParty[0]); await game.killPokemon(game.scene.currentBattle.enemyParty[0]);
expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true); expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true);
await game.phaseInterceptor.to(VictoryPhase, false); await game.phaseInterceptor.to(VictoryPhase, false);

View File

@ -102,7 +102,7 @@ describe("Battle Mechanics - Damage Calculation", () => {
game.move.select(Moves.JUMP_KICK); game.move.select(Moves.JUMP_KICK);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(shedinja.hp).toBe(shedinja.getMaxHp() - 1); expect(shedinja.hp).toBe(shedinja.getMaxHp() - 1);
}); });

View File

@ -1,4 +1,4 @@
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -48,7 +48,7 @@ describe("Items - Leftovers", () => {
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
// We should have less hp after the attack // We should have less hp after the attack
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
const leadHpAfterDamage = leadPokemon.hp; const leadHpAfterDamage = leadPokemon.hp;

View File

@ -135,4 +135,57 @@ describe("Items - Multi Lens", () => {
expect(damageResults[0]).toBe(Math.floor(playerPokemon.level * 0.75)); expect(damageResults[0]).toBe(Math.floor(playerPokemon.level * 0.75));
expect(damageResults[1]).toBe(Math.floor(playerPokemon.level * 0.25)); expect(damageResults[1]).toBe(Math.floor(playerPokemon.level * 0.25));
}); });
it("should result in correct damage for hp% attacks with 1 lens", async () => {
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }])
.moveset(Moves.SUPER_FANG)
.ability(Abilities.COMPOUND_EYES)
.enemyLevel(1000)
.enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SUPER_FANG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5);
});
it("should result in correct damage for hp% attacks with 2 lenses", async () => {
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }])
.moveset(Moves.SUPER_FANG)
.ability(Abilities.COMPOUND_EYES)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(1000)
.enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SUPER_FANG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5);
});
it("should result in correct damage for hp% attacks with 2 lenses + Parental Bond", async () => {
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }])
.moveset(Moves.SUPER_FANG)
.ability(Abilities.PARENTAL_BOND)
.passiveAbility(Abilities.COMPOUND_EYES)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(1000)
.enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SUPER_FANG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.25, 5);
});
}); });

View File

@ -1,5 +1,9 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Status } from "#app/data/status-effect";
import { Challenges } from "#enums/challenges";
import { StatusEffect } from "#enums/status-effect";
import { Type } from "#enums/type";
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";
@ -193,4 +197,122 @@ describe("Moves - Dragon Tail", () => {
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2)); expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
expect(game.scene.getPlayerField().length).toBe(1); expect(game.scene.getPlayerField().length).toBe(1);
}); });
it("should force switches randomly", async () => {
game.override.enemyMoveset(Moves.DRAGON_TAIL)
.startingLevel(100)
.enemyLevel(1);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.DRAGON_TAIL);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(true);
expect(squirtle.isOnField()).toBe(false);
expect(bulbasaur.getInverseHp()).toBeGreaterThan(0);
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min + 1;
});
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(false);
expect(squirtle.isOnField()).toBe(true);
expect(charmander.getInverseHp()).toBeGreaterThan(0);
});
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
game.override.enemyMoveset(Moves.DRAGON_TAIL)
.startingLevel(100)
.enemyLevel(1);
// Mono-Water challenge, Eevee is ineligible
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
expect(lapras.getInverseHp()).toBeGreaterThan(0);
});
it("should not force a switch to a fainted Pokemon", async () => {
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
.startingLevel(100)
.enemyLevel(1);
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.DRAGON_TAIL);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
expect(lapras.getInverseHp()).toBeGreaterThan(0);
});
it("should not force a switch if there are no available Pokemon to switch into", async () => {
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
.startingLevel(100)
.enemyLevel(1);
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
const [ lapras, eevee ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.DRAGON_TAIL);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(true);
expect(eevee.isOnField()).toBe(false);
expect(lapras.getInverseHp()).toBeGreaterThan(0);
});
}); });

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -51,7 +51,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000); }, 20000);
@ -65,7 +65,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000); }, 20000);
@ -82,7 +82,7 @@ describe("Moves - Dynamax Cannon", () => {
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.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120);
}, 20000); }, 20000);
@ -99,7 +99,7 @@ describe("Moves - Dynamax Cannon", () => {
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.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140);
}, 20000); }, 20000);
@ -116,7 +116,7 @@ describe("Moves - Dynamax Cannon", () => {
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.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160);
}, 20000); }, 20000);
@ -133,7 +133,7 @@ describe("Moves - Dynamax Cannon", () => {
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.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180);
}, 20000); }, 20000);
@ -150,7 +150,7 @@ describe("Moves - Dynamax Cannon", () => {
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.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
@ -165,7 +165,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
}); });

View File

@ -1,7 +1,7 @@
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { Species } from "#app/enums/species"; import { Species } from "#app/enums/species";
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -56,7 +56,7 @@ describe("Moves - Fissure", () => {
game.override.enemyAbility(Abilities.FUR_COAT); game.override.enemyAbility(Abilities.FUR_COAT);
game.move.select(Moves.FISSURE); game.move.select(Moves.FISSURE);
await game.phaseInterceptor.to(DamagePhase, true); await game.phaseInterceptor.to(DamageAnimPhase, true);
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
}); });

View File

@ -1,7 +1,7 @@
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
@ -58,12 +58,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
@ -81,12 +81,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
@ -104,7 +104,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEndPhase); await game.phaseInterceptor.to(MoveEndPhase);
@ -114,7 +114,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
@ -133,7 +133,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEndPhase); await game.phaseInterceptor.to(MoveEndPhase);
@ -142,7 +142,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000); }, 20000);
@ -160,12 +160,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
@ -209,22 +209,22 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
@ -268,22 +268,22 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);
}); });

View File

@ -41,11 +41,11 @@ describe("Moves - Glaive Rush", () => {
enemy.hp = 1000; enemy.hp = 1000;
game.move.select(Moves.SHADOW_SNEAK); game.move.select(Moves.SHADOW_SNEAK);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
const damageDealt = 1000 - enemy.hp; const damageDealt = 1000 - enemy.hp;
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.SHADOW_SNEAK); game.move.select(Moves.SHADOW_SNEAK);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamageAnimPhase");
expect(enemy.hp).toBeLessThanOrEqual(1001 - (damageDealt * 3)); expect(enemy.hp).toBeLessThanOrEqual(1001 - (damageDealt * 3));
}); });

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
@ -48,7 +48,7 @@ describe("Moves - Scale Shot", () => {
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to(DamageAnimPhase);
//check that stats haven't changed after one or two hits have occurred //check that stats haven't changed after one or two hits have occurred
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);

View File

@ -26,7 +26,7 @@ describe("Moves - Shell Side Arm", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.moveset([ Moves.SHELL_SIDE_ARM ]) .moveset([ Moves.SHELL_SIDE_ARM, Moves.SPLASH ])
.battleType("single") .battleType("single")
.startingLevel(100) .startingLevel(100)
.enemyLevel(100) .enemyLevel(100)
@ -69,6 +69,9 @@ describe("Moves - Shell Side Arm", () => {
vi.spyOn(shellSideArmAttr, "apply"); vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SPLASH);
await game.toNextTurn();
game.move.select(Moves.SHELL_SIDE_ARM); game.move.select(Moves.SHELL_SIDE_ARM);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);

View File

@ -1,11 +1,15 @@
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Challenges } from "#enums/challenges";
import { Type } from "#enums/type";
import { MoveResult } from "#app/field/pokemon"; 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";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { Status } from "#app/data/status-effect";
import { StatusEffect } from "#enums/status-effect";
describe("Moves - Whirlwind", () => { describe("Moves - Whirlwind", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -25,8 +29,9 @@ describe("Moves - Whirlwind", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleType("single") .battleType("single")
.moveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.WHIRLWIND) .enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ])
.enemySpecies(Species.PIDGEY); .enemySpecies(Species.PIDGEY);
}); });
@ -41,10 +46,114 @@ describe("Moves - Whirlwind", () => {
const staraptor = game.scene.getPlayerPokemon()!; const staraptor = game.scene.getPlayerPokemon()!;
game.move.select(move); game.move.select(move);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
}); });
it("should force switches randomly", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(true);
expect(squirtle.isOnField()).toBe(false);
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min + 1;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(false);
expect(squirtle.isOnField()).toBe(true);
});
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
// Mono-Water challenge, Eevee is ineligible
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
});
it("should not force a switch to a fainted Pokemon", async () => {
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
});
it("should not force a switch if there are no available Pokemon to switch into", async () => {
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
const [ lapras, eevee ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(true);
expect(eevee.isOnField()).toBe(false);
});
}); });

View File

@ -33,7 +33,7 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb
}, () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase)); }, () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase));
if (isBattle) { if (isBattle) {
game.onNextPrompt("DamagePhase", Mode.MESSAGE, () => { game.onNextPrompt("DamageAnimPhase", Mode.MESSAGE, () => {
game.setMode(Mode.MESSAGE); game.setMode(Mode.MESSAGE);
game.endPhase(); game.endPhase();
}, () => game.isCurrentPhase(CommandPhase)); }, () => game.isCurrentPhase(CommandPhase));

View File

@ -0,0 +1,173 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#enums/biome";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils";
import BattleScene from "#app/battle-scene";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { getSafariSpeciesSpawn, SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
const namespace = "mysteryEncounters/safariZone";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
const defaultBiome = Biome.SWAMP;
const defaultWave = 45;
describe("Safari Zone - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves();
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[ Biome.VOLCANO, [ MysteryEncounterType.FIGHT_OR_FLIGHT ]],
[ Biome.FOREST, [ MysteryEncounterType.SAFARI_ZONE ]],
[ Biome.SWAMP, [ MysteryEncounterType.SAFARI_ZONE ]],
[ Biome.JUNGLE, [ MysteryEncounterType.SAFARI_ZONE ]],
]);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
expect(SafariZoneEncounter.encounterType).toBe(MysteryEncounterType.SAFARI_ZONE);
expect(SafariZoneEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT);
expect(SafariZoneEncounter.dialogue).toBeDefined();
expect(SafariZoneEncounter.dialogue.intro).toStrictEqual([
{ text: `${namespace}:intro` },
]);
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`);
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`);
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`);
expect(SafariZoneEncounter.options.length).toBe(2);
});
it("should not spawn outside of the forest, swamp, or jungle biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.SAFARI_ZONE);
});
it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(SafariZoneEncounter);
const encounter = scene.currentBattle.mysteryEncounter!;
scene.currentBattle.waveIndex = defaultWave;
const { onInit } = encounter;
expect(encounter.onInit).toBeDefined();
encounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit!(scene);
expect(onInitResult).toBe(true);
});
describe("Option 1 - Enter", () => {
it("should have the correct properties", () => {
const option = SafariZoneEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option.1.label`,
buttonTooltip: `${namespace}:option.1.tooltip`,
selected: [
{
text: `${namespace}:option.1.selected`,
},
],
});
});
it("should NOT be selectable if the player doesn't have enough money", async () => {
game.scene.money = 0;
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
const encounterPhase = scene.getCurrentPhase();
expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name);
const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase;
vi.spyOn(mysteryEncounterPhase, "continueEncounter");
vi.spyOn(mysteryEncounterPhase, "handleOptionSelect");
vi.spyOn(scene.ui, "playError");
await runSelectMysteryEncounterOption(game, 1);
expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name);
expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled
expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled();
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
});
it("should not spawn any Paradox Pokemon", async () => {
const NUM_ROLLS = 2000; // As long as this is greater than total number of species, this should cover all possible RNG rolls
let rngSweepProgress = 0; // Will simulate full range of RNG rolls by steadily increasing from 0 to 1
vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => {
return rngSweepProgress * (max - min) + min;
});
vi.spyOn(Phaser.Math.RND, "shuffle").mockImplementation((arr: any[]) => arr);
for (let i = 0; i < NUM_ROLLS; i++) {
rngSweepProgress = (2 * i + 1) / (2 * NUM_ROLLS);
const simSpecies = getSafariSpeciesSpawn().speciesId;
expect(NON_LEGEND_PARADOX_POKEMON).not.toContain(simSpecies);
}
});
// TODO: Tests for player actions inside the Safari Zone (Pokeball, Mud, Bait, Flee)
});
describe("Option 2 - Leave", () => {
it("should have the correct properties", () => {
const option = SafariZoneEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`,
selected: [
{
text: `${namespace}:option.2.selected`,
},
],
});
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});

View File

@ -9,11 +9,12 @@ import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { PlayerPokemon } from "#app/field/pokemon"; import { PlayerPokemon } from "#app/field/pokemon";
import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
import { ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter"; import { getSalesmanSpeciesOffer, ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
const namespace = "mysteryEncounters/thePokemonSalesman"; const namespace = "mysteryEncounters/thePokemonSalesman";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -172,6 +173,22 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
}); });
it("should not offer any Paradox Pokemon", async () => {
const NUM_ROLLS = 2000; // As long as this is greater than total number of species, this should cover all possible RNG rolls
let rngSweepProgress = 0; // Will simulate full range of RNG rolls by steadily increasing from 0 to 1
vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => {
return rngSweepProgress * (max - min) + min;
});
vi.spyOn(Phaser.Math.RND, "shuffle").mockImplementation((arr: any[]) => arr);
for (let i = 0; i < NUM_ROLLS; i++) {
rngSweepProgress = (2 * i + 1) / (2 * NUM_ROLLS);
const simSpecies = getSalesmanSpeciesOffer().speciesId;
expect(NON_LEGEND_PARADOX_POKEMON).not.toContain(simSpecies);
}
});
it("should leave encounter without battle", async () => { it("should leave encounter without battle", async () => {
scene.money = 20000; scene.money = 20000;
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");

View File

@ -2,7 +2,7 @@ import BattleScene from "#app/battle-scene";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterCost, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Type } from "#enums/type"; import { Type } from "#enums/type";
import { MessagePhase } from "#app/phases/message-phase"; import { MessagePhase } from "#app/phases/message-phase";
@ -204,9 +204,9 @@ describe("Mystery Encounter Utils", () => {
}); });
}); });
describe("getRandomSpeciesByStarterTier", () => { describe("getRandomSpeciesByStarterCost", () => {
it("gets species for a starter tier", () => { it("gets species for a starter tier", () => {
const result = getRandomSpeciesByStarterTier(5); const result = getRandomSpeciesByStarterCost(5);
const pokeSpecies = getPokemonSpecies(result); const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(result); expect(pokeSpecies.speciesId).toBe(result);
@ -214,7 +214,7 @@ describe("Mystery Encounter Utils", () => {
}); });
it("gets species for a starter tier range", () => { it("gets species for a starter tier range", () => {
const result = getRandomSpeciesByStarterTier([ 5, 8 ]); const result = getRandomSpeciesByStarterCost([ 5, 8 ]);
const pokeSpecies = getPokemonSpecies(result); const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(result); expect(pokeSpecies.speciesId).toBe(result);
@ -224,14 +224,14 @@ describe("Mystery Encounter Utils", () => {
it("excludes species from search", () => { it("excludes species from search", () => {
// Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian // Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian
const result = getRandomSpeciesByStarterTier(9, [ Species.KORAIDON, Species.MIRAIDON, Species.ARCEUS, Species.RAYQUAZA, Species.KYOGRE, Species.GROUDON ]); const result = getRandomSpeciesByStarterCost(9, [ Species.KORAIDON, Species.MIRAIDON, Species.ARCEUS, Species.RAYQUAZA, Species.KYOGRE, Species.GROUDON ]);
const pokeSpecies = getPokemonSpecies(result); const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(Species.ZACIAN); expect(pokeSpecies.speciesId).toBe(Species.ZACIAN);
}); });
it("gets species of specified types", () => { it("gets species of specified types", () => {
// Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian // Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian
const result = getRandomSpeciesByStarterTier(9, undefined, [ Type.GROUND ]); const result = getRandomSpeciesByStarterCost(9, undefined, [ Type.GROUND ]);
const pokeSpecies = getPokemonSpecies(result); const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(Species.GROUDON); expect(pokeSpecies.speciesId).toBe(Species.GROUDON);
}); });

View File

@ -5,7 +5,7 @@ import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { BerryPhase } from "#app/phases/berry-phase"; import { BerryPhase } from "#app/phases/berry-phase";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
import { EncounterPhase } from "#app/phases/encounter-phase"; import { EncounterPhase } from "#app/phases/encounter-phase";
import { EndEvolutionPhase } from "#app/phases/end-evolution-phase"; import { EndEvolutionPhase } from "#app/phases/end-evolution-phase";
@ -87,7 +87,7 @@ type PhaseClass =
| typeof TurnStartPhase | typeof TurnStartPhase
| typeof MovePhase | typeof MovePhase
| typeof MoveEffectPhase | typeof MoveEffectPhase
| typeof DamagePhase | typeof DamageAnimPhase
| typeof FaintPhase | typeof FaintPhase
| typeof BerryPhase | typeof BerryPhase
| typeof TurnEndPhase | typeof TurnEndPhase
@ -146,7 +146,7 @@ type PhaseString =
| "TurnStartPhase" | "TurnStartPhase"
| "MovePhase" | "MovePhase"
| "MoveEffectPhase" | "MoveEffectPhase"
| "DamagePhase" | "DamageAnimPhase"
| "FaintPhase" | "FaintPhase"
| "BerryPhase" | "BerryPhase"
| "TurnEndPhase" | "TurnEndPhase"
@ -229,7 +229,7 @@ export default class PhaseInterceptor {
[ TurnStartPhase, this.startPhase ], [ TurnStartPhase, this.startPhase ],
[ MovePhase, this.startPhase ], [ MovePhase, this.startPhase ],
[ MoveEffectPhase, this.startPhase ], [ MoveEffectPhase, this.startPhase ],
[ DamagePhase, this.startPhase ], [ DamageAnimPhase, this.startPhase ],
[ FaintPhase, this.startPhase ], [ FaintPhase, this.startPhase ],
[ BerryPhase, this.startPhase ], [ BerryPhase, this.startPhase ],
[ TurnEndPhase, this.startPhase ], [ TurnEndPhase, this.startPhase ],

View File

@ -8,6 +8,7 @@ import { addTextObject, TextStyle } from "./text";
import { addWindow } from "./ui-theme"; import { addWindow } from "./ui-theme";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import JSZip from "jszip";
interface BuildInteractableImageOpts { interface BuildInteractableImageOpts {
scale?: number; scale?: number;
@ -27,6 +28,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
private googleImage: Phaser.GameObjects.Image; private googleImage: Phaser.GameObjects.Image;
private discordImage: Phaser.GameObjects.Image; private discordImage: Phaser.GameObjects.Image;
private usernameInfoImage: Phaser.GameObjects.Image; private usernameInfoImage: Phaser.GameObjects.Image;
private saveDownloadImage: Phaser.GameObjects.Image;
private externalPartyContainer: Phaser.GameObjects.Container; private externalPartyContainer: Phaser.GameObjects.Container;
private infoContainer: Phaser.GameObjects.Container; private infoContainer: Phaser.GameObjects.Container;
private externalPartyBg: Phaser.GameObjects.NineSlice; private externalPartyBg: Phaser.GameObjects.NineSlice;
@ -46,7 +48,13 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
scale: 0.5 scale: 0.5
}); });
this.saveDownloadImage = this.buildInteractableImage("saving_icon", "save-download-icon", {
x: 0,
scale: 0.75
});
this.infoContainer.add(this.usernameInfoImage); this.infoContainer.add(this.usernameInfoImage);
this.infoContainer.add(this.saveDownloadImage);
this.getUi().add(this.infoContainer); this.getUi().add(this.infoContainer);
this.infoContainer.setVisible(false); this.infoContainer.setVisible(false);
this.infoContainer.disableInteractive(); this.infoContainer.disableInteractive();
@ -160,7 +168,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.infoContainer.setVisible(false); this.infoContainer.setVisible(false);
this.setMouseCursorStyle("default"); //reset cursor this.setMouseCursorStyle("default"); //reset cursor
[ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown")); [ this.discordImage, this.googleImage, this.usernameInfoImage, this.saveDownloadImage ].forEach((img) => img.off("pointerdown"));
} }
private processExternalProvider(config: ModalConfig): void { private processExternalProvider(config: ModalConfig): void {
@ -178,6 +186,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.infoContainer.setVisible(true); this.infoContainer.setVisible(true);
this.getUi().moveTo(this.infoContainer, this.getUi().length - 1); this.getUi().moveTo(this.infoContainer, this.getUi().length - 1);
this.usernameInfoImage.setPositionRelative(this.infoContainer, 0, 0); this.usernameInfoImage.setPositionRelative(this.infoContainer, 0, 0);
this.saveDownloadImage.setPositionRelative(this.infoContainer, 20, 0);
this.discordImage.on("pointerdown", () => { this.discordImage.on("pointerdown", () => {
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`); const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`);
@ -229,6 +238,34 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
} }
}); });
this.saveDownloadImage.on("pointerdown", () => {
// find all data_ and sessionData keys, put them in a .txt file and download everything in a single zip
const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage
const keyToFind = "data_";
const sessionKeyToFind = "sessionData";
const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0);
const sessionKeys = localStorageKeys.filter(ls => ls.indexOf(sessionKeyToFind) >= 0);
if (dataKeys.length > 0 || sessionKeys.length > 0) {
const zip = new JSZip();
for (let i = 0; i < dataKeys.length; i++) {
zip.file(dataKeys[i] + ".prsv", localStorage.getItem(dataKeys[i])!);
}
for (let i = 0; i < sessionKeys.length; i++) {
zip.file(sessionKeys[i] + ".prsv", localStorage.getItem(sessionKeys[i])!);
}
zip.generateAsync({ type: "blob" }).then(content => {
const url = URL.createObjectURL(content);
const a = document.createElement("a");
a.href = url;
a.download = "pokerogue_saves.zip";
a.click();
URL.revokeObjectURL(url);
});
} else {
return onFail(this.ERR_NO_SAVES);
}
});
this.externalPartyContainer.setAlpha(0); this.externalPartyContainer.setAlpha(0);
this.scene.tweens.add({ this.scene.tweens.add({
targets: this.externalPartyContainer, targets: this.externalPartyContainer,

View File

@ -313,6 +313,11 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme)); this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0)); const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
this.pokemonShinyNewIcon.setVisible(!!newShinyOrVariant); this.pokemonShinyNewIcon.setVisible(!!newShinyOrVariant);
} else if ((caughtAttr & DexAttr.NON_SHINY) === BigInt(0) && ((caughtAttr & DexAttr.SHINY) === DexAttr.SHINY)) { //If the player has *only* caught any shiny variant of this species, not a non-shiny
this.pokemonShinyNewIcon.setVisible(true);
this.pokemonShinyNewIcon.setText("(+)");
this.pokemonShinyNewIcon.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme));
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
} else { } else {
this.pokemonShinyNewIcon.setVisible(false); this.pokemonShinyNewIcon.setVisible(false);
} }

View File

@ -1,8 +1,7 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { hasTouchscreen, isMobile } from "#app/touch-controls";
import { TextStyle, addTextObject } from "#app/ui/text"; import { TextStyle, addTextObject } from "#app/ui/text";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import UiHandler from "#app/ui/ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler";
import { addWindow } from "#app/ui/ui-theme"; import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar"; import { ScrollBar } from "#app/ui/scroll-bar";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
@ -15,9 +14,10 @@ import i18next from "i18next";
/** /**
* Abstract class for handling UI elements related to settings. * Abstract class for handling UI elements related to settings.
*/ */
export default class AbstractSettingsUiHandler extends UiHandler { export default class AbstractSettingsUiHandler extends MessageUiHandler {
private settingsContainer: Phaser.GameObjects.Container; private settingsContainer: Phaser.GameObjects.Container;
private optionsContainer: Phaser.GameObjects.Container; private optionsContainer: Phaser.GameObjects.Container;
private messageBoxContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu; private navigationContainer: NavigationMenu;
private scrollCursor: number; private scrollCursor: number;
@ -135,6 +135,23 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay); this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.scrollBar.setTotalRows(this.settings.length); this.scrollBar.setTotalRows(this.settings.length);
// Two-lines message box
this.messageBoxContainer = this.scene.add.container(0, this.scene.scaledCanvas.height);
this.messageBoxContainer.setName("settings-message-box");
this.messageBoxContainer.setVisible(false);
const settingsMessageBox = addWindow(this.scene, 0, -1, this.scene.scaledCanvas.width - 2, 48);
settingsMessageBox.setOrigin(0, 1);
this.messageBoxContainer.add(settingsMessageBox);
const messageText = addTextObject(this.scene, 8, -40, "", TextStyle.WINDOW, { maxLines: 2 });
messageText.setWordWrapWidth(this.scene.game.canvas.width - 60);
messageText.setName("settings-message");
messageText.setOrigin(0, 0);
this.messageBoxContainer.add(messageText);
this.message = messageText;
this.settingsContainer.add(this.optionsBg); this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.scrollBar); this.settingsContainer.add(this.scrollBar);
this.settingsContainer.add(this.navigationContainer); this.settingsContainer.add(this.navigationContainer);
@ -144,6 +161,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.settingsContainer.add(iconCancel); this.settingsContainer.add(iconCancel);
this.settingsContainer.add(actionText); this.settingsContainer.add(actionText);
this.settingsContainer.add(cancelText); this.settingsContainer.add(cancelText);
this.settingsContainer.add(this.messageBoxContainer);
ui.add(this.settingsContainer); ui.add(this.settingsContainer);
@ -326,18 +344,16 @@ export default class AbstractSettingsUiHandler extends UiHandler {
/** /**
* Set the option cursor to the specified position. * Set the option cursor to the specified position.
* *
* @param settingIndex - The index of the setting. * @param settingIndex - The index of the setting or -1 to change the current setting
* @param cursor - The cursor position to set. * @param cursor - The cursor position to set.
* @param save - Whether to save the setting to local storage. * @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully. * @returns `true` if the option cursor was set successfully.
*/ */
setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean { setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
const setting = this.settings[settingIndex]; if (settingIndex === -1) {
settingIndex = this.cursor + this.scrollCursor;
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
this.getUi().playError();
return false;
} }
const setting = this.settings[settingIndex];
const lastCursor = this.optionCursors[settingIndex]; const lastCursor = this.optionCursors[settingIndex];
@ -352,9 +368,33 @@ export default class AbstractSettingsUiHandler extends UiHandler {
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
if (save) { if (save) {
this.scene.gameData.saveSetting(setting.key, cursor); const saveSetting = () => {
if (this.reloadSettings.includes(setting)) { this.scene.gameData.saveSetting(setting.key, cursor);
this.reloadRequired = true; if (setting.requireReload) {
this.reloadRequired = true;
}
};
// For settings that ask for confirmation, display confirmation message and a Yes/No prompt before saving the setting
if (setting.options[cursor].needConfirmation) {
const confirmUpdateSetting = () => {
this.scene.ui.revertMode();
this.showText("");
saveSetting();
};
const cancelUpdateSetting = () => {
this.scene.ui.revertMode();
this.showText("");
// Put the cursor back to its previous position without saving or asking for confirmation again
this.setOptionCursor(settingIndex, lastCursor, false);
};
const confirmationMessage = setting.options[cursor].confirmationMessage ?? i18next.t("settings:defaultConfirmMessage");
this.scene.ui.showText(confirmationMessage, null, () => {
this.scene.ui.setOverlayMode(Mode.CONFIRM, confirmUpdateSetting, cancelUpdateSetting, null, null, 1, 750);
});
} else {
saveSetting();
} }
} }
@ -421,4 +461,9 @@ export default class AbstractSettingsUiHandler extends UiHandler {
} }
this.cursorObj = null; this.cursorObj = null;
} }
override showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) {
this.messageBoxContainer.setVisible(!!text?.length);
super.showText(text, delay, callback, callbackDelay, prompt, promptDelay);
}
} }