Compare commits

..

16 Commits

Author SHA1 Message Date
Sirz Benjie
44a6899424
Apply Kev's suggestions from code review
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-14 13:18:31 -05:00
Sirz Benjie
841f56812c
Add ribbons for each challenge 2025-08-14 13:18:30 -05:00
Sirz Benjie
6d44c285ec
Add tracking for each generational ribbon 2025-08-14 13:18:30 -05:00
Sirz Benjie
073d0e83b3
Replace mass getters with a single method 2025-08-14 13:18:29 -05:00
Sirz Benjie
1766c0a368
fix overlapping flag set 2025-08-14 13:18:29 -05:00
Sirz Benjie
784d94a56e
Add tracking for friendship ribbon 2025-08-14 13:18:28 -05:00
Sirz Benjie
b8ba921430
Add ribbon to legacy ui folder 2025-08-14 13:18:27 -05:00
Sirz Benjie
c005636a97
Add tracking for nuzlocke completion 2025-08-14 13:18:26 -05:00
Wlowscha
140e4ab142
[UI/UX] Party slots refactor (#6199)
* constants for position of discard button

* Moved transfer/discard button up in doubles

* Fixed the various `.setOrigin(0,0)`

* Small clean up

* Added `isBenched` property to slots; x origin of `slotBg` is now 0

* Also set y origin to 0

* Offsets are relevant to the same thing

* Introducing const object to store ui magic numbers

* More magic numbers in const

* Laid out numbers for slot positions

* Added smaller main slots for transfer mode in doubles

* Changed background to fit new slot disposition

* Apply suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Optimized PNGs

* Updated comment

* Removed "magicNumbers" container, added multiple comments

* Update src/ui/party-ui-handler.ts

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* Fainted pkmn slots displaying correctly

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: Adri1 <adrien.grivel@hotmail.fr>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-08-14 13:10:15 -05:00
fabske0
76d8357d0b
[Dev] Rename OPP_ overrides to ENEMY_ (#6255)
rename `OPP_` to `ENEMY_`
2025-08-14 18:06:24 +00:00
Bertie690
f42237d415
[Refactor] Removed map(x => x) (#6256)
* Enforced a few usages of `toCamelCase`

* Removed `map(x => x)`

* Removed more maps and sufff

* Update test/mystery-encounter/encounters/weird-dream-encounter.test.ts

* Update game-data.ts types to work
2025-08-14 10:25:44 -07:00
fabske0
b44f0a4176
[Refactor] Remove bgm param from arena constructor (#6254) 2025-08-14 16:52:56 +00:00
Sirz Benjie
076ef81691
[Bug] [UI/UX] [Beta] Fix icons not showing in save slot selection (#6262)
Fix icons not showing in save slot selection
2025-08-13 20:49:46 -05:00
fabske0
23271901cf
[Docs] Add locale key naming info to localization.md (#6260) 2025-08-14 01:12:00 +00:00
Inês Simões
1517e0512e
[UI/UX] [Feature] Save Management Tool (Rename/Delete Saves) (#5978)
* Implement Name Run Feat
Modified load session ui component, adding a submenu when selecting a 3
slot. This menu has 4 options:
Load Game -> Behaves as before, allowing the player to continue
progress from the last saved state in the slot.

Rename Run -> Overlays a rename form, allowing the player to type a
name for the run, checking for string validity, with the option to
cancel or confirm (Rename).

Delete Run -> Prompts user confirmation to delete save data, removing
the current save slot from the users save data.

Cancel -> Hides menu overlay.

Modified game data to implement a function to accept and store
runNameText to the users data.

Modified run info ui component, to display the chosen name when
viewing run information.

Example: When loading the game, the user can choose the Load Game
menu option, then select a save slot, prompting the menu, then choose
"Rename Run" and type the name "Monotype Water Run" then confirm,
thus being able to better organize their save files.

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Implement Rename Input Design and Tests for Name Run Feat
Created a test to verify Name Run Feature behaviour in the
backend (rename_run.test.ts), checking possible errors and
 expected behaviours.

Created a UiHandler RenameRunFormUiHandler
(rename-run-ui-handler.ts), creating a frontend input
overlay for the Name Run Feature.

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Fixed formating and best practices issues:
Rewrote renameSession to be more inline with other
API call funtions, removed debugging comments and
whitespaces.

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Minor Sanitization for aesthetics
Deleting the input when closing the overlay for
aesthetics purpose

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Fixed minor rebase alterations.

Signed-off-by: Matheus Alves matheus.r.noya.alves@tecnico.ulisboa.pt
Co-authored-by: Inês Simões ines.p.simoes@tecnico.ulisboa.pt

* Implemented Default Name Logic
Altered logic in save-slot-select-ui-handler.ts to
support default naming of runs based on the run
game mode with decideFallback function.

In game-data.ts, to prevent inconsistent naming,
added check for unfilled input, ignoring empty
rename requests.

Signed-off-by: Matheus Alves matheus.r.noya.alves@tecnico.ulisboa.pt
Co-authored-by: Inês Simões ines.p.simoes@tecnico.ulisboa.pt

* Replace fallback name logic: use first active challenge instead
of game mode

Previously used game mode as the fallback name, updated to use the
first active challenge instead (e.g. Monogen or Mono Type), which
better reflects the run's theme.
Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Rebasing and conflict resolution

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Lint fix

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Minor compile fix

* Dependency resolved

* Format name respected

* Add all active challenges to default challenge session name if possible

If more than 3 challenges are active, only the first 3 are added
to the name (to prevent the text going off-screen)
and then "..." is appended to the end to indicate
there were more challenges active than the ones listed

* Allow deleting malformed sessions

---------

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Signed-off-by: Matheus Alves matheus.r.noya.alves@tecnico.ulisboa.pt
Co-authored-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-08-13 20:08:12 -05:00
Bertie690
0da37a0f0c
[Move] Added laser focus locales (#6202)
* Added Laser Focus locales

* Fixed key for locales text

* Added `MessageAttr`; cleaned up a lot of other jank move attrs
2025-08-13 08:16:08 -07:00
36 changed files with 952 additions and 403 deletions

View File

@ -81,7 +81,7 @@ For example, here is how you could test a scenario where the player Pokemon has
```typescript ```typescript
const overrides = { const overrides = {
ABILITY_OVERRIDE: AbilityId.DROUGHT, ABILITY_OVERRIDE: AbilityId.DROUGHT,
OPP_MOVESET_OVERRIDE: MoveId.WATER_GUN, ENEMY_MOVESET_OVERRIDE: MoveId.WATER_GUN,
} satisfies Partial<InstanceType<typeof DefaultOverrides>>; } satisfies Partial<InstanceType<typeof DefaultOverrides>>;
``` ```

View File

@ -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. - 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. [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. - 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). 3. Your locales should use the following format:
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`. - File names should be in `kebab-case`. Example: `trainer-names.json`
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment. - 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). [^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. 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 B

After

Width:  |  Height:  |  Size: 799 B

View File

@ -0,0 +1,146 @@
{
"textures": [
{
"image": "party_slot_main_short.png",
"format": "RGBA8888",
"size": {
"w": 110,
"h": 294
},
"scale": 1,
"frames": [
{
"filename": "party_slot_main_short",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_sel",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 41,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_fnt",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 82,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_fnt_sel",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 123,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_swap",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 164,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_swap_sel",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 205,
"w": 110,
"h": 41
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:29685f2f538901cf5bf7f0ed2ea867c3:a080ea6c8cccd1e03244214053e79796:565f7afc5ca419b6ba8dbce51ea30818$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,13 +1,24 @@
import type { Pokemon } from "#field/pokemon";
import type { import type {
AttackMove, AttackMove,
ChargingAttackMove, ChargingAttackMove,
ChargingSelfStatusMove, ChargingSelfStatusMove,
Move,
MoveAttr, MoveAttr,
MoveAttrConstructorMap, MoveAttrConstructorMap,
SelfStatusMove, SelfStatusMove,
StatusMove, StatusMove,
} from "#moves/move"; } from "#moves/move";
/**
* A generic function producing a message during a Move's execution.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by the move
* @param move - The {@linkcode Move} being used
* @returns a string
*/
export type MoveMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => string;
export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
export type * from "#moves/move"; export type * from "#moves/move";

View File

@ -104,6 +104,7 @@ import {
getLuckString, getLuckString,
getLuckTextTint, getLuckTextTint,
getPartyLuckValue, getPartyLuckValue,
type ModifierType,
PokemonHeldItemModifierType, PokemonHeldItemModifierType,
} from "#modifiers/modifier-type"; } from "#modifiers/modifier-type";
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
@ -943,17 +944,17 @@ export class BattleScene extends SceneBase {
dataSource?: PokemonData, dataSource?: PokemonData,
postProcess?: (enemyPokemon: EnemyPokemon) => void, postProcess?: (enemyPokemon: EnemyPokemon) => void,
): EnemyPokemon { ): EnemyPokemon {
if (Overrides.OPP_LEVEL_OVERRIDE > 0) { if (Overrides.ENEMY_LEVEL_OVERRIDE > 0) {
level = Overrides.OPP_LEVEL_OVERRIDE; level = Overrides.ENEMY_LEVEL_OVERRIDE;
} }
if (Overrides.OPP_SPECIES_OVERRIDE) { if (Overrides.ENEMY_SPECIES_OVERRIDE) {
species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE); species = getPokemonSpecies(Overrides.ENEMY_SPECIES_OVERRIDE);
// The fact that a Pokemon is a boss or not can change based on its Species and level // The fact that a Pokemon is a boss or not can change based on its Species and level
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1; boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
} }
const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource); const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource);
if (Overrides.OPP_FUSION_OVERRIDE) { if (Overrides.ENEMY_FUSION_OVERRIDE) {
pokemon.generateFusionSpecies(); pokemon.generateFusionSpecies();
} }
@ -1203,7 +1204,9 @@ export class BattleScene extends SceneBase {
this.updateScoreText(); this.updateScoreText();
this.scoreText.setVisible(false); 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); this.newArena(Overrides.STARTING_BIOME_OVERRIDE || BiomeId.TOWN);
@ -1237,8 +1240,7 @@ export class BattleScene extends SceneBase {
Object.values(mp) Object.values(mp)
.flat() .flat()
.map(mt => mt.modifierType) .map(mt => mt.modifierType)
.filter(mt => "localize" in mt) .filter((mt): mt is ModifierType & Localizable => "localize" in mt && typeof mt.localize === "function"),
.map(lpb => lpb as unknown as Localizable),
), ),
]; ];
for (const item of localizable) { for (const item of localizable) {
@ -1513,8 +1515,8 @@ export class BattleScene extends SceneBase {
return this.currentBattle; return this.currentBattle;
} }
newArena(biome: BiomeId, playerFaints?: number): Arena { newArena(biome: BiomeId, playerFaints = 0): Arena {
this.arena = new Arena(biome, BiomeId[biome].toLowerCase(), playerFaints); this.arena = new Arena(biome, playerFaints);
this.eventTarget.dispatchEvent(new NewArenaEvent()); this.eventTarget.dispatchEvent(new NewArenaEvent());
this.arenaBg.pipelineData = { this.arenaBg.pipelineData = {
@ -1764,10 +1766,10 @@ export class BattleScene extends SceneBase {
} }
getEncounterBossSegments(waveIndex: number, level: number, species?: PokemonSpecies, forceBoss = false): number { getEncounterBossSegments(waveIndex: number, level: number, species?: PokemonSpecies, forceBoss = false): number {
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1) { if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1) {
return Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE; return Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE;
} }
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE === 1) { if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE === 1) {
// The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss // The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss
return 0; return 0;
} }
@ -2711,7 +2713,9 @@ export class BattleScene extends SceneBase {
} }
} }
this.party.map(p => p.updateInfo(instant)); this.party.forEach(p => {
p.updateInfo(instant);
});
} else { } else {
const args = [this]; const args = [this];
if (modifier.shouldApply(...args)) { if (modifier.shouldApply(...args)) {

View File

@ -74,6 +74,7 @@ import {
randSeedItem, randSeedItem,
toDmgValue, toDmgValue,
} from "#utils/common"; } from "#utils/common";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
export class Ability implements Localizable { export class Ability implements Localizable {
@ -109,13 +110,9 @@ export class Ability implements Localizable {
} }
localize(): void { localize(): void {
const i18nKey = AbilityId[this.id] const i18nKey = toCamelCase(AbilityId[this.id]);
.split("_")
.filter(f => f)
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
.join("") as string;
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) : ""; this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : "";
} }
@ -1670,6 +1667,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
constructor( constructor(
private newType: PokemonType, private newType: PokemonType,
private powerMultiplier: number, private powerMultiplier: number,
// TODO: all moves with this attr solely check the move being used...
private condition?: PokemonAttackCondition, private condition?: PokemonAttackCondition,
) { ) {
super(false); super(false);

View File

@ -1866,17 +1866,16 @@ interface PokemonPrevolutions {
export const pokemonPrevolutions: PokemonPrevolutions = {}; export const pokemonPrevolutions: PokemonPrevolutions = {};
export function initPokemonPrevolutions(): void { export function initPokemonPrevolutions(): void {
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ].map(sfk => sfk as string); // TODO: Why do we have empty strings in our array?
const prevolutionKeys = Object.keys(pokemonEvolutions); const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ];
prevolutionKeys.forEach(pk => { for (const [pk, evolutions] of Object.entries(pokemonEvolutions)) {
const evolutions = pokemonEvolutions[pk];
for (const ev of evolutions) { for (const ev of evolutions) {
if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) { if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) {
continue; continue;
} }
pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId; pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId;
} }
}); }
} }

View File

@ -86,11 +86,11 @@ import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase";
import type { AttackMoveResult } from "#types/attack-move-result"; import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales"; import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
import type { TurnMove } from "#types/turn-move"; import type { TurnMove } from "#types/turn-move";
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
import { getEnumValues } from "#utils/enums"; import { getEnumValues } from "#utils/enums";
import { toTitleCase } from "#utils/strings"; import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
import { applyChallenges } from "#utils/challenge-utils"; import { applyChallenges } from "#utils/challenge-utils";
@ -162,10 +162,16 @@ export abstract class Move implements Localizable {
} }
localize(): void { 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}` : ""; if (this.id === MoveId.NONE) {
this.effect = this.id ? `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}` : ""; this.name = "";
this.effect = ""
return;
}
this.name = `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}`;
this.effect = `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}`;
} }
/** /**
@ -1357,20 +1363,20 @@ export class MoveHeaderAttr extends MoveAttr {
/** /**
* Header attribute to queue a message at the beginning of a turn. * Header attribute to queue a message at the beginning of a turn.
* @see {@link MoveHeaderAttr}
*/ */
export class MessageHeaderAttr extends MoveHeaderAttr { export class MessageHeaderAttr extends MoveHeaderAttr {
private message: string | ((user: Pokemon, move: Move) => string); /** The message to display, or a function producing one. */
private message: string | MoveMessageFunc;
constructor(message: string | ((user: Pokemon, move: Move) => string)) { constructor(message: string | MoveMessageFunc) {
super(); super();
this.message = message; this.message = message;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
const message = typeof this.message === "string" const message = typeof this.message === "string"
? this.message ? this.message
: this.message(user, move); : this.message(user, target, move);
if (message) { if (message) {
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
@ -1418,21 +1424,21 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
*/ */
export class PreMoveMessageAttr extends MoveAttr { export class PreMoveMessageAttr extends MoveAttr {
/** The message to display or a function returning one */ /** The message to display or a function returning one */
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined); private message: string | MoveMessageFunc;
/** /**
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution. * Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
* @param message - The message to display before move use, either as a string or a function producing one. * @param message - The message to display before move use, either` a literal string or a function producing one.
* @remarks * @remarks
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed * If {@linkcode message} evaluates to an empty string (`""`), no message will be displayed
* (though the move will still succeed). * (though the move will still succeed).
*/ */
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) { constructor(message: string | MoveMessageFunc) {
super(); super();
this.message = message; this.message = message;
} }
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
const message = typeof this.message === "function" const message = typeof this.message === "function"
? this.message(user, target, move) ? this.message(user, target, move)
: this.message; : this.message;
@ -1453,18 +1459,17 @@ export class PreMoveMessageAttr extends MoveAttr {
* @extends MoveAttr * @extends MoveAttr
*/ */
export class PreUseInterruptAttr extends MoveAttr { export class PreUseInterruptAttr extends MoveAttr {
protected message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string); protected message: string | MoveMessageFunc;
protected overridesFailedMessage: boolean;
protected conditionFunc: MoveConditionFunc; protected conditionFunc: MoveConditionFunc;
/** /**
* Create a new MoveInterruptedMessageAttr. * Create a new MoveInterruptedMessageAttr.
* @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move. * @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move.
*/ */
constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) { constructor(message: string | MoveMessageFunc, conditionFunc: MoveConditionFunc) {
super(); super();
this.message = message; this.message = message;
this.conditionFunc = conditionFunc ?? (() => true); this.conditionFunc = conditionFunc;
} }
/** /**
@ -1485,11 +1490,9 @@ export class PreUseInterruptAttr extends MoveAttr {
*/ */
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined { override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
if (this.message && this.conditionFunc(user, target, move)) { if (this.message && this.conditionFunc(user, target, move)) {
const message = return typeof this.message === "string"
typeof this.message === "string" ? this.message
? (this.message as string)
: this.message(user, target, move); : this.message(user, target, move);
return message;
} }
} }
} }
@ -1694,17 +1697,30 @@ export class SurviveDamageAttr extends ModifiedDamageAttr {
} }
} }
export class SplashAttr extends MoveEffectAttr { /**
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { * Move attribute to display arbitrary text during a move's execution.
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:splash")); */
export class MessageAttr extends MoveEffectAttr {
/** The message to display, either as a string or a function returning one. */
private message: string | MoveMessageFunc;
constructor(message: string | MoveMessageFunc, options?: MoveEffectAttrOptions) {
// TODO: Do we need to respect `selfTarget` if we're just displaying text?
super(false, options)
this.message = message;
}
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
const message = typeof this.message === "function"
? this.message(user, target, move)
: this.message;
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
if (message) {
globalScene.phaseManager.queueMessage(message, 500);
return true; return true;
} }
} return false;
export class CelebrateAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username }));
return true;
} }
} }
@ -5931,38 +5947,6 @@ export class ProtectAttr extends AddBattlerTagAttr {
} }
} }
export class IgnoreAccuracyAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.IGNORE_ACCURACY, true, false, 2);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
return true;
}
}
export class FaintCountdownAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.PERISH_SONG, false, true, 4);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: this.turnCountMin - 1 }));
return true;
}
}
/** /**
* Attribute to remove all Substitutes from the field. * Attribute to remove all Substitutes from the field.
* @extends MoveEffectAttr * @extends MoveEffectAttr
@ -6603,8 +6587,10 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr {
return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move); return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move);
} }
} }
export class RemoveTypeAttr extends MoveEffectAttr { export class RemoveTypeAttr extends MoveEffectAttr {
// TODO: Remove the message callback
private removedType: PokemonType; private removedType: PokemonType;
private messageCallback: ((user: Pokemon) => void) | undefined; private messageCallback: ((user: Pokemon) => void) | undefined;
@ -8299,8 +8285,6 @@ const MoveAttrs = Object.freeze({
RandomLevelDamageAttr, RandomLevelDamageAttr,
ModifiedDamageAttr, ModifiedDamageAttr,
SurviveDamageAttr, SurviveDamageAttr,
SplashAttr,
CelebrateAttr,
RecoilAttr, RecoilAttr,
SacrificialAttr, SacrificialAttr,
SacrificialAttrOnHit, SacrificialAttrOnHit,
@ -8443,8 +8427,7 @@ const MoveAttrs = Object.freeze({
RechargeAttr, RechargeAttr,
TrapAttr, TrapAttr,
ProtectAttr, ProtectAttr,
IgnoreAccuracyAttr, MessageAttr,
FaintCountdownAttr,
RemoveAllSubstitutesAttr, RemoveAllSubstitutesAttr,
HitsTagAttr, HitsTagAttr,
HitsTagForDoubleDamageAttr, HitsTagForDoubleDamageAttr,
@ -8938,7 +8921,7 @@ export function initMoves() {
new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
.attr(RandomLevelDamageAttr), .attr(RandomLevelDamageAttr),
new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1) new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1)
.attr(SplashAttr) .attr(MessageAttr, i18next.t("moveTriggers:splash"))
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1) new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
@ -9000,7 +8983,10 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(), .reflectable(),
new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2) new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
.attr(MessageAttr, (user, target) =>
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
),
new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2) new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE) .attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE)
.condition(targetSleptOrComatoseCondition), .condition(targetSleptOrComatoseCondition),
@ -9088,7 +9074,9 @@ export function initMoves() {
return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS; return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS;
}), }),
new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2) new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2)
.attr(FaintCountdownAttr) .attr(AddBattlerTagAttr, BattlerTagType.PERISH_SONG, false, true, 4)
.attr(MessageAttr, (_user, target) =>
i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: 3 }))
.ignoresProtect() .ignoresProtect()
.soundBased() .soundBased()
.condition(failOnBossCondition) .condition(failOnBossCondition)
@ -9104,7 +9092,10 @@ export function initMoves() {
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2) new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
.attr(MessageAttr, (user, target) =>
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
),
new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2) new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2)
.attr(FrenzyAttr) .attr(FrenzyAttr)
.attr(MissEffectAttr, frenzyMissFunc) .attr(MissEffectAttr, frenzyMissFunc)
@ -9331,8 +9322,8 @@ export function initMoves() {
&& (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1) && (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1)
.attr(BypassBurnDamageReductionAttr), .attr(BypassBurnDamageReductionAttr),
new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3) new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3)
.attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) .attr(MessageHeaderAttr, (user) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(PreUseInterruptAttr, (user, target, move) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => !!user.turnData.attacksReceived.find(r => r.damage)) .attr(PreUseInterruptAttr, (user) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => user.turnData.attacksReceived.some(r => r.damage > 0))
.punchingMove(), .punchingMove(),
new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3) new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
@ -10433,7 +10424,8 @@ export function initMoves() {
new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6) new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6) new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6)
.attr(CelebrateAttr), // NB: This needs a lambda function as the user will not be logged in by the time the moves are initialized
.attr(MessageAttr, () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })),
new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6) new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6)
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
@ -10608,7 +10600,12 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.reflectable(), .reflectable(),
new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7) new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false)
.attr(MessageAttr, (user) =>
i18next.t("battlerTags:laserFocusOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
}),
),
new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7) new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()

View File

@ -12,6 +12,7 @@ import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier"; import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
import { type Constructor, coerceArray } from "#utils/common"; import { type Constructor, coerceArray } from "#utils/common";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
export abstract class SpeciesFormChangeTrigger { export abstract class SpeciesFormChangeTrigger {
@ -143,11 +144,7 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge
super(); super();
this.move = move; this.move = move;
this.known = known; this.known = known;
const moveKey = MoveId[this.move] const moveKey = toCamelCase(MoveId[this.move]);
.split("_")
.filter(f => f)
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
.join("") as unknown as string;
this.description = known this.description = known
? i18next.t("pokemonEvolutions:Forms.moveLearned", { ? i18next.t("pokemonEvolutions:Forms.moveLearned", {
move: i18next.t(`move:${moveKey}.name`), move: i18next.t(`move:${moveKey}.name`),

View File

@ -38,6 +38,7 @@ export enum UiMode {
UNAVAILABLE, UNAVAILABLE,
CHALLENGE_SELECT, CHALLENGE_SELECT,
RENAME_POKEMON, RENAME_POKEMON,
RENAME_RUN,
RUN_HISTORY, RUN_HISTORY,
RUN_INFO, RUN_INFO,
TEST_DIALOGUE, TEST_DIALOGUE,

View File

@ -54,7 +54,7 @@ export class Arena {
public bgm: string; public bgm: string;
public ignoreAbilities: boolean; public ignoreAbilities: boolean;
public ignoringEffectSource: BattlerIndex | null; public ignoringEffectSource: BattlerIndex | null;
public playerTerasUsed: number; public playerTerasUsed = 0;
/** /**
* Saves the number of times a party pokemon faints during a arena encounter. * 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). * {@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(); public readonly eventTarget: EventTarget = new EventTarget();
constructor(biome: BiomeId, bgm: string, playerFaints = 0) { constructor(biome: BiomeId, playerFaints = 0) {
this.biomeType = biome; this.biomeType = biome;
this.bgm = bgm; this.bgm = BiomeId[biome].toLowerCase();
this.trainerPool = biomeTrainerPools[biome]; this.trainerPool = biomeTrainerPools[biome];
this.updatePoolsForTimeOfDay(); this.updatePoolsForTimeOfDay();
this.playerTerasUsed = 0;
this.playerFaints = playerFaints; this.playerFaints = playerFaints;
} }

View File

@ -1827,7 +1827,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// Overrides moveset based on arrays specified in overrides.ts // Overrides moveset based on arrays specified in overrides.ts
let overrideArray: MoveId | Array<MoveId> = this.isPlayer() let overrideArray: MoveId | Array<MoveId> = this.isPlayer()
? Overrides.MOVESET_OVERRIDE ? Overrides.MOVESET_OVERRIDE
: Overrides.OPP_MOVESET_OVERRIDE; : Overrides.ENEMY_MOVESET_OVERRIDE;
overrideArray = coerceArray(overrideArray); overrideArray = coerceArray(overrideArray);
if (overrideArray.length > 0) { if (overrideArray.length > 0) {
if (!this.isPlayer()) { if (!this.isPlayer()) {
@ -2032,8 +2032,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) { if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) {
return allAbilities[Overrides.ABILITY_OVERRIDE]; return allAbilities[Overrides.ABILITY_OVERRIDE];
} }
if (Overrides.OPP_ABILITY_OVERRIDE && this.isEnemy()) { if (Overrides.ENEMY_ABILITY_OVERRIDE && this.isEnemy()) {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE];
} }
if (this.isFusion()) { if (this.isFusion()) {
if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) { if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
@ -2062,8 +2062,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) { if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) {
return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE]; return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE];
} }
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) { if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE];
} }
if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) { if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
return allAbilities[this.customPokemonData.passive]; return allAbilities[this.customPokemonData.passive];
@ -2130,14 +2130,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// returns override if valid for current case // returns override if valid for current case
if ( if (
(Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) || (Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) ||
(Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy()) (Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy())
) { ) {
return false; return false;
} }
if ( if (
((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) && ((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) &&
this.isPlayer()) || this.isPlayer()) ||
((Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE) && ((Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE) &&
this.isEnemy()) this.isEnemy())
) { ) {
return true; return true;
@ -3003,8 +3003,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) { if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) {
fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE); fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE);
} else if (this.isEnemy() && Overrides.OPP_FUSION_SPECIES_OVERRIDE) { } else if (this.isEnemy() && Overrides.ENEMY_FUSION_SPECIES_OVERRIDE) {
fusionOverride = getPokemonSpecies(Overrides.OPP_FUSION_SPECIES_OVERRIDE); fusionOverride = getPokemonSpecies(Overrides.ENEMY_FUSION_SPECIES_OVERRIDE);
} }
this.fusionSpecies = this.fusionSpecies =
@ -6257,22 +6257,22 @@ export class EnemyPokemon extends Pokemon {
this.setBoss(boss, dataSource?.bossSegments); this.setBoss(boss, dataSource?.bossSegments);
} }
if (Overrides.OPP_STATUS_OVERRIDE) { if (Overrides.ENEMY_STATUS_OVERRIDE) {
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4); this.status = new Status(Overrides.ENEMY_STATUS_OVERRIDE, 0, 4);
} }
if (Overrides.OPP_GENDER_OVERRIDE !== null) { if (Overrides.ENEMY_GENDER_OVERRIDE !== null) {
this.gender = Overrides.OPP_GENDER_OVERRIDE; this.gender = Overrides.ENEMY_GENDER_OVERRIDE;
} }
const speciesId = this.species.speciesId; const speciesId = this.species.speciesId;
if ( if (
speciesId in Overrides.OPP_FORM_OVERRIDES && speciesId in Overrides.ENEMY_FORM_OVERRIDES &&
!isNullOrUndefined(Overrides.OPP_FORM_OVERRIDES[speciesId]) && !isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId]) &&
this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]] this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]]
) { ) {
this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId]; this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId];
} else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) { } else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
const eventBoss = getDailyEventSeedBoss(globalScene.seed); const eventBoss = getDailyEventSeedBoss(globalScene.seed);
if (!isNullOrUndefined(eventBoss)) { if (!isNullOrUndefined(eventBoss)) {
@ -6282,21 +6282,21 @@ export class EnemyPokemon extends Pokemon {
if (!dataSource) { if (!dataSource) {
this.generateAndPopulateMoveset(); this.generateAndPopulateMoveset();
if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) { if (shinyLock || Overrides.ENEMY_SHINY_OVERRIDE === false) {
this.shiny = false; this.shiny = false;
} else { } else {
this.trySetShiny(); this.trySetShiny();
} }
if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) { if (!this.shiny && Overrides.ENEMY_SHINY_OVERRIDE) {
this.shiny = true; this.shiny = true;
this.initShinySparkle(); this.initShinySparkle();
} }
if (this.shiny) { if (this.shiny) {
this.variant = this.generateShinyVariant(); this.variant = this.generateShinyVariant();
if (Overrides.OPP_VARIANT_OVERRIDE !== null) { if (Overrides.ENEMY_VARIANT_OVERRIDE !== null) {
this.variant = Overrides.OPP_VARIANT_OVERRIDE; this.variant = Overrides.ENEMY_VARIANT_OVERRIDE;
} }
} }

View File

@ -123,6 +123,7 @@ export class LoadingScene extends SceneBase {
this.loadImage("party_bg_double", "ui"); this.loadImage("party_bg_double", "ui");
this.loadImage("party_bg_double_manage", "ui"); this.loadImage("party_bg_double_manage", "ui");
this.loadAtlas("party_slot_main", "ui"); this.loadAtlas("party_slot_main", "ui");
this.loadAtlas("party_slot_main_short", "ui");
this.loadAtlas("party_slot", "ui"); this.loadAtlas("party_slot", "ui");
this.loadImage("party_slot_overlay_lv", "ui"); this.loadImage("party_slot_overlay_lv", "ui");
this.loadImage("party_slot_hp_bar", "ui"); this.loadImage("party_slot_hp_bar", "ui");
@ -448,7 +449,9 @@ export class LoadingScene extends SceneBase {
); );
if (!mobile) { if (!mobile) {
loadingGraphics.map(g => g.setVisible(false)); loadingGraphics.forEach(g => {
g.setVisible(false);
});
} }
const intro = this.add.video(0, 0); const intro = this.add.video(0, 0);

View File

@ -121,8 +121,8 @@ export class ModifierBar extends Phaser.GameObjects.Container {
} }
updateModifierOverflowVisibility(ignoreLimit: boolean) { updateModifierOverflowVisibility(ignoreLimit: boolean) {
const modifierIcons = this.getAll().reverse(); const modifierIcons = this.getAll().reverse() as Phaser.GameObjects.Container[];
for (const modifier of modifierIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) { for (const modifier of modifierIcons.slice(iconOverflowIndex)) {
modifier.setVisible(ignoreLimit); modifier.setVisible(ignoreLimit);
} }
} }
@ -3755,7 +3755,7 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier {
export function overrideModifiers(isPlayer = true): void { export function overrideModifiers(isPlayer = true): void {
const modifiersOverride: ModifierOverride[] = isPlayer const modifiersOverride: ModifierOverride[] = isPlayer
? Overrides.STARTING_MODIFIER_OVERRIDE ? Overrides.STARTING_MODIFIER_OVERRIDE
: Overrides.OPP_MODIFIER_OVERRIDE; : Overrides.ENEMY_MODIFIER_OVERRIDE;
if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) { if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) {
return; return;
} }
@ -3797,7 +3797,7 @@ export function overrideModifiers(isPlayer = true): void {
export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void { export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void {
const heldItemsOverride: ModifierOverride[] = isPlayer const heldItemsOverride: ModifierOverride[] = isPlayer
? Overrides.STARTING_HELD_ITEMS_OVERRIDE ? Overrides.STARTING_HELD_ITEMS_OVERRIDE
: Overrides.OPP_HELD_ITEMS_OVERRIDE; : Overrides.ENEMY_HELD_ITEMS_OVERRIDE;
if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) { if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) {
return; return;
} }

View File

@ -179,25 +179,24 @@ class DefaultOverrides {
// -------------------------- // --------------------------
// OPPONENT / ENEMY OVERRIDES // OPPONENT / ENEMY OVERRIDES
// -------------------------- // --------------------------
// TODO: rename `OPP_` to `ENEMY_` readonly ENEMY_SPECIES_OVERRIDE: SpeciesId | number = 0;
readonly OPP_SPECIES_OVERRIDE: SpeciesId | number = 0;
/** /**
* This will make all opponents fused Pokemon * This will make all opponents fused Pokemon
*/ */
readonly OPP_FUSION_OVERRIDE: boolean = false; readonly ENEMY_FUSION_OVERRIDE: boolean = false;
/** /**
* This will override the species of the fusion only when the opponent is already a fusion * This will override the species of the fusion only when the opponent is already a fusion
*/ */
readonly OPP_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0; readonly ENEMY_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0;
readonly OPP_LEVEL_OVERRIDE: number = 0; readonly ENEMY_LEVEL_OVERRIDE: number = 0;
readonly OPP_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; readonly ENEMY_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
readonly OPP_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; readonly ENEMY_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
readonly OPP_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null; readonly ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; readonly ENEMY_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
readonly OPP_GENDER_OVERRIDE: Gender | null = null; readonly ENEMY_GENDER_OVERRIDE: Gender | null = null;
readonly OPP_MOVESET_OVERRIDE: MoveId | Array<MoveId> = []; readonly ENEMY_MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
readonly OPP_SHINY_OVERRIDE: boolean | null = null; readonly ENEMY_SHINY_OVERRIDE: boolean | null = null;
readonly OPP_VARIANT_OVERRIDE: Variant | null = null; readonly ENEMY_VARIANT_OVERRIDE: Variant | null = null;
/** /**
* Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`! * Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`!
* - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number. * - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number.
@ -207,7 +206,7 @@ class DefaultOverrides {
readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null; readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null;
/** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */ /** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */
readonly ENEMY_NATURE_OVERRIDE: Nature | null = null; readonly ENEMY_NATURE_OVERRIDE: Nature | null = null;
readonly OPP_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {}; readonly ENEMY_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {};
/** /**
* Override to give the enemy Pokemon a given amount of health segments * Override to give the enemy Pokemon a given amount of health segments
* *
@ -215,7 +214,7 @@ class DefaultOverrides {
* 1: the Pokemon will have a single health segment and therefore will not be a boss * 1: the Pokemon will have a single health segment and therefore will not be a boss
* 2+: the Pokemon will be a boss with the given number of health segments * 2+: the Pokemon will be a boss with the given number of health segments
*/ */
readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0; readonly ENEMY_HEALTH_SEGMENTS_OVERRIDE: number = 0;
// ------------- // -------------
// EGG OVERRIDES // EGG OVERRIDES
@ -277,12 +276,12 @@ class DefaultOverrides {
* *
* Note that any previous modifiers are cleared. * Note that any previous modifiers are cleared.
*/ */
readonly OPP_MODIFIER_OVERRIDE: ModifierOverride[] = []; readonly ENEMY_MODIFIER_OVERRIDE: ModifierOverride[] = [];
/** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */ /** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */
readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
/** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */ /** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */
readonly OPP_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; readonly ENEMY_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
/** /**
* Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave. * Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave.

View File

@ -229,7 +229,7 @@ export class EncounterPhase extends BattlePhase {
}), }),
); );
} else { } else {
const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1; const overridedBossSegments = Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1;
// for double battles, reduce the health segments for boss Pokemon unless there is an override // for double battles, reduce the health segments for boss Pokemon unless there is an override
if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) { if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) {
for (const enemyPokemon of battle.enemyParty) { for (const enemyPokemon of battle.enemyParty) {

View File

@ -128,6 +128,7 @@ export interface SessionSaveData {
battleType: BattleType; battleType: BattleType;
trainer: TrainerData; trainer: TrainerData;
gameVersion: string; gameVersion: string;
runNameText: string;
timestamp: number; timestamp: number;
challenges: ChallengeData[]; challenges: ChallengeData[];
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
@ -207,10 +208,12 @@ export interface StarterData {
[key: number]: StarterDataEntry; [key: number]: StarterDataEntry;
} }
export interface TutorialFlags { // TODO: Rework into a bitmask
[key: string]: boolean; export type TutorialFlags = {
} [key in Tutorial]: boolean;
};
// TODO: Rework into a bitmask
export interface SeenDialogues { export interface SeenDialogues {
[key: string]: boolean; [key: string]: boolean;
} }
@ -826,52 +829,51 @@ export class GameData {
return true; // TODO: is `true` the correct return value? return true; // TODO: is `true` the correct return value?
} }
private loadGamepadSettings(): boolean { private loadGamepadSettings(): void {
Object.values(SettingGamepad) Object.values(SettingGamepad).forEach(setting => {
.map(setting => setting as SettingGamepad) setSettingGamepad(setting, settingGamepadDefaults[setting]);
.forEach(setting => setSettingGamepad(setting, settingGamepadDefaults[setting])); });
if (!localStorage.hasOwnProperty("settingsGamepad")) { if (!localStorage.hasOwnProperty("settingsGamepad")) {
return false; return;
} }
const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")!); // TODO: is this bang correct? const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")!); // TODO: is this bang correct?
for (const setting of Object.keys(settingsGamepad)) { for (const setting of Object.keys(settingsGamepad)) {
setSettingGamepad(setting as SettingGamepad, settingsGamepad[setting]); 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); * Save the specified tutorial as having the specified completion status.
let tutorials: object = {}; * @param tutorial - The {@linkcode Tutorial} whose completion status is being saved
if (localStorage.hasOwnProperty(key)) { * @param status - The completion status to set
tutorials = JSON.parse(localStorage.getItem(key)!); // TODO: is this bang correct? */
} 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)!)
: {};
Object.keys(Tutorial) // TODO: We shouldn't be storing this like that
.map(t => t as Tutorial) for (const key of Object.values(Tutorial)) {
.forEach(t => {
const key = Tutorial[t];
if (key === tutorial) { if (key === tutorial) {
tutorials[key] = flag; tutorials[key] = status;
} else { } else {
tutorials[key] ??= false; tutorials[key] ??= false;
} }
}); }
localStorage.setItem(key, JSON.stringify(tutorials)); localStorage.setItem(saveDataKey, JSON.stringify(tutorials));
return true;
} }
public getTutorialFlags(): TutorialFlags { public getTutorialFlags(): TutorialFlags {
const key = getDataTypeKey(GameDataType.TUTORIALS); const key = getDataTypeKey(GameDataType.TUTORIALS);
const ret: TutorialFlags = {}; const ret: TutorialFlags = Object.values(Tutorial).reduce((acc, tutorial) => {
Object.values(Tutorial) acc[Tutorial[tutorial]] = false;
.map(tutorial => tutorial as Tutorial) return acc;
.forEach(tutorial => (ret[Tutorial[tutorial]] = false)); }, {} as TutorialFlags);
if (!localStorage.hasOwnProperty(key)) { if (!localStorage.hasOwnProperty(key)) {
return ret; return ret;
@ -983,6 +985,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> { loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {

View File

@ -31,6 +31,11 @@ import { toTitleCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
const DISCARD_BUTTON_X = 60;
const DISCARD_BUTTON_X_DOUBLES = 64;
const DISCARD_BUTTON_Y = -73;
const DISCARD_BUTTON_Y_DOUBLES = -58;
const defaultMessage = i18next.t("partyUiHandler:choosePokemon"); const defaultMessage = i18next.t("partyUiHandler:choosePokemon");
/** /**
@ -301,7 +306,7 @@ export class PartyUiHandler extends MessageUiHandler {
const partyMessageText = addTextObject(10, 8, defaultMessage, TextStyle.WINDOW, { maxLines: 2 }); const partyMessageText = addTextObject(10, 8, defaultMessage, TextStyle.WINDOW, { maxLines: 2 });
partyMessageText.setName("text-party-msg"); partyMessageText.setName("text-party-msg");
partyMessageText.setOrigin(0, 0); partyMessageText.setOrigin(0);
partyMessageBoxContainer.add(partyMessageText); partyMessageBoxContainer.add(partyMessageText);
this.message = partyMessageText; this.message = partyMessageText;
@ -317,10 +322,8 @@ export class PartyUiHandler extends MessageUiHandler {
this.iconAnimHandler = new PokemonIconAnimHandler(); this.iconAnimHandler = new PokemonIconAnimHandler();
this.iconAnimHandler.setup(); this.iconAnimHandler.setup();
const partyDiscardModeButton = new PartyDiscardModeButton(60, -globalScene.game.canvas.height / 15 - 1, this); const partyDiscardModeButton = new PartyDiscardModeButton(DISCARD_BUTTON_X, DISCARD_BUTTON_Y, this);
partyContainer.add(partyDiscardModeButton); partyContainer.add(partyDiscardModeButton);
this.partyDiscardModeButton = partyDiscardModeButton; this.partyDiscardModeButton = partyDiscardModeButton;
// prepare move overlay // prepare move overlay
@ -1233,7 +1236,7 @@ export class PartyUiHandler extends MessageUiHandler {
} }
if (!this.optionsCursorObj) { if (!this.optionsCursorObj) {
this.optionsCursorObj = globalScene.add.image(0, 0, "cursor"); this.optionsCursorObj = globalScene.add.image(0, 0, "cursor");
this.optionsCursorObj.setOrigin(0, 0); this.optionsCursorObj.setOrigin(0);
this.optionsContainer.add(this.optionsCursorObj); this.optionsContainer.add(this.optionsCursorObj);
} }
this.optionsCursorObj.setPosition( this.optionsCursorObj.setPosition(
@ -1605,7 +1608,7 @@ export class PartyUiHandler extends MessageUiHandler {
optionText.setColor("#40c8f8"); optionText.setColor("#40c8f8");
optionText.setShadowColor("#006090"); optionText.setShadowColor("#006090");
} }
optionText.setOrigin(0, 0); optionText.setOrigin(0);
/** For every item that has stack bigger than 1, display the current quantity selection */ /** For every item that has stack bigger than 1, display the current quantity selection */
const itemModifiers = this.getItemModifiers(pokemon); const itemModifiers = this.getItemModifiers(pokemon);
@ -1802,6 +1805,7 @@ class PartySlot extends Phaser.GameObjects.Container {
private selected: boolean; private selected: boolean;
private transfer: boolean; private transfer: boolean;
private slotIndex: number; private slotIndex: number;
private isBenched: boolean;
private pokemon: PlayerPokemon; private pokemon: PlayerPokemon;
private slotBg: Phaser.GameObjects.Image; private slotBg: Phaser.GameObjects.Image;
@ -1812,6 +1816,7 @@ class PartySlot extends Phaser.GameObjects.Container {
public slotHpText: Phaser.GameObjects.Text; public slotHpText: Phaser.GameObjects.Text;
public slotDescriptionLabel: Phaser.GameObjects.Text; // this is used to show text instead of the HP bar i.e. for showing "Able"/"Not Able" for TMs when you try to learn them public slotDescriptionLabel: Phaser.GameObjects.Text; // this is used to show text instead of the HP bar i.e. for showing "Able"/"Not Able" for TMs when you try to learn them
private slotBgKey: string;
private pokemonIcon: Phaser.GameObjects.Container; private pokemonIcon: Phaser.GameObjects.Container;
private iconAnimHandler: PokemonIconAnimHandler; private iconAnimHandler: PokemonIconAnimHandler;
@ -1822,19 +1827,34 @@ class PartySlot extends Phaser.GameObjects.Container {
partyUiMode: PartyUiMode, partyUiMode: PartyUiMode,
tmMoveId: MoveId, tmMoveId: MoveId,
) { ) {
super( const isBenched = slotIndex >= globalScene.currentBattle.getBattlerCount();
globalScene, const isDoubleBattle = globalScene.currentBattle.double;
slotIndex >= globalScene.currentBattle.getBattlerCount() ? 230.5 : 64, const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD;
slotIndex >= globalScene.currentBattle.getBattlerCount()
? -184 + /*
(globalScene.currentBattle.double ? -40 : 0) + * Here we determine the position of the slot.
(28 + (globalScene.currentBattle.double ? 8 : 0)) * slotIndex * The x coordinate depends on whether the pokemon is on the field or in the bench.
: partyUiMode === PartyUiMode.MODIFIER_TRANSFER * The y coordinate depends on various factors, such as the number of pokémon on the field,
? -124 + (globalScene.currentBattle.double ? -20 : 0) + slotIndex * 55 * and whether the transfer/discard button is also on the screen.
: -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64, */
); const slotPositionX = isBenched ? 143 : 9;
let slotPositionY: number;
if (isBenched) {
slotPositionY = -196 + (isDoubleBattle ? -40 : 0);
slotPositionY += (28 + (isDoubleBattle ? 8 : 0)) * slotIndex;
} else {
slotPositionY = -148.5;
if (isDoubleBattle) {
slotPositionY += isItemManageMode ? -20 : -8;
}
slotPositionY += (isItemManageMode ? (isDoubleBattle ? 47 : 55) : 64) * slotIndex;
}
super(globalScene, slotPositionX, slotPositionY);
this.slotIndex = slotIndex; this.slotIndex = slotIndex;
this.isBenched = isBenched;
this.pokemon = pokemon; this.pokemon = pokemon;
this.iconAnimHandler = iconAnimHandler; this.iconAnimHandler = iconAnimHandler;
@ -1848,27 +1868,75 @@ class PartySlot extends Phaser.GameObjects.Container {
setup(partyUiMode: PartyUiMode, tmMoveId: MoveId) { setup(partyUiMode: PartyUiMode, tmMoveId: MoveId) {
const currentLanguage = i18next.resolvedLanguage ?? "en"; const currentLanguage = i18next.resolvedLanguage ?? "en";
const offsetJa = currentLanguage === "ja"; const offsetJa = currentLanguage === "ja";
const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD;
const battlerCount = globalScene.currentBattle.getBattlerCount(); this.slotBgKey = this.isBenched
? "party_slot"
: isItemManageMode && globalScene.currentBattle.double
? "party_slot_main_short"
: "party_slot_main";
const fullSlotBgKey = this.pokemon.hp ? this.slotBgKey : `${this.slotBgKey}${"_fnt"}`;
this.slotBg = globalScene.add.sprite(0, 0, this.slotBgKey, fullSlotBgKey);
this.slotBg.setOrigin(0);
this.add(this.slotBg);
const slotKey = `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`; const genderSymbol = getGenderSymbol(this.pokemon.getGender(true));
const isFusion = this.pokemon.isFusion();
const slotBg = globalScene.add.sprite(0, 0, slotKey, `${slotKey}${this.pokemon.hp ? "" : "_fnt"}`); // Here we define positions and offsets
this.slotBg = slotBg; // Base values are for the active pokemon; they are changed for benched pokemon,
// or for active pokemon if in a double battle in item management mode.
this.add(slotBg); // icon position relative to slot background
let slotPb = { x: 4, y: 4 };
// name position relative to slot background
let namePosition = { x: 24, y: 10 + (offsetJa ? 2 : 0) };
// maximum allowed length of name; must accomodate fusion symbol
let maxNameTextWidth = 76 - (isFusion ? 8 : 0);
// "Lv." label position relative to slot background
let levelLabelPosition = { x: 24 + 8, y: 10 + 12 };
// offset from "Lv." to the level number; should not be changed.
const levelTextToLevelLabelOffset = { x: 9, y: offsetJa ? 1.5 : 0 };
// offests from "Lv." to gender, spliced and status icons, these depend on the type of slot.
let genderTextToLevelLabelOffset = { x: 68 - (isFusion ? 8 : 0), y: -9 };
let splicedIconToLevelLabelOffset = { x: 68, y: 3.5 - 12 };
let statusIconToLevelLabelOffset = { x: 55, y: 0 };
// offset from the name to the shiny icon (on the left); should not be changed.
const shinyIconToNameOffset = { x: -9, y: 3 };
// hp bar position relative to slot background
let hpBarPosition = { x: 8, y: 31 };
// offsets of hp bar overlay (showing the remaining hp) and number; should not be changed.
const hpOverlayToBarOffset = { x: 16, y: 2 };
const hpTextToBarOffset = { x: -3, y: -2 + (offsetJa ? 2 : 0) };
// description position relative to slot background
let descriptionLabelPosition = { x: 32, y: 46 };
const slotPb = globalScene.add.sprite( // If in item management mode, the active slots are shorter
this.slotIndex >= battlerCount ? -85.5 : -51, if (isItemManageMode && globalScene.currentBattle.double && !this.isBenched) {
this.slotIndex >= battlerCount ? 0 : -20.5, namePosition.y -= 8;
"party_pb", levelLabelPosition.y -= 8;
); hpBarPosition.y -= 8;
this.slotPb = slotPb; descriptionLabelPosition.y -= 8;
}
this.add(slotPb); // Benched slots have significantly different parameters
if (this.isBenched) {
slotPb = { x: 2, y: 12 };
namePosition = { x: 21, y: 2 + (offsetJa ? 2 : 0) };
maxNameTextWidth = 52;
levelLabelPosition = { x: 21 + 8, y: 2 + 12 };
genderTextToLevelLabelOffset = { x: 36, y: 0 };
splicedIconToLevelLabelOffset = { x: 36 + (genderSymbol ? 8 : 0), y: 0.5 };
statusIconToLevelLabelOffset = { x: 43, y: 0 };
hpBarPosition = { x: 72, y: 6 };
descriptionLabelPosition = { x: 94, y: 16 };
}
this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, slotPb.x, slotPb.y, 0.5, 0.5, true); this.slotPb = globalScene.add.sprite(0, 0, "party_pb");
this.slotPb.setPosition(slotPb.x, slotPb.y);
this.add(this.slotPb);
this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, this.slotPb.x, this.slotPb.y, 0.5, 0.5, true);
this.add(this.pokemonIcon); this.add(this.pokemonIcon);
this.iconAnimHandler.addOrUpdate(this.pokemonIcon, PokemonIconAnimMode.PASSIVE); this.iconAnimHandler.addOrUpdate(this.pokemonIcon, PokemonIconAnimMode.PASSIVE);
@ -1882,7 +1950,7 @@ class PartySlot extends Phaser.GameObjects.Container {
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.PARTY); const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.PARTY);
nameTextWidth = nameSizeTest.displayWidth; nameTextWidth = nameSizeTest.displayWidth;
while (nameTextWidth > (this.slotIndex >= battlerCount ? 52 : 76 - (this.pokemon.fusionSpecies ? 8 : 0))) { while (nameTextWidth > maxNameTextWidth) {
displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`; displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`;
nameSizeTest.setText(displayName); nameSizeTest.setText(displayName);
nameTextWidth = nameSizeTest.displayWidth; nameTextWidth = nameSizeTest.displayWidth;
@ -1891,78 +1959,59 @@ class PartySlot extends Phaser.GameObjects.Container {
nameSizeTest.destroy(); nameSizeTest.destroy();
this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY); this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY);
this.slotName.setPositionRelative( this.slotName.setPositionRelative(this.slotBg, namePosition.x, namePosition.y);
slotBg, this.slotName.setOrigin(0);
this.slotIndex >= battlerCount ? 21 : 24,
(this.slotIndex >= battlerCount ? 2 : 10) + (offsetJa ? 2 : 0),
);
this.slotName.setOrigin(0, 0);
const slotLevelLabel = globalScene.add.image(0, 0, "party_slot_overlay_lv"); const slotLevelLabel = globalScene.add
slotLevelLabel.setPositionRelative( .image(0, 0, "party_slot_overlay_lv")
slotBg, .setPositionRelative(this.slotBg, levelLabelPosition.x, levelLabelPosition.y)
(this.slotIndex >= battlerCount ? 21 : 24) + 8, .setOrigin(0);
(this.slotIndex >= battlerCount ? 2 : 10) + 12,
);
slotLevelLabel.setOrigin(0, 0);
const slotLevelText = addTextObject( const slotLevelText = addTextObject(
0, 0,
0, 0,
this.pokemon.level.toString(), this.pokemon.level.toString(),
this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED, this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED,
); )
slotLevelText.setPositionRelative(slotLevelLabel, 9, offsetJa ? 1.5 : 0); .setPositionRelative(slotLevelLabel, levelTextToLevelLabelOffset.x, levelTextToLevelLabelOffset.y)
slotLevelText.setOrigin(0, 0.25); .setOrigin(0, 0.25);
slotInfoContainer.add([this.slotName, slotLevelLabel, slotLevelText]); slotInfoContainer.add([this.slotName, slotLevelLabel, slotLevelText]);
const genderSymbol = getGenderSymbol(this.pokemon.getGender(true));
if (genderSymbol) { if (genderSymbol) {
const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY); const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY)
slotGenderText.setColor(getGenderColor(this.pokemon.getGender(true))); .setColor(getGenderColor(this.pokemon.getGender(true)))
slotGenderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true)); .setShadowColor(getGenderColor(this.pokemon.getGender(true), true))
if (this.slotIndex >= battlerCount) { .setPositionRelative(slotLevelLabel, genderTextToLevelLabelOffset.x, genderTextToLevelLabelOffset.y)
slotGenderText.setPositionRelative(slotLevelLabel, 36, 0); .setOrigin(0, 0.25);
} else {
slotGenderText.setPositionRelative(this.slotName, 76 - (this.pokemon.fusionSpecies ? 8 : 0), 3);
}
slotGenderText.setOrigin(0, 0.25);
slotInfoContainer.add(slotGenderText); slotInfoContainer.add(slotGenderText);
} }
if (this.pokemon.fusionSpecies) { if (isFusion) {
const splicedIcon = globalScene.add.image(0, 0, "icon_spliced"); const splicedIcon = globalScene.add
splicedIcon.setScale(0.5); .image(0, 0, "icon_spliced")
splicedIcon.setOrigin(0, 0); .setScale(0.5)
if (this.slotIndex >= battlerCount) { .setOrigin(0)
splicedIcon.setPositionRelative(slotLevelLabel, 36 + (genderSymbol ? 8 : 0), 0.5); .setPositionRelative(slotLevelLabel, splicedIconToLevelLabelOffset.x, splicedIconToLevelLabelOffset.y);
} else {
splicedIcon.setPositionRelative(this.slotName, 76, 3.5);
}
slotInfoContainer.add(splicedIcon); slotInfoContainer.add(splicedIcon);
} }
if (this.pokemon.status) { if (this.pokemon.status) {
const statusIndicator = globalScene.add.sprite(0, 0, getLocalizedSpriteKey("statuses")); const statusIndicator = globalScene.add
statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase()); .sprite(0, 0, getLocalizedSpriteKey("statuses"))
statusIndicator.setOrigin(0, 0); .setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase())
statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0); .setOrigin(0)
.setPositionRelative(slotLevelLabel, statusIconToLevelLabelOffset.x, statusIconToLevelLabelOffset.y);
slotInfoContainer.add(statusIndicator); slotInfoContainer.add(statusIndicator);
} }
if (this.pokemon.isShiny()) { if (this.pokemon.isShiny()) {
const doubleShiny = this.pokemon.isDoubleShiny(false); const doubleShiny = this.pokemon.isDoubleShiny(false);
const shinyStar = globalScene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`); const shinyStar = globalScene.add
shinyStar.setOrigin(0, 0); .image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`)
shinyStar.setPositionRelative(this.slotName, -9, 3); .setOrigin(0)
shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant())); .setPositionRelative(this.slotName, shinyIconToNameOffset.x, shinyIconToNameOffset.y)
.setTint(getVariantTint(this.pokemon.getBaseVariant()));
slotInfoContainer.add(shinyStar); slotInfoContainer.add(shinyStar);
if (doubleShiny) { if (doubleShiny) {
@ -1971,50 +2020,38 @@ class PartySlot extends Phaser.GameObjects.Container {
.setOrigin(0) .setOrigin(0)
.setPosition(shinyStar.x, shinyStar.y) .setPosition(shinyStar.x, shinyStar.y)
.setTint(getVariantTint(this.pokemon.fusionVariant)); .setTint(getVariantTint(this.pokemon.fusionVariant));
slotInfoContainer.add(fusionShinyStar); slotInfoContainer.add(fusionShinyStar);
} }
} }
this.slotHpBar = globalScene.add.image(0, 0, "party_slot_hp_bar"); this.slotHpBar = globalScene.add
this.slotHpBar.setPositionRelative( .image(0, 0, "party_slot_hp_bar")
slotBg, .setOrigin(0)
this.slotIndex >= battlerCount ? 72 : 8, .setVisible(false)
this.slotIndex >= battlerCount ? 6 : 31, .setPositionRelative(this.slotBg, hpBarPosition.x, hpBarPosition.y);
);
this.slotHpBar.setOrigin(0, 0);
this.slotHpBar.setVisible(false);
const hpRatio = this.pokemon.getHpRatio(); const hpRatio = this.pokemon.getHpRatio();
this.slotHpOverlay = globalScene.add.sprite( this.slotHpOverlay = globalScene.add
0, .sprite(0, 0, "party_slot_hp_overlay", hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low")
0, .setOrigin(0)
"party_slot_hp_overlay", .setPositionRelative(this.slotHpBar, hpOverlayToBarOffset.x, hpOverlayToBarOffset.y)
hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low", .setScale(hpRatio, 1)
); .setVisible(false);
this.slotHpOverlay.setPositionRelative(this.slotHpBar, 16, 2);
this.slotHpOverlay.setOrigin(0, 0);
this.slotHpOverlay.setScale(hpRatio, 1);
this.slotHpOverlay.setVisible(false);
this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY); this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY)
this.slotHpText.setPositionRelative( .setOrigin(1, 0)
.setPositionRelative(
this.slotHpBar, this.slotHpBar,
this.slotHpBar.width - 3, this.slotHpBar.width + hpTextToBarOffset.x,
this.slotHpBar.height - 2 + (offsetJa ? 2 : 0), this.slotHpBar.height + hpTextToBarOffset.y,
); ) // TODO: annoying because it contains the width
this.slotHpText.setOrigin(1, 0); .setVisible(false);
this.slotHpText.setVisible(false);
this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE); this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE)
this.slotDescriptionLabel.setPositionRelative( .setOrigin(0, 1)
slotBg, .setVisible(false)
this.slotIndex >= battlerCount ? 94 : 32, .setPositionRelative(this.slotBg, descriptionLabelPosition.x, descriptionLabelPosition.y);
this.slotIndex >= battlerCount ? 16 : 46,
);
this.slotDescriptionLabel.setOrigin(0, 1);
this.slotDescriptionLabel.setVisible(false);
slotInfoContainer.add([this.slotHpBar, this.slotHpOverlay, this.slotHpText, this.slotDescriptionLabel]); slotInfoContainer.add([this.slotHpBar, this.slotHpOverlay, this.slotHpText, this.slotDescriptionLabel]);
@ -2076,10 +2113,9 @@ class PartySlot extends Phaser.GameObjects.Container {
} }
private updateSlotTexture(): void { private updateSlotTexture(): void {
const battlerCount = globalScene.currentBattle.getBattlerCount();
this.slotBg.setTexture( this.slotBg.setTexture(
`party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`, this.slotBgKey,
`party_slot${this.slotIndex >= battlerCount ? "" : "_main"}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`, `${this.slotBgKey}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`,
); );
} }
} }
@ -2198,10 +2234,6 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container {
this.discardIcon.setVisible(false); this.discardIcon.setVisible(false);
this.textBox.setVisible(true); this.textBox.setVisible(true);
this.textBox.setText(i18next.t("partyUiHandler:TRANSFER")); this.textBox.setText(i18next.t("partyUiHandler:TRANSFER"));
this.setPosition(
globalScene.currentBattle.double ? 64 : 60,
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
);
this.transferIcon.displayWidth = this.textBox.text.length * 9 + 3; this.transferIcon.displayWidth = this.textBox.text.length * 9 + 3;
break; break;
case PartyUiMode.DISCARD: case PartyUiMode.DISCARD:
@ -2209,13 +2241,13 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container {
this.discardIcon.setVisible(true); this.discardIcon.setVisible(true);
this.textBox.setVisible(true); this.textBox.setVisible(true);
this.textBox.setText(i18next.t("partyUiHandler:DISCARD")); this.textBox.setText(i18next.t("partyUiHandler:DISCARD"));
this.setPosition(
globalScene.currentBattle.double ? 64 : 60,
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
);
this.discardIcon.displayWidth = this.textBox.text.length * 9 + 3; this.discardIcon.displayWidth = this.textBox.text.length * 9 + 3;
break; break;
} }
this.setPosition(
globalScene.currentBattle.double ? DISCARD_BUTTON_X_DOUBLES : DISCARD_BUTTON_X,
globalScene.currentBattle.double ? DISCARD_BUTTON_Y_DOUBLES : DISCARD_BUTTON_Y,
);
} }
clear() { clear() {

View 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;
}
}

View File

@ -26,6 +26,7 @@ import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text";
import { UiHandler } from "#ui/ui-handler"; import { UiHandler } from "#ui/ui-handler";
import { addWindow } from "#ui/ui-theme"; import { addWindow } from "#ui/ui-theme";
import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common"; import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle"; import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle";
@ -207,6 +208,10 @@ export class RunInfoUiHandler extends UiHandler {
headerText.setOrigin(0, 0); headerText.setOrigin(0, 0);
headerText.setPositionRelative(headerBg, 8, 4); headerText.setPositionRelative(headerBg, 8, 4);
this.runContainer.add(headerText); 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")); rules.push(i18next.t("challenges:inverseBattle.shortName"));
break; break;
default: { default: {
const localizationKey = Challenges[this.runInfo.challenges[i].id] const localizationKey = toCamelCase(Challenges[this.runInfo.challenges[i].id]);
.split("_")
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
.join("");
rules.push(i18next.t(`challenges:${localizationKey}.name`)); rules.push(i18next.t(`challenges:${localizationKey}.name`));
break; break;
} }

View File

@ -1,12 +1,14 @@
import { GameMode } from "#app/game-mode"; import { GameMode } from "#app/game-mode";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { GameModes } from "#enums/game-modes";
import { TextStyle } from "#enums/text-style"; import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` // biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
import * as Modifier from "#modifiers/modifier"; import * as Modifier from "#modifiers/modifier";
import type { SessionSaveData } from "#system/game-data"; import type { SessionSaveData } from "#system/game-data";
import type { PokemonData } from "#system/pokemon-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 { MessageUiHandler } from "#ui/message-ui-handler";
import { RunDisplayMode } from "#ui/run-info-ui-handler"; import { RunDisplayMode } from "#ui/run-info-ui-handler";
import { addTextObject } from "#ui/text"; import { addTextObject } from "#ui/text";
@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro
import i18next from "i18next"; import i18next from "i18next";
const SESSION_SLOTS_COUNT = 5; const SESSION_SLOTS_COUNT = 5;
const SLOTS_ON_SCREEN = 3; const SLOTS_ON_SCREEN = 2;
export enum SaveSlotUiMode { export enum SaveSlotUiMode {
LOAD, LOAD,
@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
private uiMode: SaveSlotUiMode; private uiMode: SaveSlotUiMode;
private saveSlotSelectCallback: SaveSlotSelectCallback | null; private saveSlotSelectCallback: SaveSlotSelectCallback | null;
protected manageDataConfig: OptionSelectConfig;
private scrollCursor = 0; private scrollCursor = 0;
@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
processInput(button: Button): boolean { processInput(button: Button): boolean {
const ui = this.getUi(); const ui = this.getUi();
const manageDataOptions: any[] = [];
let success = false; let success = false;
let error = false; let error = false;
@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
const originalCallback = this.saveSlotSelectCallback; const originalCallback = this.saveSlotSelectCallback;
if (button === Button.ACTION) { if (button === Button.ACTION) {
const cursor = this.cursor + this.scrollCursor; 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; error = true;
} else { } else {
switch (this.uiMode) { switch (this.uiMode) {
case SaveSlotUiMode.LOAD: case SaveSlotUiMode.LOAD:
this.saveSlotSelectCallback = null; if (!sessionSlot.malformed) {
manageDataOptions.push({
label: i18next.t("menu:loadGame"),
handler: () => {
globalScene.ui.revertMode();
originalCallback?.(cursor); 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; break;
case SaveSlotUiMode.SAVE: { case SaveSlotUiMode.SAVE: {
const saveAndCallback = () => { const saveAndCallback = () => {
const originalCallback = this.saveSlotSelectCallback; const originalCallback = this.saveSlotSelectCallback;
@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
} }
} else { } else {
this.saveSlotSelectCallback = null; this.saveSlotSelectCallback = null;
ui.showText("", 0);
originalCallback?.(-1); originalCallback?.(-1);
success = true; success = true;
} }
@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
this.cursorObj = globalScene.add.container(0, 0); this.cursorObj = globalScene.add.container(0, 0);
const cursorBox = globalScene.add.nineslice( const cursorBox = globalScene.add.nineslice(
0, 0,
0, 15,
"select_cursor_highlight_thick", "select_cursor_highlight_thick",
undefined, undefined,
296, 294,
44, this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60,
6, 6,
6, 6,
6, 6,
6, 6,
); );
const rightArrow = globalScene.add.image(0, 0, "cursor"); const rightArrow = globalScene.add.image(0, 0, "cursor");
rightArrow.setPosition(160, 0); rightArrow.setPosition(160, 15);
rightArrow.setName("rightArrow"); rightArrow.setName("rightArrow");
this.cursorObj.add([cursorBox, rightArrow]); this.cursorObj.add([cursorBox, rightArrow]);
this.sessionSlotsContainer.add(this.cursorObj); this.sessionSlotsContainer.add(this.cursorObj);
} }
const cursorPosition = cursor + this.scrollCursor; const cursorPosition = cursor + this.scrollCursor;
const cursorIncrement = cursorPosition * 56; const cursorIncrement = cursorPosition * 76;
if (this.sessionSlots[cursorPosition] && this.cursorObj) { 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. // 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. // Only session slots with session data will move leftwards and have a visible arrow.
if (!hasData) { if (!hasData) {
this.cursorObj.setPosition(151, 26 + cursorIncrement); this.cursorObj.setPosition(151, 20 + cursorIncrement);
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement); this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
} else { } else {
this.cursorObj.setPosition(145, 26 + cursorIncrement); this.cursorObj.setPosition(145, 20 + cursorIncrement);
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement); this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
} }
this.setArrowVisibility(hasData); this.setArrowVisibility(hasData);
@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
revertSessionSlot(slotIndex: number): void { revertSessionSlot(slotIndex: number): void {
const sessionSlot = this.sessionSlots[slotIndex]; const sessionSlot = this.sessionSlots[slotIndex];
if (sessionSlot) { 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); this.setCursor(this.cursor, prevSlotIndex);
globalScene.tweens.add({ globalScene.tweens.add({
targets: this.sessionSlotsContainer, targets: this.sessionSlotsContainer,
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor, y: this.sessionSlotsContainerInitialY - 76 * scrollCursor,
duration: fixedInt(325), duration: fixedInt(325),
ease: "Sine.easeInOut", ease: "Sine.easeInOut",
}); });
@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
class SessionSlot extends Phaser.GameObjects.Container { class SessionSlot extends Phaser.GameObjects.Container {
public slotId: number; public slotId: number;
public hasData: boolean; 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; private loadingLabel: Phaser.GameObjects.Text;
public saveData: SessionSaveData; public saveData: SessionSaveData;
constructor(slotId: number) { constructor(slotId: number) {
super(globalScene, 0, slotId * 56); super(globalScene, 0, slotId * 76);
this.slotId = slotId; this.slotId = slotId;
@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container {
} }
setup() { setup() {
const slotWindow = addWindow(0, 0, 304, 52); this.slotWindow = addWindow(0, 0, 304, 70);
this.add(slotWindow); 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.loadingLabel.setOrigin(0.5, 0.5);
this.add(this.loadingLabel); 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) { async setupWithData(data: SessionSaveData) {
const hasName = data?.runNameText;
this.remove(this.loadingLabel, true); 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( const gameModeLabel = addTextObject(
8, 8,
5, 19,
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`, `${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
TextStyle.WINDOW, TextStyle.WINDOW,
); );
this.add(gameModeLabel); 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); 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); this.add(playTimeLabel);
const pokemonIconsContainer = globalScene.add.container(144, 4); const pokemonIconsContainer = globalScene.add.container(144, 16);
data.party.forEach((p: PokemonData, i: number) => { data.party.forEach((p: PokemonData, i: number) => {
const iconContainer = globalScene.add.container(26 * i, 0); const iconContainer = globalScene.add.container(26 * i, 0);
iconContainer.setScale(0.75); iconContainer.setScale(0.75);
@ -427,13 +594,9 @@ class SessionSlot extends Phaser.GameObjects.Container {
TextStyle.PARTY, TextStyle.PARTY,
{ fontSize: "54px", color: "#f8f8f8" }, { fontSize: "54px", color: "#f8f8f8" },
); );
text.setShadow(0, 0, undefined); text.setShadow(0, 0, undefined).setStroke("#424242", 14).setOrigin(1, 0);
text.setStroke("#424242", 14);
text.setOrigin(1, 0);
iconContainer.add(icon);
iconContainer.add(text);
iconContainer.add([icon, text]);
pokemonIconsContainer.add(iconContainer); pokemonIconsContainer.add(iconContainer);
pokemon.destroy(); pokemon.destroy();
@ -441,7 +604,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
this.add(pokemonIconsContainer); this.add(pokemonIconsContainer);
const modifierIconsContainer = globalScene.add.container(148, 30); const modifierIconsContainer = globalScene.add.container(148, 38);
modifierIconsContainer.setScale(0.5); modifierIconsContainer.setScale(0.5);
let visibleModifierIndex = 0; let visibleModifierIndex = 0;
for (const m of data.modifiers) { for (const m of data.modifiers) {
@ -464,20 +627,31 @@ class SessionSlot extends Phaser.GameObjects.Container {
load(): Promise<boolean> { load(): Promise<boolean> {
return new Promise<boolean>(resolve => { return new Promise<boolean>(resolve => {
globalScene.gameData.getSession(this.slotId).then(async sessionData => { globalScene.gameData
.getSession(this.slotId)
.then(async sessionData => {
// Ignore the results if the view was exited // Ignore the results if the view was exited
if (!this.active) { if (!this.active) {
return; return;
} }
this.hasData = !!sessionData;
if (!sessionData) { if (!sessionData) {
this.hasData = false;
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty")); this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
resolve(false); resolve(false);
return; return;
} }
this.hasData = true;
this.saveData = sessionData; this.saveData = sessionData;
await this.setupWithData(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); resolve(true);
}); });
}); });

View File

@ -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 // 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 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); .filter(t => t);

View File

@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme";
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler"; import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
import { executeIf } from "#utils/common"; import { executeIf } from "#utils/common";
import i18next from "i18next"; import i18next from "i18next";
import { RenameRunFormUiHandler } from "./rename-run-ui-handler";
const transitionModes = [ const transitionModes = [
UiMode.SAVE_SLOT, UiMode.SAVE_SLOT,
@ -98,6 +99,7 @@ const noTransitionModes = [
UiMode.SESSION_RELOAD, UiMode.SESSION_RELOAD,
UiMode.UNAVAILABLE, UiMode.UNAVAILABLE,
UiMode.RENAME_POKEMON, UiMode.RENAME_POKEMON,
UiMode.RENAME_RUN,
UiMode.TEST_DIALOGUE, UiMode.TEST_DIALOGUE,
UiMode.AUTO_COMPLETE, UiMode.AUTO_COMPLETE,
UiMode.ADMIN, UiMode.ADMIN,
@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container {
new UnavailableModalUiHandler(), new UnavailableModalUiHandler(),
new GameChallengesUiHandler(), new GameChallengesUiHandler(),
new RenameFormUiHandler(), new RenameFormUiHandler(),
new RenameRunFormUiHandler(),
new RunHistoryUiHandler(), new RunHistoryUiHandler(),
new RunInfoUiHandler(), new RunInfoUiHandler(),
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE), new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),

View File

@ -130,7 +130,7 @@ declare module "vitest" {
* @param ppUsed - The numerical amount of PP that should have been consumed, * @param ppUsed - The numerical amount of PP that should have been consumed,
* or `all` to indicate the move should be _out_ of PP * or `all` to indicate the move should be _out_ of PP
* @remarks * @remarks
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE}, * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE},
* does not contain {@linkcode expectedMove} * does not contain {@linkcode expectedMove}
* or contains the desired move more than once, this will fail the test. * or contains the desired move more than once, this will fail the test.
*/ */

View File

@ -1,4 +1,3 @@
import { globalScene } from "#app/global-scene";
import { Status } from "#data/status-effect"; import { Status } from "#data/status-effect";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
@ -179,18 +178,13 @@ describe("Moves - Whirlwind", () => {
const eligibleEnemy = enemyParty.filter(p => p.hp > 0 && p.isAllowedInBattle()); const eligibleEnemy = enemyParty.filter(p => p.hp > 0 && p.isAllowedInBattle());
expect(eligibleEnemy.length).toBe(1); expect(eligibleEnemy.length).toBe(1);
// Spy on the queueMessage function
const queueSpy = vi.spyOn(globalScene.phaseManager, "queueMessage");
// Player uses Whirlwind; opponent uses Splash // Player uses Whirlwind; opponent uses Splash
game.move.select(MoveId.WHIRLWIND); game.move.select(MoveId.WHIRLWIND);
await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
// Verify that the failure message is displayed for Whirlwind const player = game.field.getPlayerPokemon();
expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But it failed")); expect(player).toHaveUsedMove({ move: MoveId.WHIRLWIND, result: MoveResult.FAIL });
// Verify the opponent's Splash message
expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But nothing happened!"));
}); });
it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => { it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => {

View File

@ -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 () => { 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); 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()); const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal());
await runMysteryEncounterToEnd(game, 1); await runMysteryEncounterToEnd(game, 1);

View 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();
});
});
});

View File

@ -224,7 +224,7 @@ export class GameManager {
// This will consider all battle entry dialog as seens and skip them // This will consider all battle entry dialog as seens and skip them
vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true); vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) { if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0) {
this.removeEnemyHeldItems(); this.removeEnemyHeldItems();
} }

View File

@ -50,7 +50,7 @@ export class ChallengeModeHelper extends GameManagerHelper {
}); });
await this.game.phaseInterceptor.run(EncounterPhase); await this.game.phaseInterceptor.run(EncounterPhase);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
this.game.removeEnemyHeldItems(); this.game.removeEnemyHeldItems();
} }
} }

View File

@ -53,7 +53,7 @@ export class ClassicModeHelper extends GameManagerHelper {
}); });
await this.game.phaseInterceptor.to(EncounterPhase); await this.game.phaseInterceptor.to(EncounterPhase);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
this.game.removeEnemyHeldItems(); this.game.removeEnemyHeldItems();
} }
} }

View File

@ -37,7 +37,7 @@ export class DailyModeHelper extends GameManagerHelper {
await this.game.phaseInterceptor.to(EncounterPhase); await this.game.phaseInterceptor.to(EncounterPhase);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
this.game.removeEnemyHeldItems(); this.game.removeEnemyHeldItems();
} }
} }

View File

@ -228,8 +228,8 @@ export class MoveHelper extends GameManagerHelper {
console.warn("Player moveset override disabled due to use of `game.move.changeMoveset`!"); console.warn("Player moveset override disabled due to use of `game.move.changeMoveset`!");
} }
} else { } else {
if (coerceArray(Overrides.OPP_MOVESET_OVERRIDE).length > 0) { if (coerceArray(Overrides.ENEMY_MOVESET_OVERRIDE).length > 0) {
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]); vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]);
console.warn("Enemy moveset override disabled due to use of `game.move.changeMoveset`!"); console.warn("Enemy moveset override disabled due to use of `game.move.changeMoveset`!");
} }
} }
@ -302,8 +302,8 @@ export class MoveHelper extends GameManagerHelper {
(this.game.scene.phaseManager.getCurrentPhase() as EnemyCommandPhase).getFieldIndex() (this.game.scene.phaseManager.getCurrentPhase() as EnemyCommandPhase).getFieldIndex()
]; ];
if ([Overrides.OPP_MOVESET_OVERRIDE].flat().length > 0) { if ([Overrides.ENEMY_MOVESET_OVERRIDE].flat().length > 0) {
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]); vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]);
console.warn( console.warn(
"Warning: `forceEnemyMove` overwrites the Pokemon's moveset and disables the enemy moveset override!", "Warning: `forceEnemyMove` overwrites the Pokemon's moveset and disables the enemy moveset override!",
); );

View File

@ -406,7 +406,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemySpecies(species: SpeciesId | number): this { public enemySpecies(species: SpeciesId | number): this {
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species); vi.spyOn(Overrides, "ENEMY_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Enemy Pokemon species set to ${SpeciesId[species]} (=${species})!`); this.log(`Enemy Pokemon species set to ${SpeciesId[species]} (=${species})!`);
return this; return this;
} }
@ -416,7 +416,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enableEnemyFusion(): this { public enableEnemyFusion(): this {
vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true); vi.spyOn(Overrides, "ENEMY_FUSION_OVERRIDE", "get").mockReturnValue(true);
this.log("Enemy Pokemon is a random fusion!"); this.log("Enemy Pokemon is a random fusion!");
return this; return this;
} }
@ -427,7 +427,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyFusionSpecies(species: SpeciesId | number): this { public enemyFusionSpecies(species: SpeciesId | number): this {
vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species); vi.spyOn(Overrides, "ENEMY_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Enemy Pokemon fusion species set to ${SpeciesId[species]} (=${species})!`); this.log(`Enemy Pokemon fusion species set to ${SpeciesId[species]} (=${species})!`);
return this; return this;
} }
@ -438,7 +438,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyAbility(ability: AbilityId): this { public enemyAbility(ability: AbilityId): this {
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability); vi.spyOn(Overrides, "ENEMY_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
this.log(`Enemy Pokemon ability set to ${AbilityId[ability]} (=${ability})!`); this.log(`Enemy Pokemon ability set to ${AbilityId[ability]} (=${ability})!`);
return this; return this;
} }
@ -449,7 +449,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyPassiveAbility(passiveAbility: AbilityId): this { public enemyPassiveAbility(passiveAbility: AbilityId): this {
vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility); vi.spyOn(Overrides, "ENEMY_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
this.log(`Enemy Pokemon PASSIVE ability set to ${AbilityId[passiveAbility]} (=${passiveAbility})!`); this.log(`Enemy Pokemon PASSIVE ability set to ${AbilityId[passiveAbility]} (=${passiveAbility})!`);
return this; return this;
} }
@ -460,7 +460,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this { public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this {
vi.spyOn(Overrides, "OPP_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility); vi.spyOn(Overrides, "ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility);
if (hasPassiveAbility === null) { if (hasPassiveAbility === null) {
this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!"); this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!");
} else { } else {
@ -475,7 +475,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyMoveset(moveset: MoveId | MoveId[]): this { public enemyMoveset(moveset: MoveId | MoveId[]): this {
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset); vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
moveset = coerceArray(moveset); moveset = coerceArray(moveset);
const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", "); const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", ");
this.log(`Enemy Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`); this.log(`Enemy Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`);
@ -488,7 +488,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyLevel(level: number): this { public enemyLevel(level: number): this {
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level); vi.spyOn(Overrides, "ENEMY_LEVEL_OVERRIDE", "get").mockReturnValue(level);
this.log(`Enemy Pokemon level set to ${level}!`); this.log(`Enemy Pokemon level set to ${level}!`);
return this; return this;
} }
@ -499,7 +499,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyStatusEffect(statusEffect: StatusEffect): this { public enemyStatusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); vi.spyOn(Overrides, "ENEMY_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
return this; return this;
} }
@ -510,7 +510,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyHeldItems(items: ModifierOverride[]): this { public enemyHeldItems(items: ModifierOverride[]): this {
vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); vi.spyOn(Overrides, "ENEMY_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
this.log("Enemy Pokemon held items set to:", items); this.log("Enemy Pokemon held items set to:", items);
return this; return this;
} }
@ -571,7 +571,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param variant - (Optional) The enemy's shiny {@linkcode Variant}. * @param variant - (Optional) The enemy's shiny {@linkcode Variant}.
*/ */
enemyShiny(shininess: boolean | null, variant?: Variant): this { enemyShiny(shininess: boolean | null, variant?: Variant): this {
vi.spyOn(Overrides, "OPP_SHINY_OVERRIDE", "get").mockReturnValue(shininess); vi.spyOn(Overrides, "ENEMY_SHINY_OVERRIDE", "get").mockReturnValue(shininess);
if (shininess === null) { if (shininess === null) {
this.log("Disabled enemy Pokemon shiny override!"); this.log("Disabled enemy Pokemon shiny override!");
} else { } else {
@ -579,7 +579,7 @@ export class OverridesHelper extends GameManagerHelper {
} }
if (variant !== undefined) { if (variant !== undefined) {
vi.spyOn(Overrides, "OPP_VARIANT_OVERRIDE", "get").mockReturnValue(variant); vi.spyOn(Overrides, "ENEMY_VARIANT_OVERRIDE", "get").mockReturnValue(variant);
this.log(`Set enemy shiny variant to be ${variant}!`); this.log(`Set enemy shiny variant to be ${variant}!`);
} }
return this; return this;
@ -594,7 +594,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this` * @returns `this`
*/ */
public enemyHealthSegments(healthSegments: number): this { public enemyHealthSegments(healthSegments: number): this {
vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments); vi.spyOn(Overrides, "ENEMY_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
this.log("Enemy Pokemon health segments set to:", healthSegments); this.log("Enemy Pokemon health segments set to:", healthSegments);
return this; return this;
} }

View File

@ -33,7 +33,7 @@ export function toHaveUsedPP(
}; };
} }
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE; const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.ENEMY_MOVESET_OVERRIDE;
if (coerceArray(override).length > 0) { if (coerceArray(override).length > 0) {
return { return {
pass: false, pass: false,