Merge branch 'beta' into instruct
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.2 KiB |
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5930,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)
|
||||||
|
46
src/data/balance/special-species-groups.ts
Normal 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,
|
||||||
|
];
|
@ -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 {
|
||||||
@ -5975,14 +5999,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||||
switchOutTarget.leaveField(true);
|
switchOutTarget.leaveField(true);
|
||||||
const slotIndex = Utils.randIntRange(user.scene.currentBattle.getBattlerCount(), user.scene.getPlayerParty().length);
|
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||||
user.scene.prependToPhase(
|
user.scene.prependToPhase(
|
||||||
new SwitchSummonPhase(
|
new SwitchSummonPhase(
|
||||||
user.scene,
|
user.scene,
|
||||||
@ -6011,14 +6043,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
|
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
|
||||||
if (switchOutTarget.scene.getEnemyParty().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.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) {
|
||||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||||
switchOutTarget.leaveField(true);
|
switchOutTarget.leaveField(true);
|
||||||
const slotIndex = Utils.randIntRange(user.scene.currentBattle.getBattlerCount(), user.scene.getEnemyParty().length);
|
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||||
user.scene.prependToPhase(
|
user.scene.prependToPhase(
|
||||||
new SwitchSummonPhase(
|
new SwitchSummonPhase(
|
||||||
user.scene,
|
user.scene,
|
||||||
@ -6082,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -2618,8 +2618,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);
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
|
@ -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--;
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
74
src/test/abilities/honey_gather.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
173
src/test/mystery-encounter/encounters/safari-zone.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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");
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|