mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-20 14:29:28 +02:00
Compare commits
15 Commits
6495e89e7e
...
ad3b3cfe8c
Author | SHA1 | Date | |
---|---|---|---|
|
ad3b3cfe8c | ||
|
f42237d415 | ||
|
b44f0a4176 | ||
|
076ef81691 | ||
|
23271901cf | ||
|
1517e0512e | ||
|
0f0482d556 | ||
|
9091d236f1 | ||
|
aec7ee20ae | ||
|
54ec5904f0 | ||
|
1c73b7c269 | ||
|
07f25c7771 | ||
|
6cfc35f823 | ||
|
d21434c32a | ||
|
c36b0e2744 |
@ -90,9 +90,13 @@ If this feature requires new text, the text should be integrated into the code w
|
||||
- For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text.
|
||||
[Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice.
|
||||
- You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response.
|
||||
3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
3. Your locales should use the following format:
|
||||
- File names should be in `kebab-case`. Example: `trainer-names.json`
|
||||
- Key names should be in `camelCase`. Example: `aceTrainer`
|
||||
- If you make use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context), you need to use `snake_case` for the context key. Example: `aceTrainer_male`
|
||||
4. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
5. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
6. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
|
||||
[^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates).
|
||||
If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle.
|
||||
|
17
src/@types/typed-event-target.ts
Normal file
17
src/@types/typed-event-target.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Interface restricting the events emitted by an {@linkcode EventTarget} to a certain kind of {@linkcode Event}.
|
||||
* @typeParam T - The type to restrict the interface's access; must extend from {@linkcode Event}
|
||||
*/
|
||||
export interface TypedEventTarget<T extends Event = never> extends EventTarget {
|
||||
dispatchEvent(event: T): boolean;
|
||||
addEventListener(
|
||||
event: T["type"],
|
||||
callback: EventListenerOrEventListenerObject | null,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
removeEventListener(
|
||||
type: T["type"],
|
||||
callback: EventListenerOrEventListenerObject | null,
|
||||
options?: EventListenerOptions | boolean,
|
||||
): void;
|
||||
}
|
@ -72,6 +72,7 @@ import type { TrainerSlot } from "#enums/trainer-slot";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { TrainerVariant } from "#enums/trainer-variant";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import type { BattleSceneEvent } from "#events/battle-scene";
|
||||
import { NewArenaEvent } from "#events/battle-scene";
|
||||
import { Arena, ArenaBase } from "#field/arena";
|
||||
import { DamageNumberHandler } from "#field/damage-number-handler";
|
||||
@ -104,6 +105,7 @@ import {
|
||||
getLuckString,
|
||||
getLuckTextTint,
|
||||
getPartyLuckValue,
|
||||
type ModifierType,
|
||||
PokemonHeldItemModifierType,
|
||||
} from "#modifiers/modifier-type";
|
||||
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||
@ -126,6 +128,7 @@ import { vouchers } from "#system/voucher";
|
||||
import { trainerConfigs } from "#trainers/trainer-config";
|
||||
import type { HeldModifierConfig } from "#types/held-modifier-config";
|
||||
import type { Localizable } from "#types/locales";
|
||||
import type { TypedEventTarget } from "#types/typed-event-target";
|
||||
import { AbilityBar } from "#ui/ability-bar";
|
||||
import { ArenaFlyout } from "#ui/arena-flyout";
|
||||
import { CandyBar } from "#ui/candy-bar";
|
||||
@ -329,15 +332,9 @@ export class BattleScene extends SceneBase {
|
||||
public eventManager: TimedEventManager;
|
||||
|
||||
/**
|
||||
* Allows subscribers to listen for events
|
||||
*
|
||||
* Current Events:
|
||||
* - {@linkcode BattleSceneEventType.MOVE_USED} {@linkcode MoveUsedEvent}
|
||||
* - {@linkcode BattleSceneEventType.TURN_INIT} {@linkcode TurnInitEvent}
|
||||
* - {@linkcode BattleSceneEventType.TURN_END} {@linkcode TurnEndEvent}
|
||||
* - {@linkcode BattleSceneEventType.NEW_ARENA} {@linkcode NewArenaEvent}
|
||||
* Allows subscribers to listen for events.
|
||||
*/
|
||||
public readonly eventTarget: EventTarget = new EventTarget();
|
||||
public readonly eventTarget: TypedEventTarget<BattleSceneEvent> = new EventTarget();
|
||||
|
||||
constructor() {
|
||||
super("battle");
|
||||
@ -1203,7 +1200,9 @@ export class BattleScene extends SceneBase {
|
||||
this.updateScoreText();
|
||||
this.scoreText.setVisible(false);
|
||||
|
||||
[this.luckLabelText, this.luckText].map(t => t.setVisible(false));
|
||||
[this.luckLabelText, this.luckText].forEach(t => {
|
||||
t.setVisible(false);
|
||||
});
|
||||
|
||||
this.newArena(Overrides.STARTING_BIOME_OVERRIDE || BiomeId.TOWN);
|
||||
|
||||
@ -1237,8 +1236,7 @@ export class BattleScene extends SceneBase {
|
||||
Object.values(mp)
|
||||
.flat()
|
||||
.map(mt => mt.modifierType)
|
||||
.filter(mt => "localize" in mt)
|
||||
.map(lpb => lpb as unknown as Localizable),
|
||||
.filter((mt): mt is ModifierType & Localizable => "localize" in mt && typeof mt.localize === "function"),
|
||||
),
|
||||
];
|
||||
for (const item of localizable) {
|
||||
@ -1513,8 +1511,8 @@ export class BattleScene extends SceneBase {
|
||||
return this.currentBattle;
|
||||
}
|
||||
|
||||
newArena(biome: BiomeId, playerFaints?: number): Arena {
|
||||
this.arena = new Arena(biome, BiomeId[biome].toLowerCase(), playerFaints);
|
||||
newArena(biome: BiomeId, playerFaints = 0): Arena {
|
||||
this.arena = new Arena(biome, playerFaints);
|
||||
this.eventTarget.dispatchEvent(new NewArenaEvent());
|
||||
|
||||
this.arenaBg.pipelineData = {
|
||||
@ -2711,7 +2709,9 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
}
|
||||
|
||||
this.party.map(p => p.updateInfo(instant));
|
||||
this.party.forEach(p => {
|
||||
p.updateInfo(instant);
|
||||
});
|
||||
} else {
|
||||
const args = [this];
|
||||
if (modifier.shouldApply(...args)) {
|
||||
|
@ -44,7 +44,6 @@ import { BATTLE_STATS, EFFECTIVE_STATS, getStatKey, Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { BerryUsedEvent } from "#events/battle-scene";
|
||||
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#modifiers/modifier";
|
||||
import { BerryModifierType } from "#modifiers/modifier-type";
|
||||
@ -74,6 +73,7 @@ import {
|
||||
randSeedItem,
|
||||
toDmgValue,
|
||||
} from "#utils/common";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
|
||||
export class Ability implements Localizable {
|
||||
@ -109,13 +109,9 @@ export class Ability implements Localizable {
|
||||
}
|
||||
|
||||
localize(): void {
|
||||
const i18nKey = AbilityId[this.id]
|
||||
.split("_")
|
||||
.filter(f => f)
|
||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
||||
.join("") as string;
|
||||
const i18nKey = toCamelCase(AbilityId[this.id]);
|
||||
|
||||
this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`) as string}${this.nameAppend}` : "";
|
||||
this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`)}${this.nameAppend}` : "";
|
||||
this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : "";
|
||||
}
|
||||
|
||||
@ -4752,8 +4748,6 @@ export class CudChewConsumeBerryAbAttr extends AbAttr {
|
||||
// This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed.
|
||||
for (const berryType of pokemon.summonData.berriesEatenLast) {
|
||||
getBerryEffectFunc(berryType)(pokemon);
|
||||
const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1);
|
||||
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message
|
||||
}
|
||||
|
||||
// uncomment to make cheek pouch work with cud chew
|
||||
|
@ -1866,17 +1866,16 @@ interface PokemonPrevolutions {
|
||||
export const pokemonPrevolutions: PokemonPrevolutions = {};
|
||||
|
||||
export function initPokemonPrevolutions(): void {
|
||||
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ].map(sfk => sfk as string);
|
||||
const prevolutionKeys = Object.keys(pokemonEvolutions);
|
||||
prevolutionKeys.forEach(pk => {
|
||||
const evolutions = pokemonEvolutions[pk];
|
||||
// TODO: Why do we have empty strings in our array?
|
||||
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ];
|
||||
for (const [pk, evolutions] of Object.entries(pokemonEvolutions)) {
|
||||
for (const ev of evolutions) {
|
||||
if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) {
|
||||
continue;
|
||||
}
|
||||
pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { HitResult } from "#enums/hit-result";
|
||||
import { type BattleStat, Stat } from "#enums/stat";
|
||||
import { MovesetChangedEvent } from "#events/battle-scene";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { NumberHolder, randSeedInt, toDmgValue } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
@ -152,6 +153,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
|
||||
berryName: getBerryName(berryType),
|
||||
}),
|
||||
);
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(consumer.id, ppRestoreMove));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -66,7 +66,7 @@ import {
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { MoveUsedEvent } from "#events/battle-scene";
|
||||
import { MovesetChangedEvent } from "#events/battle-scene";
|
||||
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||
import {
|
||||
AttackTypeBoosterModifier,
|
||||
@ -90,7 +90,7 @@ import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindS
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
|
||||
@ -162,10 +162,16 @@ export abstract class Move implements Localizable {
|
||||
}
|
||||
|
||||
localize(): void {
|
||||
const i18nKey = MoveId[this.id].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string;
|
||||
const i18nKey = toCamelCase(MoveId[this.id])
|
||||
|
||||
this.name = this.id ? `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}` : "";
|
||||
this.effect = this.id ? `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}` : "";
|
||||
if (this.id === MoveId.NONE) {
|
||||
this.name = "";
|
||||
this.effect = ""
|
||||
return;
|
||||
}
|
||||
|
||||
this.name = `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}`;
|
||||
this.effect = `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -5926,8 +5932,8 @@ export class ProtectAttr extends AddBattlerTagAttr {
|
||||
for (const turnMove of user.getLastXMoves(-1).slice()) {
|
||||
if (
|
||||
// Quick & Wide guard increment the Protect counter without using it for fail chance
|
||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||
turnMove.result !== MoveResult.SUCCESS
|
||||
) {
|
||||
break;
|
||||
@ -7301,8 +7307,8 @@ export class ReducePpMoveAttr extends MoveEffectAttr {
|
||||
const lastPpUsed = movesetMove.ppUsed;
|
||||
movesetMove.ppUsed = Math.min(lastPpUsed + this.reduction, movesetMove.getMovePp());
|
||||
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(target.id, movesetMove.getMove(), movesetMove.ppUsed));
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed }));
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(target.id, movesetMove));
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -7406,11 +7412,13 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate summon data with a copy of the current moveset, replacing the copying move with the copied move
|
||||
// Populate summon data with a copy of the current moveset, replacing the copying move with the copied move.
|
||||
user.summonData.moveset = user.getMoveset().slice(0);
|
||||
user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id);
|
||||
const newMove = new PokemonMove(copiedMove.id);
|
||||
user.summonData.moveset[thisMoveIndex] = newMove;
|
||||
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedMove", { pokemonName: getPokemonNameWithAffix(user), moveName: copiedMove.name }));
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(user.id, newMove));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { WeatherType } from "#enums/weather-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
|
||||
import { type Constructor, coerceArray } from "#utils/common";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
|
||||
export abstract class SpeciesFormChangeTrigger {
|
||||
@ -143,11 +144,7 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge
|
||||
super();
|
||||
this.move = move;
|
||||
this.known = known;
|
||||
const moveKey = MoveId[this.move]
|
||||
.split("_")
|
||||
.filter(f => f)
|
||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
||||
.join("") as unknown as string;
|
||||
const moveKey = toCamelCase(MoveId[this.move]);
|
||||
this.description = known
|
||||
? i18next.t("pokemonEvolutions:Forms.moveLearned", {
|
||||
move: i18next.t(`move:${moveKey}.name`),
|
||||
|
@ -38,6 +38,7 @@ export enum UiMode {
|
||||
UNAVAILABLE,
|
||||
CHALLENGE_SELECT,
|
||||
RENAME_POKEMON,
|
||||
RENAME_RUN,
|
||||
RUN_HISTORY,
|
||||
RUN_INFO,
|
||||
TEST_DIALOGUE,
|
||||
|
@ -1,53 +1,65 @@
|
||||
import type { BerryModifier } from "#modifiers/modifier";
|
||||
import type { Move } from "#moves/move";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||
import type { PokemonSummonData } from "#data/pokemon-data";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
|
||||
/** Alias for all {@linkcode BattleScene} events */
|
||||
/** Enum comprising all {@linkcode BattleScene} events that can be emitted. */
|
||||
export enum BattleSceneEventType {
|
||||
/**
|
||||
* Triggers when the corresponding setting is changed
|
||||
* Emitted when the corresponding setting is changed
|
||||
* @see {@linkcode CandyUpgradeNotificationChangedEvent}
|
||||
*/
|
||||
CANDY_UPGRADE_NOTIFICATION_CHANGED = "onCandyUpgradeNotificationChanged",
|
||||
|
||||
/**
|
||||
* Triggers when a move is successfully used
|
||||
* @see {@linkcode MoveUsedEvent}
|
||||
* Emitted whenever a Pokemon's moveset is changed or altered - whether from moveset-overridding effects,
|
||||
* PP consumption or restoration.
|
||||
* @see {@linkcode MovesetChangedEvent}
|
||||
*/
|
||||
MOVE_USED = "onMoveUsed",
|
||||
/**
|
||||
* Triggers when a berry gets successfully used
|
||||
* @see {@linkcode BerryUsedEvent}
|
||||
*/
|
||||
BERRY_USED = "onBerryUsed",
|
||||
MOVESET_CHANGED = "onMovesetChanged",
|
||||
|
||||
/**
|
||||
* Triggers at the start of each new encounter
|
||||
* Emitted whenever the {@linkcode PokemonSummonData} of any {@linkcode Pokemon} is reset to its initial state
|
||||
* (such as immediately before a switch-out).
|
||||
* @see {@linkcode SummonDataResetEvent}
|
||||
*/
|
||||
SUMMON_DATA_RESET = "onSummonDataReset",
|
||||
|
||||
/**
|
||||
* Emitted at the start of each new encounter
|
||||
* @see {@linkcode EncounterPhaseEvent}
|
||||
*/
|
||||
ENCOUNTER_PHASE = "onEncounterPhase",
|
||||
/**
|
||||
* Triggers on the first turn of a new battle
|
||||
* @see {@linkcode TurnInitEvent}
|
||||
*/
|
||||
TURN_INIT = "onTurnInit",
|
||||
/**
|
||||
* Triggers after a turn ends in battle
|
||||
* Emitted after a turn ends in battle
|
||||
* @see {@linkcode TurnEndEvent}
|
||||
*/
|
||||
TURN_END = "onTurnEnd",
|
||||
|
||||
/**
|
||||
* Triggers when a new {@linkcode Arena} is created during initialization
|
||||
* Emitted when a new {@linkcode Arena} is created during initialization
|
||||
* @see {@linkcode NewArenaEvent}
|
||||
*/
|
||||
NEW_ARENA = "onNewArena",
|
||||
}
|
||||
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED} events
|
||||
* @extends Event
|
||||
* Abstract container class for all {@linkcode BattleSceneEventType} events.
|
||||
*/
|
||||
export class CandyUpgradeNotificationChangedEvent extends Event {
|
||||
abstract class BattleSceneEvent extends Event {
|
||||
public declare abstract readonly type: BattleSceneEventType; // that's a mouthful!
|
||||
// biome-ignore lint/complexity/noUselessConstructor: changes the type of the type field
|
||||
constructor(type: BattleSceneEventType) {
|
||||
super(type);
|
||||
}
|
||||
}
|
||||
|
||||
export type { BattleSceneEvent };
|
||||
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED} events
|
||||
*/
|
||||
export class CandyUpgradeNotificationChangedEvent extends BattleSceneEvent {
|
||||
declare type: BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED;
|
||||
/** The new value the setting was changed to */
|
||||
public newValue: number;
|
||||
constructor(newValue: number) {
|
||||
@ -58,61 +70,62 @@ export class CandyUpgradeNotificationChangedEvent extends Event {
|
||||
}
|
||||
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.MOVE_USED} events
|
||||
* @extends Event
|
||||
* Container class for {@linkcode BattleSceneEventType.MOVESET_CHANGED} events. \
|
||||
* Emitted whenever the moveset of any {@linkcode Pokemon} is changed, or a move's PP is increased or decreased.
|
||||
*/
|
||||
export class MoveUsedEvent extends Event {
|
||||
/** The ID of the {@linkcode Pokemon} that used the {@linkcode Move} */
|
||||
export class MovesetChangedEvent extends BattleSceneEvent {
|
||||
declare type: BattleSceneEventType.MOVESET_CHANGED;
|
||||
|
||||
/** The {@linkcode Pokemon.ID | ID} of the {@linkcode Pokemon} whose moveset has changed. */
|
||||
public pokemonId: number;
|
||||
/** The {@linkcode Move} used */
|
||||
public move: Move;
|
||||
/** The amount of PP used on the {@linkcode Move} this turn */
|
||||
public ppUsed: number;
|
||||
constructor(userId: number, move: Move, ppUsed: number) {
|
||||
super(BattleSceneEventType.MOVE_USED);
|
||||
/**
|
||||
* The {@linkcode PokemonMove} having been changed.
|
||||
* Will override the corresponding slot of the moveset flyout for that Pokemon.
|
||||
*/
|
||||
public move: PokemonMove;
|
||||
|
||||
this.pokemonId = userId;
|
||||
constructor(pokemonId: number, move: PokemonMove) {
|
||||
super(BattleSceneEventType.MOVESET_CHANGED);
|
||||
|
||||
this.pokemonId = pokemonId;
|
||||
this.move = move;
|
||||
this.ppUsed = ppUsed;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.BERRY_USED} events
|
||||
* @extends Event
|
||||
*/
|
||||
export class BerryUsedEvent extends Event {
|
||||
/** The {@linkcode BerryModifier} being used */
|
||||
public berryModifier: BerryModifier;
|
||||
constructor(berry: BerryModifier) {
|
||||
super(BattleSceneEventType.BERRY_USED);
|
||||
|
||||
this.berryModifier = berry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.ENCOUNTER_PHASE} events
|
||||
* @extends Event
|
||||
* Container class for {@linkcode BattleSceneEventType.SUMMON_DATA_RESET} events. \
|
||||
* Emitted whenever the {@linkcode PokemonSummonData} of any {@linkcode Pokemon} is reset to its initial state
|
||||
* (such as immediately before a switch-out).
|
||||
*/
|
||||
export class EncounterPhaseEvent extends Event {
|
||||
export class SummonDataResetEvent extends BattleSceneEvent {
|
||||
declare type: BattleSceneEventType.SUMMON_DATA_RESET;
|
||||
|
||||
/** The {@linkcode Pokemon.ID | ID} of the {@linkcode Pokemon} whose data has been reset. */
|
||||
public pokemonId: number;
|
||||
|
||||
constructor(pokemonId: number) {
|
||||
super(BattleSceneEventType.SUMMON_DATA_RESET);
|
||||
|
||||
this.pokemonId = pokemonId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.ENCOUNTER_PHASE} events.
|
||||
*/
|
||||
export class EncounterPhaseEvent extends BattleSceneEvent {
|
||||
declare type: BattleSceneEventType.ENCOUNTER_PHASE;
|
||||
constructor() {
|
||||
super(BattleSceneEventType.ENCOUNTER_PHASE);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.TURN_INIT} events
|
||||
* @extends Event
|
||||
*/
|
||||
export class TurnInitEvent extends Event {
|
||||
constructor() {
|
||||
super(BattleSceneEventType.TURN_INIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.TURN_END} events
|
||||
* @extends Event
|
||||
*/
|
||||
export class TurnEndEvent extends Event {
|
||||
export class TurnEndEvent extends BattleSceneEvent {
|
||||
declare type: BattleSceneEventType.TURN_END;
|
||||
/** The amount of turns in the current battle */
|
||||
public turnCount: number;
|
||||
constructor(turnCount: number) {
|
||||
@ -123,9 +136,9 @@ export class TurnEndEvent extends Event {
|
||||
}
|
||||
/**
|
||||
* Container class for {@linkcode BattleSceneEventType.NEW_ARENA} events
|
||||
* @extends Event
|
||||
*/
|
||||
export class NewArenaEvent extends Event {
|
||||
export class NewArenaEvent extends BattleSceneEvent {
|
||||
declare type: BattleSceneEventType.NEW_ARENA;
|
||||
constructor() {
|
||||
super(BattleSceneEventType.NEW_ARENA);
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export class Arena {
|
||||
public bgm: string;
|
||||
public ignoreAbilities: boolean;
|
||||
public ignoringEffectSource: BattlerIndex | null;
|
||||
public playerTerasUsed: number;
|
||||
public playerTerasUsed = 0;
|
||||
/**
|
||||
* Saves the number of times a party pokemon faints during a arena encounter.
|
||||
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
|
||||
@ -68,12 +68,11 @@ export class Arena {
|
||||
|
||||
public readonly eventTarget: EventTarget = new EventTarget();
|
||||
|
||||
constructor(biome: BiomeId, bgm: string, playerFaints = 0) {
|
||||
constructor(biome: BiomeId, playerFaints = 0) {
|
||||
this.biomeType = biome;
|
||||
this.bgm = bgm;
|
||||
this.bgm = BiomeId[biome].toLowerCase();
|
||||
this.trainerPool = biomeTrainerPools[biome];
|
||||
this.updatePoolsForTimeOfDay();
|
||||
this.playerTerasUsed = 0;
|
||||
this.playerFaints = playerFaints;
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,7 @@ import { SwitchType } from "#enums/switch-type";
|
||||
import type { TrainerSlot } from "#enums/trainer-slot";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { MovesetChangedEvent, SummonDataResetEvent } from "#events/battle-scene";
|
||||
import { doShinySparkleAnim } from "#field/anims";
|
||||
import {
|
||||
BaseStatModifier,
|
||||
@ -2823,6 +2824,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (this.summonData.moveset) {
|
||||
this.summonData.moveset[moveIndex] = move;
|
||||
}
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(this.id, move));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -5080,6 +5082,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.summonData.speciesForm = null;
|
||||
this.updateFusionPalette();
|
||||
}
|
||||
|
||||
// Emit an event to reset all temporary moveset overrides due to Mimic/Transform wearing off.
|
||||
globalScene.eventTarget.dispatchEvent(new SummonDataResetEvent(this.id));
|
||||
|
||||
this.summonData = new PokemonSummonData();
|
||||
this.tempSummonData = new PokemonTempSummonData();
|
||||
this.summonData.illusion = illusion;
|
||||
|
@ -447,7 +447,9 @@ export class LoadingScene extends SceneBase {
|
||||
);
|
||||
|
||||
if (!mobile) {
|
||||
loadingGraphics.map(g => g.setVisible(false));
|
||||
loadingGraphics.forEach(g => {
|
||||
g.setVisible(false);
|
||||
});
|
||||
}
|
||||
|
||||
const intro = this.add.video(0, 0);
|
||||
|
@ -121,8 +121,8 @@ export class ModifierBar extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
updateModifierOverflowVisibility(ignoreLimit: boolean) {
|
||||
const modifierIcons = this.getAll().reverse();
|
||||
for (const modifier of modifierIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) {
|
||||
const modifierIcons = this.getAll().reverse() as Phaser.GameObjects.Container[];
|
||||
for (const modifier of modifierIcons.slice(iconOverflowIndex)) {
|
||||
modifier.setVisible(ignoreLimit);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { BerryUsedEvent } from "#events/battle-scene";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { BerryModifier } from "#modifiers/modifier";
|
||||
import { FieldPhase } from "#phases/field-phase";
|
||||
@ -65,7 +64,6 @@ export class BerryPhase extends FieldPhase {
|
||||
berryModifier.consumed = false;
|
||||
pokemon.loseHeldItem(berryModifier);
|
||||
}
|
||||
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier));
|
||||
}
|
||||
globalScene.updateModifiers(pokemon.isPlayer());
|
||||
|
||||
|
@ -18,7 +18,7 @@ import { MoveResult } from "#enums/move-result";
|
||||
import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { MoveUsedEvent } from "#events/battle-scene";
|
||||
import { MovesetChangedEvent } from "#events/battle-scene";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { frenzyMissFunc } from "#moves/move-utils";
|
||||
@ -317,7 +317,7 @@ export class MovePhase extends BattlePhase {
|
||||
// "commit" to using the move, deducting PP.
|
||||
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
||||
this.move.usePp(ppUsed);
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon.id, move, this.move.ppUsed));
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(this.pokemon.id, this.move));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -620,10 +620,10 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
if (this.failed) {
|
||||
// TODO: should this consider struggle?
|
||||
// TODO: should this consider struggle and pressure?
|
||||
const ppUsed = isIgnorePP(this.useMode) ? 0 : 1;
|
||||
this.move.usePp(ppUsed);
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(this.pokemon.id, this.move));
|
||||
}
|
||||
|
||||
if (this.cancelled && this.pokemon.summonData.tags.some(t => t.tagType === BattlerTagType.FRENZY)) {
|
||||
|
@ -4,6 +4,7 @@ import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
|
||||
import { MovesetChangedEvent } from "#events/battle-scene";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
import i18next from "i18next";
|
||||
@ -50,12 +51,14 @@ export class PokemonTransformPhase extends PokemonPhase {
|
||||
user.setStatStage(s, target.getStatStage(s));
|
||||
}
|
||||
|
||||
user.summonData.moveset = target.getMoveset().map(m => {
|
||||
if (m) {
|
||||
user.summonData.moveset = target.getMoveset().map(oldMove => {
|
||||
if (oldMove) {
|
||||
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
|
||||
return new PokemonMove(m.moveId, 0, 0, Math.min(m.getMove().pp, 5));
|
||||
const newMove = new PokemonMove(oldMove.moveId, 0, 0, Math.min(oldMove.getMove().pp, 5));
|
||||
this.emitMovesetChange(oldMove, newMove);
|
||||
return newMove;
|
||||
}
|
||||
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
|
||||
console.warn(`Transform: somehow iterating over a ${oldMove} value when copying moveset!`);
|
||||
return new PokemonMove(MoveId.NONE);
|
||||
});
|
||||
|
||||
@ -86,4 +89,21 @@ export class PokemonTransformPhase extends PokemonPhase {
|
||||
|
||||
Promise.allSettled(promises).then(() => this.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event upon transforming and changing movesets.
|
||||
* @param origMovee - The target's original {@linkcode PokemonMove} being copied
|
||||
* @param copiedMove - The new {@linkcode PokemonMove} being added to the user's moveset
|
||||
*/
|
||||
private emitMovesetChange(origMove: PokemonMove, copiedMove: PokemonMove): void {
|
||||
const user = this.getPokemon();
|
||||
const target = globalScene.getField()[this.targetIndex];
|
||||
|
||||
// Dispatch an event for the user's moveset temporarily changing.
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(user.id, copiedMove));
|
||||
// If the user is a player having transformed into an enemy, permanently reveal the corresponding move in their moveset.
|
||||
if (user.isPlayer() && target.isEnemy()) {
|
||||
globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(target.id, origMove));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { TurnInitEvent } from "#events/battle-scene";
|
||||
import type { PlayerPokemon } from "#field/pokemon";
|
||||
import {
|
||||
handleMysteryEncounterBattleStartEffects,
|
||||
@ -46,8 +45,6 @@ export class TurnInitPhase extends FieldPhase {
|
||||
}
|
||||
});
|
||||
|
||||
globalScene.eventTarget.dispatchEvent(new TurnInitEvent());
|
||||
|
||||
handleMysteryEncounterBattleStartEffects();
|
||||
|
||||
// If true, will skip remainder of current phase (and not queue CommandPhases etc.)
|
||||
|
@ -127,6 +127,7 @@ export interface SessionSaveData {
|
||||
battleType: BattleType;
|
||||
trainer: TrainerData;
|
||||
gameVersion: string;
|
||||
runNameText: string;
|
||||
timestamp: number;
|
||||
challenges: ChallengeData[];
|
||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
||||
@ -206,10 +207,12 @@ export interface StarterData {
|
||||
[key: number]: StarterDataEntry;
|
||||
}
|
||||
|
||||
export interface TutorialFlags {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
// TODO: Rework into a bitmask
|
||||
export type TutorialFlags = {
|
||||
[key in Tutorial]: boolean;
|
||||
};
|
||||
|
||||
// TODO: Rework into a bitmask
|
||||
export interface SeenDialogues {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
@ -822,52 +825,51 @@ export class GameData {
|
||||
return true; // TODO: is `true` the correct return value?
|
||||
}
|
||||
|
||||
private loadGamepadSettings(): boolean {
|
||||
Object.values(SettingGamepad)
|
||||
.map(setting => setting as SettingGamepad)
|
||||
.forEach(setting => setSettingGamepad(setting, settingGamepadDefaults[setting]));
|
||||
private loadGamepadSettings(): void {
|
||||
Object.values(SettingGamepad).forEach(setting => {
|
||||
setSettingGamepad(setting, settingGamepadDefaults[setting]);
|
||||
});
|
||||
|
||||
if (!localStorage.hasOwnProperty("settingsGamepad")) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")!); // TODO: is this bang correct?
|
||||
|
||||
for (const setting of Object.keys(settingsGamepad)) {
|
||||
setSettingGamepad(setting as SettingGamepad, settingsGamepad[setting]);
|
||||
}
|
||||
|
||||
return true; // TODO: is `true` the correct return value?
|
||||
}
|
||||
|
||||
public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean {
|
||||
const key = getDataTypeKey(GameDataType.TUTORIALS);
|
||||
let tutorials: object = {};
|
||||
if (localStorage.hasOwnProperty(key)) {
|
||||
tutorials = JSON.parse(localStorage.getItem(key)!); // TODO: is this bang correct?
|
||||
/**
|
||||
* Save the specified tutorial as having the specified completion status.
|
||||
* @param tutorial - The {@linkcode Tutorial} whose completion status is being saved
|
||||
* @param status - The completion status to set
|
||||
*/
|
||||
public saveTutorialFlag(tutorial: Tutorial, status: boolean): void {
|
||||
// Grab the prior save data tutorial
|
||||
const saveDataKey = getDataTypeKey(GameDataType.TUTORIALS);
|
||||
const tutorials: TutorialFlags = localStorage.hasOwnProperty(saveDataKey)
|
||||
? JSON.parse(localStorage.getItem(saveDataKey)!)
|
||||
: {};
|
||||
|
||||
// TODO: We shouldn't be storing this like that
|
||||
for (const key of Object.values(Tutorial)) {
|
||||
if (key === tutorial) {
|
||||
tutorials[key] = status;
|
||||
} else {
|
||||
tutorials[key] ??= false;
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(Tutorial)
|
||||
.map(t => t as Tutorial)
|
||||
.forEach(t => {
|
||||
const key = Tutorial[t];
|
||||
if (key === tutorial) {
|
||||
tutorials[key] = flag;
|
||||
} else {
|
||||
tutorials[key] ??= false;
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(tutorials));
|
||||
|
||||
return true;
|
||||
localStorage.setItem(saveDataKey, JSON.stringify(tutorials));
|
||||
}
|
||||
|
||||
public getTutorialFlags(): TutorialFlags {
|
||||
const key = getDataTypeKey(GameDataType.TUTORIALS);
|
||||
const ret: TutorialFlags = {};
|
||||
Object.values(Tutorial)
|
||||
.map(tutorial => tutorial as Tutorial)
|
||||
.forEach(tutorial => (ret[Tutorial[tutorial]] = false));
|
||||
const ret: TutorialFlags = Object.values(Tutorial).reduce((acc, tutorial) => {
|
||||
acc[Tutorial[tutorial]] = false;
|
||||
return acc;
|
||||
}, {} as TutorialFlags);
|
||||
|
||||
if (!localStorage.hasOwnProperty(key)) {
|
||||
return ret;
|
||||
@ -979,6 +981,54 @@ export class GameData {
|
||||
});
|
||||
}
|
||||
|
||||
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
if (slotId < 0) {
|
||||
return resolve(false);
|
||||
}
|
||||
const sessionData: SessionSaveData | null = await this.getSession(slotId);
|
||||
|
||||
if (!sessionData) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
if (newName === "") {
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
sessionData.runNameText = newName;
|
||||
const updatedDataStr = JSON.stringify(sessionData);
|
||||
const encrypted = encrypt(updatedDataStr, bypassLogin);
|
||||
const secretId = this.secretId;
|
||||
const trainerId = this.trainerId;
|
||||
|
||||
if (bypassLogin) {
|
||||
localStorage.setItem(
|
||||
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
||||
encrypt(updatedDataStr, bypassLogin),
|
||||
);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
pokerogueApi.savedata.session
|
||||
.update({ slot: slotId, trainerId, secretId, clientSessionId }, encrypted)
|
||||
.then(error => {
|
||||
if (error) {
|
||||
console.error("Failed to update session name:", error);
|
||||
resolve(false);
|
||||
} else {
|
||||
localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted);
|
||||
updateUserInfo().then(success => {
|
||||
if (success !== null && !success) {
|
||||
return resolve(false);
|
||||
}
|
||||
});
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
@ -1,33 +1,41 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import type { BerryUsedEvent, MoveUsedEvent } from "#events/battle-scene";
|
||||
import type { MovesetChangedEvent, SummonDataResetEvent } from "#events/battle-scene";
|
||||
import { BattleSceneEventType } from "#events/battle-scene";
|
||||
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||
import type { Move } from "#moves/move";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||
import type { BattleInfo } from "#ui/battle-info";
|
||||
import { addTextObject } from "#ui/text";
|
||||
import { fixedInt } from "#utils/common";
|
||||
|
||||
/** Container for info about a {@linkcode Move} */
|
||||
/** Container for info about a given {@linkcode PokemonMove} having been used */
|
||||
interface MoveInfo {
|
||||
/** The {@linkcode Move} itself */
|
||||
move: Move;
|
||||
|
||||
/** The maximum PP of the {@linkcode Move} */
|
||||
maxPp: number;
|
||||
/** The amount of PP used by the {@linkcode Move} */
|
||||
ppUsed: number;
|
||||
/** The name of the {@linkcode Move} having been used. */
|
||||
name: string;
|
||||
/** The {@linkcode PokemonMove} having been used. */
|
||||
move: PokemonMove;
|
||||
}
|
||||
|
||||
/** A Flyout Menu attached to each {@linkcode BattleInfo} object on the field UI */
|
||||
/**
|
||||
* A 4-length tuple consisting of all moves that each {@linkcode Pokemon} has used in the given battle.
|
||||
* Entries that are `undefined` indicate moves which have not been used yet.
|
||||
*/
|
||||
type MoveInfoTuple = [MoveInfo?, MoveInfo?, MoveInfo?, MoveInfo?];
|
||||
|
||||
/**
|
||||
* A Flyout Menu attached to each Pokemon's {@linkcode BattleInfo} object,
|
||||
* showing all revealed moves and their current PP counts.
|
||||
* @todo Stop tracking player move usages
|
||||
*/
|
||||
export class BattleFlyout extends Phaser.GameObjects.Container {
|
||||
/** Is this object linked to a player's Pokemon? */
|
||||
private player: boolean;
|
||||
|
||||
/** The Pokemon this object is linked to */
|
||||
/** The Pokemon this object is linked to. */
|
||||
private pokemon: Pokemon;
|
||||
|
||||
/** The restricted width of the flyout which should be drawn to */
|
||||
@ -52,15 +60,22 @@ export class BattleFlyout extends Phaser.GameObjects.Container {
|
||||
|
||||
/** The array of {@linkcode Phaser.GameObjects.Text} objects which are drawn on the flyout */
|
||||
private flyoutText: Phaser.GameObjects.Text[] = new Array(4);
|
||||
/** The array of {@linkcode MoveInfo} used to track moves for the {@linkcode Pokemon} linked to the flyout */
|
||||
private moveInfo: MoveInfo[] = [];
|
||||
/** An array of {@linkcode MoveInfo}s used to track moves for the {@linkcode Pokemon} linked to the flyout. */
|
||||
private moveInfo: MoveInfoTuple = [];
|
||||
/**
|
||||
* An array of {@linkcode MoveInfo}s used to track move slots
|
||||
* temporarily overridden by {@linkcode MoveId.TRANSFORM} or {@linkcode MoveId.MIMIC}.
|
||||
*
|
||||
* Reset once {@linkcode pokemon} switches out via a {@linkcode SummonDataResetEvent}.
|
||||
*/
|
||||
private tempMoveInfo: MoveInfoTuple = [];
|
||||
|
||||
/** Current state of the flyout's visibility */
|
||||
public flyoutVisible = false;
|
||||
|
||||
// Stores callbacks in a variable so they can be unsubscribed from when destroyed
|
||||
private readonly onMoveUsedEvent = (event: Event) => this.onMoveUsed(event);
|
||||
private readonly onBerryUsedEvent = (event: Event) => this.onBerryUsed(event);
|
||||
private readonly onMovesetChangedEvent = (event: MovesetChangedEvent) => this.onMovesetChanged(event);
|
||||
private readonly onSummonDataResetEvent = (event: SummonDataResetEvent) => this.onSummonDataReset(event);
|
||||
|
||||
constructor(player: boolean) {
|
||||
super(globalScene, 0, 0);
|
||||
@ -124,79 +139,90 @@ export class BattleFlyout extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given {@linkcode Pokemon} and subscribes to the {@linkcode BattleSceneEventType.MOVE_USED} event
|
||||
* @param pokemon {@linkcode Pokemon} to link to this flyout
|
||||
* Link the given {@linkcode Pokemon} to this flyout and subscribe to the {@linkcode BattleSceneEventType.MOVESET_CHANGED} event.
|
||||
* @param pokemon - The {@linkcode Pokemon} to link to this flyout
|
||||
*/
|
||||
initInfo(pokemon: EnemyPokemon) {
|
||||
public initInfo(pokemon: Pokemon): void {
|
||||
this.pokemon = pokemon;
|
||||
|
||||
this.name = `Flyout ${getPokemonNameWithAffix(this.pokemon)}`;
|
||||
this.flyoutParent.name = `Flyout Parent ${getPokemonNameWithAffix(this.pokemon)}`;
|
||||
|
||||
globalScene.eventTarget.addEventListener(BattleSceneEventType.MOVE_USED, this.onMoveUsedEvent);
|
||||
globalScene.eventTarget.addEventListener(BattleSceneEventType.BERRY_USED, this.onBerryUsedEvent);
|
||||
globalScene.eventTarget.addEventListener(BattleSceneEventType.MOVESET_CHANGED, this.onMovesetChangedEvent);
|
||||
globalScene.eventTarget.addEventListener(BattleSceneEventType.SUMMON_DATA_RESET, this.onSummonDataResetEvent);
|
||||
}
|
||||
|
||||
/** Sets and formats the text property for all {@linkcode Phaser.GameObjects.Text} in the flyoutText array */
|
||||
private setText() {
|
||||
for (let i = 0; i < this.flyoutText.length; i++) {
|
||||
const flyoutText = this.flyoutText[i];
|
||||
const moveInfo = this.moveInfo[i];
|
||||
|
||||
if (!moveInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPp = moveInfo.maxPp - moveInfo.ppUsed;
|
||||
flyoutText.text = `${moveInfo.move.name} ${currentPp}/${moveInfo.maxPp}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates all of the {@linkcode MoveInfo} objects in the moveInfo array */
|
||||
private onMoveUsed(event: Event) {
|
||||
const moveUsedEvent = event as MoveUsedEvent;
|
||||
if (!moveUsedEvent || moveUsedEvent.pokemonId !== this.pokemon?.id || moveUsedEvent.move.id === MoveId.STRUGGLE) {
|
||||
// Ignore Struggle
|
||||
/**
|
||||
* Set and formats the text property for all {@linkcode Phaser.GameObjects.Text} in the flyoutText array.
|
||||
* @param index - The 0-indexed position of the flyout text object to update
|
||||
*/
|
||||
private updateText(index: number): void {
|
||||
// Use temp move info if present, or else the regular move info.
|
||||
const moveInfo = this.tempMoveInfo[index] ?? this.moveInfo[index];
|
||||
if (!moveInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foundInfo = this.moveInfo.find(x => x?.move.id === moveUsedEvent.move.id);
|
||||
if (foundInfo) {
|
||||
foundInfo.ppUsed = moveUsedEvent.ppUsed;
|
||||
} else {
|
||||
this.moveInfo.push({
|
||||
move: moveUsedEvent.move,
|
||||
maxPp: moveUsedEvent.move.pp,
|
||||
ppUsed: moveUsedEvent.ppUsed,
|
||||
});
|
||||
}
|
||||
|
||||
this.setText();
|
||||
const flyoutText = this.flyoutText[index];
|
||||
const maxPP = moveInfo.move.getMovePp();
|
||||
const currentPp = -moveInfo.move.ppUsed;
|
||||
flyoutText.text = `${moveInfo.name} ${currentPp}/${maxPP}`;
|
||||
}
|
||||
|
||||
private onBerryUsed(event: Event) {
|
||||
const berryUsedEvent = event as BerryUsedEvent;
|
||||
/**
|
||||
* Update the corresponding {@linkcode MoveInfo} object in the moveInfo array.
|
||||
* @param event - The {@linkcode MovesetChangedEvent} having been emitted
|
||||
*/
|
||||
private onMovesetChanged(event: MovesetChangedEvent): void {
|
||||
// Ignore other Pokemon's moves as well as Struggle and MoveId.NONE
|
||||
if (
|
||||
!berryUsedEvent ||
|
||||
berryUsedEvent.berryModifier.pokemonId !== this.pokemon?.id ||
|
||||
berryUsedEvent.berryModifier.berryType !== BerryType.LEPPA
|
||||
event.pokemonId !== this.pokemon.id ||
|
||||
event.move.moveId === MoveId.NONE ||
|
||||
event.move.moveId === MoveId.STRUGGLE
|
||||
) {
|
||||
// We only care about Leppa berries
|
||||
return;
|
||||
}
|
||||
|
||||
const foundInfo = this.moveInfo.find(info => info.ppUsed === info.maxPp);
|
||||
if (!foundInfo) {
|
||||
// This will only happen on a de-sync of PP tracking
|
||||
return;
|
||||
}
|
||||
foundInfo.ppUsed = Math.max(foundInfo.ppUsed - 10, 0);
|
||||
// Push to either the temporary or permanent move arrays, depending on which array the move was found in.
|
||||
const isPermanent = this.pokemon.getMoveset(true).includes(event.move);
|
||||
const infoArray = isPermanent ? this.moveInfo : this.tempMoveInfo;
|
||||
|
||||
this.setText();
|
||||
const index = this.pokemon.getMoveset(isPermanent).indexOf(event.move);
|
||||
if (index === -1) {
|
||||
throw new Error("Updated move passed to move flyout was not found in moveset!");
|
||||
}
|
||||
|
||||
// Update the corresponding slot in the info array with either a new entry or an updated PP reading.
|
||||
if (infoArray[index]) {
|
||||
infoArray[index].move = event.move;
|
||||
} else {
|
||||
infoArray[index] = {
|
||||
name: event.move.getMove().name,
|
||||
move: event.move,
|
||||
};
|
||||
}
|
||||
|
||||
this.updateText(index);
|
||||
}
|
||||
|
||||
/** Animates the flyout to either show or hide it by applying a fade and translation */
|
||||
toggleFlyout(visible: boolean): void {
|
||||
/**
|
||||
* Reset the linked Pokemon's temporary moveset override when it is switched out.
|
||||
* @param event - The {@linkcode SummonDataResetEvent} having been emitted
|
||||
*/
|
||||
private onSummonDataReset(event: SummonDataResetEvent): void {
|
||||
if (event.pokemonId !== this.pokemon.id) {
|
||||
// Wrong pokemon
|
||||
return;
|
||||
}
|
||||
|
||||
this.tempMoveInfo = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the flyout to either show or hide the modal.
|
||||
* @param visible - Whether the the flyout should be shown
|
||||
*/
|
||||
public toggleFlyout(visible: boolean): void {
|
||||
this.flyoutVisible = visible;
|
||||
|
||||
globalScene.tweens.add({
|
||||
@ -208,9 +234,10 @@ export class BattleFlyout extends Phaser.GameObjects.Container {
|
||||
});
|
||||
}
|
||||
|
||||
destroy(fromScene?: boolean): void {
|
||||
globalScene.eventTarget.removeEventListener(BattleSceneEventType.MOVE_USED, this.onMoveUsedEvent);
|
||||
globalScene.eventTarget.removeEventListener(BattleSceneEventType.BERRY_USED, this.onBerryUsedEvent);
|
||||
/** Destroy this element and remove all associated listeners. */
|
||||
public destroy(fromScene?: boolean): void {
|
||||
globalScene.eventTarget.removeEventListener(BattleSceneEventType.MOVESET_CHANGED, this.onMovesetChangedEvent);
|
||||
globalScene.eventTarget.removeEventListener(BattleSceneEventType.SUMMON_DATA_RESET, this.onSummonDataResetEvent);
|
||||
|
||||
super.destroy(fromScene);
|
||||
}
|
||||
|
54
src/ui/rename-run-ui-handler.ts
Normal file
54
src/ui/rename-run-ui-handler.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import i18next from "i18next";
|
||||
import type { InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import type { ModalConfig } from "./modal-ui-handler";
|
||||
|
||||
export class RenameRunFormUiHandler extends FormModalUiHandler {
|
||||
getModalTitle(_config?: ModalConfig): string {
|
||||
return i18next.t("menu:renamerun");
|
||||
}
|
||||
|
||||
getWidth(_config?: ModalConfig): number {
|
||||
return 160;
|
||||
}
|
||||
|
||||
getMargin(_config?: ModalConfig): [number, number, number, number] {
|
||||
return [0, 0, 48, 0];
|
||||
}
|
||||
|
||||
getButtonLabels(_config?: ModalConfig): string[] {
|
||||
return [i18next.t("menu:rename"), i18next.t("menu:cancel")];
|
||||
}
|
||||
|
||||
getReadableErrorMessage(error: string): string {
|
||||
const colonIndex = error?.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
error = error.slice(0, colonIndex);
|
||||
}
|
||||
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
return [{ label: i18next.t("menu:runName") }];
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
if (!super.show(args)) {
|
||||
return false;
|
||||
}
|
||||
if (this.inputs?.length) {
|
||||
this.inputs.forEach(input => {
|
||||
input.text = "";
|
||||
});
|
||||
}
|
||||
const config = args[0] as ModalConfig;
|
||||
this.submitAction = _ => {
|
||||
this.sanitizeInputs();
|
||||
const sanitizedName = btoa(encodeURIComponent(this.inputs[0].text));
|
||||
config.buttonActions[0](sanitizedName);
|
||||
return true;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text";
|
||||
import { UiHandler } from "#ui/ui-handler";
|
||||
import { addWindow } from "#ui/ui-theme";
|
||||
import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle";
|
||||
|
||||
@ -207,6 +208,10 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
headerText.setOrigin(0, 0);
|
||||
headerText.setPositionRelative(headerBg, 8, 4);
|
||||
this.runContainer.add(headerText);
|
||||
const runName = addTextObject(0, 0, this.runInfo.runNameText, TextStyle.WINDOW);
|
||||
runName.setOrigin(0, 0);
|
||||
runName.setPositionRelative(headerBg, 60, 4);
|
||||
this.runContainer.add(runName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -702,10 +707,7 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
rules.push(i18next.t("challenges:inverseBattle.shortName"));
|
||||
break;
|
||||
default: {
|
||||
const localizationKey = Challenges[this.runInfo.challenges[i].id]
|
||||
.split("_")
|
||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
||||
.join("");
|
||||
const localizationKey = toCamelCase(Challenges[this.runInfo.challenges[i].id]);
|
||||
rules.push(i18next.t(`challenges:${localizationKey}.name`));
|
||||
break;
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
||||
import * as Modifier from "#modifiers/modifier";
|
||||
import type { SessionSaveData } from "#system/game-data";
|
||||
import type { PokemonData } from "#system/pokemon-data";
|
||||
import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler";
|
||||
import { MessageUiHandler } from "#ui/message-ui-handler";
|
||||
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
||||
import { addTextObject } from "#ui/text";
|
||||
@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro
|
||||
import i18next from "i18next";
|
||||
|
||||
const SESSION_SLOTS_COUNT = 5;
|
||||
const SLOTS_ON_SCREEN = 3;
|
||||
const SLOTS_ON_SCREEN = 2;
|
||||
|
||||
export enum SaveSlotUiMode {
|
||||
LOAD,
|
||||
@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
private uiMode: SaveSlotUiMode;
|
||||
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
||||
protected manageDataConfig: OptionSelectConfig;
|
||||
|
||||
private scrollCursor = 0;
|
||||
|
||||
@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
const manageDataOptions: any[] = [];
|
||||
|
||||
let success = false;
|
||||
let error = false;
|
||||
@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
if (button === Button.ACTION) {
|
||||
const cursor = this.cursor + this.scrollCursor;
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !this.sessionSlots[cursor].hasData) {
|
||||
const sessionSlot = this.sessionSlots[cursor];
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !sessionSlot.hasData) {
|
||||
error = true;
|
||||
} else {
|
||||
switch (this.uiMode) {
|
||||
case SaveSlotUiMode.LOAD:
|
||||
this.saveSlotSelectCallback = null;
|
||||
originalCallback?.(cursor);
|
||||
if (!sessionSlot.malformed) {
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menu:loadGame"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
originalCallback?.(cursor);
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:renameRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.setOverlayMode(
|
||||
UiMode.RENAME_RUN,
|
||||
{
|
||||
buttonActions: [
|
||||
(sanitizedName: string) => {
|
||||
const name = decodeURIComponent(atob(sanitizedName));
|
||||
globalScene.gameData.renameSession(cursor, name).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
},
|
||||
],
|
||||
},
|
||||
"",
|
||||
);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.manageDataConfig = {
|
||||
xOffset: 0,
|
||||
yOffset: 48,
|
||||
options: manageDataOptions,
|
||||
maxOptions: 4,
|
||||
};
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:deleteRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.showText(i18next.t("saveSlotSelectUiHandler:deleteData"), null, () => {
|
||||
ui.setOverlayMode(
|
||||
UiMode.CONFIRM,
|
||||
() => {
|
||||
globalScene.gameData.tryClearSession(cursor).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
},
|
||||
false,
|
||||
0,
|
||||
19,
|
||||
import.meta.env.DEV ? 300 : 2000,
|
||||
);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menuUiHandler:cancel"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
return true;
|
||||
},
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
ui.setOverlayMode(UiMode.MENU_OPTION_SELECT, this.manageDataConfig);
|
||||
break;
|
||||
|
||||
case SaveSlotUiMode.SAVE: {
|
||||
const saveAndCallback = () => {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
} else {
|
||||
this.saveSlotSelectCallback = null;
|
||||
ui.showText("", 0);
|
||||
originalCallback?.(-1);
|
||||
success = true;
|
||||
}
|
||||
@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.cursorObj = globalScene.add.container(0, 0);
|
||||
const cursorBox = globalScene.add.nineslice(
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
"select_cursor_highlight_thick",
|
||||
undefined,
|
||||
296,
|
||||
44,
|
||||
294,
|
||||
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
);
|
||||
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
||||
rightArrow.setPosition(160, 0);
|
||||
rightArrow.setPosition(160, 15);
|
||||
rightArrow.setName("rightArrow");
|
||||
this.cursorObj.add([cursorBox, rightArrow]);
|
||||
this.sessionSlotsContainer.add(this.cursorObj);
|
||||
}
|
||||
const cursorPosition = cursor + this.scrollCursor;
|
||||
const cursorIncrement = cursorPosition * 56;
|
||||
const cursorIncrement = cursorPosition * 76;
|
||||
if (this.sessionSlots[cursorPosition] && this.cursorObj) {
|
||||
const hasData = this.sessionSlots[cursorPosition].hasData;
|
||||
const session = this.sessionSlots[cursorPosition];
|
||||
const hasData = session.hasData && !session.malformed;
|
||||
// If the session slot lacks session data, it does not move from its default, central position.
|
||||
// Only session slots with session data will move leftwards and have a visible arrow.
|
||||
if (!hasData) {
|
||||
this.cursorObj.setPosition(151, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(151, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
||||
} else {
|
||||
this.cursorObj.setPosition(145, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(145, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
||||
}
|
||||
this.setArrowVisibility(hasData);
|
||||
@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
revertSessionSlot(slotIndex: number): void {
|
||||
const sessionSlot = this.sessionSlots[slotIndex];
|
||||
if (sessionSlot) {
|
||||
sessionSlot.setPosition(0, slotIndex * 56);
|
||||
const valueHeight = 76;
|
||||
sessionSlot.setPosition(0, slotIndex * valueHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,7 +448,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.setCursor(this.cursor, prevSlotIndex);
|
||||
globalScene.tweens.add({
|
||||
targets: this.sessionSlotsContainer,
|
||||
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
|
||||
y: this.sessionSlotsContainerInitialY - 76 * scrollCursor,
|
||||
duration: fixedInt(325),
|
||||
ease: "Sine.easeInOut",
|
||||
});
|
||||
@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
class SessionSlot extends Phaser.GameObjects.Container {
|
||||
public slotId: number;
|
||||
public hasData: boolean;
|
||||
/** Indicates the save slot ran into an error while being loaded */
|
||||
public malformed: boolean;
|
||||
private slotWindow: Phaser.GameObjects.NineSlice;
|
||||
private loadingLabel: Phaser.GameObjects.Text;
|
||||
|
||||
public saveData: SessionSaveData;
|
||||
|
||||
constructor(slotId: number) {
|
||||
super(globalScene, 0, slotId * 56);
|
||||
super(globalScene, 0, slotId * 76);
|
||||
|
||||
this.slotId = slotId;
|
||||
|
||||
@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
setup() {
|
||||
const slotWindow = addWindow(0, 0, 304, 52);
|
||||
this.add(slotWindow);
|
||||
this.slotWindow = addWindow(0, 0, 304, 70);
|
||||
this.add(this.slotWindow);
|
||||
|
||||
this.loadingLabel = addTextObject(152, 26, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel = addTextObject(152, 33, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel.setOrigin(0.5, 0.5);
|
||||
this.add(this.loadingLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a name for sessions that don't have a name yet.
|
||||
* @param data - The {@linkcode SessionSaveData} being checked
|
||||
* @returns The default name for the given data.
|
||||
*/
|
||||
decideFallback(data: SessionSaveData): string {
|
||||
let fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
switch (data.gameMode) {
|
||||
case GameModes.CLASSIC:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.classicSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.ENDLESS:
|
||||
case GameModes.SPLICED_ENDLESS:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.endlessSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.DAILY: {
|
||||
const runDay = new Date(data.timestamp).toLocaleDateString();
|
||||
fallbackName += ` (${runDay})`;
|
||||
break;
|
||||
}
|
||||
case GameModes.CHALLENGE: {
|
||||
const activeChallenges = data.challenges.filter(c => c.value !== 0);
|
||||
if (activeChallenges.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
fallbackName = "";
|
||||
for (const challenge of activeChallenges.slice(0, 3)) {
|
||||
if (fallbackName !== "") {
|
||||
fallbackName += ", ";
|
||||
}
|
||||
fallbackName += challenge.toChallenge().getName();
|
||||
}
|
||||
|
||||
if (activeChallenges.length > 3) {
|
||||
fallbackName += ", ...";
|
||||
} else if (fallbackName === "") {
|
||||
// Something went wrong when retrieving the names of the active challenges,
|
||||
// so fall back to just naming the run "Challenge"
|
||||
fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
async setupWithData(data: SessionSaveData) {
|
||||
const hasName = data?.runNameText;
|
||||
this.remove(this.loadingLabel, true);
|
||||
if (hasName) {
|
||||
const nameLabel = addTextObject(8, 5, data.runNameText, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
} else {
|
||||
const fallbackName = this.decideFallback(data);
|
||||
await globalScene.gameData.renameSession(this.slotId, fallbackName);
|
||||
const nameLabel = addTextObject(8, 5, fallbackName, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
}
|
||||
|
||||
const gameModeLabel = addTextObject(
|
||||
8,
|
||||
5,
|
||||
19,
|
||||
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
||||
TextStyle.WINDOW,
|
||||
);
|
||||
this.add(gameModeLabel);
|
||||
|
||||
const timestampLabel = addTextObject(8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
const timestampLabel = addTextObject(8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
this.add(timestampLabel);
|
||||
|
||||
const playTimeLabel = addTextObject(8, 33, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
const playTimeLabel = addTextObject(8, 47, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
this.add(playTimeLabel);
|
||||
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 4);
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 16);
|
||||
data.party.forEach((p: PokemonData, i: number) => {
|
||||
const iconContainer = globalScene.add.container(26 * i, 0);
|
||||
iconContainer.setScale(0.75);
|
||||
@ -427,13 +594,9 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
TextStyle.PARTY,
|
||||
{ fontSize: "54px", color: "#f8f8f8" },
|
||||
);
|
||||
text.setShadow(0, 0, undefined);
|
||||
text.setStroke("#424242", 14);
|
||||
text.setOrigin(1, 0);
|
||||
|
||||
iconContainer.add(icon);
|
||||
iconContainer.add(text);
|
||||
text.setShadow(0, 0, undefined).setStroke("#424242", 14).setOrigin(1, 0);
|
||||
|
||||
iconContainer.add([icon, text]);
|
||||
pokemonIconsContainer.add(iconContainer);
|
||||
|
||||
pokemon.destroy();
|
||||
@ -441,7 +604,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
this.add(pokemonIconsContainer);
|
||||
|
||||
const modifierIconsContainer = globalScene.add.container(148, 30);
|
||||
const modifierIconsContainer = globalScene.add.container(148, 38);
|
||||
modifierIconsContainer.setScale(0.5);
|
||||
let visibleModifierIndex = 0;
|
||||
for (const m of data.modifiers) {
|
||||
@ -464,22 +627,33 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
load(): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
globalScene.gameData.getSession(this.slotId).then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
if (!sessionData) {
|
||||
this.hasData = false;
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.hasData = true;
|
||||
this.saveData = sessionData;
|
||||
await this.setupWithData(sessionData);
|
||||
resolve(true);
|
||||
});
|
||||
globalScene.gameData
|
||||
.getSession(this.slotId)
|
||||
.then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
this.hasData = !!sessionData;
|
||||
if (!sessionData) {
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.saveData = sessionData;
|
||||
this.setupWithData(sessionData);
|
||||
resolve(true);
|
||||
})
|
||||
.catch(e => {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
console.warn(`Failed to load session slot #${this.slotId}:`, e);
|
||||
this.loadingLabel.setText(i18next.t("menu:failedToLoadSession"));
|
||||
this.hasData = true;
|
||||
this.malformed = true;
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler {
|
||||
// we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key
|
||||
|
||||
// Return in the format expected by i18next
|
||||
return middleKey ? `${topKey}:${middleKey.map(m => m).join(".")}.${t}` : `${topKey}:${t}`;
|
||||
return middleKey ? `${topKey}:${middleKey.join(".")}.${t}` : `${topKey}:${t}`;
|
||||
}
|
||||
})
|
||||
.filter(t => t);
|
||||
|
@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme";
|
||||
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
||||
import { executeIf } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
import { RenameRunFormUiHandler } from "./rename-run-ui-handler";
|
||||
|
||||
const transitionModes = [
|
||||
UiMode.SAVE_SLOT,
|
||||
@ -98,6 +99,7 @@ const noTransitionModes = [
|
||||
UiMode.SESSION_RELOAD,
|
||||
UiMode.UNAVAILABLE,
|
||||
UiMode.RENAME_POKEMON,
|
||||
UiMode.RENAME_RUN,
|
||||
UiMode.TEST_DIALOGUE,
|
||||
UiMode.AUTO_COMPLETE,
|
||||
UiMode.ADMIN,
|
||||
@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container {
|
||||
new UnavailableModalUiHandler(),
|
||||
new GameChallengesUiHandler(),
|
||||
new RenameFormUiHandler(),
|
||||
new RenameRunFormUiHandler(),
|
||||
new RunHistoryUiHandler(),
|
||||
new RunInfoUiHandler(),
|
||||
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
||||
|
@ -112,7 +112,7 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||
|
||||
const pokemonPrior = scene.getPlayerParty().map(pokemon => pokemon);
|
||||
const pokemonPrior = scene.getPlayerParty().slice();
|
||||
const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal());
|
||||
|
||||
await runMysteryEncounterToEnd(game, 1);
|
||||
|
82
test/system/rename-run.test.ts
Normal file
82
test/system/rename-run.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as account from "#app/account";
|
||||
import * as bypassLoginModule from "#app/global-vars/bypass-login";
|
||||
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
|
||||
import type { SessionSaveData } from "#app/system/game-data";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("System - Rename Run", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.battleStyle("single")
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
describe("renameSession", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(false);
|
||||
vi.spyOn(account, "updateUserInfo").mockImplementation(async () => [true, 1]);
|
||||
});
|
||||
|
||||
it("should return false if slotId < 0", async () => {
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false if getSession returns null", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue(null as unknown as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if bypassLogin is true", async () => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(true);
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if api returns error", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("Unknown Error!");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if api is succesfull", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
expect(account.updateUserInfo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user