Merge remote-tracking branch 'upstream/beta' into future-sight

This commit is contained in:
Bertie690 2025-07-30 22:12:28 -04:00
commit 5d89e703f7
159 changed files with 3458 additions and 2248 deletions

View File

@ -4,6 +4,7 @@ on:
push:
branches:
- main
- beta
pull_request:
branches:
- main

View File

@ -44,4 +44,4 @@ jobs:
run: pnpm i
- name: Run tests
run: pnpm exec vitest --project ${{ inputs.project }} --no-isolate --shard=${{ inputs.shard }}/${{ inputs.totalShards }} ${{ !runner.debug && '--silent' || '' }}
run: pnpm test:silent --shard=${{ inputs.shard }}/${{ inputs.totalShards }}

View File

@ -11,6 +11,7 @@ on:
- beta
merge_group:
types: [checks_requested]
workflow_dispatch:
jobs:
check-path-change-filter:

View File

@ -11,17 +11,18 @@ _cfg: &cfg
ls:
<<: *cfg
src:
src: &src
<<: *cfg
.dir: kebab-case | regex:@types
.js: exists:0
src/system/version-migration/versions:
.ts: snake_case
<<: *cfg
test: *src
ignore:
- node_modules
- .vscode
- .github
- .git
- public
- dist

View File

@ -177,9 +177,10 @@
}
},
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes)
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes),
// as well as in all TS files in `scripts/` (which are assumed to be boilerplate templates).
{
"includes": ["**/src/overrides.ts", "**/src/enums/**/*"],
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts"],
"linter": {
"rules": {
"correctness": {
@ -189,7 +190,7 @@
}
},
{
"includes": ["**/src/overrides.ts"],
"includes": ["**/src/overrides.ts", "**/scripts/**/*.ts"],
"linter": {
"rules": {
"style": {

View File

@ -12,7 +12,7 @@
"test": "vitest run --no-isolate",
"test:cov": "vitest run --coverage --no-isolate",
"test:watch": "vitest watch --coverage --no-isolate",
"test:silent": "vitest run --silent --no-isolate",
"test:silent": "vitest run --silent='passed-only' --no-isolate",
"test:create": "node scripts/create-test/create-test.js",
"typecheck": "tsc --noEmit",
"eslint": "eslint --fix .",
@ -30,18 +30,19 @@
"@biomejs/biome": "2.0.0",
"@ls-lint/ls-lint": "2.3.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.16.3",
"@types/node": "^22.16.5",
"@vitest/coverage-istanbul": "^3.2.4",
"@vitest/expect": "^3.2.4",
"chalk": "^5.4.1",
"dependency-cruiser": "^16.10.4",
"inquirer": "^12.7.0",
"inquirer": "^12.8.2",
"jsdom": "^26.1.0",
"lefthook": "^1.12.2",
"msw": "^2.10.4",
"phaser3spectorjs": "^0.0.8",
"typedoc": "^0.28.7",
"typedoc": "^0.28.8",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite": "^7.0.6",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4",
"vitest-canvas-mock": "^0.3.3"

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit 362b2c4fcc20b31a7be6c2dab537055fbaeb247f
Subproject commit e2fbba17ea7a96068970ea98a8a84ed3e25b6f07

View File

@ -17,15 +17,20 @@ const version = "2.0.1";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, "..", "..");
const boilerplateFilePath = path.join(__dirname, "test-boilerplate.ts");
const choices = [
{ label: "Move", dir: "moves" },
{ label: "Ability", dir: "abilities" },
{ label: "Item", dir: "items" },
{ label: "Mystery Encounter", dir: "mystery-encounter/encounters" },
{ label: "Utils", dir: "utils" },
{ label: "UI", dir: "ui" },
];
const choices = /** @type {const} */ (["Move", "Ability", "Item", "Reward", "Mystery Encounter", "Utils", "UI"]);
/** @typedef {choices[number]} choiceType */
/** @satisfies {{[k in choiceType]: string}} */
const choicesToDirs = /** @type {const} */ ({
Move: "moves",
Ability: "abilities",
Item: "items",
Reward: "rewards",
"Mystery Encounter": "mystery-encounter/encounters",
Utils: "utils",
UI: "ui",
});
//#endregion
//#region Functions
@ -41,46 +46,47 @@ function getTestFolderPath(...folders) {
/**
* Prompts the user to select a type via list.
* @returns {Promise<{selectedOption: {label: string, dir: string}}>} the selected type
* @returns {Promise<choiceType>} the selected type
*/
async function promptTestType() {
const typeAnswer = await inquirer.prompt([
/** @type {choiceType | "EXIT"} */
const choice = await inquirer
.prompt([
{
type: "list",
name: "selectedOption",
message: "What type of test would you like to create?",
choices: [...choices.map(choice => ({ name: choice.label, value: choice })), "EXIT"],
choices: [...choices, "EXIT"],
},
]);
])
.then(ta => ta.selectedOption);
if (typeAnswer.selectedOption === "EXIT") {
if (choice === "EXIT") {
console.log("Exiting...");
return process.exit();
}
if (!choices.some(choice => choice.dir === typeAnswer.selectedOption.dir)) {
console.error(`Please provide a valid type: (${choices.map(choice => choice.label).join(", ")})!`);
return await promptTestType();
return process.exit(0);
}
return typeAnswer;
return choice;
}
/**
* Prompts the user to provide a file name.
* @param {string} selectedType
* @returns {Promise<{userInput: string}>} the selected file name
* @param {choiceType} selectedType The chosen string (used to display console logs)
* @returns {Promise<string>} the selected file name
*/
async function promptFileName(selectedType) {
/** @type {{userInput: string}} */
const fileNameAnswer = await inquirer.prompt([
/** @type {string} */
const fileNameAnswer = await inquirer
.prompt([
{
type: "input",
name: "userInput",
message: `Please provide the name of the ${selectedType}:`,
message: `Please provide the name of the ${selectedType}.`,
},
]);
])
.then(fa => fa.userInput);
if (!fileNameAnswer.userInput || fileNameAnswer.userInput.trim().length === 0) {
if (fileNameAnswer.trim().length === 0) {
console.error("Please provide a valid file name!");
return await promptFileName(selectedType);
}
@ -88,51 +94,66 @@ async function promptFileName(selectedType) {
return fileNameAnswer;
}
/**
* Obtain the path to the boilerplate file based on the current option.
* @param {choiceType} choiceType The choice selected
* @returns {string} The path to the boilerplate file
*/
function getBoilerplatePath(choiceType) {
switch (choiceType) {
// case "Reward":
// return path.join(__dirname, "boilerplates/reward.ts");
default:
return path.join(__dirname, "boilerplates/default.ts");
}
}
/**
* Runs the interactive test:create "CLI"
* @returns {Promise<void>}
*/
async function runInteractive() {
console.group(chalk.grey(`Create Test - v${version}\n`));
console.group(chalk.grey(`🧪 Create Test - v${version}\n`));
try {
const typeAnswer = await promptTestType();
const fileNameAnswer = await promptFileName(typeAnswer.selectedOption.label);
const choice = await promptTestType();
const fileNameAnswer = await promptFileName(choice);
const type = typeAnswer.selectedOption;
// Convert fileName from snake_case or camelCase to kebab-case
const fileName = fileNameAnswer.userInput
const fileName = fileNameAnswer
.replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes)
.replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case
.replace(/\s+/g, "-") // Replace spaces with dashes
.toLowerCase(); // Ensure all lowercase
// Format the description for the test case
// Format the description for the test case in Title Case
const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase());
const description = `${choice} - ${formattedName}`;
// Determine the directory based on the type
const dir = getTestFolderPath(type.dir);
const description = `${type.label} - ${formattedName}`;
const localDir = choicesToDirs[choice];
const absoluteDir = getTestFolderPath(localDir);
// Define the content template
const content = fs.readFileSync(boilerplateFilePath, "utf8").replace("{{description}}", description);
const content = fs.readFileSync(getBoilerplatePath(choice), "utf8").replace("{{description}}", description);
// Ensure the directory exists
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
if (!fs.existsSync(absoluteDir)) {
fs.mkdirSync(absoluteDir, { recursive: true });
}
// Create the file with the given name
const filePath = path.join(dir, `${fileName}.test.ts`);
const filePath = path.join(absoluteDir, `${fileName}.test.ts`);
if (fs.existsSync(filePath)) {
console.error(chalk.red.bold(`\n✗ File "${fileName}.test.ts" already exists!\n`));
console.error(chalk.red.bold(`✗ File "${fileName}.test.ts" already exists!\n`));
process.exit(1);
}
// Write the template content to the file
fs.writeFileSync(filePath, content, "utf8");
console.log(chalk.green.bold(`\n✔ File created at: test/${type.dir}/${fileName}.test.ts\n`));
console.log(chalk.green.bold(`✔ File created at: test/${localDir}/${fileName}.test.ts\n`));
console.groupEnd();
} catch (err) {
console.error(chalk.red("✗ Error: ", err.message));

View File

@ -2,7 +2,9 @@
// biome-ignore lint/performance/noNamespaceImport: This is how you import fs from node
import * as fs from "node:fs";
import { AES, enc } from "crypto-js";
import crypto_js from "crypto-js";
const { AES, enc } = crypto_js;
const SAVE_KEY = "x0i2O7WRiANTqPmZ";
@ -144,7 +146,7 @@ function main() {
process.exit(0);
}
writeToFile(destPath, decrypt);
writeToFile(args[1], decrypt);
}
main();

View File

@ -1,6 +1,6 @@
import type { ArenaTagTypeMap } from "#data/arena-tag";
import type { ArenaTagType } from "#enums/arena-tag-type";
import type { NonFunctionProperties } from "./type-helpers";
import type { NonFunctionProperties } from "#types/type-helpers";
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
export type ArenaTrapTagType =

119
src/@types/battler-tags.ts Normal file
View File

@ -0,0 +1,119 @@
// biome-ignore-start lint/correctness/noUnusedImports: Used in a TSDoc comment
import type { AbilityBattlerTag, BattlerTagTypeMap, SerializableBattlerTag, TypeBoostTag } from "#data/battler-tags";
import type { AbilityId } from "#enums/ability-id";
// biome-ignore-end lint/correctness/noUnusedImports: end
import type { BattlerTagType } from "#enums/battler-tag-type";
/**
* Subset of {@linkcode BattlerTagType}s that restrict the use of moves.
*/
export type MoveRestrictionBattlerTagType =
| BattlerTagType.THROAT_CHOPPED
| BattlerTagType.TORMENT
| BattlerTagType.TAUNT
| BattlerTagType.IMPRISON
| BattlerTagType.HEAL_BLOCK
| BattlerTagType.ENCORE
| BattlerTagType.DISABLED
| BattlerTagType.GORILLA_TACTICS;
/**
* Subset of {@linkcode BattlerTagType}s that block damage from moves.
*/
export type FormBlockDamageBattlerTagType = BattlerTagType.ICE_FACE | BattlerTagType.DISGUISE;
/**
* Subset of {@linkcode BattlerTagType}s that are related to trapping effects.
*/
export type TrappingBattlerTagType =
| BattlerTagType.BIND
| BattlerTagType.WRAP
| BattlerTagType.FIRE_SPIN
| BattlerTagType.WHIRLPOOL
| BattlerTagType.CLAMP
| BattlerTagType.SAND_TOMB
| BattlerTagType.MAGMA_STORM
| BattlerTagType.SNAP_TRAP
| BattlerTagType.THUNDER_CAGE
| BattlerTagType.INFESTATION
| BattlerTagType.INGRAIN
| BattlerTagType.OCTOLOCK
| BattlerTagType.NO_RETREAT;
/**
* Subset of {@linkcode BattlerTagType}s that are related to protection effects.
*/
export type ProtectionBattlerTagType = BattlerTagType.PROTECTED | BattlerTagType.SPIKY_SHIELD | DamageProtectedTagType;
/**
* Subset of {@linkcode BattlerTagType}s related to protection effects that block damage but not status moves.
*/
export type DamageProtectedTagType = ContactSetStatusProtectedTagType | ContactStatStageChangeProtectedTagType;
/**
* Subset of {@linkcode BattlerTagType}s related to protection effects that set a status effect on the attacker.
*/
export type ContactSetStatusProtectedTagType = BattlerTagType.BANEFUL_BUNKER | BattlerTagType.BURNING_BULWARK;
/**
* Subset of {@linkcode BattlerTagType}s related to protection effects that change stat stages of the attacker.
*/
export type ContactStatStageChangeProtectedTagType =
| BattlerTagType.KINGS_SHIELD
| BattlerTagType.SILK_TRAP
| BattlerTagType.OBSTRUCT;
/** Subset of {@linkcode BattlerTagType}s that provide the Endure effect */
export type EndureTagType = BattlerTagType.ENDURE_TOKEN | BattlerTagType.ENDURING;
/**
* Subset of {@linkcode BattlerTagType}s that are related to semi-invulnerable states.
*/
export type SemiInvulnerableTagType =
| BattlerTagType.FLYING
| BattlerTagType.UNDERGROUND
| BattlerTagType.UNDERWATER
| BattlerTagType.HIDDEN;
/**
* Subset of {@linkcode BattlerTagType}s corresponding to {@linkcode AbilityBattlerTag}s
*
* @remarks
* {@linkcode AbilityId.FLASH_FIRE | Flash Fire}'s {@linkcode BattlerTagType.FIRE_BOOST} is not included as it
* subclasses {@linkcode TypeBoostTag} and not `AbilityBattlerTag`.
*/
export type AbilityBattlerTagType =
| BattlerTagType.PROTOSYNTHESIS
| BattlerTagType.QUARK_DRIVE
| BattlerTagType.UNBURDEN
| BattlerTagType.SLOW_START
| BattlerTagType.TRUANT;
/**
* Subset of {@linkcode BattlerTagType}s related to abilities that boost the highest stat.
*/
export type HighestStatBoostTagType =
| BattlerTagType.QUARK_DRIVE // formatting
| BattlerTagType.PROTOSYNTHESIS;
/**
* Subset of {@linkcode BattlerTagType}s that are able to persist between turns and should therefore be serialized
*/
export type SerializableBattlerTagType = keyof {
[K in keyof BattlerTagTypeMap as BattlerTagTypeMap[K] extends SerializableBattlerTag
? K
: never]: BattlerTagTypeMap[K];
};
/**
* Subset of {@linkcode BattlerTagType}s that are not able to persist across waves and should therefore not be serialized
*/
export type NonSerializableBattlerTagType = Exclude<BattlerTagType, SerializableBattlerTagType>;
/**
* Dummy, typescript-only declaration to ensure that
* {@linkcode BattlerTagTypeMap} has an entry for all `BattlerTagType`s.
*
* If a battler tag is missing from the map, Typescript will throw an error on this statement.
*
* Does not actually exist at runtime, so it must not be used!
*/
declare const EnsureAllBattlerTagTypesAreMapped: BattlerTagTypeMap[BattlerTagType] & never;

View File

@ -1,18 +1,14 @@
import type { ObjectValues } from "#types/type-helpers";
/** Union type accepting any TS Enum or `const object`, with or without reverse mapping. */
export type EnumOrObject = Record<string | number, string | number>;
/**
* Utility type to extract the enum values from a `const object`,
* or convert an `enum` interface produced by `typeof Enum` into the union type representing its values.
*/
export type EnumValues<E> = E[keyof E];
/**
* Generic type constraint representing a TS numeric enum with reverse mappings.
* @example
* TSNumericEnum<typeof WeatherType>
*/
export type TSNumericEnum<T extends EnumOrObject> = number extends EnumValues<T> ? T : never;
export type TSNumericEnum<T extends EnumOrObject> = number extends ObjectValues<T> ? T : never;
/** Generic type constraint representing a non reverse-mapped TS enum or `const object`. */
export type NormalEnum<T extends EnumOrObject> = Exclude<T, TSNumericEnum<T>>;

View File

@ -6,8 +6,6 @@
import type { AbAttr } from "#abilities/ability";
// biome-ignore-end lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { EnumValues } from "#types/enum-types";
/**
* Exactly matches the type of the argument, preventing adding additional properties.
*
@ -37,16 +35,25 @@ export type Mutable<T> = {
};
/**
* Type helper to obtain the keys associated with a given value inside a `const object`.
* Type helper to obtain the keys associated with a given value inside an object.
* @typeParam O - The type of the object
* @typeParam V - The type of one of O's values
*/
export type InferKeys<O extends Record<keyof any, unknown>, V extends EnumValues<O>> = {
export type InferKeys<O extends object, V extends ObjectValues<O>> = {
[K in keyof O]: O[K] extends V ? K : never;
}[keyof O];
/**
* Type helper that matches any `Function` type. Equivalent to `Function`, but will not raise a warning from Biome.
* Utility type to obtain the values of a given object. \
* Functions similar to `keyof E`, except producing the values instead of the keys.
* @remarks
* This can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members.
*/
export type ObjectValues<E extends object> = E[keyof E];
/**
* Type helper that matches any `Function` type.
* Equivalent to `Function`, but will not raise a warning from Biome.
*/
export type AnyFn = (...args: any[]) => any;
@ -65,6 +72,7 @@ export type NonFunctionProperties<T> = {
/**
* Type helper to extract out non-function properties from a type, recursively applying to nested properties.
* This can be used to mimic the effects of JSON serialization and de-serialization on a given type.
*/
export type NonFunctionPropertiesRecursive<Class> = {
[K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array<infer U>
@ -75,3 +83,14 @@ export type NonFunctionPropertiesRecursive<Class> = {
};
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
/**
* Type helper that iterates through the fields of the type and coerces any `null` properties to `undefined` (including in union types).
*
* @remarks
* This is primarily useful when an object with nullable properties wants to be serialized and have its `null`
* properties coerced to `undefined`.
*/
export type CoerceNullPropertiesToUndefined<T extends object> = {
[K in keyof T]: null extends T[K] ? Exclude<T[K], null> | undefined : T[K];
};

View File

@ -8,20 +8,14 @@ import type { Variant } from "#sprites/variant";
* Data pertaining to a Pokemon's Illusion.
*/
export interface IllusionData {
basePokemon: {
/** The actual name of the Pokemon */
/** The name of pokemon featured in the illusion */
name: string;
/** The actual nickname of the Pokemon */
nickname: string;
/** Whether the base pokemon is shiny or not */
/** The nickname of the pokemon featured in the illusion */
nickname?: string;
/** Whether the pokemon featured in the illusion is shiny or not */
shiny: boolean;
/** The shiny variant of the base pokemon */
/** The variant of the pokemon featured in the illusion */
variant: Variant;
/** Whether the fusion species of the base pokemon is shiny or not */
fusionShiny: boolean;
/** The variant of the fusion species of the base pokemon */
fusionVariant: Variant;
};
/** The species of the illusion */
species: SpeciesId;
/** The formIndex of the illusion */
@ -34,6 +28,10 @@ export interface IllusionData {
fusionSpecies?: PokemonSpecies;
/** The fusionFormIndex of the illusion */
fusionFormIndex?: number;
/** Whether the fusion species of the pokemon featured in the illusion is shiny or not */
fusionShiny?: boolean;
/** The variant of the fusion species of the pokemon featured in the illusion */
fusionVariant?: Variant;
/** The fusionGender of the illusion if it's a fusion */
fusionGender?: Gender;
/** The level of the illusion (not used currently) */

View File

@ -3,6 +3,7 @@
import type { Pokemon } from "#field/pokemon";
import type { ModifierConstructorMap } from "#modifiers/modifier";
import type { ModifierType, WeightedModifierType } from "#modifiers/modifier-type";
import type { ObjectValues } from "#types/type-helpers";
export type ModifierTypeFunc = () => ModifierType;
export type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number;
@ -19,7 +20,7 @@ export type ModifierInstanceMap = {
/**
* Union type of all modifier constructors.
*/
export type ModifierClass = ModifierConstructorMap[keyof ModifierConstructorMap];
export type ModifierClass = ObjectValues<ModifierConstructorMap>;
/**
* Union type of all modifier names as strings.

View File

@ -1,4 +1,5 @@
import type { PhaseConstructorMap } from "#app/phase-manager";
import type { ObjectValues } from "#types/type-helpers";
// Intentionally export the types of everything in phase-manager, as this file is meant to be
// the centralized place for type definitions for the phase system.
@ -17,7 +18,7 @@ export type PhaseMap = {
/**
* Union type of all phase constructors.
*/
export type PhaseClass = PhaseConstructorMap[keyof PhaseConstructorMap];
export type PhaseClass = ObjectValues<PhaseConstructorMap>;
/**
* Union type of all phase names as strings.

10
src/@types/ui.ts Normal file
View File

@ -0,0 +1,10 @@
import type Phaser from "phaser";
import type InputText from "phaser3-rex-plugins/plugins/gameobjects/dom/inputtext/InputText";
export interface TextStyleOptions {
scale: number;
styleOptions: Phaser.Types.GameObjects.Text.TextStyle | InputText.IConfig;
shadowColor: string;
shadowXpos: number;
shadowYpos: number;
}

View File

@ -67,6 +67,7 @@ import { PokemonType } from "#enums/pokemon-type";
import { ShopCursorTarget } from "#enums/shop-cursor-target";
import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect";
import { TextStyle } from "#enums/text-style";
import type { TrainerSlot } from "#enums/trainer-slot";
import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant";
@ -132,7 +133,7 @@ import { CharSprite } from "#ui/char-sprite";
import { PartyExpBar } from "#ui/party-exp-bar";
import { PokeballTray } from "#ui/pokeball-tray";
import { PokemonInfoContainer } from "#ui/pokemon-info-container";
import { addTextObject, getTextColor, TextStyle } from "#ui/text";
import { addTextObject, getTextColor } from "#ui/text";
import { UI } from "#ui/ui";
import { addUiThemeOverrides } from "#ui/ui-theme";
import {
@ -236,6 +237,7 @@ export class BattleScene extends SceneBase {
public enableTouchControls = false;
public enableVibration = false;
public showBgmBar = true;
public hideUsername = false;
/** Determines the selected battle style. */
public battleStyle: BattleStyle = BattleStyle.SWITCH;
/**
@ -699,16 +701,16 @@ export class BattleScene extends SceneBase {
if (expSpriteKeys.size > 0) {
return;
}
this.cachedFetch("./exp-sprites.json")
.then(res => res.json())
.then(keys => {
if (Array.isArray(keys)) {
for (const key of keys) {
expSpriteKeys.add(key);
const res = await this.cachedFetch("./exp-sprites.json");
const keys = await res.json();
if (!Array.isArray(keys)) {
throw new Error("EXP Sprites were not array when fetched!");
}
// TODO: Optimize this
for (const k of keys) {
expSpriteKeys.add(k);
}
Promise.resolve();
});
}
/**
@ -1669,6 +1671,11 @@ export class BattleScene extends SceneBase {
case SpeciesId.MAUSHOLD:
case SpeciesId.DUDUNSPARCE:
return !randSeedInt(4) ? 1 : 0;
case SpeciesId.SINISTEA:
case SpeciesId.POLTEAGEIST:
case SpeciesId.POLTCHAGEIST:
case SpeciesId.SINISTCHA:
return !randSeedInt(16) ? 1 : 0;
case SpeciesId.PIKACHU:
if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) {
return 0; // Ban Cosplay and Partner Pika from Trainers before wave 30

View File

@ -15,6 +15,7 @@ import { SpeciesFormChangeAbilityTrigger, SpeciesFormChangeWeatherTrigger } from
import { Gender } from "#data/gender";
import { getPokeballName } from "#data/pokeball";
import { pokemonFormChanges } from "#data/pokemon-forms";
import type { PokemonSpecies } from "#data/pokemon-species";
import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#data/status-effect";
import { TerrainType } from "#data/terrain";
import type { Weather } from "#data/weather";
@ -6001,8 +6002,13 @@ export class IllusionPreSummonAbAttr extends PreSummonAbAttr {
const party: Pokemon[] = (pokemon.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(
p => p.isAllowedInBattle(),
);
const lastPokemon: Pokemon = party.filter(p => p !== pokemon).at(-1) || pokemon;
pokemon.setIllusion(lastPokemon);
let illusionPokemon: Pokemon | PokemonSpecies;
if (pokemon.hasTrainer()) {
illusionPokemon = party.filter(p => p !== pokemon).at(-1) || pokemon;
} else {
illusionPokemon = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, pokemon.level);
}
pokemon.setIllusion(illusionPokemon);
}
/** @returns Whether the illusion can be applied. */

View File

@ -86,7 +86,7 @@ export enum BiomePoolTier {
export const uncatchableSpecies: SpeciesId[] = [];
export interface SpeciesTree {
interface SpeciesTree {
[key: number]: SpeciesId[]
}
@ -94,11 +94,11 @@ export interface PokemonPools {
[key: number]: (SpeciesId | SpeciesTree)[]
}
export interface BiomeTierPokemonPools {
interface BiomeTierPokemonPools {
[key: number]: PokemonPools
}
export interface BiomePokemonPools {
interface BiomePokemonPools {
[key: number]: BiomeTierPokemonPools
}
@ -2022,7 +2022,6 @@ export const biomeTrainerPools: BiomeTrainerPools = {
}
};
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: init methods are expected to have many lines.
export function initBiomes() {
const pokemonBiomes = [
[ SpeciesId.BULBASAUR, PokemonType.GRASS, PokemonType.POISON, [

View File

@ -1,8 +1,8 @@
import { allMoves } from "#data/data-lists";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { toReadableString } from "#utils/common";
import { getEnumKeys, getEnumValues } from "#utils/enums";
import { toTitleCase } from "#utils/strings";
export const speciesEggMoves = {
[SpeciesId.BULBASAUR]: [ MoveId.SAPPY_SEED, MoveId.MALIGNANT_CHAIN, MoveId.EARTH_POWER, MoveId.MATCHA_GOTCHA ],
@ -617,7 +617,7 @@ function parseEggMoves(content: string): void {
}
if (eggMoves.every(m => m === MoveId.NONE)) {
console.warn(`Species ${toReadableString(SpeciesId[species])} could not be parsed, excluding from output...`)
console.warn(`Species ${toTitleCase(SpeciesId[species])} could not be parsed, excluding from output...`)
} else {
output += `[SpeciesId.${SpeciesId[species]}]: [ ${eggMoves.map(m => `MoveId.${MoveId[m]}`).join(", ")} ],\n`;
}

View File

@ -7,8 +7,9 @@ import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } fro
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import type { Pokemon } from "#field/pokemon";
import { animationFileName, coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common";
import { coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common";
import { getEnumKeys, getEnumValues } from "#utils/enums";
import { toKebabCase } from "#utils/strings";
import Phaser from "phaser";
export class AnimConfig {
@ -412,7 +413,7 @@ export function initCommonAnims(): Promise<void> {
const commonAnimId = commonAnimIds[ca];
commonAnimFetches.push(
globalScene
.cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/_/g, "-")}.json`)
.cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimNames[ca])}.json`)
.then(response => response.json())
.then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))),
);
@ -450,7 +451,7 @@ export function initMoveAnim(move: MoveId): Promise<void> {
const fetchAnimAndResolve = (move: MoveId) => {
globalScene
.cachedFetch(`./battle-anims/${animationFileName(move)}.json`)
.cachedFetch(`./battle-anims/${toKebabCase(MoveId[move])}.json`)
.then(response => {
const contentType = response.headers.get("content-type");
if (!response.ok || contentType?.indexOf("application/json") === -1) {
@ -506,7 +507,7 @@ function useDefaultAnim(move: MoveId, defaultMoveAnim: MoveId) {
* @remarks use {@linkcode useDefaultAnim} to use a default animation
*/
function logMissingMoveAnim(move: MoveId, ...optionalParams: any[]) {
const moveName = animationFileName(move);
const moveName = toKebabCase(MoveId[move]);
console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams);
}
@ -524,7 +525,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte
}
encounterAnimFetches.push(
globalScene
.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/_/g, "-")}.json`)
.cachedFetch(`./battle-anims/encounter-${toKebabCase(encounterAnimNames[anim])}.json`)
.then(response => response.json())
.then(cas => encounterAnims.set(anim, new AnimConfig(cas))),
);
@ -548,7 +549,7 @@ export function initMoveChargeAnim(chargeAnim: ChargeAnim): Promise<void> {
} else {
chargeAnims.set(chargeAnim, null);
globalScene
.cachedFetch(`./battle-anims/${ChargeAnim[chargeAnim].toLowerCase().replace(/_/g, "-")}.json`)
.cachedFetch(`./battle-anims/${toKebabCase(ChargeAnim[chargeAnim])}.json`)
.then(response => response.json())
.then(ca => {
if (Array.isArray(ca)) {
@ -1405,7 +1406,9 @@ export async function populateAnims() {
const chargeAnimIds = getEnumValues(ChargeAnim);
const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
const moveNameToId = {};
// Exclude MoveId.NONE;
for (const move of getEnumValues(MoveId).slice(1)) {
// KARATE_CHOP => KARATECHOP
const moveName = MoveId[move].toUpperCase().replace(/_/g, "");
moveNameToId[moveName] = move;
}

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import { defaultStarterSpecies } from "#app/constants";
import { globalScene } from "#app/global-scene";
import { pokemonEvolutions } from "#balance/pokemon-evolutions";
import { speciesStarterCosts } from "#balance/starters";
import { getEggTierForSpecies } from "#data/egg";
import { pokemonFormChanges } from "#data/pokemon-forms";
import type { PokemonSpecies } from "#data/pokemon-species";
import { getPokemonSpeciesForm } from "#data/pokemon-species";
@ -11,6 +12,7 @@ import { BattleType } from "#enums/battle-type";
import { ChallengeType } from "#enums/challenge-type";
import { Challenges } from "#enums/challenges";
import { TypeColor, TypeShadow } from "#enums/color";
import { EggTier } from "#enums/egg-type";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { ModifierTier } from "#enums/modifier-tier";
import type { MoveId } from "#enums/move-id";
@ -27,6 +29,7 @@ import type { DexAttrProps, GameData } from "#system/game-data";
import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common";
import { deepCopy } from "#utils/data";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toCamelCase, toSnakeCase } from "#utils/strings";
import i18next from "i18next";
/** A constant for the default max cost of the starting party before a run */
@ -67,14 +70,11 @@ export abstract class Challenge {
}
/**
* Gets the localisation key for the challenge
* @returns {@link string} The i18n key for this challenge
* Gets the localization key for the challenge
* @returns The i18n key for this challenge as camel case.
*/
geti18nKey(): string {
return Challenges[this.id]
.split("_")
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
.join("");
return toCamelCase(Challenges[this.id]);
}
/**
@ -105,23 +105,22 @@ export abstract class Challenge {
}
/**
* Returns the textual representation of a challenge's current value.
* @param overrideValue {@link number} The value to check for. If undefined, gets the current value.
* @returns {@link string} The localised name for the current value.
* Return the textual representation of a challenge's current value.
* @param overrideValue - The value to check for; default {@linkcode this.value}
* @returns The localised text for the current value.
*/
getValue(overrideValue?: number): string {
const value = overrideValue ?? this.value;
return i18next.t(`challenges:${this.geti18nKey()}.value.${value}`);
getValue(overrideValue: number = this.value): string {
return i18next.t(`challenges:${this.geti18nKey()}.value.${overrideValue}`);
}
/**
* Returns the description of a challenge's current value.
* @param overrideValue {@link number} The value to check for. If undefined, gets the current value.
* @returns {@link string} The localised description for the current value.
* Return the description of a challenge's current value.
* @param overrideValue - The value to check for; default {@linkcode this.value}
* @returns The localised description for the current value.
*/
getDescription(overrideValue?: number): string {
const value = overrideValue ?? this.value;
return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${value}`, `challenges:${this.geti18nKey()}.desc`])}`;
// TODO: Do we need an override value here? it's currently unused
getDescription(overrideValue: number = this.value): string {
return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${overrideValue}`, `challenges:${this.geti18nKey()}.desc`])}`;
}
/**
@ -579,31 +578,19 @@ export class SingleGenerationChallenge extends Challenge {
return this.value > 0 ? 1 : 0;
}
/**
* Returns the textual representation of a challenge's current value.
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
* @returns {string} The localised name for the current value.
*/
getValue(overrideValue?: number): string {
const value = overrideValue ?? this.value;
if (value === 0) {
getValue(overrideValue: number = this.value): string {
if (overrideValue === 0) {
return i18next.t("settings:off");
}
return i18next.t(`starterSelectUiHandler:gen${value}`);
return i18next.t(`starterSelectUiHandler:gen${overrideValue}`);
}
/**
* Returns the description of a challenge's current value.
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
* @returns {string} The localised description for the current value.
*/
getDescription(overrideValue?: number): string {
const value = overrideValue ?? this.value;
if (value === 0) {
getDescription(overrideValue: number = this.value): string {
if (overrideValue === 0) {
return i18next.t("challenges:singleGeneration.desc_default");
}
return i18next.t("challenges:singleGeneration.desc", {
gen: i18next.t(`challenges:singleGeneration.gen_${value}`),
gen: i18next.t(`challenges:singleGeneration.gen_${overrideValue}`),
});
}
@ -671,29 +658,13 @@ export class SingleTypeChallenge extends Challenge {
return this.value > 0 ? 1 : 0;
}
/**
* Returns the textual representation of a challenge's current value.
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
* @returns {string} The localised name for the current value.
*/
getValue(overrideValue?: number): string {
if (overrideValue === undefined) {
overrideValue = this.value;
}
return PokemonType[this.value - 1].toLowerCase();
getValue(overrideValue: number = this.value): string {
return toSnakeCase(PokemonType[overrideValue - 1]);
}
/**
* Returns the description of a challenge's current value.
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
* @returns {string} The localised description for the current value.
*/
getDescription(overrideValue?: number): string {
if (overrideValue === undefined) {
overrideValue = this.value;
}
const type = i18next.t(`pokemonInfo:Type.${PokemonType[this.value - 1]}`);
const typeColor = `[color=${TypeColor[PokemonType[this.value - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`;
getDescription(overrideValue: number = this.value): string {
const type = i18next.t(`pokemonInfo:Type.${PokemonType[overrideValue - 1]}`);
const typeColor = `[color=${TypeColor[PokemonType[overrideValue - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`;
const defaultDesc = i18next.t("challenges:singleType.desc_default");
const typeDesc = i18next.t("challenges:singleType.desc", {
type: typeColor,
@ -714,11 +685,14 @@ export class SingleTypeChallenge extends Challenge {
*/
export class FreshStartChallenge extends Challenge {
constructor() {
super(Challenges.FRESH_START, 1);
super(Challenges.FRESH_START, 3);
}
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
if (!defaultStarterSpecies.includes(pokemon.speciesId)) {
if (
(this.value === 1 && !defaultStarterSpecies.includes(pokemon.speciesId)) ||
(this.value === 2 && getEggTierForSpecies(pokemon) >= EggTier.EPIC)
) {
valid.value = false;
return true;
}
@ -726,15 +700,12 @@ export class FreshStartChallenge extends Challenge {
}
applyStarterCost(species: SpeciesId, cost: NumberHolder): boolean {
if (defaultStarterSpecies.includes(species)) {
cost.value = speciesStarterCosts[species];
return true;
}
return false;
}
applyStarterModify(pokemon: Pokemon): boolean {
pokemon.abilityIndex = 0; // Always base ability, not hidden ability
pokemon.abilityIndex = pokemon.abilityIndex % 2; // Always base ability, if you set it to hidden it wraps to first ability
pokemon.passive = false; // Passive isn't unlocked
pokemon.nature = Nature.HARDY; // Neutral nature
pokemon.moveset = pokemon.species
@ -746,7 +717,22 @@ export class FreshStartChallenge extends Challenge {
pokemon.luck = 0; // No luck
pokemon.shiny = false; // Not shiny
pokemon.variant = 0; // Not shiny
pokemon.formIndex = 0; // Froakie should be base form
if (pokemon.species.speciesId === SpeciesId.ZYGARDE && pokemon.formIndex >= 2) {
pokemon.formIndex -= 2; // Sets 10%-PC to 10%-AB and 50%-PC to 50%-AB
} else if (
pokemon.formIndex > 0 &&
[
SpeciesId.PIKACHU,
SpeciesId.EEVEE,
SpeciesId.PICHU,
SpeciesId.ROTOM,
SpeciesId.MELOETTA,
SpeciesId.FROAKIE,
SpeciesId.ROCKRUFF,
].includes(pokemon.species.speciesId)
) {
pokemon.formIndex = 0; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes
}
pokemon.ivs = [15, 15, 15, 15, 15, 15]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0)
pokemon.teraType = pokemon.species.type1; // Always primary tera type
return true;
@ -832,13 +818,7 @@ export class LowerStarterMaxCostChallenge extends Challenge {
super(Challenges.LOWER_MAX_STARTER_COST, 9);
}
/**
* @override
*/
getValue(overrideValue?: number): string {
if (overrideValue === undefined) {
overrideValue = this.value;
}
getValue(overrideValue: number = this.value): string {
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
}
@ -866,13 +846,7 @@ export class LowerStarterPointsChallenge extends Challenge {
super(Challenges.LOWER_STARTER_POINTS, 9);
}
/**
* @override
*/
getValue(overrideValue?: number): string {
if (overrideValue === undefined) {
overrideValue = this.value;
}
getValue(overrideValue: number = this.value): string {
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
}

View File

@ -1,6 +1,7 @@
import { BattleSpec } from "#enums/battle-spec";
import { TrainerType } from "#enums/trainer-type";
import { trainerConfigs } from "#trainers/trainer-config";
import { capitalizeFirstLetter } from "#utils/strings";
export interface TrainerTypeMessages {
encounter?: string | string[];
@ -1755,8 +1756,7 @@ export function initTrainerTypeDialogue(): void {
trainerConfigs[trainerType][`${messageType}Messages`] = messages[0][messageType];
}
if (messages.length > 1) {
trainerConfigs[trainerType][`female${messageType.slice(0, 1).toUpperCase()}${messageType.slice(1)}Messages`] =
messages[1][messageType];
trainerConfigs[trainerType][`female${capitalizeFirstLetter(messageType)}Messages`] = messages[1][messageType];
}
} else {
trainerConfigs[trainerType][`${messageType}Messages`] = messages[messageType];

View File

@ -27,6 +27,28 @@ export function isFieldTargeted(move: Move): boolean {
return false;
}
/**
* Determine whether a move is a spread move.
*
* @param move - The {@linkcode Move} to check
* @returns Whether {@linkcode move} is spread-targeted.
* @remarks
* Examples include:
* - Moves targeting all adjacent Pokemon (like Surf)
* - Moves targeting all adjacent enemies (like Air Cutter)
*/
export function isSpreadMove(move: Move): boolean {
switch (move.moveTarget) {
case MoveTarget.ALL_ENEMIES:
case MoveTarget.ALL_NEAR_ENEMIES:
case MoveTarget.ALL_OTHERS:
case MoveTarget.ALL_NEAR_OTHERS:
return true;
}
return false;
}
export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: MoveTarget): MoveTargetSet {
const variableTarget = new NumberHolder(0);
user.getOpponents(false).forEach(p => applyMoveAttrs("VariableTargetAttr", user, p, allMoves[move], variableTarget));

View File

@ -89,8 +89,9 @@ import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types";
import type { TurnMove } from "#types/turn-move";
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue, toReadableString } from "#utils/common";
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { toTitleCase } from "#utils/strings";
import i18next from "i18next";
/**
@ -810,16 +811,14 @@ export abstract class Move implements Localizable {
}
const power = new NumberHolder(this.power);
applyMoveAttrs("VariablePowerAttr", source, target, this, power);
const typeChangeMovePowerMultiplier = new NumberHolder(1);
const typeChangeHolder = new NumberHolder(this.type);
applyAbAttrs("MoveTypeChangeAbAttr", {pokemon: source, opponent: target, move: this, simulated: true, moveType: typeChangeHolder, power: typeChangeMovePowerMultiplier});
const sourceTeraType = source.getTeraType();
if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
power.value = 60;
}
const abAttrParams: PreAttackModifyPowerAbAttrParams = {
pokemon: source,
opponent: target,
@ -834,6 +833,13 @@ export abstract class Move implements Localizable {
applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally});
}
// Non-priority, single-hit moves of the user's Tera Type are always a bare minimum of 60 power
const sourceTeraType = source.getTeraType();
if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
power.value = 60;
}
const fieldAuras = new Set(
globalScene.getField(true)
.map((p) => p.getAbilityAttrs("FieldMoveTypePowerBoostAbAttr").filter(attr => {
@ -857,7 +863,6 @@ export abstract class Move implements Localizable {
power.value *= typeBoost.boostValue;
}
applyMoveAttrs("VariablePowerAttr", source, target, this, power);
if (!this.hasAttr("TypelessAttr")) {
globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power);
@ -8189,7 +8194,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
}
const type = validTypes[user.randBattleSeedInt(validTypes.length)];
user.summonData.types = [ type ];
globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: toReadableString(PokemonType[type]) }));
globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: toTitleCase(PokemonType[type]) }));
user.updateInfo();
return true;
@ -10857,7 +10862,7 @@ export function initMoves() {
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== MoveId.NO_RETREAT), // fails if the user is currently trapped by No Retreat
.condition((user, target, move) => user.getTag(TrappedTag)?.tagType !== BattlerTagType.NO_RETREAT), // fails if the user is currently trapped by No Retreat
new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)

View File

@ -39,6 +39,7 @@ import { addPokemonDataToDexAndValidateAchievements } from "#mystery-encounters/
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requirements";
import { PokemonData } from "#system/pokemon-data";
import { MusicPreference } from "#system/settings";
import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
@ -151,7 +152,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
return true;
})
.withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members
.withHasDexProgress(true)
.withDialogue({
buttonLabel: `${namespace}:option.1.label`,
@ -257,7 +259,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
.build(),
)
.withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members
.withHasDexProgress(true)
.withDialogue({
buttonLabel: `${namespace}:option.2.label`,

View File

@ -1,4 +1,4 @@
import type { TextStyle } from "#ui/text";
import type { TextStyle } from "#enums/text-style";
export class TextDisplay {
speaker?: string;

View File

@ -25,7 +25,8 @@ import {
StatusEffectRequirement,
WaveRangeRequirement,
} from "#mystery-encounters/mystery-encounter-requirements";
import { capitalizeFirstLetter, coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common";
import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common";
import { capitalizeFirstLetter } from "#utils/strings";
export interface EncounterStartOfBattleEffect {
sourcePokemon?: Pokemon;

View File

@ -1,6 +1,6 @@
import { globalScene } from "#app/global-scene";
import type { TextStyle } from "#enums/text-style";
import { UiTheme } from "#enums/ui-theme";
import type { TextStyle } from "#ui/text";
import { getTextWithColors } from "#ui/text";
import { isNullOrUndefined } from "#utils/common";
import i18next from "i18next";

View File

@ -1,8 +1,9 @@
import { Nature } from "#enums/nature";
import { EFFECTIVE_STATS, getShortenedStatKey, Stat } from "#enums/stat";
import { TextStyle } from "#enums/text-style";
import { UiTheme } from "#enums/ui-theme";
import { getBBCodeFrag, TextStyle } from "#ui/text";
import { toReadableString } from "#utils/common";
import { getBBCodeFrag } from "#ui/text";
import { toTitleCase } from "#utils/strings";
import i18next from "i18next";
export function getNatureName(
@ -12,7 +13,7 @@ export function getNatureName(
ignoreBBCode = false,
uiTheme: UiTheme = UiTheme.DEFAULT,
): string {
let ret = toReadableString(Nature[nature]);
let ret = toTitleCase(Nature[nature]);
//Translating nature
if (i18next.exists(`nature:${ret}`)) {
ret = i18next.t(`nature:${ret}` as any);

View File

@ -29,15 +29,9 @@ import type { Variant, VariantSet } from "#sprites/variant";
import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant";
import type { StarterMoveset } from "#system/game-data";
import type { Localizable } from "#types/locales";
import {
capitalizeString,
isNullOrUndefined,
randSeedFloat,
randSeedGauss,
randSeedInt,
randSeedItem,
} from "#utils/common";
import { isNullOrUndefined, randSeedFloat, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toCamelCase, toPascalCase } from "#utils/strings";
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
import i18next from "i18next";
@ -91,6 +85,7 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po
return retSpecies;
}
// TODO: Clean this up and seriously review alternate means of fusion naming
export function getFusedSpeciesName(speciesAName: string, speciesBName: string): string {
const fragAPattern = /([a-z]{2}.*?[aeiou(?:y$)\-']+)(.*?)$/i;
const fragBPattern = /([a-z]{2}.*?[aeiou(?:y$)\-'])(.*?)$/i;
@ -904,14 +899,14 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
* @returns the pokemon-form locale key for the single form name ("Alolan Form", "Eternal Flower" etc)
*/
getFormNameToDisplay(formIndex = 0, append = false): string {
const formKey = this.forms?.[formIndex!]?.formKey;
const formText = capitalizeString(formKey, "-", false, false) || "";
const speciesName = capitalizeString(SpeciesId[this.speciesId], "_", true, false);
const formKey = this.forms[formIndex]?.formKey ?? "";
const formText = toPascalCase(formKey);
const speciesName = toCamelCase(SpeciesId[this.speciesId]);
let ret = "";
const region = this.getRegion();
if (this.speciesId === SpeciesId.ARCEUS) {
ret = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`);
ret = i18next.t(`pokemonInfo:Type.${formText.toUpperCase()}`);
} else if (
[
SpeciesFormKey.MEGA,
@ -937,7 +932,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
if (i18next.exists(i18key)) {
ret = i18next.t(i18key);
} else {
const rootSpeciesName = capitalizeString(SpeciesId[this.getRootSpeciesId()], "_", true, false);
const rootSpeciesName = toCamelCase(SpeciesId[this.getRootSpeciesId()]);
const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`;
ret = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText;
}
@ -2851,11 +2846,11 @@ export function initSpecies() {
new PokemonSpecies(SpeciesId.GRAPPLOCT, 8, false, false, false, "Jujitsu Pokémon", PokemonType.FIGHTING, null, 1.6, 39, AbilityId.LIMBER, AbilityId.NONE, AbilityId.TECHNICIAN, 480, 80, 118, 90, 70, 80, 42, 45, 50, 168, GrowthRate.MEDIUM_SLOW, 50, false),
new PokemonSpecies(SpeciesId.SINISTEA, 8, false, false, false, "Black Tea Pokémon", PokemonType.GHOST, null, 0.1, 0.2, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, GrowthRate.MEDIUM_FAST, null, false, false,
new PokemonForm("Phony Form", "phony", PokemonType.GHOST, null, 0.1, 0.2, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, "", true),
new PokemonForm("Antique Form", "antique", PokemonType.GHOST, null, 0.1, 0.2, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, "", true, true),
new PokemonForm("Antique Form", "antique", PokemonType.GHOST, null, 0.1, 0.2, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, "", true),
),
new PokemonSpecies(SpeciesId.POLTEAGEIST, 8, false, false, false, "Black Tea Pokémon", PokemonType.GHOST, null, 0.2, 0.4, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 508, 60, 65, 65, 134, 114, 70, 60, 50, 178, GrowthRate.MEDIUM_FAST, null, false, false,
new PokemonForm("Phony Form", "phony", PokemonType.GHOST, null, 0.2, 0.4, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 508, 60, 65, 65, 134, 114, 70, 60, 50, 178, false, "", true),
new PokemonForm("Antique Form", "antique", PokemonType.GHOST, null, 0.2, 0.4, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 508, 60, 65, 65, 134, 114, 70, 60, 50, 178, false, "", true, true),
new PokemonForm("Antique Form", "antique", PokemonType.GHOST, null, 0.2, 0.4, AbilityId.WEAK_ARMOR, AbilityId.NONE, AbilityId.CURSED_BODY, 508, 60, 65, 65, 134, 114, 70, 60, 50, 178, false, "", true),
),
new PokemonSpecies(SpeciesId.HATENNA, 8, false, false, false, "Calm Pokémon", PokemonType.PSYCHIC, null, 0.4, 3.4, AbilityId.HEALER, AbilityId.ANTICIPATION, AbilityId.MAGIC_BOUNCE, 265, 42, 30, 45, 56, 53, 39, 235, 50, 53, GrowthRate.SLOW, 0, false),
new PokemonSpecies(SpeciesId.HATTREM, 8, false, false, false, "Serene Pokémon", PokemonType.PSYCHIC, null, 0.6, 4.8, AbilityId.HEALER, AbilityId.ANTICIPATION, AbilityId.MAGIC_BOUNCE, 370, 57, 40, 65, 86, 73, 49, 120, 50, 130, GrowthRate.SLOW, 0, false),
@ -3109,11 +3104,11 @@ export function initSpecies() {
new PokemonSpecies(SpeciesId.DIPPLIN, 9, false, false, false, "Candy Apple Pokémon", PokemonType.GRASS, PokemonType.DRAGON, 0.4, 4.4, AbilityId.SUPERSWEET_SYRUP, AbilityId.GLUTTONY, AbilityId.STICKY_HOLD, 485, 80, 80, 110, 95, 80, 40, 45, 50, 170, GrowthRate.ERRATIC, 50, false),
new PokemonSpecies(SpeciesId.POLTCHAGEIST, 9, false, false, false, "Matcha Pokémon", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, GrowthRate.SLOW, null, false, false,
new PokemonForm("Counterfeit Form", "counterfeit", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, null, true),
new PokemonForm("Artisan Form", "artisan", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, null, false, true),
new PokemonForm("Artisan Form", "artisan", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, "counterfeit", true),
),
new PokemonSpecies(SpeciesId.SINISTCHA, 9, false, false, false, "Matcha Pokémon", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178, GrowthRate.SLOW, null, false, false,
new PokemonForm("Unremarkable Form", "unremarkable", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178),
new PokemonForm("Masterpiece Form", "masterpiece", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178, false, null, false, true),
new PokemonForm("Unremarkable Form", "unremarkable", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178, false, null, true),
new PokemonForm("Masterpiece Form", "masterpiece", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, AbilityId.HOSPITALITY, AbilityId.NONE, AbilityId.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178, false, "unremarkable", true),
),
new PokemonSpecies(SpeciesId.OKIDOGI, 9, true, false, false, "Retainer Pokémon", PokemonType.POISON, PokemonType.FIGHTING, 1.8, 92.2, AbilityId.TOXIC_CHAIN, AbilityId.NONE, AbilityId.GUARD_DOG, 555, 88, 128, 115, 58, 86, 80, 3, 0, 276, GrowthRate.SLOW, 100, false),
new PokemonSpecies(SpeciesId.MUNKIDORI, 9, true, false, false, "Retainer Pokémon", PokemonType.POISON, PokemonType.PSYCHIC, 1, 12.2, AbilityId.TOXIC_CHAIN, AbilityId.NONE, AbilityId.FRISK, 555, 88, 75, 66, 130, 90, 106, 3, 0, 276, GrowthRate.SLOW, 100, false),

View File

@ -1,18 +1,30 @@
import { type BattlerTag, loadBattlerTag } from "#data/battler-tags";
import type { BattlerTag } from "#data/battler-tags";
import { loadBattlerTag, SerializableBattlerTag } from "#data/battler-tags";
import { allSpecies } from "#data/data-lists";
import type { Gender } from "#data/gender";
import { PokemonMove } from "#data/moves/pokemon-move";
import type { PokemonSpeciesForm } from "#data/pokemon-species";
import { getPokemonSpeciesForm, type PokemonSpeciesForm } from "#data/pokemon-species";
import type { TypeDamageMultiplier } from "#data/type";
import type { AbilityId } from "#enums/ability-id";
import type { BerryType } from "#enums/berry-type";
import type { MoveId } from "#enums/move-id";
import type { Nature } from "#enums/nature";
import type { PokemonType } from "#enums/pokemon-type";
import type { SpeciesId } from "#enums/species-id";
import type { AttackMoveResult } from "#types/attack-move-result";
import type { IllusionData } from "#types/illusion-data";
import type { TurnMove } from "#types/turn-move";
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
import { isNullOrUndefined } from "#utils/common";
/**
* The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it.
*/
type SerializedSpeciesForm = {
id: SpeciesId;
formIdx: number;
};
/**
* Permanent data that can customize a Pokemon in non-standard ways from its Species.
* Includes abilities, nature, changed types, etc.
@ -41,9 +53,59 @@ export class CustomPokemonData {
}
}
/**
* Deserialize a pokemon species form from an object containing `id` and `formIdx` properties.
* @param value - The value to deserialize
* @returns The `PokemonSpeciesForm` or `null` if the fields could not be properly discerned
*/
function deserializePokemonSpeciesForm(value: SerializedSpeciesForm | PokemonSpeciesForm): PokemonSpeciesForm | null {
// @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on
let { id, formIdx } = value;
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
// @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm
id = value.speciesId;
// @ts-expect-error: Same as above (plus we are accessing a protected property)
formIdx = value._formIndex;
}
// If for some reason either of these fields are null/undefined, we cannot reconstruct the species form
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
return null;
}
return getPokemonSpeciesForm(id, formIdx);
}
interface SerializedIllusionData extends Omit<IllusionData, "fusionSpecies"> {
/** The id of the illusioned fusion species, or `undefined` if not a fusion */
fusionSpecies?: SpeciesId;
}
interface SerializedPokemonSummonData {
statStages: number[];
moveQueue: TurnMove[];
tags: BattlerTag[];
abilitySuppressed: boolean;
speciesForm?: SerializedSpeciesForm;
fusionSpeciesForm?: SerializedSpeciesForm;
ability?: AbilityId;
passiveAbility?: AbilityId;
gender?: Gender;
fusionGender?: Gender;
stats: number[];
moveset?: PokemonMove[];
types: PokemonType[];
addedType?: PokemonType;
illusion?: SerializedIllusionData;
illusionBroken: boolean;
berriesEatenLast: BerryType[];
moveHistory: TurnMove[];
}
/**
* Persistent in-battle data for a {@linkcode Pokemon}.
* Resets on switch or new battle.
*
* @sealed
*/
export class PokemonSummonData {
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
@ -86,7 +148,7 @@ export class PokemonSummonData {
*/
public moveHistory: TurnMove[] = [];
constructor(source?: PokemonSummonData | Partial<PokemonSummonData>) {
constructor(source?: PokemonSummonData | SerializedPokemonSummonData) {
if (isNullOrUndefined(source)) {
return;
}
@ -97,19 +159,88 @@ export class PokemonSummonData {
continue;
}
if (key === "speciesForm" || key === "fusionSpeciesForm") {
this[key] = deserializePokemonSpeciesForm(value);
}
if (key === "illusion" && typeof value === "object") {
// Make a copy so as not to mutate provided value
const illusionData = {
...value,
};
if (!isNullOrUndefined(illusionData.fusionSpecies)) {
switch (typeof illusionData.fusionSpecies) {
case "object":
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId];
break;
case "number":
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies];
break;
default:
illusionData.fusionSpecies = undefined;
}
}
this[key] = illusionData as IllusionData;
}
if (key === "moveset") {
this.moveset = value?.map((m: any) => PokemonMove.loadMove(m));
continue;
}
if (key === "tags") {
// load battler tags
this.tags = value.map((t: BattlerTag) => loadBattlerTag(t));
if (key === "tags" && Array.isArray(value)) {
// load battler tags, discarding any that are not serializable
this.tags = value
.map((t: SerializableBattlerTag) => loadBattlerTag(t))
.filter((t): t is SerializableBattlerTag => t instanceof SerializableBattlerTag);
continue;
}
this[key] = value;
}
}
/**
* Serialize this PokemonSummonData to JSON, converting {@linkcode PokemonSpeciesForm} and {@linkcode IllusionData.fusionSpecies}
* into simpler types instead of serializing all of their fields.
*
* @remarks
* - `IllusionData.fusionSpecies` is serialized as just the species ID
* - `PokemonSpeciesForm` and `PokemonSpeciesForm.fusionSpeciesForm` are converted into {@linkcode SerializedSpeciesForm} objects
*/
public toJSON(): SerializedPokemonSummonData {
// Pokemon species forms are never saved, only the species ID.
const illusion = this.illusion;
const speciesForm = this.speciesForm;
const fusionSpeciesForm = this.fusionSpeciesForm;
const illusionSpeciesForm = illusion?.fusionSpecies;
const t = {
// the "as omit" is required to avoid TS resolving the overwritten properties to "never"
// We coerce null to undefined in the type, as the for loop below replaces `null` with `undefined`
...(this as Omit<
CoerceNullPropertiesToUndefined<PokemonSummonData>,
"speciesForm" | "fusionSpeciesForm" | "illusion"
>),
speciesForm: isNullOrUndefined(speciesForm)
? undefined
: { id: speciesForm.speciesId, formIdx: speciesForm.formIndex },
fusionSpeciesForm: isNullOrUndefined(fusionSpeciesForm)
? undefined
: { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex },
illusion: isNullOrUndefined(illusion)
? undefined
: {
...(this.illusion as Omit<typeof illusion, "fusionSpecies">),
fusionSpecies: illusionSpeciesForm?.speciesId,
},
};
// Replace `null` with `undefined`, as `undefined` never gets serialized
for (const [key, value] of Object.entries(t)) {
if (value === null) {
t[key] = undefined;
}
}
return t;
}
}
// TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added

View File

@ -3,6 +3,7 @@ import type { BattlerIndex } from "#enums/battler-index";
import { PokemonType } from "#enums/pokemon-type";
import type { Pokemon } from "#field/pokemon";
import type { Move } from "#moves/move";
import { isFieldTargeted, isSpreadMove } from "#moves/move-utils";
import i18next from "i18next";
export enum TerrainType {
@ -60,14 +61,20 @@ export class Terrain {
isMoveTerrainCancelled(user: Pokemon, targets: BattlerIndex[], move: Move): boolean {
switch (this.terrainType) {
case TerrainType.PSYCHIC:
if (!move.hasAttr("ProtectAttr")) {
// Cancels move if the move has positive priority and targets a Pokemon grounded on the Psychic Terrain
// Cf https://bulbapedia.bulbagarden.net/wiki/Psychic_Terrain_(move)#Generation_VII
// Psychic terrain will only cancel a move if it:
return (
// ... is neither spread nor field-targeted,
!isFieldTargeted(move) &&
!isSpreadMove(move) &&
// .. has positive final priority,
move.getPriority(user) > 0 &&
user.getOpponents(true).some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded())
// ...and is targeting at least 1 grounded opponent
user
.getOpponents(true)
.some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded())
);
}
}
return false;
}

View File

@ -1,12 +1,12 @@
import { TrainerType } from "#enums/trainer-type";
import { toReadableString } from "#utils/common";
import { toPascalSnakeCase } from "#utils/strings";
class TrainerNameConfig {
public urls: string[];
public femaleUrls: string[] | null;
constructor(type: TrainerType, ...urls: string[]) {
this.urls = urls.length ? urls : [toReadableString(TrainerType[type]).replace(/ /g, "_")];
this.urls = urls.length ? urls : [toPascalSnakeCase(TrainerType[type])];
}
hasGenderVariant(...femaleUrls: string[]): TrainerNameConfig {

View File

@ -41,15 +41,9 @@ import type {
TrainerConfigs,
TrainerTierPools,
} from "#types/trainer-funcs";
import {
coerceArray,
isNullOrUndefined,
randSeedInt,
randSeedIntRange,
randSeedItem,
toReadableString,
} from "#utils/common";
import { coerceArray, isNullOrUndefined, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toSnakeCase, toTitleCase } from "#utils/strings";
import i18next from "i18next";
/** Minimum BST for Pokemon generated onto the Elite Four's teams */
@ -140,7 +134,7 @@ export class TrainerConfig {
constructor(trainerType: TrainerType, allowLegendaries?: boolean) {
this.trainerType = trainerType;
this.trainerAI = new TrainerAI();
this.name = toReadableString(TrainerType[this.getDerivedType()]);
this.name = toTitleCase(TrainerType[this.getDerivedType()]);
this.battleBgm = "battle_trainer";
this.mixedBattleBgm = "battle_trainer";
this.victoryBgm = "victory_trainer";
@ -734,7 +728,7 @@ export class TrainerConfig {
}
// Localize the trainer's name by converting it to lowercase and replacing spaces with underscores.
const nameForCall = this.name.toLowerCase().replace(/\s/g, "_");
const nameForCall = toSnakeCase(this.name);
this.name = i18next.t(`trainerNames:${nameForCall}`);
// Set the title to "elite_four". (this is the key in the i18n file)

View File

@ -1,3 +1,5 @@
import type { ObjectValues } from "#types/type-helpers";
/**
* Not to be confused with an Ability Attribute.
* This is an object literal storing the slot that an ability can occupy.
@ -8,4 +10,4 @@ export const AbilityAttr = Object.freeze({
ABILITY_HIDDEN: 4,
});
export type AbilityAttr = typeof AbilityAttr[keyof typeof AbilityAttr];
export type AbilityAttr = ObjectValues<typeof AbilityAttr>;

View File

@ -1,5 +1,4 @@
export enum BattlerTagType {
NONE = "NONE",
RECHARGING = "RECHARGING",
FLINCHED = "FLINCHED",
INTERRUPTED = "INTERRUPTED",

View File

@ -1,3 +1,5 @@
import type { ObjectValues } from "#types/type-helpers";
export const DexAttr = Object.freeze({
NON_SHINY: 1n,
SHINY: 2n,
@ -8,4 +10,4 @@ export const DexAttr = Object.freeze({
VARIANT_3: 64n,
DEFAULT_FORM: 128n,
});
export type DexAttr = typeof DexAttr[keyof typeof DexAttr];
export type DexAttr = ObjectValues<typeof DexAttr>;

View File

@ -1,7 +1,9 @@
import type { ObjectValues } from "#types/type-helpers";
export const GachaType = Object.freeze({
MOVE: 0,
LEGENDARY: 1,
SHINY: 2
});
export type GachaType = typeof GachaType[keyof typeof GachaType];
export type GachaType = ObjectValues<typeof GachaType>;

View File

@ -1,3 +1,5 @@
import type { ObjectValues } from "#types/type-helpers";
/** The result of a hit check calculation */
export const HitCheckResult = {
/** Hit checks haven't been evaluated yet in this pass */
@ -20,4 +22,4 @@ export const HitCheckResult = {
ERROR: 8,
} as const;
export type HitCheckResult = typeof HitCheckResult[keyof typeof HitCheckResult];
export type HitCheckResult = ObjectValues<typeof HitCheckResult>;

59
src/enums/text-style.ts Normal file
View File

@ -0,0 +1,59 @@
export const TextStyle = Object.freeze({
MESSAGE: 1,
WINDOW: 2,
WINDOW_ALT: 3,
WINDOW_BATTLE_COMMAND: 4,
BATTLE_INFO: 5,
PARTY: 6,
PARTY_RED: 7,
PARTY_CANCEL_BUTTON: 8,
INSTRUCTIONS_TEXT: 9,
MOVE_LABEL: 10,
SUMMARY: 11,
SUMMARY_DEX_NUM: 12,
SUMMARY_DEX_NUM_GOLD: 13,
SUMMARY_ALT: 14,
SUMMARY_HEADER: 15,
SUMMARY_RED: 16,
SUMMARY_BLUE: 17,
SUMMARY_PINK: 18,
SUMMARY_GOLD: 19,
SUMMARY_GRAY: 20,
SUMMARY_GREEN: 21,
SUMMARY_STATS: 22,
SUMMARY_STATS_BLUE: 23,
SUMMARY_STATS_PINK: 24,
SUMMARY_STATS_GOLD: 25,
LUCK_VALUE: 26,
STATS_HEXAGON: 27,
GROWTH_RATE_TYPE: 28,
MONEY: 29, // Money default styling (pale yellow)
MONEY_WINDOW: 30, // Money displayed in Windows (needs different colors based on theme)
HEADER_LABEL: 31,
STATS_LABEL: 32,
STATS_VALUE: 33,
SETTINGS_VALUE: 34,
SETTINGS_LABEL: 35,
SETTINGS_LABEL_NAVBAR: 36,
SETTINGS_SELECTED: 37,
SETTINGS_LOCKED: 38,
EGG_LIST: 39,
EGG_SUMMARY_NAME: 40,
EGG_SUMMARY_DEX: 41,
STARTER_VALUE_LIMIT: 42,
TOOLTIP_TITLE: 43,
TOOLTIP_CONTENT: 44,
FILTER_BAR_MAIN: 45,
MOVE_INFO_CONTENT: 46,
MOVE_PP_FULL: 47,
MOVE_PP_HALF_FULL: 48,
MOVE_PP_NEAR_EMPTY: 49,
MOVE_PP_EMPTY: 50,
SMALLER_WINDOW_ALT: 51,
BGM_BAR: 52,
PERFECT_IV: 53,
ME_OPTION_DEFAULT: 54, // Default style for choices in ME
ME_OPTION_SPECIAL: 55, // Style for choices with special requirements in ME
SHADOW_TEXT: 56 // to obscure unavailable options
})
export type TextStyle = typeof TextStyle[keyof typeof TextStyle];

View File

@ -1,9 +1,10 @@
import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#enums/battler-index";
import { HitResult } from "#enums/hit-result";
import { TextStyle } from "#enums/text-style";
import type { Pokemon } from "#field/pokemon";
import type { DamageResult } from "#types/damage-result";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { fixedInt, formatStat } from "#utils/common";
type TextAndShadowArr = [string | null, string | null];

View File

@ -5,10 +5,11 @@ import { coerceArray, fixedInt, randInt } from "#utils/common";
export class PokemonSpriteSparkleHandler {
private sprites: Set<Phaser.GameObjects.Sprite>;
private counterTween?: Phaser.Tweens.Tween;
setup(): void {
this.sprites = new Set();
globalScene.tweens.addCounter({
this.counterTween = globalScene.tweens.addCounter({
duration: fixedInt(200),
from: 0,
to: 1,
@ -78,4 +79,12 @@ export class PokemonSpriteSparkleHandler {
this.sprites.delete(s);
}
}
destroy(): void {
this.removeAll();
if (this.counterTween) {
this.counterTween.destroy();
this.counterTween = undefined;
}
}
}

View File

@ -213,8 +213,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon
*/
public id: number;
public name: string;
public nickname: string;
/**
* The Pokemon's current nickname, or `undefined` if it currently lacks one.
* If omitted, references to this should refer to the default name for this Pokemon's species.
*/
public nickname?: string;
public species: PokemonSpecies;
public formIndex: number;
public abilityIndex: number;
@ -442,10 +445,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns The name to render for this {@linkcode Pokemon}.
*/
getNameToRender(useIllusion = true) {
const name: string =
!useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.name : this.name;
const nickname: string =
!useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.nickname : this.nickname;
const illusion = this.summonData.illusion;
const name = useIllusion ? (illusion?.name ?? this.name) : this.name;
const nickname: string | undefined = useIllusion ? illusion?.nickname : this.nickname;
try {
if (nickname) {
return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...
@ -463,7 +465,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns The {@linkcode PokeballType} that will be shown when this Pokemon is sent out into battle.
*/
getPokeball(useIllusion = false): PokeballType {
return useIllusion && this.summonData.illusion ? this.summonData.illusion.pokeball : this.pokeball;
return useIllusion ? (this.summonData.illusion?.pokeball ?? this.pokeball) : this.pokeball;
}
init(): void {
@ -609,24 +611,33 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Generate an illusion of the last pokemon in the party, as other wild pokemon in the area.
* Set this pokemon's illusion to the data of the given pokemon.
*
* @remarks
* When setting the illusion of a wild pokemon, a {@linkcode PokemonSpecies} is generally passed.
* When setting the illusion of a pokemon in this way, the fields required by illusion data
* but missing from `PokemonSpecies` are set as follows
* - `pokeball` and `nickname` are both inherited from this pokemon
* - `shiny` will always be set if this pokemon OR its fusion is shiny
* - `variant` will always be 0
* - Fields related to fusion will be set to `undefined` or `0` as appropriate
* - The gender is set to be the same as this pokemon, if it is compatible with the provided pokemon.
* - If the provided pokemon can only ever exist as one gender, it is always that gender
* - If this pokemon is genderless but the provided pokemon isn't, then a gender roll is done based on this
* pokemon's ID
*/
setIllusion(pokemon: Pokemon): boolean {
if (this.summonData.illusion) {
setIllusion(pokemon: Pokemon | PokemonSpecies): boolean {
this.breakIllusion();
}
if (this.hasTrainer()) {
if (pokemon instanceof Pokemon) {
const speciesId = pokemon.species.speciesId;
this.summonData.illusion = {
basePokemon: {
name: this.name,
nickname: this.nickname,
shiny: this.shiny,
variant: this.variant,
fusionShiny: this.fusionShiny,
fusionVariant: this.fusionVariant,
},
name: pokemon.name,
nickname: pokemon.nickname,
shiny: pokemon.shiny,
variant: pokemon.variant,
fusionShiny: pokemon.fusionShiny,
fusionVariant: pokemon.fusionVariant,
species: speciesId,
formIndex: pokemon.formIndex,
gender: pokemon.gender,
@ -636,54 +647,61 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
fusionGender: pokemon.fusionGender,
};
this.name = pokemon.name;
this.nickname = pokemon.nickname;
this.shiny = pokemon.shiny;
this.variant = pokemon.variant;
this.fusionVariant = pokemon.fusionVariant;
this.fusionShiny = pokemon.fusionShiny;
if (this.shiny) {
if (pokemon.shiny || pokemon.fusionShiny) {
this.initShinySparkle();
}
this.loadAssets(false, true).then(() => this.playAnim());
this.updateInfo();
} else {
const randomIllusion: PokemonSpecies = globalScene.arena.randomSpecies(
globalScene.currentBattle.waveIndex,
this.level,
);
// Correct the gender in case the illusioned species has a gender incompatible with this pokemon
let gender = this.gender;
switch (pokemon.malePercent) {
case null:
gender = Gender.GENDERLESS;
break;
case 0:
gender = Gender.FEMALE;
break;
case 100:
gender = Gender.MALE;
break;
default:
gender = (this.id % 256) * 0.390625 < pokemon.malePercent ? Gender.MALE : Gender.FEMALE;
}
/*
TODO: Allow setting `variant` to something other than 0, which would require first loading the
assets for the provided species, as its entry would otherwise not
be guaranteed to exist in the `variantData` map. But this would prevent `summonData` from being populated
until the assets are loaded, which would cause issues as this method cannot be easily promisified.
*/
this.summonData.illusion = {
basePokemon: {
name: this.name,
fusionShiny: false,
fusionVariant: 0,
shiny: this.shiny || this.fusionShiny,
variant: 0,
nickname: this.nickname,
shiny: this.shiny,
variant: this.variant,
fusionShiny: this.fusionShiny,
fusionVariant: this.fusionVariant,
},
species: randomIllusion.speciesId,
formIndex: randomIllusion.formIndex,
gender: this.gender,
name: pokemon.name,
species: pokemon.speciesId,
formIndex: pokemon.formIndex,
gender,
pokeball: this.pokeball,
};
this.name = randomIllusion.name;
this.loadAssets(false, true).then(() => this.playAnim());
if (this.shiny || this.fusionShiny) {
this.initShinySparkle();
}
}
this.loadAssets(false, true).then(() => this.playAnim());
this.updateInfo();
return true;
}
/**
* Break the illusion of this pokemon, if it has an active illusion.
* @returns Whether an illusion was broken.
*/
breakIllusion(): boolean {
if (!this.summonData.illusion) {
return false;
}
this.name = this.summonData.illusion.basePokemon.name;
this.nickname = this.summonData.illusion.basePokemon.nickname;
this.shiny = this.summonData.illusion.basePokemon.shiny;
this.variant = this.summonData.illusion.basePokemon.variant;
this.fusionVariant = this.summonData.illusion.basePokemon.fusionVariant;
this.fusionShiny = this.summonData.illusion.basePokemon.fusionShiny;
this.summonData.illusion = null;
if (this.isOnField()) {
globalScene.playSound("PRSFX- Transform");
@ -718,8 +736,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// Assets for moves
loadPromises.push(loadMoveAnimations(this.getMoveset().map(m => m.getMove().id)));
/** alias for `this.summonData.illusion`; bangs on this are safe when guarded with `useIllusion` being true */
const illusion = this.summonData.illusion;
useIllusion = useIllusion && !!illusion;
// Load the assets for the species form
const formIndex = useIllusion && this.summonData.illusion ? this.summonData.illusion.formIndex : this.formIndex;
const formIndex = useIllusion ? illusion!.formIndex : this.formIndex;
loadPromises.push(
this.getSpeciesForm(false, useIllusion).loadAssets(
this.getGender(useIllusion) === Gender.FEMALE,
@ -736,16 +758,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
);
}
if (this.getFusionSpeciesForm()) {
const fusionFormIndex =
useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionFormIndex : this.fusionFormIndex;
const fusionShiny =
!useIllusion && this.summonData.illusion?.basePokemon
? this.summonData.illusion.basePokemon.fusionShiny
: this.fusionShiny;
const fusionVariant =
!useIllusion && this.summonData.illusion?.basePokemon
? this.summonData.illusion.basePokemon.fusionVariant
: this.fusionVariant;
const { fusionFormIndex, fusionShiny, fusionVariant } = useIllusion ? illusion! : this;
loadPromises.push(
this.getFusionSpeciesForm(false, useIllusion).loadAssets(
this.getFusionGender(false, useIllusion) === Gender.FEMALE,
@ -933,8 +946,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return this.getSpeciesForm(ignoreOverride, false).getSpriteKey(
this.getGender(ignoreOverride) === Gender.FEMALE,
this.formIndex,
this.summonData.illusion?.basePokemon.shiny ?? this.shiny,
this.summonData.illusion?.basePokemon.variant ?? this.variant,
this.isShiny(false),
this.getVariant(false),
);
}
@ -977,11 +990,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
getIconAtlasKey(ignoreOverride = false, useIllusion = true): string {
// TODO: confirm the correct behavior here (is it intentional that the check fails if `illusion.formIndex` is `0`?)
const formIndex =
useIllusion && this.summonData.illusion?.formIndex ? this.summonData.illusion.formIndex : this.formIndex;
const variant =
!useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.variant : this.variant;
const illusion = this.summonData.illusion;
const { formIndex, variant } = useIllusion && illusion ? illusion : this;
return this.getSpeciesForm(ignoreOverride, useIllusion).getIconAtlasKey(
formIndex,
this.isBaseShiny(useIllusion),
@ -990,15 +1000,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
getFusionIconAtlasKey(ignoreOverride = false, useIllusion = true): string {
// TODO: confirm the correct behavior here (is it intentional that the check fails if `illusion.fusionFormIndex` is `0`?)
const fusionFormIndex =
useIllusion && this.summonData.illusion?.fusionFormIndex
? this.summonData.illusion.fusionFormIndex
: this.fusionFormIndex;
const fusionVariant =
!useIllusion && this.summonData.illusion
? this.summonData.illusion.basePokemon.fusionVariant
: this.fusionVariant;
const illusion = this.summonData.illusion;
const { fusionFormIndex, fusionVariant } = useIllusion && illusion ? illusion : this;
return this.getFusionSpeciesForm(ignoreOverride, useIllusion).getIconAtlasKey(
fusionFormIndex,
this.isFusionShiny(),
@ -1006,11 +1009,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
);
}
getIconId(ignoreOverride?: boolean, useIllusion = true): string {
const formIndex =
useIllusion && this.summonData.illusion?.formIndex ? this.summonData.illusion?.formIndex : this.formIndex;
const variant =
!useIllusion && !!this.summonData.illusion ? this.summonData.illusion?.basePokemon.variant : this.variant;
getIconId(ignoreOverride?: boolean, useIllusion = false): string {
const illusion = this.summonData.illusion;
const { formIndex, variant } = useIllusion && illusion ? illusion : this;
return this.getSpeciesForm(ignoreOverride, useIllusion).getIconId(
this.getGender(ignoreOverride, useIllusion) === Gender.FEMALE,
formIndex,
@ -1020,14 +1021,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
getFusionIconId(ignoreOverride?: boolean, useIllusion = true): string {
const fusionFormIndex =
useIllusion && this.summonData.illusion?.fusionFormIndex
? this.summonData.illusion?.fusionFormIndex
: this.fusionFormIndex;
const fusionVariant =
!useIllusion && !!this.summonData.illusion
? this.summonData.illusion?.basePokemon.fusionVariant
: this.fusionVariant;
const illusion = this.summonData.illusion;
const { fusionFormIndex, fusionVariant } = useIllusion && illusion ? illusion : this;
return this.getFusionSpeciesForm(ignoreOverride, useIllusion).getIconId(
this.getFusionGender(ignoreOverride, useIllusion) === Gender.FEMALE,
fusionFormIndex,
@ -1702,29 +1697,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns Whether this Pokemon is shiny
*/
isShiny(useIllusion = false): boolean {
if (!useIllusion && this.summonData.illusion) {
return (
this.summonData.illusion.basePokemon?.shiny ||
(this.summonData.illusion.fusionSpecies && this.summonData.illusion.basePokemon?.fusionShiny) ||
false
);
}
return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny);
return this.isBaseShiny(useIllusion) || this.isFusionShiny(useIllusion);
}
isBaseShiny(useIllusion = false) {
if (!useIllusion && this.summonData.illusion) {
return !!this.summonData.illusion.basePokemon?.shiny;
}
return this.shiny;
return useIllusion ? (this.summonData.illusion?.shiny ?? this.shiny) : this.shiny;
}
isFusionShiny(useIllusion = false) {
if (!useIllusion && this.summonData.illusion) {
return !!this.summonData.illusion.basePokemon?.fusionShiny;
if (!this.isFusion(useIllusion)) {
return false;
}
return this.isFusion(useIllusion) && this.fusionShiny;
return useIllusion ? (this.summonData.illusion?.fusionShiny ?? this.fusionShiny) : this.fusionShiny;
}
/**
@ -1733,39 +1717,48 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns Whether this pokemon's base and fusion counterparts are both shiny.
*/
isDoubleShiny(useIllusion = false): boolean {
if (!useIllusion && this.summonData.illusion?.basePokemon) {
return (
this.isFusion(false) &&
this.summonData.illusion.basePokemon.shiny &&
this.summonData.illusion.basePokemon.fusionShiny
);
}
return this.isFusion(useIllusion) && this.shiny && this.fusionShiny;
return this.isFusion(useIllusion) && this.isBaseShiny(useIllusion) && this.isFusionShiny(useIllusion);
}
/**
* Return this Pokemon's {@linkcode Variant | shiny variant}.
* If a fusion, returns the maximum of the two variants.
* Only meaningful if this pokemon is actually shiny.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns The shiny variant of this Pokemon.
*/
getVariant(useIllusion = false): Variant {
if (!useIllusion && this.summonData.illusion) {
return !this.isFusion(false)
? this.summonData.illusion.basePokemon!.variant
: (Math.max(this.variant, this.fusionVariant) as Variant);
const illusion = this.summonData.illusion;
const baseVariant = useIllusion ? (illusion?.variant ?? this.variant) : this.variant;
if (!this.isFusion(useIllusion)) {
return baseVariant;
}
const fusionVariant = useIllusion ? (illusion?.fusionVariant ?? this.fusionVariant) : this.fusionVariant;
return Math.max(baseVariant, fusionVariant) as Variant;
}
return !this.isFusion(true) ? this.variant : (Math.max(this.variant, this.fusionVariant) as Variant);
/**
* Return the base pokemon's variant. Equivalent to {@linkcode getVariant} if this pokemon is not a fusion.
* @returns The shiny variant of this Pokemon's base species.
*/
getBaseVariant(useIllusion = false): Variant {
const illusion = this.summonData.illusion;
return useIllusion && illusion ? (illusion.variant ?? this.variant) : this.variant;
}
// TODO: Clarify how this differs from `getVariant`
getBaseVariant(doubleShiny: boolean): Variant {
if (doubleShiny) {
return this.summonData.illusion?.basePokemon?.variant ?? this.variant;
/**
* Return the fused pokemon's variant.
*
* @remarks
* Always returns `0` if the pokemon is not a fusion.
* @returns The shiny variant of this pokemon's fusion species.
*/
getFusionVariant(useIllusion = false): Variant {
if (!this.isFusion(useIllusion)) {
return 0;
}
return this.getVariant();
const illusion = this.summonData.illusion;
return illusion ? (illusion.fusionVariant ?? this.fusionVariant) : this.fusionVariant;
}
/**
@ -1782,7 +1775,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns Whether this Pokemon is currently fused with another species.
*/
isFusion(useIllusion = false): boolean {
return useIllusion && this.summonData.illusion ? !!this.summonData.illusion.fusionSpecies : !!this.fusionSpecies;
return useIllusion ? !!this.summonData.illusion?.fusionSpecies : !!this.fusionSpecies;
}
/**
@ -1792,9 +1785,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @see {@linkcode getNameToRender} - gets this Pokemon's display name.
*/
getName(useIllusion = false): string {
return !useIllusion && this.summonData.illusion?.basePokemon
? this.summonData.illusion.basePokemon.name
: this.name;
return useIllusion ? (this.summonData.illusion?.name ?? this.name) : this.name;
}
/**
@ -5676,7 +5667,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
export class PlayerPokemon extends Pokemon {
protected battleInfo: PlayerBattleInfo;
protected declare battleInfo: PlayerBattleInfo;
public compatibleTms: MoveId[];
constructor(
@ -6205,7 +6196,7 @@ export class PlayerPokemon extends Pokemon {
}
export class EnemyPokemon extends Pokemon {
protected battleInfo: EnemyBattleInfo;
protected declare battleInfo: EnemyBattleInfo;
public trainerSlot: TrainerSlot;
public aiType: AiType;
public bossSegments: number;

View File

@ -23,13 +23,13 @@ import {
} from "#trainers/trainer-party-template";
import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toSnakeCase } from "#utils/strings";
import i18next from "i18next";
export class Trainer extends Phaser.GameObjects.Container {
public config: TrainerConfig;
public variant: TrainerVariant;
public partyTemplateIndex: number;
public name: string;
public partnerName: string;
public nameKey: string;
public partnerNameKey: string | undefined;
@ -170,7 +170,7 @@ export class Trainer extends Phaser.GameObjects.Container {
const evilTeamTitles = ["grunt"];
if (this.name === "" && evilTeamTitles.some(t => name.toLocaleLowerCase().includes(t))) {
// This is a evil team grunt so we localize it by only using the "name" as the title
title = i18next.t(`trainerClasses:${name.toLowerCase().replace(/\s/g, "_")}`);
title = i18next.t(`trainerClasses:${toSnakeCase(name)}`);
console.log("Localized grunt name: " + title);
// Since grunts are not named we can just return the title
return title;
@ -187,7 +187,7 @@ export class Trainer extends Phaser.GameObjects.Container {
}
// Get the localized trainer class name from the i18n file and set it as the title.
// This is used for trainer class names, not titles like "Elite Four, Champion, etc."
title = i18next.t(`trainerClasses:${name.toLowerCase().replace(/\s/g, "_")}`);
title = i18next.t(`trainerClasses:${toSnakeCase(name)}`);
}
// If no specific trainer slot is set.
@ -208,7 +208,7 @@ export class Trainer extends Phaser.GameObjects.Container {
if (this.config.titleDouble && this.variant === TrainerVariant.DOUBLE && !this.config.doubleOnly) {
title = this.config.titleDouble;
name = i18next.t(`trainerNames:${this.config.nameDouble.toLowerCase().replace(/\s/g, "_")}`);
name = i18next.t(`trainerNames:${toSnakeCase(this.config.nameDouble)}`);
}
console.log(title ? `${title} ${name}` : name);

35
src/init/init.ts Normal file
View File

@ -0,0 +1,35 @@
import { initAbilities } from "#abilities/ability";
import { initBiomes } from "#balance/biomes";
import { initEggMoves } from "#balance/egg-moves";
import { initPokemonPrevolutions, initPokemonStarters } from "#balance/pokemon-evolutions";
import { initChallenges } from "#data/challenge";
import { initTrainerTypeDialogue } from "#data/dialogue";
import { initPokemonForms } from "#data/pokemon-forms";
import { initSpecies } from "#data/pokemon-species";
import { initModifierPools } from "#modifiers/init-modifier-pools";
import { initModifierTypes } from "#modifiers/modifier-type";
import { initMoves } from "#moves/move";
import { initMysteryEncounters } from "#mystery-encounters/mystery-encounters";
import { initAchievements } from "#system/achv";
import { initVouchers } from "#system/voucher";
import { initStatsKeys } from "#ui/game-stats-ui-handler";
/** Initialize the game. */
export function initializeGame() {
initModifierTypes();
initModifierPools();
initAchievements();
initVouchers();
initStatsKeys();
initPokemonPrevolutions();
initPokemonStarters();
initBiomes();
initEggMoves();
initPokemonForms();
initTrainerTypeDialogue();
initSpecies();
initMoves();
initAbilities();
initChallenges();
initMysteryEncounters();
}

View File

@ -1,29 +1,16 @@
import { initAbilities } from "#abilities/ability";
import { timedEventManager } from "#app/global-event-manager";
import { initializeGame } from "#app/init/init";
import { SceneBase } from "#app/scene-base";
import { isMobile } from "#app/touch-controls";
import { initBiomes } from "#balance/biomes";
import { initEggMoves } from "#balance/egg-moves";
import { initPokemonPrevolutions, initPokemonStarters } from "#balance/pokemon-evolutions";
import { initChallenges } from "#data/challenge";
import { initTrainerTypeDialogue } from "#data/dialogue";
import { initPokemonForms } from "#data/pokemon-forms";
import { initSpecies } from "#data/pokemon-species";
import { BiomeId } from "#enums/biome-id";
import { GachaType } from "#enums/gacha-types";
import { getBiomeHasProps } from "#field/arena";
import { initModifierPools } from "#modifiers/init-modifier-pools";
import { initModifierTypes } from "#modifiers/modifier-type";
import { initMoves } from "#moves/move";
import { initMysteryEncounters } from "#mystery-encounters/mystery-encounters";
import { CacheBustedLoaderPlugin } from "#plugins/cache-busted-loader-plugin";
import { initAchievements } from "#system/achv";
import { initVouchers } from "#system/voucher";
import { initStatsKeys } from "#ui/game-stats-ui-handler";
import { getWindowVariantSuffix, WindowVariant } from "#ui/ui-theme";
import { hasAllLocalizedSprites, localPing } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import i18next from "i18next";
import type { GameObjects } from "phaser";
export class LoadingScene extends SceneBase {
public static readonly KEY = "loading";
@ -366,30 +353,12 @@ export class LoadingScene extends SceneBase {
this.loadLoadingScreen();
initModifierTypes();
initModifierPools();
initAchievements();
initVouchers();
initStatsKeys();
initPokemonPrevolutions();
initPokemonStarters();
initBiomes();
initEggMoves();
initPokemonForms();
initTrainerTypeDialogue();
initSpecies();
initMoves();
initAbilities();
initChallenges();
initMysteryEncounters();
initializeGame();
}
loadLoadingScreen() {
const mobile = isMobile();
const loadingGraphics: any[] = [];
const bg = this.add.image(0, 0, "");
bg.setOrigin(0, 0);
bg.setScale(6);
@ -460,6 +429,7 @@ export class LoadingScene extends SceneBase {
});
disclaimerDescriptionText.setOrigin(0.5, 0.5);
const loadingGraphics: (GameObjects.Image | GameObjects.Graphics | GameObjects.Text)[] = [];
loadingGraphics.push(
bg,
graphics,

View File

@ -23,6 +23,7 @@ import type { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import { BATTLE_STATS, type PermanentStat, Stat, TEMP_BATTLE_STATS, type TempBattleStat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { TextStyle } from "#enums/text-style";
import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import type {
DoubleBattleChanceBoosterModifierType,
@ -40,7 +41,7 @@ import type {
} from "#modifiers/modifier-type";
import type { VoucherType } from "#system/voucher";
import type { ModifierInstanceMap, ModifierString } from "#types/modifier-types";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common";
import { getModifierType } from "#utils/modifier-utils";
import i18next from "i18next";
@ -461,7 +462,7 @@ export abstract class LapsingPersistentModifier extends PersistentModifier {
* @see {@linkcode apply}
*/
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
public override type: DoubleBattleChanceBoosterModifierType;
public declare type: DoubleBattleChanceBoosterModifierType;
match(modifier: Modifier): boolean {
return modifier instanceof DoubleBattleChanceBoosterModifier && modifier.getMaxBattles() === this.getMaxBattles();
@ -935,7 +936,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
* Currently used by Shuckle Juice item
*/
export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
public override type: PokemonBaseStatTotalModifierType;
public declare type: PokemonBaseStatTotalModifierType;
public isTransferable = false;
public statModifier: 10 | -15;
@ -2073,7 +2074,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
}
export class TerrastalizeModifier extends ConsumablePokemonModifier {
public override type: TerastallizeModifierType;
public declare type: TerastallizeModifierType;
public teraType: PokemonType;
constructor(type: TerastallizeModifierType, pokemonId: number, teraType: PokemonType) {
@ -2317,7 +2318,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
}
export class TmModifier extends ConsumablePokemonModifier {
public override type: TmModifierType;
public declare type: TmModifierType;
/**
* Applies {@linkcode TmModifier}
@ -2364,7 +2365,7 @@ export class RememberMoveModifier extends ConsumablePokemonModifier {
}
export class EvolutionItemModifier extends ConsumablePokemonModifier {
public override type: EvolutionItemModifierType;
public declare type: EvolutionItemModifierType;
/**
* Applies {@linkcode EvolutionItemModifier}
* @param playerPokemon The {@linkcode PlayerPokemon} that should evolve via item
@ -2529,7 +2530,7 @@ export class ExpBoosterModifier extends PersistentModifier {
}
export class PokemonExpBoosterModifier extends PokemonHeldItemModifier {
public override type: PokemonExpBoosterModifierType;
public declare type: PokemonExpBoosterModifierType;
private boostMultiplier: number;
@ -2626,7 +2627,7 @@ export class ExpBalanceModifier extends PersistentModifier {
}
export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier {
public override type: PokemonFriendshipBoosterModifierType;
public declare type: PokemonFriendshipBoosterModifierType;
matchType(modifier: Modifier): boolean {
return modifier instanceof PokemonFriendshipBoosterModifier;
@ -2683,7 +2684,7 @@ export class PokemonNatureWeightModifier extends PokemonHeldItemModifier {
}
export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier {
public override type: PokemonMoveAccuracyBoosterModifierType;
public declare type: PokemonMoveAccuracyBoosterModifierType;
private accuracyAmount: number;
constructor(type: PokemonMoveAccuracyBoosterModifierType, pokemonId: number, accuracy: number, stackCount?: number) {
@ -2735,7 +2736,7 @@ export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier
}
export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
public override type: PokemonMultiHitModifierType;
public declare type: PokemonMultiHitModifierType;
matchType(modifier: Modifier): boolean {
return modifier instanceof PokemonMultiHitModifier;
@ -2816,7 +2817,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
}
export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier {
public override type: FormChangeItemModifierType;
public declare type: FormChangeItemModifierType;
public formChangeItem: FormChangeItem;
public active: boolean;
public isTransferable = false;

View File

@ -9,6 +9,7 @@ import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
import { AttemptRunPhase } from "#phases/attempt-run-phase";
import { BattleEndPhase } from "#phases/battle-end-phase";
import { BerryPhase } from "#phases/berry-phase";
import { CheckInterludePhase } from "#phases/check-interlude-phase";
import { CheckStatusEffectPhase } from "#phases/check-status-effect-phase";
import { CheckSwitchPhase } from "#phases/check-switch-phase";
import { CommandPhase } from "#phases/command-phase";
@ -122,6 +123,7 @@ const PHASES = Object.freeze({
AttemptRunPhase,
BattleEndPhase,
BerryPhase,
CheckInterludePhase,
CheckStatusEffectPhase,
CheckSwitchPhase,
CommandPhase,
@ -667,4 +669,15 @@ export class PhaseManager {
): void {
this.startDynamicPhase(this.create(phase, ...args));
}
/** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */
public onInterlude(): void {
const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"];
this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName));
const turnEndPhase = this.findPhase<TurnEndPhase>(p => p.phaseName === "TurnEndPhase");
if (turnEndPhase) {
turnEndPhase.upcomingInterlude = true;
}
}
}

View File

@ -279,6 +279,7 @@ export class AttemptCapturePhase extends PokemonPhase {
globalScene.updateModifiers(true);
removePokemon();
if (newPokemon) {
newPokemon.leaveField(true, true, false);
newPokemon.loadAssets().then(end);
} else {
end();

View File

@ -0,0 +1,18 @@
import { globalScene } from "#app/global-scene";
import { Phase } from "#app/phase";
export class CheckInterludePhase extends Phase {
public override readonly phaseName = "CheckInterludePhase";
public override start(): void {
super.start();
const { phaseManager } = globalScene;
const { waveIndex } = globalScene.currentBattle;
if (waveIndex % 10 === 0 && globalScene.getEnemyParty().every(p => p.isFainted())) {
phaseManager.onInterlude();
}
this.end();
}
}

View File

@ -2,7 +2,6 @@ import type { TurnCommand } from "#app/battle";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import { speciesStarterCosts } from "#balance/starters";
import type { EncoreTag } from "#data/battler-tags";
import { TrappedTag } from "#data/battler-tags";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side";
@ -22,49 +21,63 @@ import type { MoveTargetSet } from "#moves/move";
import { getMoveTargets } from "#moves/move-utils";
import { FieldPhase } from "#phases/field-phase";
import type { TurnMove } from "#types/turn-move";
import { isNullOrUndefined } from "#utils/common";
import i18next from "i18next";
export class CommandPhase extends FieldPhase {
public readonly phaseName = "CommandPhase";
protected fieldIndex: number;
/**
* Whether the command phase is handling a switch command
*/
private isSwitch = false;
constructor(fieldIndex: number) {
super();
this.fieldIndex = fieldIndex;
}
start() {
super.start();
globalScene.updateGameInfo();
/**
* Resets the cursor to the position of {@linkcode Command.FIGHT} if any of the following are true
* - The setting to remember the last action is not enabled
* - This is the first turn of a mystery encounter, trainer battle, or the END biome
* - The cursor is currently on the POKEMON command
*/
private resetCursorIfNeeded(): void {
const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND];
const { arena, commandCursorMemory, currentBattle } = globalScene;
const { battleType, turn } = currentBattle;
const { biomeType } = arena;
// If one of these conditions is true, we always reset the cursor to Command.FIGHT
const cursorResetEvent =
globalScene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER ||
globalScene.currentBattle.battleType === BattleType.TRAINER ||
globalScene.arena.biomeType === BiomeId.END;
battleType === BattleType.MYSTERY_ENCOUNTER || battleType === BattleType.TRAINER || biomeType === BiomeId.END;
if (commandUiHandler) {
if (!commandUiHandler) {
return;
}
if (
(globalScene.currentBattle.turn === 1 && (!globalScene.commandCursorMemory || cursorResetEvent)) ||
(turn === 1 && (!commandCursorMemory || cursorResetEvent)) ||
commandUiHandler.getCursor() === Command.POKEMON
) {
commandUiHandler.setCursor(Command.FIGHT);
} else {
commandUiHandler.setCursor(commandUiHandler.getCursor());
}
}
if (this.fieldIndex) {
/**
* Submethod of {@linkcode start} that validates field index logic for nonzero field indices.
* Must only be called if the field index is nonzero.
*/
private handleFieldIndexLogic(): void {
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
// TODO: Prevent this from happening in the first place
if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) {
this.fieldIndex = FieldPosition.CENTER;
} else {
return;
}
const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1];
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
@ -73,8 +86,12 @@ export class CommandPhase extends FieldPhase {
};
}
}
}
/**
* Submethod of {@linkcode start} that sets the turn command to skip if this pokemon
* is commanding its ally via {@linkcode AbilityId.COMMANDER}.
*/
private checkCommander(): void {
// If the Pokemon has applied Commander's effects to its ally, skip this command
if (
globalScene.currentBattle?.double &&
@ -86,55 +103,92 @@ export class CommandPhase extends FieldPhase {
skip: true,
};
}
// Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP.
const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag | undefined;
if (encoreTag) {
this.getPokemon().lapseTag(BattlerTagType.ENCORE);
}
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
return this.end();
/**
* Clear out all unusable moves in front of the currently acting pokemon's move queue.
*/
// TODO: Refactor move queue handling to ensure that this method is not necessary.
private clearUnusuableMoves(): void {
const playerPokemon = this.getPokemon();
const moveQueue = playerPokemon.getMoveQueue();
if (moveQueue.length === 0) {
return;
}
let entriesToDelete = 0;
const moveset = playerPokemon.getMoveset();
for (const queuedMove of moveQueue) {
const movesetQueuedMove = moveset.find(m => m.moveId === queuedMove.move);
if (
queuedMove.move !== MoveId.NONE &&
!isVirtual(queuedMove.useMode) &&
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))
) {
entriesToDelete++;
} else {
break;
}
}
if (entriesToDelete) {
moveQueue.splice(0, entriesToDelete);
}
}
/**
* Attempt to execute the first usable move in this Pokemon's move queue
* @returns Whether a queued move was successfully set to be executed.
*/
private tryExecuteQueuedMove(): boolean {
this.clearUnusuableMoves();
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
const moveQueue = playerPokemon.getMoveQueue();
while (
moveQueue.length &&
moveQueue[0] &&
moveQueue[0].move &&
!isVirtual(moveQueue[0].useMode) &&
(!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) ||
!playerPokemon
.getMoveset()
[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(
playerPokemon,
isIgnorePP(moveQueue[0].useMode),
))
) {
moveQueue.shift();
if (moveQueue.length === 0) {
return false;
}
// TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured
if (moveQueue.length > 0) {
const queuedMove = moveQueue[0];
if (!queuedMove.move) {
this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL);
} else {
if (queuedMove.move === MoveId.NONE) {
this.handleCommand(Command.FIGHT, -1);
return true;
}
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
if (
(moveIndex > -1 &&
playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))) ||
isVirtual(queuedMove.useMode)
) {
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
} else {
if (!isVirtual(queuedMove.useMode) && moveIndex === -1) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
}
} else {
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
}
return true;
}
public override start(): void {
super.start();
globalScene.updateGameInfo();
this.resetCursorIfNeeded();
if (this.fieldIndex) {
this.handleFieldIndexLogic();
}
this.checkCommander();
const playerPokemon = this.getPokemon();
// Note: It is OK to call this if the target is not under the effect of encore; it will simply do nothing.
playerPokemon.lapseTag(BattlerTagType.ENCORE);
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
this.end();
return;
}
if (this.tryExecuteQueuedMove()) {
return;
}
if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
globalScene.currentBattle.mysteryEncounter?.skipToFightInput
@ -145,86 +199,20 @@ export class CommandPhase extends FieldPhase {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
}
}
/**
* TODO: Remove `args` and clean this thing up
* Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseMode`.
* Submethod of {@linkcode handleFightCommand} responsible for queuing the appropriate
* error message when a move cannot be used.
* @param user - The pokemon using the move
* @param cursor - The index of the move in the moveset
*/
handleCommand(command: Command, cursor: number, ...args: any[]): boolean {
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
let success = false;
switch (command) {
// TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove
case Command.TERA:
case Command.FIGHT: {
let useStruggle = false;
const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined;
if (
cursor === -1 ||
playerPokemon.trySelectMove(cursor, isIgnorePP(args[0] as MoveUseMode)) ||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)
) {
let moveId: MoveId;
if (useStruggle) {
moveId = MoveId.STRUGGLE;
} else if (turnMove !== undefined) {
moveId = turnMove.move;
} else if (cursor > -1) {
moveId = playerPokemon.getMoveset()[cursor].moveId;
} else {
moveId = MoveId.NONE;
}
const turnCommand: TurnCommand = {
command: Command.FIGHT,
cursor: cursor,
move: { move: moveId, targets: [], useMode: args[0] },
args: args,
};
const preTurnCommand: TurnCommand = {
command: command,
targets: [this.fieldIndex],
skip: command === Command.FIGHT,
};
const moveTargets: MoveTargetSet =
turnMove === undefined
? getMoveTargets(playerPokemon, moveId)
: {
targets: turnMove.targets,
multiple: turnMove.targets.length > 1,
};
if (!moveId) {
turnCommand.targets = [this.fieldIndex];
}
console.log(moveTargets, getPokemonNameWithAffix(playerPokemon));
if (moveTargets.targets.length > 1 && moveTargets.multiple) {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) {
turnCommand.move.targets = moveTargets.targets;
} else if (
turnCommand.move &&
playerPokemon.getTag(BattlerTagType.CHARGING) &&
playerPokemon.getMoveQueue().length >= 1
) {
turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets;
} else {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand;
globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
success = true;
} else if (cursor < playerPokemon.getMoveset().length) {
const move = playerPokemon.getMoveset()[cursor];
private queueFightErrorMessage(user: PlayerPokemon, cursor: number) {
const move = user.getMoveset()[cursor];
globalScene.ui.setMode(UiMode.MESSAGE);
// Decides between a Disabled, Not Implemented, or No PP translation message
const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon)
? playerPokemon
.getRestrictingTag(move.moveId, playerPokemon)!
.selectionDeniedText(playerPokemon, move.moveId)
const errorMessage = user.isMoveRestricted(move.moveId, user)
? user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId)
: move.getName().endsWith(" (N)")
? "battle:moveNotImplemented"
: "battle:moveNoPP";
@ -241,52 +229,120 @@ export class CommandPhase extends FieldPhase {
true,
);
}
break;
/**
* Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase
* based on the move passed in or the cursor.
*
* Does not check if the move is usable or not, that should be handled by the caller.
*/
private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move: TurnMove | undefined): MoveId {
return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE);
}
case Command.BALL: {
const notInDex =
globalScene
.getEnemyField()
.filter(p => p.isActive(true))
.some(p => !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) &&
globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1;
if (
globalScene.arena.biomeType === BiomeId.END &&
(!globalScene.gameMode.isClassic || globalScene.gameMode.isFreshStartChallenge() || notInDex)
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballForce"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballTrainer"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
/**
* Process the logic for executing a fight-related command
*
* @remarks
* - Validates whether the move can be used, using struggle if not
* - Constructs the turn command and inserts it into the battle's turn commands
*
* @param command - The command to handle (FIGHT or TERA)
* @param cursor - The index that the cursor is placed on, or -1 if no move can be selected.
* @param ignorePP - Whether to ignore PP when checking if the move can be used.
* @param move - The move to force the command to use, if any.
*/
private handleFightCommand(
command: Command.FIGHT | Command.TERA,
cursor: number,
useMode: MoveUseMode = MoveUseMode.NORMAL,
move?: TurnMove,
): boolean {
const playerPokemon = this.getPokemon();
const ignorePP = isIgnorePP(useMode);
let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP);
// Ternary here ensures we don't compute struggle conditions unless necessary
const useStruggle = canUse
? false
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon));
canUse ||= useStruggle;
if (!canUse) {
this.queueFightErrorMessage(playerPokemon, cursor);
return false;
}
const moveId = useStruggle ? MoveId.STRUGGLE : this.computeMoveId(playerPokemon, cursor, move);
const turnCommand: TurnCommand = {
command: Command.FIGHT,
cursor,
move: { move: moveId, targets: [], useMode },
args: [useMode, move],
};
const preTurnCommand: TurnCommand = {
command,
targets: [this.fieldIndex],
skip: command === Command.FIGHT,
};
const moveTargets: MoveTargetSet =
move === undefined
? getMoveTargets(playerPokemon, moveId)
: {
targets: move.targets,
multiple: move.targets.length > 1,
};
if (moveId === MoveId.NONE) {
turnCommand.targets = [this.fieldIndex];
}
console.log(
"Move:",
MoveId[moveId],
"Move targets:",
moveTargets,
"\nPlayer Pokemon:",
getPokemonNameWithAffix(playerPokemon),
);
if (moveTargets.targets.length > 1 && moveTargets.multiple) {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) {
turnCommand.move.targets = moveTargets.targets;
} else if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
!globalScene.currentBattle.mysteryEncounter!.catchAllowed
turnCommand.move &&
playerPokemon.getTag(BattlerTagType.CHARGING) &&
playerPokemon.getMoveQueue().length >= 1
) {
turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets;
} else {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand;
globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
return true;
}
/**
* Set the mode in preparation to show the text, and then show the text.
* Only works for parameterless i18next keys.
* @param key - The i18next key for the text to show
*/
private queueShowText(key: string): void {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballMysteryEncounter"),
i18next.t(key),
null,
() => {
globalScene.ui.showText("", 0);
@ -295,45 +351,84 @@ export class CommandPhase extends FieldPhase {
null,
true,
);
}
/**
* Helper method for {@linkcode handleBallCommand} that checks if a pokeball can be thrown
* and displays the appropriate error message.
*
* @remarks
* The pokeball may not be thrown if any of the following are true:
* - It is a trainer battle
* - The player is in the {@linkcode BiomeId.END | End} biome and
* - it is not classic mode; or
* - the fresh start challenge is active; or
* - the player has not caught the target before and the player is still missing more than one starter
* - The player is in a mystery encounter that disallows catching the pokemon
* @returns Whether a pokeball can be thrown
*/
private checkCanUseBall(): boolean {
const { arena, currentBattle, gameData, gameMode } = globalScene;
const { battleType } = currentBattle;
const { biomeType } = arena;
const { isClassic } = gameMode;
const { dexData } = gameData;
const someUncaughtSpeciesOnField = globalScene
.getEnemyField()
.some(p => p.isActive() && !dexData[p.species.speciesId].caughtAttr);
const missingMultipleStarters =
gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1;
if (
biomeType === BiomeId.END &&
(!isClassic || gameMode.isFreshStartChallenge() || (someUncaughtSpeciesOnField && missingMultipleStarters))
) {
this.queueShowText("battle:noPokeballForce");
} else if (battleType === BattleType.TRAINER) {
this.queueShowText("battle:noPokeballTrainer");
} else if (currentBattle.isBattleMysteryEncounter() && !currentBattle.mysteryEncounter!.catchAllowed) {
this.queueShowText("battle:noPokeballMysteryEncounter");
} else {
return true;
}
return false;
}
/**
* Helper method for {@linkcode handleCommand} that handles the logic when the selected command is to use a pokeball.
*
* @param cursor - The index of the pokeball to use
* @returns Whether the command was successfully initiated
*/
private handleBallCommand(cursor: number): boolean {
const targets = globalScene
.getEnemyField()
.filter(p => p.isActive(true))
.map(p => p.getBattlerIndex());
if (targets.length > 1) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballMulti"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else if (cursor < 5) {
const targetPokemon = globalScene.getEnemyField().find(p => p.isActive(true));
this.queueShowText("battle:noPokeballMulti");
return false;
}
if (!this.checkCanUseBall()) {
return false;
}
const numBallTypes = 5;
if (cursor < numBallTypes) {
const targetPokemon = globalScene.getEnemyPokemon();
if (
targetPokemon?.isBoss() &&
targetPokemon?.bossSegmentIndex >= 1 &&
// TODO: Decouple this hardcoded exception for wonder guard and just check the target...
!targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) &&
cursor < PokeballType.MASTER_BALL
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballStrong"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else {
this.queueShowText("battle:noPokeballStrong");
return false;
}
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: Command.BALL,
cursor: cursor,
@ -342,64 +437,29 @@ export class CommandPhase extends FieldPhase {
if (this.fieldIndex) {
globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
success = true;
return true;
}
return false;
}
}
break;
}
case Command.POKEMON:
case Command.RUN: {
const isSwitch = command === Command.POKEMON;
const { currentBattle, arena } = globalScene;
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed;
if (
!isSwitch &&
(arena.biomeType === BiomeId.END ||
(!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed))
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noEscapeForce"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else if (
!isSwitch &&
(currentBattle.battleType === BattleType.TRAINER ||
currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noEscapeTrainer"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else {
const batonPass = isSwitch && (args[0] as boolean);
/**
* Submethod of {@linkcode tryLeaveField} to handle the logic for effects that prevent the pokemon from leaving the field
* due to trapping abilities or effects.
*
* This method queues the proper messages in the case of trapping abilities or effects.
*
* @returns Whether the pokemon is currently trapped
*/
private handleTrap(): boolean {
const playerPokemon = this.getPokemon();
const trappedAbMessages: string[] = [];
if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) {
currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
success = true;
if (!isSwitch && this.fieldIndex) {
currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
const isSwitch = this.isSwitch;
if (!playerPokemon.isTrapped(trappedAbMessages)) {
return false;
}
} else if (trappedAbMessages.length > 0) {
if (!isSwitch) {
if (trappedAbMessages.length > 0) {
if (isSwitch) {
globalScene.ui.setMode(UiMode.MESSAGE);
}
globalScene.ui.showText(
@ -407,7 +467,7 @@ export class CommandPhase extends FieldPhase {
null,
() => {
globalScene.ui.showText("", 0);
if (!isSwitch) {
if (isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
@ -418,15 +478,86 @@ export class CommandPhase extends FieldPhase {
const trapTag = playerPokemon.getTag(TrappedTag);
const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER);
if (!trapTag && !fairyLockTag) {
i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`);
break;
}
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
}
const showNoEscapeText = (tag: any) => {
if (trapTag) {
this.showNoEscapeText(trapTag, false);
} else if (fairyLockTag) {
this.showNoEscapeText(fairyLockTag, false);
}
}
return true;
}
/**
* Common helper method that attempts to have the pokemon leave the field.
* Checks for trapping abilities and effects.
*
* @param cursor - The index of the option that the cursor is on
* @returns Whether the pokemon is able to leave the field, indicating the command phase should end
*/
private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean {
const currentBattle = globalScene.currentBattle;
if (isBatonSwitch || !this.handleTrap()) {
currentBattle.turnCommands[this.fieldIndex] = this.isSwitch
? {
command: Command.POKEMON,
cursor,
args: [isBatonSwitch],
}
: {
command: Command.RUN,
};
if (!this.isSwitch && this.fieldIndex) {
currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
return true;
}
return false;
}
/**
* Helper method for {@linkcode handleCommand} that handles the logic when the selected command is RUN.
*
* @remarks
* Checks if the player is allowed to flee, and if not, queues the appropriate message.
*
* The player cannot flee if:
* - The player is in the {@linkcode BiomeId.END | End} biome
* - The player is in a trainer battle
* - The player is in a mystery encounter that disallows fleeing
* - The player's pokemon is trapped by an ability or effect
* @returns Whether the pokemon is able to leave the field, indicating the command phase should end
*/
private handleRunCommand(): boolean {
const { currentBattle, arena } = globalScene;
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed ?? true;
if (arena.biomeType === BiomeId.END || !mysteryEncounterFleeAllowed) {
this.queueShowText("battle:noEscapeForce");
return false;
}
if (
currentBattle.battleType === BattleType.TRAINER ||
currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE
) {
this.queueShowText("battle:noEscapeTrainer");
return false;
}
const success = this.tryLeaveField();
return success;
}
/**
* Show a message indicating that the pokemon cannot escape, and then return to the command phase.
*/
private showNoEscapeText(tag: any, isSwitch: boolean): void {
globalScene.ui.showText(
i18next.t("battle:noEscapePokemon", {
pokemonName:
@ -434,7 +565,7 @@ export class CommandPhase extends FieldPhase {
? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!)
: "",
moveName: tag.getMoveName(),
escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee"),
escapeVerb: i18next.t(isSwitch ? "battle:escapeVerbSwitch" : "battle:escapeVerbFlee"),
}),
null,
() => {
@ -446,17 +577,47 @@ export class CommandPhase extends FieldPhase {
null,
true,
);
};
}
if (trapTag) {
showNoEscapeText(trapTag);
} else if (fairyLockTag) {
showNoEscapeText(fairyLockTag);
}
}
}
// Overloads for handleCommand to provide a more specific signature for the different options
/**
* Process the command phase logic based on the selected command
*
* @param command - The kind of command to handle
* @param cursor - The index of option that the cursor is on, or -1 if no option is selected
* @param useMode - The mode to use for the move, if applicable. For switches, a boolean that specifies whether the switch is a Baton switch.
* @param move - For {@linkcode Command.FIGHT}, the move to use
* @returns Whether the command was successful
*/
handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean;
handleCommand(command: Command.BALL, cursor: number): boolean;
handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean;
handleCommand(command: Command.RUN, cursor: number): boolean;
handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean;
public handleCommand(
command: Command,
cursor: number,
useMode: boolean | MoveUseMode = false,
move?: TurnMove,
): boolean {
let success = false;
switch (command) {
case Command.TERA:
case Command.FIGHT:
success = this.handleFightCommand(command, cursor, typeof useMode === "boolean" ? undefined : useMode, move);
break;
}
case Command.BALL:
success = this.handleBallCommand(cursor);
break;
case Command.POKEMON:
this.isSwitch = true;
success = this.tryLeaveField(cursor, typeof useMode === "boolean" ? useMode : undefined);
this.isSwitch = false;
break;
case Command.RUN:
success = this.handleRunCommand();
}
if (success) {

View File

@ -1,7 +1,8 @@
import { globalScene } from "#app/global-scene";
import { Phase } from "#app/phase";
import { PlayerGender } from "#enums/player-gender";
import { addTextObject, TextStyle } from "#ui/text";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import i18next from "i18next";
export class EndCardPhase extends Phase {

View File

@ -135,7 +135,7 @@ export class EvolutionPhase extends Phase {
sprite
.setPipelineData("ignoreTimeTint", true)
.setPipelineData("spriteKey", pokemon.getSpriteKey())
.setPipelineData("spriteKey", spriteKey)
.setPipelineData("shiny", pokemon.shiny)
.setPipelineData("variant", pokemon.variant);

View File

@ -2,9 +2,10 @@ import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import type { BattlerIndex } from "#enums/battler-index";
import { PERMANENT_STATS, Stat } from "#enums/stat";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { PokemonPhase } from "#phases/pokemon-phase";
import { getTextColor, TextStyle } from "#ui/text";
import { getTextColor } from "#ui/text";
import i18next from "i18next";
export class ScanIvsPhase extends PokemonPhase {

View File

@ -18,6 +18,8 @@ import i18next from "i18next";
export class TurnEndPhase extends FieldPhase {
public readonly phaseName = "TurnEndPhase";
public upcomingInterlude = false;
start() {
super.start();
@ -59,9 +61,11 @@ export class TurnEndPhase extends FieldPhase {
pokemon.tempSummonData.waveTurnCount++;
};
if (!this.upcomingInterlude) {
this.executeForAll(handlePokemon);
globalScene.arena.lapseTags();
}
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
globalScene.arena.trySetWeather(WeatherType.NONE);

View File

@ -218,6 +218,7 @@ export class TurnStartPhase extends FieldPhase {
break;
}
}
phaseManager.pushNew("CheckInterludePhase");
// TODO: Re-order these phases to be consistent with mainline turn order:
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
@ -231,10 +232,10 @@ export class TurnStartPhase extends FieldPhase {
phaseManager.pushNew("PositionalTagPhase");
phaseManager.pushNew("TurnEndPhase");
/**
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front
* of the queue and dequeues to start the next phase
* this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence
/*
* `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend`
* (aka everything that is queued via `unshift()`) to the front of the queue and dequeues to start the next phase.
* This is important since stuff like `SwitchSummonPhase`, `AttemptRunPhase`, and `AttemptCapturePhase` break the "flow" and should take precedence
*/
this.end();
}

View File

@ -1,5 +1,5 @@
import pkg from "#package.json";
import { camelCaseToKebabCase } from "#utils/common";
import { toKebabCase } from "#utils/strings";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend";
@ -194,14 +194,16 @@ export async function initI18n(): Promise<void> {
],
backend: {
loadPath(lng: string, [ns]: string[]) {
// Use namespace maps where required
let fileName: string;
if (namespaceMap[ns]) {
fileName = namespaceMap[ns];
} else if (ns.startsWith("mysteryEncounters/")) {
fileName = camelCaseToKebabCase(ns + "Dialogue");
fileName = toKebabCase(ns + "-dialogue"); // mystery-encounters/a-trainers-test-dialogue
} else {
fileName = camelCaseToKebabCase(ns);
fileName = toKebabCase(ns);
}
// ex: "./locales/en/move-anims"
return `./locales/${lng}/${fileName}.json?v=${pkg.version}`;
},
},

View File

@ -890,7 +890,7 @@ export const achvs = {
100,
c =>
c instanceof FreshStartChallenge &&
c.value > 0 &&
c.value === 1 &&
!globalScene.gameMode.challenges.some(
c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0,
),

View File

@ -1459,11 +1459,10 @@ export class GameData {
reader.onload = (_ => {
return e => {
let dataName: string;
let dataName = GameDataType[dataType].toLowerCase();
let dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct?
let valid = false;
try {
dataName = GameDataType[dataType].toLowerCase();
switch (dataType) {
case GameDataType.SYSTEM: {
dataStr = this.convertSystemDataStr(dataStr);
@ -1498,7 +1497,6 @@ export class GameData {
const displayError = (error: string) =>
globalScene.ui.showText(error, null, () => globalScene.ui.showText("", 0), fixedInt(1500));
dataName = dataName!; // tell TS compiler that dataName is defined!
if (!valid) {
return globalScene.ui.showText(

View File

@ -88,12 +88,12 @@ export class PokemonData {
this.id = source.id;
this.player = sourcePokemon?.isPlayer() ?? source.player;
this.species = sourcePokemon?.species.speciesId ?? source.species;
this.nickname = sourcePokemon?.summonData.illusion?.basePokemon.nickname ?? source.nickname;
this.nickname = source.nickname;
this.formIndex = Math.max(Math.min(source.formIndex, getPokemonSpecies(this.species).forms.length - 1), 0);
this.abilityIndex = source.abilityIndex;
this.passive = source.passive;
this.shiny = sourcePokemon?.summonData.illusion?.basePokemon.shiny ?? source.shiny;
this.variant = sourcePokemon?.summonData.illusion?.basePokemon.variant ?? source.variant;
this.shiny = source.shiny;
this.variant = source.variant;
this.pokeball = source.pokeball ?? PokeballType.POKEBALL;
this.level = source.level;
this.exp = source.exp;
@ -134,8 +134,8 @@ export class PokemonData {
this.fusionSpecies = sourcePokemon?.fusionSpecies?.speciesId ?? source.fusionSpecies;
this.fusionFormIndex = source.fusionFormIndex;
this.fusionAbilityIndex = source.fusionAbilityIndex;
this.fusionShiny = sourcePokemon?.summonData.illusion?.basePokemon.fusionShiny ?? source.fusionShiny;
this.fusionVariant = sourcePokemon?.summonData.illusion?.basePokemon.fusionVariant ?? source.fusionVariant;
this.fusionShiny = source.fusionShiny;
this.fusionVariant = source.fusionVariant;
this.fusionGender = source.fusionGender;
this.fusionLuck = source.fusionLuck ?? (source.fusionShiny ? source.fusionVariant + 1 : 0);
this.fusionTeraType = (source.fusionTeraType ?? 0) as PokemonType;

View File

@ -171,6 +171,7 @@ export const SettingKeys = {
UI_Volume: "UI_SOUND_EFFECTS",
Battle_Music: "BATTLE_MUSIC",
Show_BGM_Bar: "SHOW_BGM_BAR",
Hide_Username: "HIDE_USERNAME",
Move_Touch_Controls: "MOVE_TOUCH_CONTROLS",
Shop_Overlay_Opacity: "SHOP_OVERLAY_OPACITY",
};
@ -625,6 +626,13 @@ export const Setting: Array<Setting> = [
default: 1,
type: SettingType.DISPLAY,
},
{
key: SettingKeys.Hide_Username,
label: i18next.t("settings:hideUsername"),
options: OFF_ON,
default: 0,
type: SettingType.DISPLAY,
},
{
key: SettingKeys.Master_Volume,
label: i18next.t("settings:masterVolume"),
@ -792,6 +800,9 @@ export function setSetting(setting: string, value: number): boolean {
case SettingKeys.Show_BGM_Bar:
globalScene.showBgmBar = Setting[index].options[value].value === "On";
break;
case SettingKeys.Hide_Username:
globalScene.hideUsername = Setting[index].options[value].value === "On";
break;
case SettingKeys.Candy_Upgrade_Notification:
if (globalScene.candyUpgradeNotification === value) {
break;

View File

@ -5,8 +5,9 @@ import { Challenges } from "#enums/challenges";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id";
import { TextStyle } from "#enums/text-style";
import { WeatherType } from "#enums/weather-type";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import type { nil } from "#utils/common";
import { isNullOrUndefined } from "#utils/common";
import i18next from "i18next";

View File

@ -1,5 +1,6 @@
import { globalScene } from "#app/global-scene";
import { addTextObject, TextStyle } from "#ui/text";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import i18next from "i18next";
const barWidth = 118;

View File

@ -1,7 +1,8 @@
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { addBBCodeTextObject, getTextColor, getTextStyleOptions, TextStyle } from "#ui/text";
import { addBBCodeTextObject, getTextColor, getTextStyleOptions } from "#ui/text";
import { UiHandler } from "#ui/ui-handler";
import { addWindow } from "#ui/ui-theme";
import { fixedInt, rgbHexToRgba } from "#utils/common";

View File

@ -1,8 +1,9 @@
import { globalScene } from "#app/global-scene";
import type { PlayerGender } from "#enums/player-gender";
import { TextStyle } from "#enums/text-style";
import { Achv, getAchievementDescription } from "#system/achv";
import { Voucher } from "#system/voucher";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
export class AchvBar extends Phaser.GameObjects.Container {
private defaultWidth: number;

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { PlayerGender } from "#enums/player-gender";
import { TextStyle } from "#enums/text-style";
import type { UiMode } from "#enums/ui-mode";
import type { Achv } from "#system/achv";
import { achvs, getAchievementDescription } from "#system/achv";
@ -9,7 +10,7 @@ import type { Voucher } from "#system/voucher";
import { getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#system/voucher";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { ScrollBar } from "#ui/scroll-bar";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import i18next from "i18next";

View File

@ -1,12 +1,12 @@
import { pokerogueApi } from "#api/pokerogue-api";
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
import type { ModalConfig } from "#ui/modal-ui-handler";
import { TextStyle } from "#ui/text";
import { formatText } from "#utils/common";
import { toTitleCase } from "#utils/strings";
type AdminUiHandlerService = "discord" | "google";
type AdminUiHandlerServiceMode = "Link" | "Unlink";
@ -21,9 +21,9 @@ export class AdminUiHandler extends FormModalUiHandler {
private readonly httpUserNotFoundErrorCode: number = 404;
private readonly ERR_REQUIRED_FIELD = (field: string) => {
if (field === "username") {
return `${formatText(field)} is required`;
return `${toTitleCase(field)} is required`;
}
return `${formatText(field)} Id is required`;
return `${toTitleCase(field)} Id is required`;
};
// returns a string saying whether a username has been successfully linked/unlinked to discord/google
private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => {

View File

@ -3,6 +3,7 @@ import { ArenaTrapTag } from "#data/arena-tag";
import { TerrainType } from "#data/terrain";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type";
import { TextStyle } from "#enums/text-style";
import { WeatherType } from "#enums/weather-type";
import type { ArenaEvent } from "#events/arena";
import {
@ -14,10 +15,11 @@ import {
} from "#events/arena";
import type { TurnEndEvent } from "#events/battle-scene";
import { BattleSceneEventType } from "#events/battle-scene";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { TimeOfDayWidget } from "#ui/time-of-day-widget";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import { fixedInt, formatText, toCamelCaseString } from "#utils/common";
import { fixedInt } from "#utils/common";
import { toCamelCase, toTitleCase } from "#utils/strings";
import type { ParseKeys } from "i18next";
import i18next from "i18next";
@ -48,10 +50,10 @@ export function getFieldEffectText(arenaTagType: string): string {
if (!arenaTagType || arenaTagType === ArenaTagType.NONE) {
return arenaTagType;
}
const effectName = toCamelCaseString(arenaTagType);
const effectName = toCamelCase(arenaTagType);
const i18nKey = `arenaFlyout:${effectName}` as ParseKeys;
const resultName = i18next.t(i18nKey);
return !resultName || resultName === i18nKey ? formatText(arenaTagType) : resultName;
return !resultName || resultName === i18nKey ? toTitleCase(arenaTagType) : resultName;
}
export class ArenaFlyout extends Phaser.GameObjects.Container {
@ -86,14 +88,14 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
private flyoutTextHeaderPlayer: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} header used to indicate the enemy's effects */
private flyoutTextHeaderEnemy: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} header used to indicate neutral effects */
/** The {@linkcode Phaser.GameObjects.Text} header used to indicate field effects */
private flyoutTextHeaderField: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} used to indicate the player's effects */
private flyoutTextPlayer: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} used to indicate the enemy's effects */
private flyoutTextEnemy: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} used to indicate neutral effects */
/** The {@linkcode Phaser.GameObjects.Text} used to indicate field effects */
private flyoutTextField: Phaser.GameObjects.Text;
/** Container for all field effects observed by this object */
@ -163,7 +165,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
this.flyoutTextHeaderField = addTextObject(
this.flyoutWidth / 2,
5,
i18next.t("arenaFlyout:neutral"),
i18next.t("arenaFlyout:field"),
TextStyle.SUMMARY_GREEN,
);
this.flyoutTextHeaderField.setFontSize(54);

View File

@ -2,9 +2,10 @@ import { globalScene } from "#app/global-scene";
import { getPokeballName } from "#data/pokeball";
import { Button } from "#enums/buttons";
import { Command } from "#enums/command";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import type { CommandPhase } from "#phases/command-phase";
import { addTextObject, getTextStyleOptions, TextStyle } from "#ui/text";
import { addTextObject, getTextStyleOptions } from "#ui/text";
import { UiHandler } from "#ui/ui-handler";
import { addWindow } from "#ui/ui-theme";
import i18next from "i18next";

View File

@ -1,6 +1,7 @@
import type { InfoToggle } from "#app/battle-scene";
import { globalScene } from "#app/global-scene";
import { addTextObject, TextStyle } from "#ui/text";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import { fixedInt } from "#utils/common";
import i18next from "i18next";

View File

@ -2,12 +2,13 @@ import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import { BerryType } from "#enums/berry-type";
import { MoveId } from "#enums/move-id";
import { TextStyle } from "#enums/text-style";
import { UiTheme } from "#enums/ui-theme";
import type { BerryUsedEvent, MoveUsedEvent } from "#events/battle-scene";
import { BattleSceneEventType } from "#events/battle-scene";
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
import type { Move } from "#moves/move";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { fixedInt } from "#utils/common";
/** Container for info about a {@linkcode Move} */

View File

@ -4,9 +4,10 @@ import { getTypeRgb } from "#data/type";
import { PokemonType } from "#enums/pokemon-type";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { TextStyle } from "#enums/text-style";
import type { Pokemon } from "#field/pokemon";
import { getVariantTint } from "#sprites/variant";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { fixedInt, getLocalizedSpriteKey, getShinyDescriptor } from "#utils/common";
import i18next from "i18next";

View File

@ -1,10 +1,11 @@
import { globalScene } from "#app/global-scene";
import { Stat } from "#enums/stat";
import { TextStyle } from "#enums/text-style";
import type { EnemyPokemon } from "#field/pokemon";
import { BattleFlyout } from "#ui/battle-flyout";
import type { BattleInfoParamList } from "#ui/battle-info";
import { BattleInfo } from "#ui/battle-info";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import i18next from "i18next";
import type { GameObjects } from "phaser";

View File

@ -1,9 +1,10 @@
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { getStatKey, PERMANENT_STATS } from "#enums/stat";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#ui/text";
import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import i18next from "i18next";
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene";
import { addTextObject, TextStyle } from "#ui/text";
import { formatText } from "#utils/common";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import { toTitleCase } from "#utils/strings";
import i18next from "i18next";
const hiddenX = -150;
@ -100,7 +101,7 @@ export class BgmBar extends Phaser.GameObjects.Container {
getRealBgmName(bgmName: string): string {
return i18next.t([`bgmName:${bgmName}`, "bgmName:missing_entries"], {
name: formatText(bgmName),
name: toTitleCase(bgmName),
});
}
}

View File

@ -1,7 +1,8 @@
import { globalScene } from "#app/global-scene";
import { starterColors } from "#app/global-vars/starter-colors";
import type { SpeciesId } from "#enums/species-id";
import { addTextObject, TextStyle } from "#ui/text";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import { rgbHexToRgba } from "#utils/common";
import { argbFromRgba } from "@material/material-color-utilities";

View File

@ -3,8 +3,9 @@ import type { Challenge } from "#data/challenge";
import { Button } from "#enums/buttons";
import { Challenges } from "#enums/challenges";
import { Color, ShadowColor } from "#enums/color";
import { TextStyle } from "#enums/text-style";
import type { UiMode } from "#enums/ui-mode";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { UiHandler } from "#ui/ui-handler";
import { addWindow } from "#ui/ui-theme";
import { getLocalizedSpriteKey } from "#utils/common";

View File

@ -5,11 +5,12 @@ import { Button } from "#enums/buttons";
import { Command } from "#enums/command";
import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { TerastallizeAccessModifier } from "#modifiers/modifier";
import type { CommandPhase } from "#phases/command-phase";
import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { UiHandler } from "#ui/ui-handler";
import i18next from "i18next";

View File

@ -1,6 +1,7 @@
import { pokerogueApi } from "#api/pokerogue-api";
import { globalScene } from "#app/global-scene";
import { addTextObject, TextStyle } from "#ui/text";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import { executeIf } from "#utils/common";
import { getEnumKeys } from "#utils/enums";

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene";
import { TextStyle } from "#enums/text-style";
import { ScrollBar } from "#ui/scroll-bar";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import i18next from "i18next";

View File

@ -1,8 +1,9 @@
import { globalScene } from "#app/global-scene";
import { TextStyle } from "#enums/text-style";
import type { EggCountChangedEvent } from "#events/egg";
import { EggEventType } from "#events/egg";
import type { EggHatchSceneHandler } from "#ui/egg-hatch-scene-handler";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
/**

View File

@ -6,10 +6,11 @@ import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#data/egg";
import { Button } from "#enums/buttons";
import { EggTier } from "#enums/egg-type";
import { GachaType } from "#enums/gacha-types";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { getVoucherTypeIcon, VoucherType } from "#system/voucher";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { addTextObject, getEggTierTextTint, getTextStyleOptions, TextStyle } from "#ui/text";
import { addTextObject, getEggTierTextTint, getTextStyleOptions } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import { fixedInt, randSeedShuffle } from "#utils/common";
import { getEnumValues } from "#utils/enums";
@ -74,7 +75,7 @@ export class EggGachaUiHandler extends MessageUiHandler {
const gachaInfoContainer = globalScene.add.container(160, 46);
const currentLanguage = i18next.resolvedLanguage ?? "en";
let gachaTextStyle = TextStyle.WINDOW_ALT;
let gachaTextStyle: TextStyle = TextStyle.WINDOW_ALT;
let gachaX = 4;
let gachaY = 0;
let pokemonIconX = -20;

View File

@ -1,11 +1,12 @@
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler";
import { ScrollBar } from "#ui/scroll-bar";
import { ScrollableGridUiHandler } from "#ui/scrollable-grid-handler";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import i18next from "i18next";

View File

@ -1,8 +1,9 @@
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
export class EvolutionSceneHandler extends MessageUiHandler {
public evolutionContainer: Phaser.GameObjects.Container;

View File

@ -7,12 +7,13 @@ import { Command } from "#enums/command";
import { MoveCategory } from "#enums/move-category";
import { MoveUseMode } from "#enums/move-use-mode";
import { PokemonType } from "#enums/pokemon-type";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
import type { PokemonMove } from "#moves/pokemon-move";
import type { CommandPhase } from "#phases/command-phase";
import { MoveInfoOverlay } from "#ui/move-info-overlay";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { UiHandler } from "#ui/ui-handler";
import { fixedInt, getLocalizedSpriteKey, padInt } from "#utils/common";
import i18next from "i18next";
@ -284,7 +285,7 @@ export class FightUiHandler extends UiHandler implements InfoToggle {
const ppColorStyle = FightUiHandler.ppRatioToColor(pp / maxPP);
//** Changes the text color and shadow according to the determined TextStyle */
// Changes the text color and shadow according to the determined TextStyle
this.ppText.setColor(this.getTextColor(ppColorStyle, false)).setShadowColor(this.getTextColor(ppColorStyle, true));
this.moveInfoOverlay.show(pokemonMove.getMove());

View File

@ -1,10 +1,11 @@
import { globalScene } from "#app/global-scene";
import type { DropDownColumn } from "#enums/drop-down-column";
import { TextStyle } from "#enums/text-style";
import type { UiTheme } from "#enums/ui-theme";
import type { DropDown } from "#ui/dropdown";
import { DropDownType } from "#ui/dropdown";
import type { StarterContainer } from "#ui/starter-container";
import { addTextObject, getTextColor, TextStyle } from "#ui/text";
import { addTextObject, getTextColor } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
export class FilterBar extends Phaser.GameObjects.Container {

View File

@ -1,9 +1,10 @@
import { globalScene } from "#app/global-scene";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import type { UiTheme } from "#enums/ui-theme";
import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler";
import type { StarterContainer } from "#ui/starter-container";
import { addTextObject, getTextColor, TextStyle } from "#ui/text";
import { addTextObject, getTextColor } from "#ui/text";
import type { UI } from "#ui/ui";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import i18next from "i18next";

View File

@ -1,9 +1,10 @@
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style";
import type { UiMode } from "#enums/ui-mode";
import type { ModalConfig } from "#ui/modal-ui-handler";
import { ModalUiHandler } from "#ui/modal-ui-handler";
import { addTextInputObject, addTextObject, TextStyle } from "#ui/text";
import { addTextInputObject, addTextObject } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import { fixedInt } from "#utils/common";
import type InputText from "phaser3-rex-plugins/plugins/inputtext";
@ -71,6 +72,10 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
(hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(),
"",
TextStyle.TOOLTIP_CONTENT,
{
fontSize: "42px",
wordWrap: { width: 850 },
},
);
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
@ -83,20 +88,28 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
this.inputs = [];
this.formLabels = [];
fieldsConfig.forEach((config, f) => {
const label = addTextObject(10, (hasTitle ? 31 : 5) + 20 * f, config.label, TextStyle.TOOLTIP_CONTENT);
// The Pokédex Scan Window uses width `300` instead of `160` like the other forms
// Therefore, the label does not need to be shortened
const label = addTextObject(
10,
(hasTitle ? 31 : 5) + 20 * f,
config.label.length > 25 && this.getWidth() < 200 ? config.label.slice(0, 20) + "..." : config.label,
TextStyle.TOOLTIP_CONTENT,
);
label.name = "formLabel" + f;
this.formLabels.push(label);
this.modalContainer.add(this.formLabels[this.formLabels.length - 1]);
const inputContainer = globalScene.add.container(70, (hasTitle ? 28 : 2) + 20 * f);
const inputWidth = label.width < 320 ? 80 : 80 - (label.width - 320) / 5.5;
const inputContainer = globalScene.add.container(70 + (80 - inputWidth), (hasTitle ? 28 : 2) + 20 * f);
inputContainer.setVisible(false);
const inputBg = addWindow(0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN);
const inputBg = addWindow(0, 0, inputWidth, 16, false, false, 0, 0, WindowVariant.XTHIN);
const isPassword = config?.isPassword;
const isReadOnly = config?.isReadOnly;
const input = addTextInputObject(4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, {
const input = addTextInputObject(4, -2, inputWidth * 5.5, 116, TextStyle.TOOLTIP_CONTENT, {
type: isPassword ? "password" : "text",
maxLength: isPassword ? 64 : 20,
readOnly: isReadOnly,

View File

@ -2,12 +2,14 @@ import { globalScene } from "#app/global-scene";
import { speciesStarterCosts } from "#balance/starters";
import { Button } from "#enums/buttons";
import { DexAttr } from "#enums/dex-attr";
import { TextStyle } from "#enums/text-style";
import { UiTheme } from "#enums/ui-theme";
import type { GameData } from "#system/game-data";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { UiHandler } from "#ui/ui-handler";
import { addWindow } from "#ui/ui-theme";
import { formatFancyLargeNumber, getPlayTimeString, toReadableString } from "#utils/common";
import { formatFancyLargeNumber, getPlayTimeString } from "#utils/common";
import { toTitleCase } from "#utils/strings";
import i18next from "i18next";
import Phaser from "phaser";
@ -501,11 +503,9 @@ export function initStatsKeys() {
sourceFunc: gameData => gameData.gameStats[key].toString(),
};
}
if (!(displayStats[key] as DisplayStat).label_key) {
if (!displayStats[key].label_key) {
const splittableKey = key.replace(/([a-z]{2,})([A-Z]{1}(?:[^A-Z]|$))/g, "$1_$2");
(displayStats[key] as DisplayStat).label_key = toReadableString(
`${splittableKey[0].toUpperCase()}${splittableKey.slice(1)}`,
);
displayStats[key].label_key = toTitleCase(splittableKey);
}
}
}

View File

@ -1,6 +1,7 @@
import { TextStyle } from "#enums/text-style";
import type { UiMode } from "#enums/ui-mode";
import { ModalUiHandler } from "#ui/modal-ui-handler";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import i18next from "i18next";
export class LoadingModalUiHandler extends ModalUiHandler {

View File

@ -1,11 +1,12 @@
import { pokerogueApi } from "#api/pokerogue-api";
import { globalScene } from "#app/global-scene";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
import type { ModalConfig } from "#ui/modal-ui-handler";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import { fixedInt } from "#utils/common";
import i18next from "i18next";

View File

@ -5,13 +5,14 @@ import { bypassLogin } from "#app/global-vars/bypass-login";
import { handleTutorial, Tutorial } from "#app/tutorial";
import { Button } from "#enums/buttons";
import { GameDataType } from "#enums/game-data-type";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
import { AdminMode, getAdminModeName } from "#ui/admin-ui-handler";
import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler";
import { BgmBar } from "#ui/bgm-bar";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { addTextObject, getTextStyleOptions, TextStyle } from "#ui/text";
import { addTextObject, getTextStyleOptions } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import { fixedInt, isLocal, sessionIdKey } from "#utils/common";
import { getCookie } from "#utils/cookies";

View File

@ -1,7 +1,8 @@
import { globalScene } from "#app/global-scene";
import type { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style";
import type { UiMode } from "#enums/ui-mode";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { UiHandler } from "#ui/ui-handler";
import { addWindow, WindowVariant } from "#ui/ui-theme";
@ -151,7 +152,12 @@ export abstract class ModalUiHandler extends UiHandler {
updateContainer(config?: ModalConfig): void {
const [marginTop, marginRight, marginBottom, marginLeft] = this.getMargin(config);
const [width, height] = [this.getWidth(config), this.getHeight(config)];
/**
* If the total amount of characters for the 2 buttons exceeds ~30 characters,
* the width in `registration-form-ui-handler.ts` and `login-form-ui-handler.ts` needs to be increased.
*/
const width = this.getWidth(config);
const height = this.getHeight(config);
this.modalContainer.setPosition(
(globalScene.game.canvas.width / 6 - (width + (marginRight - marginLeft))) / 2,
(-globalScene.game.canvas.height / 6 - (height + (marginBottom - marginTop))) / 2,
@ -165,10 +171,14 @@ export abstract class ModalUiHandler extends UiHandler {
this.titleText.setX(width / 2);
this.titleText.setVisible(!!title);
for (let b = 0; b < this.buttonContainers.length; b++) {
const sliceWidth = width / (this.buttonContainers.length + 1);
this.buttonContainers[b].setPosition(sliceWidth * (b + 1), this.modalBg.height - (this.buttonBgs[b].height + 8));
if (this.buttonContainers.length > 0) {
const spacing = 12;
const totalWidth = this.buttonBgs.reduce((sum, bg) => sum + bg.width, 0) + spacing * (this.buttonBgs.length - 1);
let x = (this.modalBg.width - totalWidth) / 2;
this.buttonContainers.forEach((container, i) => {
container.setPosition(x + this.buttonBgs[i].width / 2, this.modalBg.height - (this.buttonBgs[i].height + 8));
x += this.buttonBgs[i].width + spacing;
});
}
}

View File

@ -6,13 +6,14 @@ import { getPokeballAtlasKey } from "#data/pokeball";
import { Button } from "#enums/buttons";
import type { PokeballType } from "#enums/pokeball";
import { ShopCursorTarget } from "#enums/shop-cursor-target";
import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode";
import { HealShopCostModifier, LockModifierTiersModifier, PokemonHeldItemModifier } from "#modifiers/modifier";
import type { ModifierTypeOption } from "#modifiers/modifier-type";
import { getPlayerShopModifierTypeOptionsForWave, TmModifierType } from "#modifiers/modifier-type";
import { AwaitableUiHandler } from "#ui/awaitable-ui-handler";
import { MoveInfoOverlay } from "#ui/move-info-overlay";
import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions, TextStyle } from "#ui/text";
import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions } from "#ui/text";
import { formatMoney, NumberHolder } from "#utils/common";
import i18next from "i18next";
import Phaser from "phaser";
@ -273,12 +274,23 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
// causing errors if reroll is selected
this.awaitingActionInput = false;
// TODO: Replace with `Promise.withResolvers` when possible.
let tweenResolve: () => void;
const tweenPromise = new Promise<void>(resolve => (tweenResolve = resolve));
const { promise: tweenPromise, resolve: tweenResolve } = Promise.withResolvers<void>();
let i = 0;
// TODO: Rework this bespoke logic for animating the modifier options.
// #region: animation
/** Holds promises that resolve once each reward's *upgrade animation* has finished playing */
const rewardAnimPromises: Promise<void>[] = [];
/** Holds promises that resolves once *all* animations for a reward have finished playing */
const rewardAnimAllSettledPromises: Promise<void>[] = [];
/*
* A counter here is used instead of a loop to "stagger" the apperance of each reward,
* using `sine.easeIn` to speed up the appearance of the rewards as each animation progresses.
*
* The `onComplete` callback for this tween is set to resolve once the upgrade animations
* for each reward has finished playing, allowing for the next set of animations to
* start to appear.
*/
globalScene.tweens.addCounter({
ease: "Sine.easeIn",
duration: 1250,
@ -288,30 +300,35 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
const index = Math.floor(value * typeOptions.length);
if (index > i && index <= typeOptions.length) {
const option = this.options[i];
option?.show(
if (option) {
rewardAnimPromises.push(
option.show(
Math.floor((1 - value) * 1250) * 0.325 + 2000 * maxUpgradeCount,
-(maxUpgradeCount - typeOptions[i].upgradeCount),
rewardAnimAllSettledPromises,
),
);
}
i++;
}
},
onComplete: () => {
tweenResolve();
Promise.allSettled(rewardAnimPromises).then(() => tweenResolve());
},
});
let shopResolve: () => void;
const shopPromise = new Promise<void>(resolve => (shopResolve = resolve));
tweenPromise.then(() => {
globalScene.time.delayedCall(1000, () => {
/** Holds promises that resolve once each shop item has finished animating */
const shopAnimPromises: Promise<void>[] = [];
globalScene.time.delayedCall(1000 + maxUpgradeCount * 2000, () => {
for (const shopOption of this.shopOptionsRows.flat()) {
shopOption.show(0, 0);
// It is safe to skip awaiting the `show` method here,
// as the promise it returns is also part of the promise appended to `shopAnimPromises`,
// which is awaited later on.
shopOption.show(0, 0, shopAnimPromises, false);
}
shopResolve();
});
});
shopPromise.then(() => {
tweenPromise.then(() => {
globalScene.time.delayedCall(500, () => {
if (partyHasHeldItem) {
this.transferButtonContainer.setAlpha(0);
@ -344,6 +361,11 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
duration: 250,
});
// Ensure that the reward animations have completed before allowing input to proceed.
// Required to ensure that the user cannot interact with the UI before the animations
// have completed, (which, among other things, would allow the GameObjects to be destroyed
// before the animations have completed, causing errors).
Promise.allSettled([...shopAnimPromises, ...rewardAnimAllSettledPromises]).then(() => {
const updateCursorTarget = () => {
if (globalScene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) {
this.setRowCursor(0);
@ -368,6 +390,9 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
});
});
});
});
// #endregion: animation
return true;
}
@ -820,14 +845,45 @@ class ModifierOption extends Phaser.GameObjects.Container {
}
}
show(remainingDuration: number, upgradeCountOffset: number) {
if (!this.modifierTypeOption.cost) {
/**
* Start the tweens responsible for animating the option's appearance
*
* @privateremarks
* This method is unusual. It "returns" (one via the actual return, one by via appending to the `promiseHolder`
* parameter) two promises. The promise returned by the method resolves once the option's appearance animations have
* completed, and is meant to allow callers to synchronize with the completion of the option's appearance animations.
* The promise appended to `promiseHolder` resolves once *all* animations started by this method have completed,
* and should be used by callers to ensure that all animations have completed before proceeding.
*
* @param remainingDuration - The duration in milliseconds that the animation can play for
* @param upgradeCountOffset - The offset to apply to the upgrade count for options whose rarity is being upgraded
* @param promiseHolder - A promise that resolves once all tweens started by this method have completed will be pushed to this array.
* @param isReward - Whether the option being shown is a reward, meaning it should show pokeball and upgrade animations.
* @returns A promise that resolves once the *option's apperance animations* have completed. This promise will resolve _before_ all
* promises that are initiated in this method complete. Instead, the `promiseHolder` array will contain a new promise
* that will resolve once all animations have completed.
*
*/
async show(
remainingDuration: number,
upgradeCountOffset: number,
promiseHolder: Promise<void>[],
isReward = true,
): Promise<void> {
/** Promises for the pokeball and upgrade animations */
const animPromises: Promise<void>[] = [];
if (isReward) {
const { promise: bouncePromise, resolve: resolveBounce } = Promise.withResolvers<void>();
globalScene.tweens.add({
targets: this.pb,
y: 0,
duration: 1250,
ease: "Bounce.Out",
onComplete: () => {
resolveBounce();
},
});
animPromises.push(bouncePromise);
let lastValue = 1;
let bounceCount = 0;
@ -857,7 +913,9 @@ class ModifierOption extends Phaser.GameObjects.Container {
// TODO: Figure out proper delay between chains and then convert this into a single tween chain
// rather than starting multiple tween chains.
for (let u = 0; u < this.modifierTypeOption.upgradeCount; u++) {
const { resolve, promise } = Promise.withResolvers<void>();
globalScene.tweens.chain({
tweens: [
{
@ -883,65 +941,99 @@ class ModifierOption extends Phaser.GameObjects.Container {
ease: "Sine.easeOut",
onComplete: () => {
this.pbTint.setVisible(false);
resolve();
},
},
],
});
animPromises.push(promise);
}
}
const finalPromises: Promise<void>[] = [];
globalScene.time.delayedCall(remainingDuration + 2000, () => {
if (!globalScene) {
return;
}
if (!this.modifierTypeOption.cost) {
if (isReward) {
this.pb.setTexture("pb", `${this.getPbAtlasKey(0)}_open`);
globalScene.playSound("se/pb_rel");
const { resolve: pbResolve, promise: pbPromise } = Promise.withResolvers<void>();
globalScene.tweens.add({
targets: this.pb,
duration: 500,
delay: 250,
ease: "Sine.easeIn",
alpha: 0,
onComplete: () => this.pb.destroy(),
onComplete: () => {
Promise.allSettled(animPromises).then(() => this.pb.destroy());
pbResolve();
},
});
finalPromises.push(pbPromise);
}
/** Delay for the rest of the tweens to ensure they show after the pokeball animation begins to appear */
const delay = isReward ? 250 : 0;
const { resolve: itemResolve, promise: itemPromise } = Promise.withResolvers<void>();
globalScene.tweens.add({
targets: this.itemContainer,
delay,
duration: 500,
ease: "Elastic.Out",
scale: 2,
alpha: 1,
onComplete: () => {
itemResolve();
},
});
if (!this.modifierTypeOption.cost) {
finalPromises.push(itemPromise);
if (isReward) {
const { resolve: itemTintResolve, promise: itemTintPromise } = Promise.withResolvers<void>();
globalScene.tweens.add({
targets: this.itemTint,
alpha: 0,
delay,
duration: 500,
ease: "Sine.easeIn",
onComplete: () => this.itemTint.destroy(),
onComplete: () => {
this.itemTint.destroy();
itemTintResolve();
},
});
finalPromises.push(itemTintPromise);
}
const { resolve: itemTextResolve, promise: itemTextPromise } = Promise.withResolvers<void>();
globalScene.tweens.add({
targets: this.itemText,
delay,
duration: 500,
alpha: 1,
y: 25,
ease: "Cubic.easeInOut",
onComplete: () => itemTextResolve(),
});
finalPromises.push(itemTextPromise);
if (this.itemCostText) {
const { resolve: itemCostResolve, promise: itemCostPromise } = Promise.withResolvers<void>();
globalScene.tweens.add({
targets: this.itemCostText,
delay,
duration: 500,
alpha: 1,
y: 35,
ease: "Cubic.easeInOut",
onComplete: () => itemCostResolve(),
});
finalPromises.push(itemCostPromise);
}
});
// The `.then` suppresses the return type for the Promise.allSettled so that it returns void.
promiseHolder.push(Promise.allSettled([...animPromises, ...finalPromises]).then());
await Promise.allSettled(animPromises);
}
getPbAtlasKey(tierOffset = 0) {

View File

@ -2,8 +2,9 @@ import type { InfoToggle } from "#app/battle-scene";
import { globalScene } from "#app/global-scene";
import { MoveCategory } from "#enums/move-category";
import { PokemonType } from "#enums/pokemon-type";
import { TextStyle } from "#enums/text-style";
import type { Move } from "#moves/move";
import { addTextObject, TextStyle } from "#ui/text";
import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import { fixedInt, getLocalizedSpriteKey } from "#utils/common";
import i18next from "i18next";

Some files were not shown because too many files have changed in this diff Show More