Merge branch 'beta' into WorkingDiscardFunction

This commit is contained in:
Mikhail Shueb 2025-07-29 09:11:35 +01:00 committed by GitHub
commit dc00b26664
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
625 changed files with 2849 additions and 1945 deletions

View File

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

View File

@ -41,4 +41,7 @@ jobs:
run: pnpm biome-ci
- name: Check dependencies with depcruise
run: pnpm depcruise
run: pnpm depcruise
- name: Lint with ls-lint
run: pnpm ls-lint

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:

28
.ls-lint.yml Normal file
View File

@ -0,0 +1,28 @@
# Base settings to use
# Note that the `_cfg` key isn't part of ls-lint's configuration, it's just a YAML anchor for reuse.
_cfg: &cfg
.ps1: kebab-case
.ts: kebab-case
.js: kebab-case
.*.ts: kebab-case
.*.js: kebab-case
.dir: kebab-case
.py: snake_case # python files should always use snake_case
ls:
<<: *cfg
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

@ -104,7 +104,7 @@ Most non-trivial changes (*especially bug fixes*) should come along with new tes
- Test edge cases. A good strategy is to think of edge cases beforehand and create tests for them using `it.todo`. Once the edge case has been handled, you can remove the `todo` marker.
## 😈 Development Save File
> Some issues may require you to have unlocks on your save file which go beyond normal overrides. For this reason, the repository contains a [save file](../test/testUtils/saves/everything.psrv) with _everything_ unlocked (even ones not legitimately obtainable, like unimplemented variant shinies).
> Some issues may require you to have unlocks on your save file which go beyond normal overrides. For this reason, the repository contains a [save file](../test/test-utils/saves/everything.psrv) with _everything_ unlocked (even ones not legitimately obtainable, like unimplemented variant shinies).
1. Start the game up locally and navigate to `Menu -> Manage Data -> Import Data`
2. Select [everything.prsv](test/testUtils/saves/everything.prsv) (`test/testUtils/saves/everything.prsv`) and confirm.
2. Select [everything.prsv](test/test-utils/saves/everything.prsv) (`test/test-utils/saves/everything.prsv`) and confirm.

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": {

2
global.d.ts vendored
View File

@ -8,7 +8,7 @@ declare global {
* Can technically be undefined/null but for ease of use we are going to assume it is always defined.
* Used to load i18n files exclusively.
*
* To set up your own server in a test see `game_data.test.ts`
* To set up your own server in a test see `game-data.test.ts`
*/
var server: SetupServerApi;
}

View File

@ -1,11 +1,13 @@
pre-commit:
skip:
- merge
- rebase
commands:
biome-lint:
run: pnpm exec biome check --write --reporter=summary --staged --no-errors-on-unmatched
stage_fixed: true
skip:
- merge
- rebase
ls-lint:
run: pnpm exec ls-lint
post-merge:
commands:

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 .",
@ -28,9 +28,11 @@
},
"devDependencies": {
"@biomejs/biome": "2.0.0",
"@ls-lint/ls-lint": "2.3.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.16.3",
"@vitest/coverage-istanbul": "^3.2.4",
"@vitest/expect": "^3.2.4",
"chalk": "^5.4.1",
"dependency-cruiser": "^16.10.4",
"inquirer": "^12.7.0",

View File

@ -45,6 +45,9 @@ importers:
'@biomejs/biome':
specifier: 2.0.0
version: 2.0.0
'@ls-lint/ls-lint':
specifier: 2.3.1
version: 2.3.1
'@types/jsdom':
specifier: ^21.1.7
version: 21.1.7
@ -54,6 +57,9 @@ importers:
'@vitest/coverage-istanbul':
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0))
'@vitest/expect':
specifier: ^3.2.4
version: 3.2.4
chalk:
specifier: ^5.4.1
version: 5.4.1
@ -565,6 +571,12 @@ packages:
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
'@ls-lint/ls-lint@2.3.1':
resolution: {integrity: sha512-vPe6IDByQnQRTxcAYjTxrmga/tSIui50VBFTB5KIJWY3OOFmxE2VtymjeSEfQfiMbhZV/ZPAqYy2lt8pZFQ0Rw==}
cpu: [x64, arm64, s390x, ppc64le]
os: [darwin, linux, win32]
hasBin: true
'@material/material-color-utilities@0.2.7':
resolution: {integrity: sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==}
@ -2452,6 +2464,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.4
'@ls-lint/ls-lint@2.3.1': {}
'@material/material-color-utilities@0.2.7': {}
'@mswjs/interceptors@0.39.2':

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

View File

@ -1,7 +1,7 @@
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { GameManager } from "#test/testUtils/gameManager";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";

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: "list",
name: "selectedOption",
message: "What type of test would you like to create?",
choices: [...choices.map(choice => ({ name: choice.label, value: choice })), "EXIT"],
},
]);
/** @type {choiceType | "EXIT"} */
const choice = await inquirer
.prompt([
{
type: "list",
name: "selectedOption",
message: "What type of test would you like to create?",
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: "input",
name: "userInput",
message: `Please provide the name of the ${selectedType}:`,
},
]);
/** @type {string} */
const fileNameAnswer = await inquirer
.prompt([
{
type: "input",
name: "userInput",
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

@ -2,13 +2,13 @@ import re
filenames = [['src/enums/moves.ts', 'move'], ['src/enums/abilities.ts', 'ability'], ['src/enums/species.ts', 'Pokémon']]
commentBlockStart = re.compile('\/\*[^\*].*') # Regex for the start of a comment block
commentBlockEnd = re.compile('.*,\*\/') # Regex for the end of a comment block
commentBlockStart = re.compile(r'\/\*[^\*].*') # Regex for the start of a comment block
commentBlockEnd = re.compile(r'.*,\*\/') # Regex for the end of a comment block
commentExp = re.compile('(?:\/\*\*.*\*\/)') # Regex for a url comment that already existed in the file
commentExp = re.compile(r'(?:\/\*\*.*\*\/)') # Regex for a url comment that already existed in the file
enumExp = re.compile('.*,') # Regex for a regular enum line
numberExp = re.compile(' +\= +\d+,')
numberExp = re.compile(r' +\= +\d+,')
replaceList = ['ALOLA', 'ETERNAL', 'GALAR', 'HISUI', 'PALDEA', 'BLOODMOON']

View File

@ -1,4 +1,4 @@
import type { UserInfo } from "#types/UserInfo";
import type { UserInfo } from "#types/user-info";
export interface AccountInfoResponse extends UserInfo {}

View File

@ -1,4 +1,4 @@
export class UpdateSessionSavedataRequest {
export interface UpdateSessionSavedataRequest {
slot: number;
trainerId: number;
secretId: number;

View File

@ -4,7 +4,7 @@ export interface GetSystemSavedataRequest {
clientSessionId: string;
}
export class UpdateSystemSavedataRequest {
export interface UpdateSystemSavedataRequest {
clientSessionId: string;
trainerId?: number;
secretId?: number;

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 */
name: string;
/** The actual nickname of the Pokemon */
nickname: string;
/** Whether the base pokemon is shiny or not */
shiny: boolean;
/** The shiny variant of the base pokemon */
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 name of pokemon featured in the illusion */
name: string;
/** 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 variant of the pokemon featured in the illusion */
variant: 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

@ -75,3 +75,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];
};

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

@ -1,6 +1,6 @@
import { pokerogueApi } from "#api/pokerogue-api";
import { bypassLogin } from "#app/global-vars/bypass-login";
import type { UserInfo } from "#types/UserInfo";
import type { UserInfo } from "#types/user-info";
import { randomString } from "#utils/common";
export let loggedInUser: UserInfo | null = null;

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);
}
}
Promise.resolve();
});
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);
}
}
/**
@ -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";
@ -28,12 +29,12 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import type { BerryType } from "#enums/berry-type";
import { Command } from "#enums/command";
import { HitResult } from "#enums/hit-result";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveFlags } from "#enums/MoveFlags";
import { MoveTarget } from "#enums/MoveTarget";
import { CommonAnim } from "#enums/move-anims-common";
import { MoveCategory } from "#enums/move-category";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { MoveTarget } from "#enums/move-target";
import { MoveUseMode } from "#enums/move-use-mode";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PokemonType } from "#enums/pokemon-type";
@ -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

@ -9,10 +9,10 @@ import { ArenaTagType } from "#enums/arena-tag-type";
import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type";
import { HitResult } from "#enums/hit-result";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveTarget } from "#enums/MoveTarget";
import { CommonAnim } from "#enums/move-anims-common";
import { MoveCategory } from "#enums/move-category";
import { MoveId } from "#enums/move-id";
import { MoveTarget } from "#enums/move-target";
import { MoveUseMode } from "#enums/move-use-mode";
import { PokemonType } from "#enums/pokemon-type";
import { Stat } from "#enums/stat";

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

@ -3,12 +3,13 @@ import { allMoves } from "#data/data-lists";
import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type";
import { EncounterAnim } from "#enums/encounter-anims";
import { MoveFlags } from "#enums/MoveFlags";
import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } from "#enums/move-anims-common";
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;
}

View File

@ -11,9 +11,9 @@ import { AbilityId } from "#enums/ability-id";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { HitResult } from "#enums/hit-result";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveFlags } from "#enums/MoveFlags";
import { ChargeAnim, CommonAnim } from "#enums/move-anims-common";
import { MoveCategory } from "#enums/move-category";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { MoveUseMode } from "#enums/move-use-mode";

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;
cost.value = speciesStarterCosts[species];
return true;
}
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

@ -1,8 +1,8 @@
import { allMoves } from "#data/data-lists";
import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveTarget } from "#enums/MoveTarget";
import type { MoveId } from "#enums/move-id";
import { MoveTarget } from "#enums/move-target";
import { PokemonType } from "#enums/pokemon-type";
import type { Pokemon } from "#field/pokemon";
import { applyMoveAttrs } from "#moves/apply-attrs";
@ -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

@ -48,11 +48,11 @@ import { ChargeAnim } from "#enums/move-anims-common";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MoveFlags } from "#enums/MoveFlags";
import { MoveTarget } from "#enums/MoveTarget";
import { MultiHitType } from "#enums/MultiHitType";
import { MoveCategory } from "#enums/move-category";
import { MoveEffectTrigger } from "#enums/move-effect-trigger";
import { MoveFlags } from "#enums/move-flags";
import { MoveTarget } from "#enums/move-target";
import { MultiHitType } from "#enums/multi-hit-type";
import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import {
@ -87,8 +87,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";
/**
@ -808,16 +809,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,
@ -832,6 +831,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 => {
@ -855,7 +861,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);
@ -8133,7 +8138,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;

View File

@ -8,9 +8,9 @@ import { BattlerIndex } from "#enums/battler-index";
import { BerryType } from "#enums/berry-type";
import { Challenges } from "#enums/challenges";
import { EncounterAnim } from "#enums/encounter-anims";
import { MoveCategory } from "#enums/MoveCategory";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import { ModifierTier } from "#enums/modifier-tier";
import { MoveCategory } from "#enums/move-category";
import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";

View File

@ -1,7 +1,7 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveCategory } from "#enums/move-category";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";

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

@ -18,7 +18,7 @@ import {
} from "#data/form-change-triggers";
import { AbilityId } from "#enums/ability-id";
import { FormChangeItem } from "#enums/form-change-item";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveCategory } from "#enums/move-category";
import { MoveId } from "#enums/move-id";
import { SpeciesFormKey } from "#enums/species-form-key";
import { SpeciesId } from "#enums/species-id";

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,29 @@
import { type BattlerTag, loadBattlerTag } 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 +52,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 +147,7 @@ export class PokemonSummonData {
*/
public moveHistory: TurnMove[] = [];
constructor(source?: PokemonSummonData | Partial<PokemonSummonData>) {
constructor(source?: PokemonSummonData | SerializedPokemonSummonData) {
if (isNullOrUndefined(source)) {
return;
}
@ -97,6 +158,30 @@ 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;
@ -110,6 +195,49 @@ export class PokemonSummonData {
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,13 +61,19 @@ 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
return (
move.getPriority(user) > 0 &&
user.getOpponents(true).some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded())
);
}
// 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 &&
// ...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)

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

@ -82,11 +82,11 @@ import { DexAttr } from "#enums/dex-attr";
import { FieldPosition } from "#enums/field-position";
import { HitResult } from "#enums/hit-result";
import { LearnMoveSituation } from "#enums/learn-move-situation";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveFlags } from "#enums/MoveFlags";
import { MoveTarget } from "#enums/MoveTarget";
import { ModifierTier } from "#enums/modifier-tier";
import { MoveCategory } from "#enums/move-category";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import { MoveTarget } from "#enums/move-target";
import { isIgnorePP, isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { Nature } from "#enums/nature";
import { PokeballType } from "#enums/pokeball";
@ -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) {
this.breakIllusion();
}
if (this.hasTrainer()) {
setIllusion(pokemon: Pokemon | PokemonSpecies): boolean {
this.breakIllusion();
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,
nickname: this.nickname,
shiny: this.shiny,
variant: this.variant,
fusionShiny: this.fusionShiny,
fusionVariant: this.fusionVariant,
},
species: randomIllusion.speciesId,
formIndex: randomIllusion.formIndex,
gender: this.gender,
fusionShiny: false,
fusionVariant: 0,
shiny: this.shiny || this.fusionShiny,
variant: 0,
nickname: this.nickname,
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;
}
return !this.isFusion(true) ? this.variant : (Math.max(this.variant, this.fusionVariant) as Variant);
const fusionVariant = useIllusion ? (illusion?.fusionVariant ?? this.fusionVariant) : this.fusionVariant;
return Math.max(baseVariant, fusionVariant) as Variant;
}
// TODO: Clarify how this differs from `getVariant`
getBaseVariant(doubleShiny: boolean): Variant {
if (doubleShiny) {
return this.summonData.illusion?.basePokemon?.variant ?? this.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;
}
/**
* 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

@ -3,13 +3,13 @@ import { TouchControl } from "#app/touch-controls";
import { Button } from "#enums/buttons";
import { Device } from "#enums/devices";
import { UiMode } from "#enums/ui-mode";
import cfg_keyboard_qwerty from "#inputs/cfg_keyboard_qwerty";
import { assign, getButtonWithKeycode, getIconForLatestInput, swap } from "#inputs/configHandler";
import pad_dualshock from "#inputs/pad_dualshock";
import pad_generic from "#inputs/pad_generic";
import pad_procon from "#inputs/pad_procon";
import pad_unlicensedSNES from "#inputs/pad_unlicensedSNES";
import pad_xbox360 from "#inputs/pad_xbox360";
import cfg_keyboard_qwerty from "#inputs/cfg-keyboard-qwerty";
import { assign, getButtonWithKeycode, getIconForLatestInput, swap } from "#inputs/config-handler";
import pad_dualshock from "#inputs/pad-dualshock";
import pad_generic from "#inputs/pad-generic";
import pad_procon from "#inputs/pad-procon";
import pad_unlicensedSNES from "#inputs/pad-unlicensed-snes";
import pad_xbox360 from "#inputs/pad-xbox360";
import type { SettingGamepad } from "#system/settings-gamepad";
import type { SettingKeyboard } from "#system/settings-keyboard";
import { MoveTouchControlsHandler } from "#ui/move-touch-controls-handler";

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";
@ -369,30 +356,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);
@ -463,6 +432,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

@ -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

@ -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,59 +21,77 @@ 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 (
(globalScene.currentBattle.turn === 1 && (!globalScene.commandCursorMemory || cursorResetEvent)) ||
commandUiHandler.getCursor() === Command.POKEMON
) {
commandUiHandler.setCursor(Command.FIGHT);
} else {
commandUiHandler.setCursor(commandUiHandler.getCursor());
}
if (!commandUiHandler) {
return;
}
if (
(turn === 1 && (!commandCursorMemory || cursorResetEvent)) ||
commandUiHandler.getCursor() === Command.POKEMON
) {
commandUiHandler.setCursor(Command.FIGHT);
}
}
/**
* 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;
return;
}
if (this.fieldIndex) {
// 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
if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) {
this.fieldIndex = FieldPosition.CENTER;
} else {
const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1];
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: allyCommand?.command,
skip: true,
};
}
}
const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1];
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: allyCommand?.command,
skip: true,
};
}
}
/**
* 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,377 +103,521 @@ 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();
}
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
/**
* 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();
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;
}
// 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 {
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 {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
}
} else {
let entriesToDelete = 0;
const moveset = playerPokemon.getMoveset();
for (const queuedMove of moveQueue) {
const movesetQueuedMove = moveset.find(m => m.moveId === queuedMove.move);
if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
globalScene.currentBattle.mysteryEncounter?.skipToFightInput
queuedMove.move !== MoveId.NONE &&
!isVirtual(queuedMove.useMode) &&
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))
) {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
entriesToDelete++;
} else {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
break;
}
}
if (entriesToDelete) {
moveQueue.splice(0, entriesToDelete);
}
}
/**
* 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`.
* Attempt to execute the first usable move in this Pokemon's move queue
* @returns Whether a queued move was successfully set to be executed.
*/
handleCommand(command: Command, cursor: number, ...args: any[]): boolean {
private tryExecuteQueuedMove(): boolean {
this.clearUnusuableMoves();
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
const moveQueue = playerPokemon.getMoveQueue();
if (moveQueue.length === 0) {
return false;
}
const queuedMove = moveQueue[0];
if (queuedMove.move === MoveId.NONE) {
this.handleCommand(Command.FIGHT, -1);
return true;
}
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
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
) {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
} else {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
}
/**
* 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
*/
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 = user.isMoveRestricted(move.moveId, user)
? user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId)
: move.getName().endsWith(" (N)")
? "battle:moveNotImplemented"
: "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
globalScene.ui.showText(
i18next.t(errorMessage, { moveName: moveName }),
null,
() => {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
},
null,
true,
);
}
/**
* 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);
}
/**
* 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 (
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(key),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
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) {
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
) {
this.queueShowText("battle:noPokeballStrong");
return false;
}
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: Command.BALL,
cursor: cursor,
};
globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets;
if (this.fieldIndex) {
globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
return true;
}
return false;
}
/**
* 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[] = [];
const isSwitch = this.isSwitch;
if (!playerPokemon.isTrapped(trappedAbMessages)) {
return false;
}
if (trappedAbMessages.length > 0) {
if (isSwitch) {
globalScene.ui.setMode(UiMode.MESSAGE);
}
globalScene.ui.showText(
trappedAbMessages[0],
null,
() => {
globalScene.ui.showText("", 0);
if (isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
} else {
const trapTag = playerPokemon.getTag(TrappedTag);
const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
}
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:
tag.sourceId && globalScene.getPokemonById(tag.sourceId)
? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!)
: "",
moveName: tag.getMoveName(),
escapeVerb: i18next.t(isSwitch ? "battle:escapeVerbSwitch" : "battle:escapeVerbFlee"),
}),
null,
() => {
globalScene.ui.showText("", 0);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
}
// 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) {
// 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];
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)
: move.getName().endsWith(" (N)")
? "battle:moveNotImplemented"
: "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
globalScene.ui.showText(
i18next.t(errorMessage, { moveName: moveName }),
null,
() => {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
},
null,
true,
);
}
case Command.FIGHT:
success = this.handleFightCommand(command, cursor, typeof useMode === "boolean" ? undefined : useMode, move);
break;
}
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,
);
} else if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
!globalScene.currentBattle.mysteryEncounter!.catchAllowed
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballMysteryEncounter"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else {
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));
if (
targetPokemon?.isBoss() &&
targetPokemon?.bossSegmentIndex >= 1 &&
!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 {
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: Command.BALL,
cursor: cursor,
};
globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets;
if (this.fieldIndex) {
globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
success = true;
}
}
}
case Command.BALL:
success = this.handleBallCommand(cursor);
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);
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;
}
} else if (trappedAbMessages.length > 0) {
if (!isSwitch) {
globalScene.ui.setMode(UiMode.MESSAGE);
}
globalScene.ui.showText(
trappedAbMessages[0],
null,
() => {
globalScene.ui.showText("", 0);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
} else {
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) => {
globalScene.ui.showText(
i18next.t("battle:noEscapePokemon", {
pokemonName:
tag.sourceId && globalScene.getPokemonById(tag.sourceId)
? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!)
: "",
moveName: tag.getMoveName(),
escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee"),
}),
null,
() => {
globalScene.ui.showText("", 0);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
};
if (trapTag) {
showNoEscapeText(trapTag);
} else if (fairyLockTag) {
showNoEscapeText(fairyLockTag);
}
}
}
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

@ -13,12 +13,12 @@ import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { HitCheckResult } from "#enums/hit-check-result";
import { HitResult } from "#enums/hit-result";
import { MoveCategory } from "#enums/MoveCategory";
import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MoveFlags } from "#enums/MoveFlags";
import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/move-category";
import { MoveEffectTrigger } from "#enums/move-effect-trigger";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { MoveTarget } from "#enums/move-target";
import { isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { PokemonType } from "#enums/pokemon-type";
import type { Pokemon } from "#field/pokemon";

View File

@ -13,8 +13,8 @@ import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveFlags } from "#enums/MoveFlags";
import { CommonAnim } from "#enums/move-anims-common";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";

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

@ -2,8 +2,8 @@ import { globalScene } from "#app/global-scene";
import { getTerrainColor, TerrainType } from "#data/terrain";
import { getCurrentTime } from "#utils/common";
import Phaser from "phaser";
import fieldSpriteFragShader from "./glsl/fieldSpriteFragShader.frag?raw";
import spriteVertShader from "./glsl/spriteShader.vert?raw";
import fieldSpriteFragShader from "./glsl/field-sprite-frag-shader.frag?raw";
import spriteVertShader from "./glsl/sprite-shader.vert?raw";
export class FieldSpritePipeline extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline {
constructor(game: Phaser.Game, config?: Phaser.Types.Renderer.WebGL.WebGLPipelineConfig) {

View File

@ -5,8 +5,8 @@ import { Pokemon } from "#field/pokemon";
import { Trainer } from "#field/trainer";
import { variantColorCache } from "#sprites/variant";
import { rgbHexToRgba } from "#utils/common";
import spriteFragShader from "./glsl/spriteFragShader.frag?raw";
import spriteVertShader from "./glsl/spriteShader.vert?raw";
import spriteFragShader from "./glsl/sprite-frag-shader.frag?raw";
import spriteVertShader from "./glsl/sprite-shader.vert?raw";
export class SpritePipeline extends FieldSpritePipeline {
private _tone: number[];

View File

@ -5,7 +5,7 @@ import type {
AccountLoginRequest,
AccountLoginResponse,
AccountRegisterRequest,
} from "#types/PokerogueAccountApi";
} from "#types/api/pokerogue-account-api";
import { removeCookie, setCookie } from "#utils/cookies";
/**

View File

@ -6,7 +6,7 @@ import type {
SearchAccountResponse,
UnlinkAccountFromDiscordIdRequest,
UnlinkAccountFromGoogledIdRequest,
} from "#types/PokerogueAdminApi";
} from "#types/api/pokerogue-admin-api";
export class PokerogueAdminApi extends ApiBase {
public readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!";

View File

@ -3,7 +3,7 @@ import { PokerogueAccountApi } from "#api/pokerogue-account-api";
import { PokerogueAdminApi } from "#api/pokerogue-admin-api";
import { PokerogueDailyApi } from "#api/pokerogue-daily-api";
import { PokerogueSavedataApi } from "#api/pokerogue-savedata-api";
import type { TitleStatsResponse } from "#types/PokerogueApi";
import type { TitleStatsResponse } from "#types/api/pokerogue-api-types";
/**
* A wrapper for PokéRogue API requests.

View File

@ -1,5 +1,5 @@
import { ApiBase } from "#api/api-base";
import type { GetDailyRankingsPageCountRequest, GetDailyRankingsRequest } from "#types/PokerogueDailyApi";
import type { GetDailyRankingsPageCountRequest, GetDailyRankingsRequest } from "#types/api/pokerogue-daily-api";
import type { RankingEntry } from "#ui/daily-run-scoreboard";
/**

View File

@ -2,7 +2,7 @@ import { ApiBase } from "#api/api-base";
import { PokerogueSessionSavedataApi } from "#api/pokerogue-session-savedata-api";
import { PokerogueSystemSavedataApi } from "#api/pokerogue-system-savedata-api";
import { MAX_INT_ATTR_VALUE } from "#app/constants";
import type { UpdateAllSavedataRequest } from "#types/PokerogueSavedataApi";
import type { UpdateAllSavedataRequest } from "#types/api/pokerogue-save-data-api";
/**
* A wrapper for PokéRogue savedata API requests.

View File

@ -7,7 +7,7 @@ import type {
GetSessionSavedataRequest,
NewClearSessionSavedataRequest,
UpdateSessionSavedataRequest,
} from "#types/PokerogueSessionSavedataApi";
} from "#types/api/pokerogue-session-save-data-api";
/**
* A wrapper for PokéRogue session savedata API requests.

View File

@ -4,7 +4,7 @@ import type {
UpdateSystemSavedataRequest,
VerifySystemSavedataRequest,
VerifySystemSavedataResponse,
} from "#types/PokerogueSystemSavedataApi";
} from "#types/api/pokerogue-system-save-data-api";
/**
* A wrapper for PokéRogue system savedata API requests.

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

@ -57,7 +57,7 @@ import {
applySessionVersionMigration,
applySettingsVersionMigration,
applySystemVersionMigration,
} from "#system/version_converter";
} from "#system/version-migration/version-converter";
import { VoucherType, vouchers } from "#system/voucher";
import { trainerConfigs } from "#trainers/trainer-config";
import type { DexData, DexEntry } from "#types/dex-data";
@ -1454,11 +1454,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);
@ -1493,7 +1492,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;

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