Merge branch 'beta' into bug4604

This commit is contained in:
Mumble 2024-10-11 09:28:14 -07:00 committed by GitHub
commit 480660f535
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 352 additions and 133 deletions

View File

@ -2676,7 +2676,7 @@ export default class BattleScene extends SceneBase {
} }
/** /**
* Removes all modifiers from enemy of PersistentModifier type * Removes all modifiers from enemy pokemon of {@linkcode PersistentModifier} type
*/ */
clearEnemyModifiers(): void { clearEnemyModifiers(): void {
const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PersistentModifier); const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PersistentModifier);
@ -2687,10 +2687,11 @@ export default class BattleScene extends SceneBase {
} }
/** /**
* Removes all modifiers from enemy of PokemonHeldItemModifier type * Removes all modifiers from enemy pokemon of {@linkcode PokemonHeldItemModifier} type
* @param pokemon - If specified, only removes held items from that {@linkcode Pokemon}
*/ */
clearEnemyHeldItemModifiers(): void { clearEnemyHeldItemModifiers(pokemon?: Pokemon): void {
const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PokemonHeldItemModifier); const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PokemonHeldItemModifier && (!pokemon || m.getPokemon(this) === pokemon));
for (const m of modifiersToRemove) { for (const m of modifiersToRemove) {
this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1); this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1);
} }

View File

@ -28,20 +28,13 @@ export enum ArenaTagSide {
} }
export abstract class ArenaTag { export abstract class ArenaTag {
public tagType: ArenaTagType; constructor(
public turnCount: integer; public tagType: ArenaTagType,
public sourceMove?: Moves; public turnCount: number,
public sourceId?: integer; public sourceMove?: Moves,
public side: ArenaTagSide; public sourceId?: number,
public side: ArenaTagSide = ArenaTagSide.BOTH
) {}
constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId?: integer, side: ArenaTagSide = ArenaTagSide.BOTH) {
this.tagType = tagType;
this.turnCount = turnCount;
this.sourceMove = sourceMove;
this.sourceId = sourceId;
this.side = side;
}
apply(arena: Arena, args: any[]): boolean { apply(arena: Arena, args: any[]): boolean {
return true; return true;
@ -66,6 +59,18 @@ export abstract class ArenaTag {
? allMoves[this.sourceMove].name ? allMoves[this.sourceMove].name
: null; : null;
} }
/**
* When given a arena tag or json representing one, load the data for it.
* This is meant to be inherited from by any arena tag with custom attributes
* @param {ArenaTag | any} source An arena tag
*/
loadTag(source : ArenaTag | any) : void {
this.turnCount = source.turnCount;
this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId;
this.side = source.side;
}
} }
/** /**
@ -73,7 +78,7 @@ export abstract class ArenaTag {
* Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist. * Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist.
*/ */
export class MistTag extends ArenaTag { export class MistTag extends ArenaTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.MIST, turnCount, Moves.MIST, sourceId, side); super(ArenaTagType.MIST, turnCount, Moves.MIST, sourceId, side);
} }
@ -117,7 +122,7 @@ export class WeakenMoveScreenTag extends ArenaTag {
* @param side - The side (player or enemy) the tag affects. * @param side - The side (player or enemy) the tag affects.
* @param weakenedCategories - The categories of moves that are weakened by this tag. * @param weakenedCategories - The categories of moves that are weakened by this tag.
*/ */
constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, weakenedCategories: MoveCategory[]) { constructor(tagType: ArenaTagType, turnCount: number, sourceMove: Moves, sourceId: number, side: ArenaTagSide, weakenedCategories: MoveCategory[]) {
super(tagType, turnCount, sourceMove, sourceId, side); super(tagType, turnCount, sourceMove, sourceId, side);
this.weakenedCategories = weakenedCategories; this.weakenedCategories = weakenedCategories;
@ -148,7 +153,7 @@ export class WeakenMoveScreenTag extends ArenaTag {
* Used by {@linkcode Moves.REFLECT} * Used by {@linkcode Moves.REFLECT}
*/ */
class ReflectTag extends WeakenMoveScreenTag { class ReflectTag extends WeakenMoveScreenTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.REFLECT, turnCount, Moves.REFLECT, sourceId, side, [ MoveCategory.PHYSICAL ]); super(ArenaTagType.REFLECT, turnCount, Moves.REFLECT, sourceId, side, [ MoveCategory.PHYSICAL ]);
} }
@ -164,7 +169,7 @@ class ReflectTag extends WeakenMoveScreenTag {
* Used by {@linkcode Moves.LIGHT_SCREEN} * Used by {@linkcode Moves.LIGHT_SCREEN}
*/ */
class LightScreenTag extends WeakenMoveScreenTag { class LightScreenTag extends WeakenMoveScreenTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.LIGHT_SCREEN, turnCount, Moves.LIGHT_SCREEN, sourceId, side, [ MoveCategory.SPECIAL ]); super(ArenaTagType.LIGHT_SCREEN, turnCount, Moves.LIGHT_SCREEN, sourceId, side, [ MoveCategory.SPECIAL ]);
} }
@ -180,7 +185,7 @@ class LightScreenTag extends WeakenMoveScreenTag {
* Used by {@linkcode Moves.AURORA_VEIL} * Used by {@linkcode Moves.AURORA_VEIL}
*/ */
class AuroraVeilTag extends WeakenMoveScreenTag { class AuroraVeilTag extends WeakenMoveScreenTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.AURORA_VEIL, turnCount, Moves.AURORA_VEIL, sourceId, side, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ]); super(ArenaTagType.AURORA_VEIL, turnCount, Moves.AURORA_VEIL, sourceId, side, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ]);
} }
@ -203,7 +208,7 @@ export class ConditionalProtectTag extends ArenaTag {
/** Does this apply to all moves, including those that ignore other forms of protection? */ /** Does this apply to all moves, including those that ignore other forms of protection? */
protected ignoresBypass: boolean; protected ignoresBypass: boolean;
constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass: boolean = false) { constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: number, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass: boolean = false) {
super(tagType, 1, sourceMove, sourceId, side); super(tagType, 1, sourceMove, sourceId, side);
this.protectConditionFunc = condition; this.protectConditionFunc = condition;
@ -265,7 +270,7 @@ export class ConditionalProtectTag extends ArenaTag {
*/ */
const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
const move = allMoves[moveId]; const move = allMoves[moveId];
const priority = new Utils.IntegerHolder(move.priority); const priority = new Utils.NumberHolder(move.priority);
const effectPhase = arena.scene.getCurrentPhase(); const effectPhase = arena.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase) { if (effectPhase instanceof MoveEffectPhase) {
@ -281,7 +286,7 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
* Condition: The incoming move has increased priority. * Condition: The incoming move has increased priority.
*/ */
class QuickGuardTag extends ConditionalProtectTag { class QuickGuardTag extends ConditionalProtectTag {
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.QUICK_GUARD, Moves.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); super(ArenaTagType.QUICK_GUARD, Moves.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc);
} }
} }
@ -312,7 +317,7 @@ const WideGuardConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean =
* can be an ally or enemy. * can be an ally or enemy.
*/ */
class WideGuardTag extends ConditionalProtectTag { class WideGuardTag extends ConditionalProtectTag {
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.WIDE_GUARD, Moves.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); super(ArenaTagType.WIDE_GUARD, Moves.WIDE_GUARD, sourceId, side, WideGuardConditionFunc);
} }
} }
@ -334,7 +339,7 @@ const MatBlockConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean =>
* Condition: The incoming move is a Physical or Special attack move. * Condition: The incoming move is a Physical or Special attack move.
*/ */
class MatBlockTag extends ConditionalProtectTag { class MatBlockTag extends ConditionalProtectTag {
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.MAT_BLOCK, Moves.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); super(ArenaTagType.MAT_BLOCK, Moves.MAT_BLOCK, sourceId, side, MatBlockConditionFunc);
} }
@ -372,7 +377,7 @@ const CraftyShieldConditionFunc: ProtectConditionFunc = (arena, moveId) => {
* not target all Pokemon or sides of the field. * not target all Pokemon or sides of the field.
*/ */
class CraftyShieldTag extends ConditionalProtectTag { class CraftyShieldTag extends ConditionalProtectTag {
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.CRAFTY_SHIELD, Moves.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); super(ArenaTagType.CRAFTY_SHIELD, Moves.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true);
} }
} }
@ -384,12 +389,12 @@ class CraftyShieldTag extends ConditionalProtectTag {
export class NoCritTag extends ArenaTag { export class NoCritTag extends ArenaTag {
/** /**
* Constructor method for the NoCritTag class * Constructor method for the NoCritTag class
* @param turnCount `integer` the number of turns this effect lasts * @param turnCount `number` the number of turns this effect lasts
* @param sourceMove {@linkcode Moves} the move that created this effect * @param sourceMove {@linkcode Moves} the move that created this effect
* @param sourceId `integer` the ID of the {@linkcode Pokemon} that created this effect * @param sourceId `number` the ID of the {@linkcode Pokemon} that created this effect
* @param side {@linkcode ArenaTagSide} the side to which this effect belongs * @param side {@linkcode ArenaTagSide} the side to which this effect belongs
*/ */
constructor(turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceMove: Moves, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side); super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side);
} }
@ -419,7 +424,7 @@ class WishTag extends ArenaTag {
private triggerMessage: string; private triggerMessage: string;
private healHp: number; private healHp: number;
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.WISH, turnCount, Moves.WISH, sourceId, side); super(ArenaTagType.WISH, turnCount, Moves.WISH, sourceId, side);
} }
@ -460,7 +465,7 @@ export class WeakenMoveTypeTag extends ArenaTag {
* @param sourceMove - The move that created the tag. * @param sourceMove - The move that created the tag.
* @param sourceId - The ID of the source of the tag. * @param sourceId - The ID of the source of the tag.
*/ */
constructor(tagType: ArenaTagType, turnCount: integer, type: Type, sourceMove: Moves, sourceId: integer) { constructor(tagType: ArenaTagType, turnCount: number, type: Type, sourceMove: Moves, sourceId: number) {
super(tagType, turnCount, sourceMove, sourceId); super(tagType, turnCount, sourceMove, sourceId);
this.weakenedType = type; this.weakenedType = type;
@ -481,7 +486,7 @@ export class WeakenMoveTypeTag extends ArenaTag {
* Weakens Electric type moves for a set amount of turns, usually 5. * Weakens Electric type moves for a set amount of turns, usually 5.
*/ */
class MudSportTag extends WeakenMoveTypeTag { class MudSportTag extends WeakenMoveTypeTag {
constructor(turnCount: integer, sourceId: integer) { constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.MUD_SPORT, turnCount, Type.ELECTRIC, Moves.MUD_SPORT, sourceId); super(ArenaTagType.MUD_SPORT, turnCount, Type.ELECTRIC, Moves.MUD_SPORT, sourceId);
} }
@ -499,7 +504,7 @@ class MudSportTag extends WeakenMoveTypeTag {
* Weakens Fire type moves for a set amount of turns, usually 5. * Weakens Fire type moves for a set amount of turns, usually 5.
*/ */
class WaterSportTag extends WeakenMoveTypeTag { class WaterSportTag extends WeakenMoveTypeTag {
constructor(turnCount: integer, sourceId: integer) { constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.WATER_SPORT, turnCount, Type.FIRE, Moves.WATER_SPORT, sourceId); super(ArenaTagType.WATER_SPORT, turnCount, Type.FIRE, Moves.WATER_SPORT, sourceId);
} }
@ -550,8 +555,8 @@ export class IonDelugeTag extends ArenaTag {
* Abstract class to implement arena traps. * Abstract class to implement arena traps.
*/ */
export class ArenaTrapTag extends ArenaTag { export class ArenaTrapTag extends ArenaTag {
public layers: integer; public layers: number;
public maxLayers: integer; public maxLayers: number;
/** /**
* Creates a new instance of the ArenaTrapTag class. * Creates a new instance of the ArenaTrapTag class.
@ -562,7 +567,7 @@ export class ArenaTrapTag extends ArenaTag {
* @param side - The side (player or enemy) the tag affects. * @param side - The side (player or enemy) the tag affects.
* @param maxLayers - The maximum amount of layers this tag can have. * @param maxLayers - The maximum amount of layers this tag can have.
*/ */
constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, maxLayers: integer) { constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: number, side: ArenaTagSide, maxLayers: number) {
super(tagType, 0, sourceMove, sourceId, side); super(tagType, 0, sourceMove, sourceId, side);
this.layers = 1; this.layers = 1;
@ -593,6 +598,12 @@ export class ArenaTrapTag extends ArenaTag {
getMatchupScoreMultiplier(pokemon: Pokemon): number { getMatchupScoreMultiplier(pokemon: Pokemon): number {
return pokemon.isGrounded() ? 1 : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); return pokemon.isGrounded() ? 1 : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
} }
loadTag(source: any): void {
super.loadTag(source);
this.layers = source.layers;
this.maxLayers = source.maxLayers;
}
} }
/** /**
@ -601,7 +612,7 @@ export class ArenaTrapTag extends ArenaTag {
* in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap. * in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap.
*/ */
class SpikesTag extends ArenaTrapTag { class SpikesTag extends ArenaTrapTag {
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.SPIKES, Moves.SPIKES, sourceId, side, 3); super(ArenaTagType.SPIKES, Moves.SPIKES, sourceId, side, 3);
} }
@ -645,7 +656,7 @@ class SpikesTag extends ArenaTrapTag {
class ToxicSpikesTag extends ArenaTrapTag { class ToxicSpikesTag extends ArenaTrapTag {
private neutralized: boolean; private neutralized: boolean;
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.TOXIC_SPIKES, Moves.TOXIC_SPIKES, sourceId, side, 2); super(ArenaTagType.TOXIC_SPIKES, Moves.TOXIC_SPIKES, sourceId, side, 2);
this.neutralized = false; this.neutralized = false;
} }
@ -703,7 +714,7 @@ class ToxicSpikesTag extends ArenaTrapTag {
class DelayedAttackTag extends ArenaTag { class DelayedAttackTag extends ArenaTag {
public targetIndex: BattlerIndex; public targetIndex: BattlerIndex;
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: integer, targetIndex: BattlerIndex) { constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) {
super(tagType, 3, sourceMove, sourceId); super(tagType, 3, sourceMove, sourceId);
this.targetIndex = targetIndex; this.targetIndex = targetIndex;
@ -728,7 +739,7 @@ class DelayedAttackTag extends ArenaTag {
* who is summoned into the trap, based on the Rock type's type effectiveness. * who is summoned into the trap, based on the Rock type's type effectiveness.
*/ */
class StealthRockTag extends ArenaTrapTag { class StealthRockTag extends ArenaTrapTag {
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.STEALTH_ROCK, Moves.STEALTH_ROCK, sourceId, side, 1); super(ArenaTagType.STEALTH_ROCK, Moves.STEALTH_ROCK, sourceId, side, 1);
} }
@ -804,7 +815,7 @@ class StealthRockTag extends ArenaTrapTag {
* to any Pokémon who is summoned into this trap. * to any Pokémon who is summoned into this trap.
*/ */
class StickyWebTag extends ArenaTrapTag { class StickyWebTag extends ArenaTrapTag {
constructor(sourceId: integer, side: ArenaTagSide) { constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.STICKY_WEB, Moves.STICKY_WEB, sourceId, side, 1); super(ArenaTagType.STICKY_WEB, Moves.STICKY_WEB, sourceId, side, 1);
} }
@ -848,7 +859,7 @@ export class ArenaRoomTag extends ArenaTag {
* also reversing the turn order for all Pokémon on the field as well. * also reversing the turn order for all Pokémon on the field as well.
*/ */
export class TrickRoomTag extends ArenaRoomTag { export class TrickRoomTag extends ArenaRoomTag {
constructor(turnCount: integer, sourceId: integer) { constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId); super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId);
} }
@ -876,7 +887,7 @@ export class TrickRoomTag extends ArenaRoomTag {
* {@linkcode Abilities.LEVITATE} for the duration of the arena tag, usually 5 turns. * {@linkcode Abilities.LEVITATE} for the duration of the arena tag, usually 5 turns.
*/ */
export class GravityTag extends ArenaTag { export class GravityTag extends ArenaTag {
constructor(turnCount: integer) { constructor(turnCount: number) {
super(ArenaTagType.GRAVITY, turnCount, Moves.GRAVITY); super(ArenaTagType.GRAVITY, turnCount, Moves.GRAVITY);
} }
@ -900,7 +911,7 @@ export class GravityTag extends ArenaTag {
* Applies this arena tag for 4 turns (including the turn the move was used). * Applies this arena tag for 4 turns (including the turn the move was used).
*/ */
class TailwindTag extends ArenaTag { class TailwindTag extends ArenaTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.TAILWIND, turnCount, Moves.TAILWIND, sourceId, side); super(ArenaTagType.TAILWIND, turnCount, Moves.TAILWIND, sourceId, side);
} }
@ -938,7 +949,7 @@ class TailwindTag extends ArenaTag {
* Doubles the prize money from trainers and money moves like {@linkcode Moves.PAY_DAY} and {@linkcode Moves.MAKE_IT_RAIN}. * Doubles the prize money from trainers and money moves like {@linkcode Moves.PAY_DAY} and {@linkcode Moves.MAKE_IT_RAIN}.
*/ */
class HappyHourTag extends ArenaTag { class HappyHourTag extends ArenaTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.HAPPY_HOUR, turnCount, Moves.HAPPY_HOUR, sourceId, side); super(ArenaTagType.HAPPY_HOUR, turnCount, Moves.HAPPY_HOUR, sourceId, side);
} }
@ -952,7 +963,7 @@ class HappyHourTag extends ArenaTag {
} }
class SafeguardTag extends ArenaTag { class SafeguardTag extends ArenaTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.SAFEGUARD, turnCount, Moves.SAFEGUARD, sourceId, side); super(ArenaTagType.SAFEGUARD, turnCount, Moves.SAFEGUARD, sourceId, side);
} }
@ -965,6 +976,11 @@ class SafeguardTag extends ArenaTag {
} }
} }
class NoneTag extends ArenaTag {
constructor() {
super(ArenaTagType.NONE, 0);
}
}
/** /**
* This arena tag facilitates the application of the move Imprison * This arena tag facilitates the application of the move Imprison
* Imprison remains in effect as long as the source Pokemon is active and present on the field. * Imprison remains in effect as long as the source Pokemon is active and present on the field.
@ -1112,7 +1128,8 @@ class GrassWaterPledgeTag extends ArenaTag {
} }
} }
export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
switch (tagType) { switch (tagType) {
case ArenaTagType.MIST: case ArenaTagType.MIST:
return new MistTag(turnCount, sourceId, side); return new MistTag(turnCount, sourceId, side);
@ -1173,3 +1190,16 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
return null; return null;
} }
} }
/**
* When given a battler tag or json representing one, creates an actual ArenaTag object with the same data.
* @param {ArenaTag | any} source An arena tag
* @return {ArenaTag} The valid arena tag
*/
export function loadArenaTag(source: ArenaTag | any): ArenaTag {
const tag = getArenaTag(source.tagType, source.turnCount, source.sourceMove, source.sourceId, source.targetIndex, source.side)
?? new NoneTag();
tag.loadTag(source);
return tag;
}

View File

@ -3635,7 +3635,7 @@ export function overrideHeldItems(scene: BattleScene, pokemon: Pokemon, isPlayer
} }
if (!isPlayer) { if (!isPlayer) {
scene.clearEnemyHeldItemModifiers(); scene.clearEnemyHeldItemModifiers(pokemon);
} }
heldItemsOverride.forEach(item => { heldItemsOverride.forEach(item => {

View File

@ -0,0 +1,23 @@
import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase";
import { Phase } from "#app/phase";
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
export class CheckStatusEffectPhase extends Phase {
private order : BattlerIndex[];
constructor(scene : BattleScene, order : BattlerIndex[]) {
super(scene);
this.scene = scene;
this.order = order;
}
start() {
const field = this.scene.getField();
for (const o of this.order) {
if (field[o].status && field[o].status.isPostTurn()) {
this.scene.unshiftPhase(new PostTurnStatusEffectPhase(this.scene, o));
}
}
this.end();
}
}

View File

@ -6,7 +6,6 @@ import { StatusEffect } from "#app/enums/status-effect";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase";
export class ObtainStatusEffectPhase extends PokemonPhase { export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect?: StatusEffect | undefined; private statusEffect?: StatusEffect | undefined;
@ -33,9 +32,6 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
pokemon.updateInfo(true); pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => {
this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined)); this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined));
if (pokemon.status?.isPostTurn()) {
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex));
}
this.end(); this.end();
}); });
return; return;

View File

@ -13,10 +13,10 @@ import { BerryPhase } from "./berry-phase";
import { FieldPhase } from "./field-phase"; import { FieldPhase } from "./field-phase";
import { MoveHeaderPhase } from "./move-header-phase"; import { MoveHeaderPhase } from "./move-header-phase";
import { MovePhase } from "./move-phase"; import { MovePhase } from "./move-phase";
import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase";
import { SwitchSummonPhase } from "./switch-summon-phase"; import { SwitchSummonPhase } from "./switch-summon-phase";
import { TurnEndPhase } from "./turn-end-phase"; import { TurnEndPhase } from "./turn-end-phase";
import { WeatherEffectPhase } from "./weather-effect-phase"; import { WeatherEffectPhase } from "./weather-effect-phase";
import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { TrickRoomTag } from "#app/data/arena-tag"; import { TrickRoomTag } from "#app/data/arena-tag";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
@ -206,11 +206,8 @@ export class TurnStartPhase extends FieldPhase {
this.scene.pushPhase(new WeatherEffectPhase(this.scene)); this.scene.pushPhase(new WeatherEffectPhase(this.scene));
for (const o of moveOrder) { /** Add a new phase to check who should be taking status damage */
if (field[o].status && field[o].status.isPostTurn()) { this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, moveOrder));
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, o));
}
}
this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new BerryPhase(this.scene));
this.scene.pushPhase(new TurnEndPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene));

View File

@ -1,5 +1,5 @@
import { Arena } from "../field/arena"; import { Arena } from "../field/arena";
import { ArenaTag } from "../data/arena-tag"; import { ArenaTag, loadArenaTag } from "../data/arena-tag";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Weather } from "../data/weather"; import { Weather } from "../data/weather";
import { Terrain } from "#app/data/terrain"; import { Terrain } from "#app/data/terrain";
@ -15,6 +15,10 @@ export default class ArenaData {
this.biome = sourceArena ? sourceArena.biomeType : source.biome; this.biome = sourceArena ? sourceArena.biomeType : source.biome;
this.weather = sourceArena ? sourceArena.weather : source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; this.weather = sourceArena ? sourceArena.weather : source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null;
this.terrain = sourceArena ? sourceArena.terrain : source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; this.terrain = sourceArena ? sourceArena.terrain : source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null;
this.tags = sourceArena ? sourceArena.tags : []; this.tags = [];
if (source.tags) {
this.tags = source.tags.map(t => loadArenaTag(t));
}
} }
} }

View File

@ -31,7 +31,7 @@ import { TrainerVariant } from "#app/field/trainer";
import { Variant } from "#app/data/variant"; import { Variant } from "#app/data/variant";
import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "#app/system/settings/settings-gamepad"; import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "#app/system/settings/settings-gamepad";
import { setSettingKeyboard, SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { setSettingKeyboard, SettingKeyboard } from "#app/system/settings/settings-keyboard";
import { TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; import { TagAddedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena";
import * as Modifier from "#app/modifier/modifier"; import * as Modifier from "#app/modifier/modifier";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import ChallengeData from "#app/system/challenge-data"; import ChallengeData from "#app/system/challenge-data";
@ -50,6 +50,7 @@ import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatch
import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokerogueApiClearSessionData } from "#app/@types/pokerogue-api"; import { PokerogueApiClearSessionData } from "#app/@types/pokerogue-api";
import { ArenaTrapTag } from "#app/data/arena-tag";
export const defaultStarterSpecies: Species[] = [ export const defaultStarterSpecies: Species[] = [
Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE,
@ -1085,8 +1086,18 @@ export class GameData {
scene.arena.terrain = sessionData.arena.terrain; scene.arena.terrain = sessionData.arena.terrain;
scene.arena.eventTarget.dispatchEvent(new TerrainChangedEvent(TerrainType.NONE, scene.arena.terrain?.terrainType!, scene.arena.terrain?.turnsLeft!)); // TODO: is this bang correct? scene.arena.eventTarget.dispatchEvent(new TerrainChangedEvent(TerrainType.NONE, scene.arena.terrain?.terrainType!, scene.arena.terrain?.turnsLeft!)); // TODO: is this bang correct?
// TODO
//scene.arena.tags = sessionData.arena.tags; scene.arena.tags = sessionData.arena.tags;
if (scene.arena.tags) {
for (const tag of scene.arena.tags) {
if (tag instanceof ArenaTrapTag) {
const { tagType, side, turnCount, layers, maxLayers } = tag as ArenaTrapTag;
scene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers));
} else {
scene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount));
}
}
}
for (const modifierData of sessionData.modifiers) { for (const modifierData of sessionData.modifiers) {
const modifier = modifierData.toModifier(scene, Modifier[modifierData.className]); const modifier = modifierData.toModifier(scene, Modifier[modifierData.className]);

View File

@ -1,7 +1,4 @@
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { MessagePhase } from "#app/phases/message-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -10,6 +7,7 @@ import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Items - Toxic orb", () => { describe("Items - Toxic orb", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -27,40 +25,36 @@ describe("Items - Toxic orb", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
const moveToUse = Moves.GROWTH; game.override
const oppMoveToUse = Moves.TACKLE; .battleType("single")
game.override.battleType("single"); .enemySpecies(Species.RATTATA)
game.override.enemySpecies(Species.RATTATA); .ability(Abilities.BALL_FETCH)
game.override.ability(Abilities.INSOMNIA); .enemyAbility(Abilities.BALL_FETCH)
game.override.enemyAbility(Abilities.INSOMNIA); .moveset([ Moves.SPLASH ])
game.override.startingLevel(2000); .enemyMoveset(Moves.SPLASH)
game.override.moveset([ moveToUse ]); .startingHeldItems([{
game.override.enemyMoveset([ oppMoveToUse, oppMoveToUse, oppMoveToUse, oppMoveToUse ]);
game.override.startingHeldItems([{
name: "TOXIC_ORB", name: "TOXIC_ORB",
}]); }]);
vi.spyOn(i18next, "t"); vi.spyOn(i18next, "t");
}); });
it("TOXIC ORB", async () => { it("badly poisons the holder", async () => {
const moveToUse = Moves.GROWTH; await game.classicMode.startBattle([ Species.MIGHTYENA ]);
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
expect(game.scene.modifiers[0].type.id).toBe("TOXIC_ORB");
game.move.select(moveToUse); const player = game.scene.getPlayerField()[0];
// will run the 13 phase from enemyCommandPhase to TurnEndPhase game.move.select(Moves.SPLASH);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
// Toxic orb should trigger here // Toxic orb should trigger here
await game.phaseInterceptor.run(MessagePhase); await game.phaseInterceptor.run("MessagePhase");
expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything()); expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything());
await game.phaseInterceptor.run(MessagePhase); await game.toNextTurn();
expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.activation", expect.anything());
expect(game.scene.getParty()[0].status!.effect).toBe(StatusEffect.TOXIC); expect(player.status?.effect).toBe(StatusEffect.TOXIC);
}, 20000); // Damage should not have ticked yet.
expect(player.status?.turnCount).toBe(0);
}, TIMEOUT);
}); });

View File

@ -0,0 +1,136 @@
import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
import { StatusEffect } from "#app/data/status-effect";
import { decrypt, encrypt, GameData, SessionSaveData } from "#app/system/game-data";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
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";
const TIMEOUT = 20 * 1000;
describe("Moves - Toxic Spikes", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.startingWave(5)
.enemySpecies(Species.RATTATA)
.enemyAbility(Abilities.BALL_FETCH)
.ability(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.moveset([ Moves.TOXIC_SPIKES, Moves.SPLASH, Moves.ROAR ]);
});
it("should not affect the opponent if they do not switch", async() => {
await game.classicMode.runToSummon([ Species.MIGHTYENA, Species.POOCHYENA ]);
const enemy = game.scene.getEnemyField()[0];
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
game.doSwitchPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.hp).toBe(enemy.getMaxHp());
expect(enemy.status?.effect).toBeUndefined();
}, TIMEOUT);
it("should poison the opponent if they switch into 1 layer", async() => {
await game.classicMode.runToSummon([ Species.MIGHTYENA ]);
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.ROAR);
await game.phaseInterceptor.to("TurnEndPhase");
const enemy = game.scene.getEnemyField()[0];
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(enemy.status?.effect).toBe(StatusEffect.POISON);
}, TIMEOUT);
it("should badly poison the opponent if they switch into 2 layers", async() => {
await game.classicMode.runToSummon([ Species.MIGHTYENA ]);
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.ROAR);
await game.phaseInterceptor.to("TurnEndPhase");
const enemy = game.scene.getEnemyField()[0];
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(enemy.status?.effect).toBe(StatusEffect.TOXIC);
}, TIMEOUT);
it("should be removed if a grounded poison pokemon switches in", async() => {
game.override.enemySpecies(Species.GRIMER);
await game.classicMode.runToSummon([ Species.MIGHTYENA ]);
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.ROAR);
await game.phaseInterceptor.to("TurnEndPhase");
const enemy = game.scene.getEnemyField()[0];
expect(enemy.hp).toBe(enemy.getMaxHp());
expect(enemy.status?.effect).toBeUndefined();
expect(game.scene.arena.tags.length).toBe(0);
}, TIMEOUT);
it("shouldn't create multiple layers per use in doubles", async() => {
await game.classicMode.runToSummon([ Species.MIGHTYENA, Species.POOCHYENA ]);
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
const arenaTags = (game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag);
expect(arenaTags.tagType).toBe(ArenaTagType.TOXIC_SPIKES);
expect(arenaTags.layers).toBe(1);
}, TIMEOUT);
it("should persist through reload", async() => {
game.override.startingWave(1);
const scene = game.scene;
const gameData = new GameData(scene);
await game.classicMode.runToSummon([ Species.MIGHTYENA ]);
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.phaseInterceptor.to("BattleEndPhase");
await game.toNextWave();
const sessionData : SessionSaveData = gameData["getSessionSaveData"](game.scene);
localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true));
const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true));
gameData.loadSession(game.scene, 0, recoveredData);
expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags);
localStorage.removeItem("sessionTestData");
}, TIMEOUT);
});

View File

@ -40,7 +40,9 @@ export default class AchvsUiHandler extends MessageUiHandler {
private iconsBg: Phaser.GameObjects.NineSlice; private iconsBg: Phaser.GameObjects.NineSlice;
private icons: Phaser.GameObjects.Sprite[]; private icons: Phaser.GameObjects.Sprite[];
private titleBg: Phaser.GameObjects.NineSlice;
private titleText: Phaser.GameObjects.Text; private titleText: Phaser.GameObjects.Text;
private scoreContainer: Phaser.GameObjects.Container;
private scoreText: Phaser.GameObjects.Text; private scoreText: Phaser.GameObjects.Text;
private unlockText: Phaser.GameObjects.Text; private unlockText: Phaser.GameObjects.Text;
@ -114,29 +116,31 @@ export default class AchvsUiHandler extends MessageUiHandler {
const titleBg = addWindow(this.scene, 0, this.headerBg.height + this.iconsBg.height, 174, 24); const titleBg = addWindow(this.scene, 0, this.headerBg.height + this.iconsBg.height, 174, 24);
titleBg.setOrigin(0, 0); titleBg.setOrigin(0, 0);
this.titleBg = titleBg;
this.titleText = addTextObject(this.scene, 0, 0, "", TextStyle.WINDOW); this.titleText = addTextObject(this.scene, 0, 0, "", TextStyle.WINDOW);
const textSize = languageSettings[i18next.language]?.TextSize ?? this.titleText.style.fontSize; const textSize = languageSettings[i18next.language]?.TextSize ?? this.titleText.style.fontSize;
this.titleText.setFontSize(textSize); this.titleText.setFontSize(textSize);
this.titleText.setOrigin(0, 0);
const titleBgCenterX = titleBg.x + titleBg.width / 2; const titleBgCenterX = titleBg.x + titleBg.width / 2;
const titleBgCenterY = titleBg.y + titleBg.height / 2; const titleBgCenterY = titleBg.y + titleBg.height / 2;
this.titleText.setOrigin(0.5, 0.5); this.titleText.setOrigin(0.5, 0.5);
this.titleText.setPosition(titleBgCenterX, titleBgCenterY); this.titleText.setPosition(titleBgCenterX, titleBgCenterY);
const scoreBg = addWindow(this.scene, titleBg.x + titleBg.width, titleBg.y, 46, 24); this.scoreContainer = this.scene.add.container(titleBg.x + titleBg.width, titleBg.y);
const scoreBg = addWindow(this.scene, 0, 0, 46, 24);
scoreBg.setOrigin(0, 0); scoreBg.setOrigin(0, 0);
this.scoreContainer.add(scoreBg);
this.scoreText = addTextObject(this.scene, 0, 0, "", TextStyle.WINDOW); this.scoreText = addTextObject(this.scene, scoreBg.width / 2, scoreBg.height / 2, "", TextStyle.WINDOW);
this.scoreText.setOrigin(0, 0); this.scoreText.setOrigin(0.5, 0.5);
this.scoreText.setPositionRelative(scoreBg, 8, 4); this.scoreContainer.add(this.scoreText);
const unlockBg = addWindow(this.scene, scoreBg.x + scoreBg.width, scoreBg.y, 98, 24); const unlockBg = addWindow(this.scene, this.scoreContainer.x + scoreBg.width, titleBg.y, 98, 24);
unlockBg.setOrigin(0, 0); unlockBg.setOrigin(0, 0);
this.unlockText = addTextObject(this.scene, 0, 0, "", TextStyle.WINDOW); this.unlockText = addTextObject(this.scene, 0, 0, "", TextStyle.WINDOW);
this.unlockText.setOrigin(0, 0); this.unlockText.setOrigin(0.5, 0.5);
this.unlockText.setPositionRelative(unlockBg, 8, 4); this.unlockText.setPositionRelative(unlockBg, unlockBg.width / 2, unlockBg.height / 2);
const descriptionBg = addWindow(this.scene, 0, titleBg.y + titleBg.height, (this.scene.game.canvas.width / 6) - 2, 42); const descriptionBg = addWindow(this.scene, 0, titleBg.y + titleBg.height, (this.scene.game.canvas.width / 6) - 2, 42);
descriptionBg.setOrigin(0, 0); descriptionBg.setOrigin(0, 0);
@ -157,8 +161,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.add(this.iconsContainer); this.mainContainer.add(this.iconsContainer);
this.mainContainer.add(titleBg); this.mainContainer.add(titleBg);
this.mainContainer.add(this.titleText); this.mainContainer.add(this.titleText);
this.mainContainer.add(scoreBg); this.mainContainer.add(this.scoreContainer);
this.mainContainer.add(this.scoreText);
this.mainContainer.add(unlockBg); this.mainContainer.add(unlockBg);
this.mainContainer.add(this.unlockText); this.mainContainer.add(this.unlockText);
this.mainContainer.add(descriptionBg); this.mainContainer.add(descriptionBg);
@ -167,8 +170,6 @@ export default class AchvsUiHandler extends MessageUiHandler {
ui.add(this.mainContainer); ui.add(this.mainContainer);
this.currentPage = Page.ACHIEVEMENTS; this.currentPage = Page.ACHIEVEMENTS;
this.setCursor(0);
this.setScrollCursor(0);
this.mainContainer.setVisible(false); this.mainContainer.setVisible(false);
} }
@ -316,9 +317,19 @@ export default class AchvsUiHandler extends MessageUiHandler {
if (update || pageChange) { if (update || pageChange) {
switch (this.currentPage) { switch (this.currentPage) {
case Page.ACHIEVEMENTS: case Page.ACHIEVEMENTS:
if (pageChange) {
this.titleBg.width = 174;
this.titleText.x = this.titleBg.width / 2;
this.scoreContainer.setVisible(true);
}
this.showAchv(achvs[Object.keys(achvs)[cursor + this.scrollCursor * this.COLS]]); this.showAchv(achvs[Object.keys(achvs)[cursor + this.scrollCursor * this.COLS]]);
break; break;
case Page.VOUCHERS: case Page.VOUCHERS:
if (pageChange) {
this.titleBg.width = 220;
this.titleText.x = this.titleBg.width / 2;
this.scoreContainer.setVisible(false);
}
this.showVoucher(vouchers[Object.keys(vouchers)[cursor + this.scrollCursor * this.COLS]]); this.showVoucher(vouchers[Object.keys(vouchers)[cursor + this.scrollCursor * this.COLS]]);
break; break;
} }
@ -442,6 +453,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.currentPage = Page.ACHIEVEMENTS; this.currentPage = Page.ACHIEVEMENTS;
this.mainContainer.setVisible(false); this.mainContainer.setVisible(false);
this.setScrollCursor(0); this.setScrollCursor(0);
this.setCursor(0, true);
this.eraseCursor(); this.eraseCursor();
} }

View File

@ -12,7 +12,8 @@ import { Mode } from "./ui";
import { addWindow } from "./ui-theme"; import { addWindow } from "./ui-theme";
import { RunDisplayMode } from "#app/ui/run-info-ui-handler"; import { RunDisplayMode } from "#app/ui/run-info-ui-handler";
const sessionSlotCount = 5; const SESSION_SLOTS_COUNT = 5;
const SLOTS_ON_SCREEN = 3;
export enum SaveSlotUiMode { export enum SaveSlotUiMode {
LOAD, LOAD,
@ -84,12 +85,10 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
this.saveSlotSelectCallback = args[1] as SaveSlotSelectCallback; this.saveSlotSelectCallback = args[1] as SaveSlotSelectCallback;
this.saveSlotSelectContainer.setVisible(true); this.saveSlotSelectContainer.setVisible(true);
this.populateSessionSlots() this.populateSessionSlots();
.then(() => {
this.setScrollCursor(0); this.setScrollCursor(0);
this.setCursor(0); this.setCursor(0);
});
return true; return true;
} }
@ -161,9 +160,9 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
} }
break; break;
case Button.DOWN: case Button.DOWN:
if (this.cursor < 2) { if (this.cursor < (SLOTS_ON_SCREEN - 1)) {
success = this.setCursor(this.cursor + 1, this.cursor); success = this.setCursor(this.cursor + 1, cursorPosition);
} else if (this.scrollCursor < sessionSlotCount - 3) { } else if (this.scrollCursor < SESSION_SLOTS_COUNT - SLOTS_ON_SCREEN) {
success = this.setScrollCursor(this.scrollCursor + 1, cursorPosition); success = this.setScrollCursor(this.scrollCursor + 1, cursorPosition);
} }
break; break;
@ -184,13 +183,19 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
return success || error; return success || error;
} }
async populateSessionSlots() { populateSessionSlots() {
for (let s = 0; s < sessionSlotCount; s++) { for (let s = 0; s < SESSION_SLOTS_COUNT; s++) {
const sessionSlot = new SessionSlot(this.scene, s); const sessionSlot = new SessionSlot(this.scene, s);
await sessionSlot.load();
this.scene.add.existing(sessionSlot); this.scene.add.existing(sessionSlot);
this.sessionSlotsContainer.add(sessionSlot); this.sessionSlotsContainer.add(sessionSlot);
this.sessionSlots.push(sessionSlot); this.sessionSlots.push(sessionSlot);
sessionSlot.load().then((success) => {
// If the cursor was moved to this slot while the session was loading
// call setCursor again to shift the slot position and show the arrow for save preview
if (success && (this.cursor + this.scrollCursor) === s) {
this.setCursor(s);
}
});
} }
} }
@ -209,12 +214,12 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
} }
/** /**
* setCursor takes user navigation as an input and positions the cursor accordingly * Move the cursor to a new position and update the view accordingly
* @param cursor the index provided to the cursor * @param cursor the new cursor position, between `0` and `SLOTS_ON_SCREEN - 1`
* @param prevCursor the previous index occupied by the cursor - optional * @param prevSlotIndex index of the previous session occupied by the cursor, between `0` and `SESSION_SLOTS_COUNT - 1` - optional
* @returns `true` if the cursor position has changed | `false` if it has not * @returns `true` if the cursor position has changed | `false` if it has not
*/ */
override setCursor(cursor: integer, prevCursor?: integer): boolean { override setCursor(cursor: integer, prevSlotIndex?: integer): boolean {
const changed = super.setCursor(cursor); const changed = super.setCursor(cursor);
if (!this.cursorObj) { if (!this.cursorObj) {
@ -241,21 +246,20 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
} }
this.setArrowVisibility(hasData); this.setArrowVisibility(hasData);
} }
if (!Utils.isNullOrUndefined(prevCursor)) { if (!Utils.isNullOrUndefined(prevSlotIndex)) {
this.revertSessionSlot(prevCursor); this.revertSessionSlot(prevSlotIndex);
} }
return changed; return changed;
} }
/** /**
* Helper function that resets the session slot position to its default central position * Helper function that resets the given session slot to its default central position
* @param prevCursor the previous location of the cursor
*/ */
revertSessionSlot(prevCursor: integer): void { revertSessionSlot(slotIndex: integer): void {
const sessionSlot = this.sessionSlots[prevCursor]; const sessionSlot = this.sessionSlots[slotIndex];
if (sessionSlot) { if (sessionSlot) {
sessionSlot.setPosition(0, prevCursor * 56); sessionSlot.setPosition(0, slotIndex * 56);
} }
} }
@ -270,12 +274,18 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
} }
} }
setScrollCursor(scrollCursor: integer, priorCursor?: integer): boolean { /**
* Move the scrolling cursor to a new position and update the view accordingly
* @param scrollCursor the new cursor position, between `0` and `SESSION_SLOTS_COUNT - SLOTS_ON_SCREEN`
* @param prevSlotIndex index of the previous slot occupied by the cursor, between `0` and `SESSION_SLOTS_COUNT-1` - optional
* @returns `true` if the cursor position has changed | `false` if it has not
*/
setScrollCursor(scrollCursor: integer, prevSlotIndex?: integer): boolean {
const changed = scrollCursor !== this.scrollCursor; const changed = scrollCursor !== this.scrollCursor;
if (changed) { if (changed) {
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.setCursor(this.cursor, priorCursor); this.setCursor(this.cursor, prevSlotIndex);
this.scene.tweens.add({ this.scene.tweens.add({
targets: this.sessionSlotsContainer, targets: this.sessionSlotsContainer,
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor, y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
@ -290,6 +300,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
clear() { clear() {
super.clear(); super.clear();
this.saveSlotSelectContainer.setVisible(false); this.saveSlotSelectContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor(); this.eraseCursor();
this.saveSlotSelectCallback = null; this.saveSlotSelectCallback = null;
this.clearSessionSlots(); this.clearSessionSlots();
@ -391,6 +402,10 @@ class SessionSlot extends Phaser.GameObjects.Container {
load(): Promise<boolean> { load(): Promise<boolean> {
return new Promise<boolean>(resolve => { return new Promise<boolean>(resolve => {
this.scene.gameData.getSession(this.slotId).then(async sessionData => { this.scene.gameData.getSession(this.slotId).then(async sessionData => {
// Ignore the results if the view was exited
if (!this.active) {
return;
}
if (!sessionData) { if (!sessionData) {
this.hasData = false; this.hasData = false;
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty")); this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));