mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 15:03:24 +02:00
Merge remote-tracking branch 'upstream/beta' into new-battle
This commit is contained in:
commit
ad018db015
11
biome.jsonc
11
biome.jsonc
@ -254,16 +254,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes),
|
// Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes),
|
||||||
// as well as inside script boilerplate files.
|
// as well as inside script boilerplate files (whose imports will _presumably_ be used in the generated file).
|
||||||
{
|
{
|
||||||
// TODO: Rename existing boilerplates in the folder and remove this last alias
|
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/*.d.ts", "scripts/**/*.boilerplate.ts"],
|
||||||
"includes": [
|
|
||||||
"**/src/overrides.ts",
|
|
||||||
"**/src/enums/**/*",
|
|
||||||
"**/*.d.ts",
|
|
||||||
"scripts/**/*.boilerplate.ts",
|
|
||||||
"**/boilerplates/*.ts"
|
|
||||||
],
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"correctness": {
|
"correctness": {
|
||||||
|
@ -47,6 +47,6 @@ describe("{{description}}", () => {
|
|||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS });
|
expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS });
|
||||||
expect(game.textInterceptor.logs).toContain(i18next.t("moveTriggers:splash"));
|
expect(game).toHaveShownMessage(i18next.t("moveTriggers:splash"));
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -102,9 +102,9 @@ async function promptFileName(selectedType) {
|
|||||||
function getBoilerplatePath(choiceType) {
|
function getBoilerplatePath(choiceType) {
|
||||||
switch (choiceType) {
|
switch (choiceType) {
|
||||||
// case "Reward":
|
// case "Reward":
|
||||||
// return path.join(__dirname, "boilerplates/reward.ts");
|
// return path.join(__dirname, "boilerplates/reward.boilerplate.ts");
|
||||||
default:
|
default:
|
||||||
return path.join(__dirname, "boilerplates/default.ts");
|
return path.join(__dirname, "boilerplates/default.boilerplate.ts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
715
src/ai/ai-moveset-gen.ts
Normal file
715
src/ai/ai-moveset-gen.ts
Normal file
@ -0,0 +1,715 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { speciesEggMoves } from "#balance/egg-moves";
|
||||||
|
import {
|
||||||
|
BASE_WEIGHT_MULTIPLIER,
|
||||||
|
BOSS_EXTRA_WEIGHT_MULTIPLIER,
|
||||||
|
COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
COMMON_TM_MOVESET_WEIGHT,
|
||||||
|
EGG_MOVE_LEVEL_REQUIREMENT,
|
||||||
|
EGG_MOVE_TO_LEVEL_WEIGHT,
|
||||||
|
EGG_MOVE_WEIGHT_MAX,
|
||||||
|
EVOLUTION_MOVE_WEIGHT,
|
||||||
|
GREAT_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
GREAT_TM_MOVESET_WEIGHT,
|
||||||
|
getMaxEggMoveCount,
|
||||||
|
getMaxTmCount,
|
||||||
|
RARE_EGG_MOVE_LEVEL_REQUIREMENT,
|
||||||
|
STAB_BLACKLIST,
|
||||||
|
ULTRA_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
ULTRA_TM_MOVESET_WEIGHT,
|
||||||
|
} from "#balance/moveset-generation";
|
||||||
|
import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves";
|
||||||
|
import { speciesTmMoves, tmPoolTiers } from "#balance/tms";
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import { ModifierTier } from "#enums/modifier-tier";
|
||||||
|
import { MoveCategory } from "#enums/move-category";
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||||
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
|
import { NumberHolder, randSeedInt } from "#utils/common";
|
||||||
|
import { isBeta } from "#utils/utility-vars";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and assign a weight to the level-up moves currently available to the Pokémon
|
||||||
|
*
|
||||||
|
* @param pokemon - The Pokémon to generate a level-based move pool for
|
||||||
|
* @returns A map of move IDs to their computed weights
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* A move's weight is determined by its level, as follows:
|
||||||
|
* 1. If the level is an {@linkcode EVOLVE_MOVE} move, weight is 60
|
||||||
|
* 2. If it is level 1 with 80+ BP, it is considered a "move reminder" move and
|
||||||
|
* weight is 60
|
||||||
|
* 3. If the Pokémon has a trainer and the move is a {@linkcode RELEARN_MOVE},
|
||||||
|
* weight is 60
|
||||||
|
* 4. Otherwise, weight is the earliest level the move can be learned + 20
|
||||||
|
*/
|
||||||
|
function getAndWeightLevelMoves(pokemon: Pokemon): Map<MoveId, number> {
|
||||||
|
const movePool = new Map<MoveId, number>();
|
||||||
|
let allLevelMoves: [number, MoveId][];
|
||||||
|
// TODO: Investigate why there needs to be error handling here
|
||||||
|
try {
|
||||||
|
allLevelMoves = pokemon.getLevelMoves(1, true, true, pokemon.hasTrainer());
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error encountered trying to generate moveset for %s: %s", pokemon.species.name, e);
|
||||||
|
return movePool;
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = pokemon.level;
|
||||||
|
const hasTrainer = pokemon.hasTrainer();
|
||||||
|
|
||||||
|
for (const levelMove of allLevelMoves) {
|
||||||
|
const [learnLevel, id] = levelMove;
|
||||||
|
if (level < learnLevel) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const move = allMoves[id];
|
||||||
|
// Skip unimplemented moves or moves that are already in the pool
|
||||||
|
if (move.name.endsWith(" (N)") || movePool.has(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let weight = learnLevel + 20;
|
||||||
|
switch (learnLevel) {
|
||||||
|
case EVOLVE_MOVE:
|
||||||
|
weight = EVOLUTION_MOVE_WEIGHT;
|
||||||
|
break;
|
||||||
|
// Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves.
|
||||||
|
case 1:
|
||||||
|
if (move.power >= 80) {
|
||||||
|
weight = 60;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RELEARN_MOVE:
|
||||||
|
if (hasTrainer) {
|
||||||
|
weight = 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
movePool.set(id, weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return movePool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which TM tiers a Pokémon can learn based on its level
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @returns A tuple indicating whether the Pokémon can learn common, great, and ultra tier TMs
|
||||||
|
*/
|
||||||
|
function getAllowedTmTiers(level: number): [common: boolean, great: boolean, ultra: boolean] {
|
||||||
|
return [
|
||||||
|
level >= COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
level >= GREAT_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
level >= ULTRA_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the TMs that a species can learn based on its ID and formKey
|
||||||
|
* @param speciesId - The species ID of the Pokémon
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @param formKey - The form key of the Pokémon
|
||||||
|
* @param levelPool - The current level-based move pool, to avoid duplicates
|
||||||
|
* @param tmPool - The TM move pool to add to, which will be modified in place
|
||||||
|
* @param allowedTiers - The tiers of TMs the Pokémon is allowed to learn
|
||||||
|
*
|
||||||
|
* @privateRemarks
|
||||||
|
* Split out from `getAndWeightTmMoves` to allow fusion species to add their TMs
|
||||||
|
* without duplicating code.
|
||||||
|
*/
|
||||||
|
function getTmPoolForSpecies(
|
||||||
|
speciesId: number,
|
||||||
|
level: number,
|
||||||
|
formKey: string,
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: ReadonlyMap<MoveId, number>,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
allowedTiers = getAllowedTmTiers(level),
|
||||||
|
): void {
|
||||||
|
const [allowCommon, allowGreat, allowUltra] = allowedTiers;
|
||||||
|
const tms = speciesTmMoves[speciesId];
|
||||||
|
|
||||||
|
let moveId: MoveId;
|
||||||
|
for (const tm of tms) {
|
||||||
|
if (Array.isArray(tm)) {
|
||||||
|
if (tm[0] !== formKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
moveId = tm[1];
|
||||||
|
} else {
|
||||||
|
moveId = tm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (levelPool.has(moveId) || eggPool.has(moveId) || tmPool.has(moveId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (tmPoolTiers[moveId]) {
|
||||||
|
case ModifierTier.COMMON:
|
||||||
|
allowCommon && tmPool.set(moveId, COMMON_TM_MOVESET_WEIGHT);
|
||||||
|
break;
|
||||||
|
case ModifierTier.GREAT:
|
||||||
|
allowGreat && tmPool.set(moveId, GREAT_TM_MOVESET_WEIGHT);
|
||||||
|
break;
|
||||||
|
case ModifierTier.ULTRA:
|
||||||
|
allowUltra && tmPool.set(moveId, ULTRA_TM_MOVESET_WEIGHT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and assign a weight to the TM moves currently available to the Pokémon
|
||||||
|
* @param pokemon - The Pokémon to generate a TM-based move pool for
|
||||||
|
* @param currentSet - The current movepool, to avoid duplicates
|
||||||
|
* @param tmPool - The TM move pool to add to, which will be modified in place
|
||||||
|
* @returns A map of move IDs to their computed weights
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Only trainer pokemon can learn TM moves, and there are restrictions
|
||||||
|
* as to how many and which TMs are available based on the level of the Pokémon.
|
||||||
|
* 1. Before level 25, no TM moves are available
|
||||||
|
* 2. Between levels 25 and 40, only COMMON tier TMs are available,
|
||||||
|
*/
|
||||||
|
function getAndWeightTmMoves(
|
||||||
|
pokemon: Pokemon,
|
||||||
|
currentPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: ReadonlyMap<MoveId, number>,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
): void {
|
||||||
|
const level = pokemon.level;
|
||||||
|
const allowedTiers = getAllowedTmTiers(level);
|
||||||
|
if (!allowedTiers.includes(true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = pokemon.species.forms[pokemon.formIndex]?.formKey ?? "";
|
||||||
|
getTmPoolForSpecies(pokemon.species.speciesId, level, form, currentPool, eggPool, tmPool, allowedTiers);
|
||||||
|
const fusionFormKey = pokemon.getFusionFormKey();
|
||||||
|
const fusionSpecies = pokemon.fusionSpecies?.speciesId;
|
||||||
|
if (fusionSpecies != null && fusionFormKey != null && fusionFormKey !== "") {
|
||||||
|
getTmPoolForSpecies(fusionSpecies, level, fusionFormKey, currentPool, eggPool, tmPool, allowedTiers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the weight multiplier for an egg move
|
||||||
|
* @param levelPool - Map of level up moves to their weights
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @param forRare - Whether this is for a rare egg move
|
||||||
|
* @param isBoss - Whether the Pokémon having the egg move generated is a boss Pokémon
|
||||||
|
*/
|
||||||
|
export function getEggMoveWeight(
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Saved to allow this algorithm to be tweaked easily without adjusting signatures
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
level: number,
|
||||||
|
forRare: boolean,
|
||||||
|
isBoss: boolean,
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedFunctionParameters: Endrange
|
||||||
|
): number {
|
||||||
|
const levelUpWeightedEggMoveWeight = Math.round(Math.max(...levelPool.values()) * EGG_MOVE_TO_LEVEL_WEIGHT);
|
||||||
|
// Rare egg moves are always weighted at 5/6 the weight of normal egg moves
|
||||||
|
return Math.min(levelUpWeightedEggMoveWeight, EGG_MOVE_WEIGHT_MAX) * (forRare ? 5 / 6 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submethod of {@linkcode getAndWeightEggMoves} that adds egg moves for a specific species to the egg move pool
|
||||||
|
*
|
||||||
|
* @param rootSpeciesId - The ID of the root species for which to generate the egg move pool.
|
||||||
|
* @param levelPool - A readonly map of move IDs to their levels, representing moves already learned by leveling up.
|
||||||
|
* @param eggPool - A map to be populated with egg move IDs and their corresponding weights.
|
||||||
|
* @param eggMoveWeight - The default weight to assign to regular egg moves.
|
||||||
|
* @param excludeRare - If true, excludes rare egg moves
|
||||||
|
* @param rareEggMoveWeight - The weight to assign to rare egg moves; default 0
|
||||||
|
*
|
||||||
|
* @privateRemarks
|
||||||
|
* Split from `getAndWeightEggMoves` to allow fusion species to add their egg moves without duplicating code.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Moves present in `levelPool` are excluded from the egg pool.
|
||||||
|
* - If `excludeRare` is true, rare egg moves (at index 3) are skipped.
|
||||||
|
* - Rare egg moves are assigned `rareEggMoveWeight`, while others receive `eggMoveWeight`.
|
||||||
|
*/
|
||||||
|
function getEggPoolForSpecies(
|
||||||
|
rootSpeciesId: SpeciesId,
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: Map<MoveId, number>,
|
||||||
|
eggMoveWeight: number,
|
||||||
|
excludeRare: boolean,
|
||||||
|
rareEggMoveWeight = 0,
|
||||||
|
): void {
|
||||||
|
for (const [idx, moveId] of speciesEggMoves[rootSpeciesId].entries()) {
|
||||||
|
if (levelPool.has(moveId) || (idx === 3 && excludeRare)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
eggPool.set(Math.max(moveId, eggPool.get(moveId) ?? 0), idx === 3 ? rareEggMoveWeight : eggMoveWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and assign a weight to the egg moves currently available to the Pokémon
|
||||||
|
* @param pokemon - The Pokémon to generate egg moves for
|
||||||
|
* @param levelPool - The map of level-based moves to their weights
|
||||||
|
* @param eggPool - A map of move IDs to their weights for egg moves that will be modified in place
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This function checks if the Pokémon meets the requirements to learn egg moves,
|
||||||
|
* and if allowed, calculates the weights for regular and rare egg moves using the provided pools.
|
||||||
|
*/
|
||||||
|
function getAndWeightEggMoves(
|
||||||
|
pokemon: Pokemon,
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: Map<MoveId, number>,
|
||||||
|
): void {
|
||||||
|
const level = pokemon.level;
|
||||||
|
if (level < EGG_MOVE_LEVEL_REQUIREMENT || !globalScene.currentBattle?.trainer?.config.allowEggMoves) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isBoss = pokemon.isBoss();
|
||||||
|
const excludeRare = isBoss || level < RARE_EGG_MOVE_LEVEL_REQUIREMENT;
|
||||||
|
const eggMoveWeight = getEggMoveWeight(levelPool, level, false, isBoss);
|
||||||
|
let rareEggMoveWeight: number | undefined;
|
||||||
|
if (!excludeRare) {
|
||||||
|
rareEggMoveWeight = getEggMoveWeight(levelPool, level, true, isBoss);
|
||||||
|
}
|
||||||
|
getEggPoolForSpecies(
|
||||||
|
pokemon.species.getRootSpeciesId(),
|
||||||
|
levelPool,
|
||||||
|
eggPool,
|
||||||
|
eggMoveWeight,
|
||||||
|
excludeRare,
|
||||||
|
rareEggMoveWeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fusionSpecies = pokemon.fusionSpecies?.getRootSpeciesId();
|
||||||
|
if (fusionSpecies != null) {
|
||||||
|
getEggPoolForSpecies(fusionSpecies, levelPool, eggPool, eggMoveWeight, excludeRare, rareEggMoveWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a move pool, removing moves that are not allowed based on conditions
|
||||||
|
* @param pool - The move pool to filter
|
||||||
|
* @param isBoss - Whether the Pokémon is a boss
|
||||||
|
* @param hasTrainer - Whether the Pokémon has a trainer
|
||||||
|
*/
|
||||||
|
function filterMovePool(pool: Map<MoveId, number>, isBoss: boolean, hasTrainer: boolean): void {
|
||||||
|
for (const [moveId, weight] of pool) {
|
||||||
|
if (weight <= 0) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
// Forbid unimplemented moves
|
||||||
|
if (move.name.endsWith(" (N)")) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Bosses never get self ko moves or Pain Split
|
||||||
|
if (isBoss && (move.hasAttr("SacrificialAttr") || move.hasAttr("HpSplitAttr"))) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No one gets Memento or Final Gambit
|
||||||
|
if (move.hasAttr("SacrificialAttrOnHit")) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trainers never get OHKO moves
|
||||||
|
if (hasTrainer && move.hasAttr("OneHitKOAttr")) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform Trainer-specific adjustments to move weights in a move pool
|
||||||
|
* @param pool - The move pool to adjust
|
||||||
|
*/
|
||||||
|
function adjustWeightsForTrainer(pool: Map<MoveId, number>): void {
|
||||||
|
for (const [moveId, weight] of pool.entries()) {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
let adjustedWeight = weight;
|
||||||
|
// Half the weight of self KO moves on trainers
|
||||||
|
adjustedWeight *= move.hasAttr("SacrificialAttr") ? 0.5 : 1;
|
||||||
|
|
||||||
|
// Trainers get a weight bump to stat buffing moves
|
||||||
|
adjustedWeight *= move.getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1;
|
||||||
|
|
||||||
|
// Trainers get a weight decrease to multiturn moves
|
||||||
|
adjustedWeight *= !!move.isChargingMove() || !!move.hasAttr("RechargeAttr") ? 0.7 : 1;
|
||||||
|
if (adjustedWeight !== weight) {
|
||||||
|
pool.set(moveId, adjustedWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust weights of damaging moves in a move pool based on their power and category
|
||||||
|
*
|
||||||
|
* @param pool - The move pool to adjust
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
* @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false`
|
||||||
|
* @remarks
|
||||||
|
* Caps max power at 90 to avoid something like hyper beam ruining the stats.
|
||||||
|
* pokemon is a pretty soft weighting factor, although it is scaled with the weight multiplier.
|
||||||
|
*/
|
||||||
|
function adjustDamageMoveWeights(pool: Map<MoveId, number>, pokemon: Pokemon, willTera = false): void {
|
||||||
|
// begin max power at 40 to avoid inflating weights too much when there are only low power moves
|
||||||
|
let maxPower = 40;
|
||||||
|
for (const moveId of pool.keys()) {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
maxPower = Math.max(maxPower, move.calculateEffectivePower());
|
||||||
|
if (maxPower >= 90) {
|
||||||
|
maxPower = 90;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const atk = pokemon.getStat(Stat.ATK);
|
||||||
|
const spAtk = pokemon.getStat(Stat.SPATK);
|
||||||
|
const lowerStat = Math.min(atk, spAtk);
|
||||||
|
const higherStat = Math.max(atk, spAtk);
|
||||||
|
const worseCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
||||||
|
const statRatio = lowerStat / higherStat;
|
||||||
|
const adjustmentRatio = Math.min(Math.pow(statRatio, 3) * 1.3, 1);
|
||||||
|
|
||||||
|
for (const [moveId, weight] of pool) {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
let adjustedWeight = weight;
|
||||||
|
if (move.category === MoveCategory.STATUS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Scale weight based on their ratio to the highest power move, capping at 50% reduction
|
||||||
|
adjustedWeight *= Math.max(Math.min(move.calculateEffectivePower() / maxPower, 1), 0.5);
|
||||||
|
|
||||||
|
// Scale weight based the stat it uses to deal damage, based on the ratio between said stat
|
||||||
|
// and the higher stat
|
||||||
|
if (move.hasAttr("DefAtkAttr")) {
|
||||||
|
const def = pokemon.getStat(Stat.DEF);
|
||||||
|
const defRatio = def / higherStat;
|
||||||
|
const defAdjustRatio = Math.min(Math.pow(defRatio, 3) * 1.3, 1.1);
|
||||||
|
adjustedWeight *= defAdjustRatio;
|
||||||
|
} else if (
|
||||||
|
move.category === worseCategory
|
||||||
|
&& !move.hasAttr("PhotonGeyserCategoryAttr")
|
||||||
|
&& !move.hasAttr("ShellSideArmCategoryAttr")
|
||||||
|
&& !(move.hasAttr("TeraMoveCategoryAttr") && willTera)
|
||||||
|
) {
|
||||||
|
// Raw multiply each move's category by the stat it uses to deal damage
|
||||||
|
// moves that always use the higher offensive stat are left unadjusted
|
||||||
|
adjustedWeight *= adjustmentRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adjustedWeight !== weight) {
|
||||||
|
pool.set(moveId, adjustedWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total weight of all moves in a move pool
|
||||||
|
* @param pool - The move pool to calculate the total weight for
|
||||||
|
* @returns The total weight of all moves in the pool
|
||||||
|
*/
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: May be useful
|
||||||
|
function calculateTotalPoolWeight(pool: Map<MoveId, number>): number {
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (const weight of pool.values()) {
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
return totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a pool and return a new array of moves that pass the predicate
|
||||||
|
* @param pool - The move pool to filter
|
||||||
|
* @param predicate - The predicate function to determine if a move should be included
|
||||||
|
* @param totalWeight - An output parameter to hold the total weight of the filtered pool. Its value is reset to 0 if provided.
|
||||||
|
* @returns An array of move ID and weight tuples that pass the predicate
|
||||||
|
*/
|
||||||
|
function filterPool(
|
||||||
|
pool: ReadonlyMap<MoveId, number>,
|
||||||
|
predicate: (moveId: MoveId) => boolean,
|
||||||
|
totalWeight?: NumberHolder,
|
||||||
|
): [id: MoveId, weight: number][] {
|
||||||
|
let hasTotalWeight = false;
|
||||||
|
if (totalWeight != null) {
|
||||||
|
totalWeight.value = 0;
|
||||||
|
hasTotalWeight = true;
|
||||||
|
}
|
||||||
|
const newPool: [id: MoveId, weight: number][] = [];
|
||||||
|
for (const [moveId, weight] of pool) {
|
||||||
|
if (predicate(moveId)) {
|
||||||
|
newPool.push([moveId, weight]);
|
||||||
|
if (hasTotalWeight) {
|
||||||
|
// Bang is safe here because we set `hasTotalWeight` in the if check above
|
||||||
|
totalWeight!.value += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcibly add a STAB move to the Pokémon's moveset from the provided pools
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* If no STAB move is available, add any damaging move.
|
||||||
|
* If no damaging move is available, no move is added
|
||||||
|
* @param pool - The master move pool
|
||||||
|
* @param tmPool - The TM move pool
|
||||||
|
* @param eggPool - The egg move pool
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
* @param tmCount - A holder for the count of TM moves selected
|
||||||
|
* @param eggMoveCount - A holder for the count of egg moves selected
|
||||||
|
* @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false`
|
||||||
|
* @param forceAnyDamageIfNoStab - If true, will force any damaging move if no STAB move is available
|
||||||
|
*/
|
||||||
|
// biome-ignore lint/nursery/useMaxParams: This is a complex function that needs all these parameters
|
||||||
|
function forceStabMove(
|
||||||
|
pool: Map<MoveId, number>,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
eggPool: Map<MoveId, number>,
|
||||||
|
pokemon: Pokemon,
|
||||||
|
tmCount: NumberHolder,
|
||||||
|
eggMoveCount: NumberHolder,
|
||||||
|
willTera = false,
|
||||||
|
forceAnyDamageIfNoStab = false,
|
||||||
|
): void {
|
||||||
|
// All Pokemon force a STAB move first
|
||||||
|
const totalWeight = new NumberHolder(0);
|
||||||
|
const stabMovePool = filterPool(
|
||||||
|
pool,
|
||||||
|
moveId => {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
return (
|
||||||
|
move.category !== MoveCategory.STATUS
|
||||||
|
&& (pokemon.isOfType(move.type)
|
||||||
|
|| (willTera && move.hasAttr("TeraBlastTypeAttr") && pokemon.getTeraType() !== PokemonType.STELLAR))
|
||||||
|
&& !STAB_BLACKLIST.has(moveId)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
totalWeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const chosenPool =
|
||||||
|
stabMovePool.length > 0 || !forceAnyDamageIfNoStab
|
||||||
|
? stabMovePool
|
||||||
|
: filterPool(
|
||||||
|
pool,
|
||||||
|
m => allMoves[m[0]].category !== MoveCategory.STATUS && !STAB_BLACKLIST.has(m[0]),
|
||||||
|
totalWeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (chosenPool.length > 0) {
|
||||||
|
let rand = randSeedInt(totalWeight.value);
|
||||||
|
let index = 0;
|
||||||
|
while (rand > chosenPool[index][1]) {
|
||||||
|
rand -= chosenPool[index++][1];
|
||||||
|
}
|
||||||
|
const selectedId = chosenPool[index][0];
|
||||||
|
pool.delete(selectedId);
|
||||||
|
if (tmPool.has(selectedId)) {
|
||||||
|
tmPool.delete(selectedId);
|
||||||
|
tmCount.value++;
|
||||||
|
} else if (eggPool.has(selectedId)) {
|
||||||
|
eggPool.delete(selectedId);
|
||||||
|
eggMoveCount.value++;
|
||||||
|
}
|
||||||
|
pokemon.moveset.push(new PokemonMove(selectedId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust weights in the remaining move pool based on existing moves in the Pokémon's moveset
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Submethod for step 5 of moveset generation
|
||||||
|
* @param pool - The move pool to filter
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
*/
|
||||||
|
function filterRemainingTrainerMovePool(pool: [id: MoveId, weight: number][], pokemon: Pokemon) {
|
||||||
|
// Sqrt the weight of any damaging moves with overlapping types. pokemon is about a 0.05 - 0.1 multiplier.
|
||||||
|
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB.
|
||||||
|
// Status moves remain unchanged on weight, pokemon encourages 1-2
|
||||||
|
for (const [idx, [moveId, weight]] of pool.entries()) {
|
||||||
|
let ret: number;
|
||||||
|
if (
|
||||||
|
pokemon.moveset.some(
|
||||||
|
mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[moveId].type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ret = Math.ceil(Math.sqrt(weight));
|
||||||
|
} else if (allMoves[moveId].category !== MoveCategory.STATUS) {
|
||||||
|
ret = Math.ceil(
|
||||||
|
(weight / Math.max(Math.pow(4, pokemon.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5))
|
||||||
|
* (pokemon.isOfType(allMoves[moveId].type) && !STAB_BLACKLIST.has(moveId) ? 20 : 1),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ret = weight;
|
||||||
|
}
|
||||||
|
pool[idx] = [moveId, ret];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill in the remaining slots in the Pokémon's moveset from the provided pools
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
* @param tmPool - The TM move pool
|
||||||
|
* @param eggMovePool - The egg move pool
|
||||||
|
* @param tmCount - A holder for the count of moves that have been added to the moveset from TMs
|
||||||
|
* @param eggMoveCount - A holder for the count of moves that have been added to the moveset from egg moves
|
||||||
|
* @param baseWeights - The base weights of all moves in the master pool
|
||||||
|
* @param remainingPool - The remaining move pool to select from
|
||||||
|
*/
|
||||||
|
function fillInRemainingMovesetSlots(
|
||||||
|
pokemon: Pokemon,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
eggMovePool: Map<MoveId, number>,
|
||||||
|
tmCount: NumberHolder,
|
||||||
|
eggMoveCount: NumberHolder,
|
||||||
|
baseWeights: Map<MoveId, number>,
|
||||||
|
remainingPool: [id: MoveId, weight: number][],
|
||||||
|
): void {
|
||||||
|
const tmCap = getMaxTmCount(pokemon.level);
|
||||||
|
const eggCap = getMaxEggMoveCount(pokemon.level);
|
||||||
|
const remainingPoolWeight = new NumberHolder(0);
|
||||||
|
while (remainingPool.length > pokemon.moveset.length && pokemon.moveset.length < 4) {
|
||||||
|
const nonLevelMoveCount = tmCount.value + eggMoveCount.value;
|
||||||
|
remainingPool = filterPool(
|
||||||
|
baseWeights,
|
||||||
|
(m: MoveId) =>
|
||||||
|
!pokemon.moveset.some(
|
||||||
|
mo =>
|
||||||
|
m === mo.moveId || (allMoves[m]?.hasAttr("SacrificialAttr") && mo.getMove()?.hasAttr("SacrificialAttr")), // Only one self-KO move allowed
|
||||||
|
)
|
||||||
|
&& (nonLevelMoveCount < tmCap || !tmPool.has(m))
|
||||||
|
&& (nonLevelMoveCount < eggCap || !eggMovePool.has(m)),
|
||||||
|
remainingPoolWeight,
|
||||||
|
);
|
||||||
|
if (pokemon.hasTrainer()) {
|
||||||
|
filterRemainingTrainerMovePool(remainingPool, pokemon);
|
||||||
|
}
|
||||||
|
const totalWeight = remainingPool.reduce((v, m) => v + m[1], 0);
|
||||||
|
let rand = randSeedInt(totalWeight);
|
||||||
|
let index = 0;
|
||||||
|
while (rand > remainingPool[index][1]) {
|
||||||
|
rand -= remainingPool[index++][1];
|
||||||
|
}
|
||||||
|
const selectedMoveId = remainingPool[index][0];
|
||||||
|
baseWeights.delete(selectedMoveId);
|
||||||
|
if (tmPool.has(selectedMoveId)) {
|
||||||
|
tmCount.value++;
|
||||||
|
tmPool.delete(selectedMoveId);
|
||||||
|
} else if (eggMovePool.has(selectedMoveId)) {
|
||||||
|
eggMoveCount.value++;
|
||||||
|
eggMovePool.delete(selectedMoveId);
|
||||||
|
}
|
||||||
|
pokemon.moveset.push(new PokemonMove(selectedMoveId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debugging function to log computed move weights for a Pokémon
|
||||||
|
* @param pokemon - The Pokémon for which the move weights were computed
|
||||||
|
* @param pool - The move pool containing move IDs and their weights
|
||||||
|
* @param note - Short note to include in the log for context
|
||||||
|
*/
|
||||||
|
function debugMoveWeights(pokemon: Pokemon, pool: Map<MoveId, number>, note: string): void {
|
||||||
|
if (isBeta || import.meta.env.DEV) {
|
||||||
|
const moveNameToWeightMap = new Map<string, number>();
|
||||||
|
const sortedByValue = Array.from(pool.entries()).sort((a, b) => b[1] - a[1]);
|
||||||
|
for (const [moveId, weight] of sortedByValue) {
|
||||||
|
moveNameToWeightMap.set(allMoves[moveId].name, weight);
|
||||||
|
}
|
||||||
|
console.log("%cComputed move weights [%s] for %s", "color: blue", note, pokemon.name, moveNameToWeightMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a moveset for a given Pokémon based on its level, types, stats, and whether it is wild or a trainer's Pokémon.
|
||||||
|
* @param pokemon - The Pokémon to generate a moveset for
|
||||||
|
* @returns A reference to the Pokémon's moveset array
|
||||||
|
*/
|
||||||
|
export function generateMoveset(pokemon: Pokemon): void {
|
||||||
|
pokemon.moveset = [];
|
||||||
|
// Step 1: Generate the pools from various sources: level up, egg moves, and TMs
|
||||||
|
const learnPool = getAndWeightLevelMoves(pokemon);
|
||||||
|
debugMoveWeights(pokemon, learnPool, "Initial Level Moves");
|
||||||
|
const hasTrainer = pokemon.hasTrainer();
|
||||||
|
const tmPool = new Map<MoveId, number>();
|
||||||
|
const eggMovePool = new Map<MoveId, number>();
|
||||||
|
|
||||||
|
if (hasTrainer) {
|
||||||
|
getAndWeightEggMoves(pokemon, learnPool, eggMovePool);
|
||||||
|
eggMovePool.size > 0 && debugMoveWeights(pokemon, eggMovePool, "Initial Egg Moves");
|
||||||
|
getAndWeightTmMoves(pokemon, learnPool, eggMovePool, tmPool);
|
||||||
|
tmPool.size > 0 && debugMoveWeights(pokemon, tmPool, "Initial Tm Moves");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, combine pools into one master pool.
|
||||||
|
// The pools are kept around so we know where the move was sourced from
|
||||||
|
const movePool = new Map<MoveId, number>([...tmPool.entries(), ...eggMovePool.entries(), ...learnPool.entries()]);
|
||||||
|
|
||||||
|
// Step 2: Filter out forbidden moves
|
||||||
|
const isBoss = pokemon.isBoss();
|
||||||
|
filterMovePool(movePool, isBoss, hasTrainer);
|
||||||
|
|
||||||
|
// Step 3: Adjust weights for trainers
|
||||||
|
if (hasTrainer) {
|
||||||
|
adjustWeightsForTrainer(movePool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine whether this pokemon will instantly tera */
|
||||||
|
const willTera =
|
||||||
|
hasTrainer
|
||||||
|
&& globalScene.currentBattle?.trainer?.config.trainerAI.instantTeras.includes(
|
||||||
|
// The cast to EnemyPokemon is safe; includes will just return false if the property doesn't exist
|
||||||
|
(pokemon as EnemyPokemon).initialTeamIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
adjustDamageMoveWeights(movePool, pokemon, willTera);
|
||||||
|
|
||||||
|
/** The higher this is, the greater the impact of weight. At `0` all moves are equal weight. */
|
||||||
|
let weightMultiplier = BASE_WEIGHT_MULTIPLIER;
|
||||||
|
if (isBoss) {
|
||||||
|
weightMultiplier += BOSS_EXTRA_WEIGHT_MULTIPLIER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseWeights = new Map<MoveId, number>(movePool);
|
||||||
|
for (const [moveId, weight] of baseWeights) {
|
||||||
|
if (weight <= 0) {
|
||||||
|
baseWeights.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
baseWeights.set(moveId, Math.ceil(Math.pow(weight, weightMultiplier) * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmCount = new NumberHolder(0);
|
||||||
|
const eggMoveCount = new NumberHolder(0);
|
||||||
|
|
||||||
|
debugMoveWeights(pokemon, baseWeights, "Pre STAB Move");
|
||||||
|
|
||||||
|
// Step 4: Force a STAB move if possible
|
||||||
|
forceStabMove(movePool, tmPool, eggMovePool, pokemon, tmCount, eggMoveCount, willTera);
|
||||||
|
// Note: To force a secondary stab, call this a second time, and pass `false` for the last parameter
|
||||||
|
// Would also tweak the function to not consider moves already in the moveset
|
||||||
|
// e.g. forceStabMove(..., false);
|
||||||
|
|
||||||
|
// Step 5: Fill in remaining slots
|
||||||
|
fillInRemainingMovesetSlots(
|
||||||
|
pokemon,
|
||||||
|
tmPool,
|
||||||
|
eggMovePool,
|
||||||
|
tmCount,
|
||||||
|
eggMoveCount,
|
||||||
|
baseWeights,
|
||||||
|
filterPool(baseWeights, (m: MoveId) => !pokemon.moveset.some(mo => m[0] === mo.moveId)),
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* @module
|
*
|
||||||
* A big file storing colors used in logging.
|
* A big file storing colors used in logging.
|
||||||
* Minified by Terser during production builds, so has no overhead.
|
* Minified by Terser during production builds, so has no overhead.
|
||||||
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Colors used in prod
|
// Colors used in prod
|
||||||
|
@ -1,39 +1,4 @@
|
|||||||
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
|
||||||
import type { BattlerTag } from "#app/data/battler-tags";
|
|
||||||
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
|
||||||
|
|
||||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
|
||||||
import { globalScene } from "#app/global-scene";
|
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
|
||||||
import { CommonBattleAnim } from "#data/battle-anims";
|
|
||||||
import { allMoves } from "#data/data-lists";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { HitResult } from "#enums/hit-result";
|
|
||||||
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 { PokemonType } from "#enums/pokemon-type";
|
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import type { Arena } from "#field/arena";
|
|
||||||
import type { Pokemon } from "#field/pokemon";
|
|
||||||
import type {
|
|
||||||
ArenaScreenTagType,
|
|
||||||
ArenaTagData,
|
|
||||||
EntryHazardTagType,
|
|
||||||
RoomArenaTagType,
|
|
||||||
SerializableArenaTagType,
|
|
||||||
} from "#types/arena-tags";
|
|
||||||
import type { Mutable } from "#types/type-helpers";
|
|
||||||
import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module
|
|
||||||
* ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
|
* ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
|
||||||
* Examples include (but are not limited to)
|
* Examples include (but are not limited to)
|
||||||
* - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
|
* - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
|
||||||
@ -76,8 +41,43 @@ import i18next from "i18next";
|
|||||||
* ```
|
* ```
|
||||||
* Notes
|
* Notes
|
||||||
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
||||||
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
import type { BattlerTag } from "#app/data/battler-tags";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
|
||||||
|
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { CommonBattleAnim } from "#data/battle-anims";
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { HitResult } from "#enums/hit-result";
|
||||||
|
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 { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import type { Arena } from "#field/arena";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import type {
|
||||||
|
ArenaScreenTagType,
|
||||||
|
ArenaTagData,
|
||||||
|
EntryHazardTagType,
|
||||||
|
RoomArenaTagType,
|
||||||
|
SerializableArenaTagType,
|
||||||
|
} from "#types/arena-tags";
|
||||||
|
import type { Mutable } from "#types/type-helpers";
|
||||||
|
import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
/** Interface containing the serializable fields of ArenaTagData. */
|
/** Interface containing the serializable fields of ArenaTagData. */
|
||||||
interface BaseArenaTag {
|
interface BaseArenaTag {
|
||||||
/**
|
/**
|
||||||
|
208
src/data/balance/moveset-generation.ts
Normal file
208
src/data/balance/moveset-generation.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-Copyright-Text: 2025 Pagefault Games
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* # Balance: Moveset Generation Configuration
|
||||||
|
*
|
||||||
|
* This module contains configuration constants and functions that control
|
||||||
|
* the limitations and rules around moveset generation for generated Pokémon.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* ### Move Weights
|
||||||
|
*
|
||||||
|
* The various move weight constants in this module control how likely
|
||||||
|
* certain categories of moves are to appear in a generated Pokémon's
|
||||||
|
* moveset. Higher weights make a move more likely to be chosen.
|
||||||
|
* The constants here specify the *base* weight for a move when first computed.
|
||||||
|
* These weights are post-processed (and then scaled up such that weights have a larger impact,
|
||||||
|
* for instance, on boss Pokémon) before being used in the actual moveset generation.
|
||||||
|
*
|
||||||
|
* Post Processing of weights includes, but is not limited to:
|
||||||
|
* - Adjusting weights of status moves
|
||||||
|
* - Adjusting weights based on the move's power relative to the highest power available
|
||||||
|
* - Adjusting weights based on the stat the move uses to calculate damage relative to the higher stat
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* All weights go through additional post-processing based on
|
||||||
|
* their expected power (accuracy * damage * expected number of hits)
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
|
||||||
|
|
||||||
|
//#region Constants
|
||||||
|
/**
|
||||||
|
* The minimum level for a Pokémon to generate with a move it can only learn
|
||||||
|
* from a common tier TM
|
||||||
|
*/
|
||||||
|
export const COMMON_TIER_TM_LEVEL_REQUIREMENT = 25;
|
||||||
|
/**
|
||||||
|
* The minimum level for a Pokémon to generate with a move it can only learn
|
||||||
|
* from a great tier TM
|
||||||
|
*/
|
||||||
|
export const GREAT_TIER_TM_LEVEL_REQUIREMENT = 40;
|
||||||
|
/**
|
||||||
|
* The minimum level for a Pokémon to generate with a move it can only learn
|
||||||
|
* from an ultra tier TM
|
||||||
|
*/
|
||||||
|
export const ULTRA_TIER_TM_LEVEL_REQUIREMENT = 55;
|
||||||
|
|
||||||
|
/** Below this level, Pokémon will be unable to generate with any egg moves */
|
||||||
|
export const EGG_MOVE_LEVEL_REQUIREMENT = 60;
|
||||||
|
/** Below this level, Pokémon will be unable to generate with rare egg moves */
|
||||||
|
export const RARE_EGG_MOVE_LEVEL_REQUIREMENT = 170;
|
||||||
|
|
||||||
|
// Note: Not exported, only for use with `getMaxTmCount
|
||||||
|
/** Below this level, Pokémon will be unable to generate with any TMs */
|
||||||
|
const ONE_TM_THRESHOLD = 25;
|
||||||
|
/** Below this level, Pokémon will generate with at most 1 TM */
|
||||||
|
const TWO_TM_THRESHOLD = 41;
|
||||||
|
/** Below this level, Pokémon will generate with at most two TMs */
|
||||||
|
const THREE_TM_THRESHOLD = 71;
|
||||||
|
/** Below this level, Pokémon will generate with at most three TMs */
|
||||||
|
const FOUR_TM_THRESHOLD = 101;
|
||||||
|
|
||||||
|
/** Below this level, Pokémon will be unable to generate any egg moves */
|
||||||
|
const ONE_EGG_MOVE_THRESHOLD = 80;
|
||||||
|
/** Below this level, Pokémon will generate with at most 1 egg moves */
|
||||||
|
const TWO_EGG_MOVE_THRESHOLD = 121;
|
||||||
|
/** Below this level, Pokémon will generate with at most 2 egg moves */
|
||||||
|
const THREE_EGG_MOVE_THRESHOLD = 161;
|
||||||
|
/** Above this level, Pokémon will generate with at most 3 egg moves */
|
||||||
|
const FOUR_EGG_MOVE_THRESHOLD = 201;
|
||||||
|
|
||||||
|
|
||||||
|
/** The weight given to TMs in the common tier during moveset generation */
|
||||||
|
export const COMMON_TM_MOVESET_WEIGHT = 12;
|
||||||
|
/** The weight given to TMs in the great tier during moveset generation */
|
||||||
|
export const GREAT_TM_MOVESET_WEIGHT = 14;
|
||||||
|
/** The weight given to TMs in the ultra tier during moveset generation */
|
||||||
|
export const ULTRA_TM_MOVESET_WEIGHT = 18;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum weight an egg move can ever have
|
||||||
|
* @remarks
|
||||||
|
* Egg moves have their weights adjusted based on the maximum weight of the Pokémon's
|
||||||
|
* level-up moves. Rare Egg moves are always 5/6th of the computed egg move weight.
|
||||||
|
* Boss pokemon are not allowed to spawn with rare egg moves.
|
||||||
|
* @see {@linkcode EGG_MOVE_TO_LEVEL_WEIGHT}
|
||||||
|
*/
|
||||||
|
export const EGG_MOVE_WEIGHT_MAX = 60;
|
||||||
|
/**
|
||||||
|
* The percentage of the Pokémon's highest weighted level move to the weight an
|
||||||
|
* egg move can generate with
|
||||||
|
*/
|
||||||
|
export const EGG_MOVE_TO_LEVEL_WEIGHT = 0.85;
|
||||||
|
/** The weight given to evolution moves */
|
||||||
|
export const EVOLUTION_MOVE_WEIGHT = 70;
|
||||||
|
/** The weight given to relearn moves */
|
||||||
|
export const RELEARN_MOVE_WEIGHT = 60;
|
||||||
|
|
||||||
|
/** The base weight multiplier to use
|
||||||
|
*
|
||||||
|
* The higher the number, the more impact weights have on the final move selection.
|
||||||
|
* i.e. if set to 0, all moves have equal chance of being selected regardless of their weight.
|
||||||
|
*/
|
||||||
|
export const BASE_WEIGHT_MULTIPLIER = 1.6;
|
||||||
|
|
||||||
|
/** The additional weight added onto {@linkcode BASE_WEIGHT_MULTIPLIER} for boss Pokémon */
|
||||||
|
export const BOSS_EXTRA_WEIGHT_MULTIPLIER = 0.4;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of moves that should be blacklisted from the forced STAB during moveset generation
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* During moveset generation, trainer pokemon attempt to force their pokemon to generate with STAB
|
||||||
|
* moves in their movesets. Moves in this list not be considered to be "STAB" moves for this purpose.
|
||||||
|
* This does *not* prevent them from appearing in the moveset, but they will never
|
||||||
|
* be selected as a forced STAB move.
|
||||||
|
*/
|
||||||
|
export const STAB_BLACKLIST: ReadonlySet<MoveId> = new Set([
|
||||||
|
MoveId.BEAT_UP,
|
||||||
|
MoveId.BELCH,
|
||||||
|
MoveId.BIDE,
|
||||||
|
MoveId.COMEUPPANCE,
|
||||||
|
MoveId.COUNTER,
|
||||||
|
MoveId.DOOM_DESIRE,
|
||||||
|
MoveId.DRAGON_RAGE,
|
||||||
|
MoveId.DREAM_EATER,
|
||||||
|
MoveId.ENDEAVOR,
|
||||||
|
MoveId.EXPLOSION,
|
||||||
|
MoveId.FAKE_OUT,
|
||||||
|
MoveId.FIRST_IMPRESSION,
|
||||||
|
MoveId.FISSURE,
|
||||||
|
MoveId.FLING,
|
||||||
|
MoveId.FOCUS_PUNCH,
|
||||||
|
MoveId.FUTURE_SIGHT,
|
||||||
|
MoveId.GUILLOTINE,
|
||||||
|
MoveId.HOLD_BACK,
|
||||||
|
MoveId.HORN_DRILL,
|
||||||
|
MoveId.LAST_RESORT,
|
||||||
|
MoveId.METAL_BURST,
|
||||||
|
MoveId.MIRROR_COAT,
|
||||||
|
MoveId.MISTY_EXPLOSION,
|
||||||
|
MoveId.NATURAL_GIFT,
|
||||||
|
MoveId.NATURES_MADNESS,
|
||||||
|
MoveId.NIGHT_SHADE,
|
||||||
|
MoveId.PSYWAVE,
|
||||||
|
MoveId.RUINATION,
|
||||||
|
MoveId.SELF_DESTRUCT,
|
||||||
|
MoveId.SHEER_COLD,
|
||||||
|
MoveId.SHELL_TRAP,
|
||||||
|
MoveId.SKY_DROP,
|
||||||
|
MoveId.SNORE,
|
||||||
|
MoveId.SONIC_BOOM,
|
||||||
|
MoveId.SPIT_UP,
|
||||||
|
MoveId.STEEL_BEAM,
|
||||||
|
MoveId.STEEL_ROLLER,
|
||||||
|
MoveId.SUPER_FANG,
|
||||||
|
MoveId.SYNCHRONOISE,
|
||||||
|
MoveId.UPPER_HAND,
|
||||||
|
]);
|
||||||
|
|
||||||
|
//#endregion Constants
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum number of TMs a Pokémon is allowed to learn based on
|
||||||
|
* its level
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @returns The number of TMs the Pokémon can learn at this level
|
||||||
|
*/
|
||||||
|
export function getMaxTmCount(level: number) {
|
||||||
|
if (level < ONE_TM_THRESHOLD) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (level < TWO_TM_THRESHOLD) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (level < THREE_TM_THRESHOLD) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (level < FOUR_TM_THRESHOLD) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getMaxEggMoveCount(level: number): number {
|
||||||
|
if (level < ONE_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (level < TWO_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (level < THREE_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (level < FOUR_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 4;
|
||||||
|
}
|
@ -166,7 +166,7 @@ export class SpeciesEvolutionCondition {
|
|||||||
return this.desc;
|
return this.desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public conditionsFulfilled(pokemon: Pokemon): boolean {
|
public conditionsFulfilled(pokemon: Pokemon, forFusion = false): boolean {
|
||||||
console.log(this.data);
|
console.log(this.data);
|
||||||
return this.data.every(cond => {
|
return this.data.every(cond => {
|
||||||
switch (cond.key) {
|
switch (cond.key) {
|
||||||
@ -186,7 +186,7 @@ export class SpeciesEvolutionCondition {
|
|||||||
m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value
|
m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value
|
||||||
);
|
);
|
||||||
case EvoCondKey.GENDER:
|
case EvoCondKey.GENDER:
|
||||||
return pokemon.gender === cond.gender;
|
return cond.gender === (forFusion ? pokemon.fusionGender : pokemon.gender);
|
||||||
case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly
|
case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly
|
||||||
return false;
|
return false;
|
||||||
case EvoCondKey.BIOME:
|
case EvoCondKey.BIOME:
|
||||||
@ -293,7 +293,7 @@ export class SpeciesFormEvolution {
|
|||||||
pokemon.level >= this.level &&
|
pokemon.level >= this.level &&
|
||||||
// Check form key, using the fusion's form key if we're checking the fusion
|
// Check form key, using the fusion's form key if we're checking the fusion
|
||||||
(this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
(this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
||||||
(this.condition == null || this.condition.conditionsFulfilled(pokemon)) &&
|
(this.condition == null || this.condition.conditionsFulfilled(pokemon, forFusion)) &&
|
||||||
((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE))
|
((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon.
|
||||||
|
* Note that before serialization, a new tag object is created, and then `loadTag` is called on the
|
||||||
|
* tag with the object that was serialized.
|
||||||
|
*
|
||||||
|
* This means it is straightforward to avoid serializing fields.
|
||||||
|
* Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized.
|
||||||
|
*
|
||||||
|
* Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature.
|
||||||
|
* Only tags that persist across waves (meaning their effect can last >1 turn) should be considered
|
||||||
|
* serializable.
|
||||||
|
*
|
||||||
|
* Serializable battler tags have strict requirements for their fields.
|
||||||
|
* Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided
|
||||||
|
* by using a private property. If access to the property is needed outside of the class, then
|
||||||
|
* a getter (and potentially, a setter) should be used instead.
|
||||||
|
*
|
||||||
|
* If a property that is intended to be private must be serialized, then it should instead
|
||||||
|
* be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property)
|
||||||
|
* use `(this as Mutable<this>).propertyName = value;`
|
||||||
|
* These rules ensure that Typescript is aware of the shape of the serialized version of the class.
|
||||||
|
*
|
||||||
|
* If any new serializable fields *are* added, then the class *must* override the
|
||||||
|
* `loadTag` method to set the new fields. Its signature *must* match the example below:
|
||||||
|
* ```
|
||||||
|
* class ExampleTag extends SerializableBattlerTag {
|
||||||
|
* // Example, if we add 2 new fields that should be serialized:
|
||||||
|
* public a: string;
|
||||||
|
* public b: number;
|
||||||
|
* // Then we must also define a loadTag method with one of the following signatures
|
||||||
|
* public override loadTag(source: BaseBattlerTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
|
||||||
|
* public override loadTag<const T extends this>(source: BaseBattlerTag & Pick<T, "tagType" | "a" | "b">): void;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* Notes
|
||||||
|
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
@ -52,45 +91,6 @@ import type { Mutable } from "#types/type-helpers";
|
|||||||
import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#utils/common";
|
import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#utils/common";
|
||||||
import { toCamelCase } from "#utils/strings";
|
import { toCamelCase } from "#utils/strings";
|
||||||
|
|
||||||
/**
|
|
||||||
* @module
|
|
||||||
* BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon.
|
|
||||||
* Note that before serialization, a new tag object is created, and then `loadTag` is called on the
|
|
||||||
* tag with the object that was serialized.
|
|
||||||
*
|
|
||||||
* This means it is straightforward to avoid serializing fields.
|
|
||||||
* Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized.
|
|
||||||
*
|
|
||||||
* Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature.
|
|
||||||
* Only tags that persist across waves (meaning their effect can last >1 turn) should be considered
|
|
||||||
* serializable.
|
|
||||||
*
|
|
||||||
* Serializable battler tags have strict requirements for their fields.
|
|
||||||
* Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided
|
|
||||||
* by using a private property. If access to the property is needed outside of the class, then
|
|
||||||
* a getter (and potentially, a setter) should be used instead.
|
|
||||||
*
|
|
||||||
* If a property that is intended to be private must be serialized, then it should instead
|
|
||||||
* be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property)
|
|
||||||
* use `(this as Mutable<this>).propertyName = value;`
|
|
||||||
* These rules ensure that Typescript is aware of the shape of the serialized version of the class.
|
|
||||||
*
|
|
||||||
* If any new serializable fields *are* added, then the class *must* override the
|
|
||||||
* `loadTag` method to set the new fields. Its signature *must* match the example below:
|
|
||||||
* ```
|
|
||||||
* class ExampleTag extends SerializableBattlerTag {
|
|
||||||
* // Example, if we add 2 new fields that should be serialized:
|
|
||||||
* public a: string;
|
|
||||||
* public b: number;
|
|
||||||
* // Then we must also define a loadTag method with one of the following signatures
|
|
||||||
* public override loadTag(source: BaseBattlerTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
|
|
||||||
* public override loadTag<const T extends this>(source: BaseBattlerTag & Pick<T, "tagType" | "a" | "b">): void;
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* Notes
|
|
||||||
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Interface containing the serializable fields of BattlerTag */
|
/** Interface containing the serializable fields of BattlerTag */
|
||||||
interface BaseBattlerTag {
|
interface BaseBattlerTag {
|
||||||
/** The tag's remaining duration */
|
/** The tag's remaining duration */
|
||||||
|
@ -3,6 +3,8 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||||
import { signatureSpecies } from "#balance/signature-species";
|
import { signatureSpecies } from "#balance/signature-species";
|
||||||
import { tmSpecies } from "#balance/tms";
|
import { tmSpecies } from "#balance/tms";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
|
||||||
|
import type { RARE_EGG_MOVE_LEVEL_REQUIREMENT } from "#data/balance/moveset-generation";
|
||||||
import { modifierTypes } from "#data/data-lists";
|
import { modifierTypes } from "#data/data-lists";
|
||||||
import { doubleBattleDialogue } from "#data/double-battle-dialogue";
|
import { doubleBattleDialogue } from "#data/double-battle-dialogue";
|
||||||
import { Gender } from "#data/gender";
|
import { Gender } from "#data/gender";
|
||||||
@ -41,6 +43,7 @@ import type {
|
|||||||
TrainerConfigs,
|
TrainerConfigs,
|
||||||
TrainerTierPools,
|
TrainerTierPools,
|
||||||
} from "#types/trainer-funcs";
|
} from "#types/trainer-funcs";
|
||||||
|
import type { Mutable } from "#types/type-helpers";
|
||||||
import { coerceArray, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
|
import { coerceArray, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
@ -119,6 +122,15 @@ export class TrainerConfig {
|
|||||||
public hasVoucher = false;
|
public hasVoucher = false;
|
||||||
public trainerAI: TrainerAI;
|
public trainerAI: TrainerAI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this trainer's Pokémon are allowed to generate with egg moves
|
||||||
|
* @defaultValue `false`
|
||||||
|
*
|
||||||
|
* @see {@linkcode setEggMovesAllowed}
|
||||||
|
* @see {@linkcode RARE_EGG_MOVE_LEVEL_THRESHOLD}
|
||||||
|
*/
|
||||||
|
public readonly allowEggMoves: boolean = false;
|
||||||
|
|
||||||
public encounterMessages: string[] = [];
|
public encounterMessages: string[] = [];
|
||||||
public victoryMessages: string[] = [];
|
public victoryMessages: string[] = [];
|
||||||
public defeatMessages: string[] = [];
|
public defeatMessages: string[] = [];
|
||||||
@ -387,8 +399,27 @@ export class TrainerConfig {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBoss(): TrainerConfig {
|
/**
|
||||||
|
* Allow this trainer's Pokémon to have egg moves when generating their movesets.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* It is redundant to call this if {@linkcode setBoss} is also called on the configuration.
|
||||||
|
* @returns `this` for method chaining
|
||||||
|
* @see {@linkcode allowEggMoves}
|
||||||
|
*/
|
||||||
|
public setEggMovesAllowed(): this {
|
||||||
|
(this as Mutable<this>).allowEggMoves = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this trainer as a boss trainer
|
||||||
|
* @returns `this` for method chaining
|
||||||
|
* @see {@linkcode isBoss}
|
||||||
|
*/
|
||||||
|
public setBoss(): TrainerConfig {
|
||||||
this.isBoss = true;
|
this.isBoss = true;
|
||||||
|
(this as Mutable<this>).allowEggMoves = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
|
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
|
||||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
|
import { generateMoveset } from "#app/ai/ai-moveset-gen";
|
||||||
import type { AnySound, BattleScene } from "#app/battle-scene";
|
import type { AnySound, BattleScene } from "#app/battle-scene";
|
||||||
import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
|
import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
|
||||||
import { timedEventManager } from "#app/global-event-manager";
|
import { timedEventManager } from "#app/global-event-manager";
|
||||||
@ -18,7 +19,7 @@ import type { LevelMoves } from "#balance/pokemon-level-moves";
|
|||||||
import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves";
|
import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves";
|
||||||
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#balance/rates";
|
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#balance/rates";
|
||||||
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#balance/starters";
|
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#balance/starters";
|
||||||
import { reverseCompatibleTms, tmPoolTiers, tmSpecies } from "#balance/tms";
|
import { reverseCompatibleTms, tmSpecies } from "#balance/tms";
|
||||||
import type { SuppressAbilitiesTag } from "#data/arena-tag";
|
import type { SuppressAbilitiesTag } from "#data/arena-tag";
|
||||||
import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag";
|
import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag";
|
||||||
import {
|
import {
|
||||||
@ -81,7 +82,6 @@ import { DexAttr } from "#enums/dex-attr";
|
|||||||
import { FieldPosition } from "#enums/field-position";
|
import { FieldPosition } from "#enums/field-position";
|
||||||
import { HitResult } from "#enums/hit-result";
|
import { HitResult } from "#enums/hit-result";
|
||||||
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
||||||
import { ModifierTier } from "#enums/modifier-tier";
|
|
||||||
import { MoveCategory } from "#enums/move-category";
|
import { MoveCategory } from "#enums/move-category";
|
||||||
import { MoveFlags } from "#enums/move-flags";
|
import { MoveFlags } from "#enums/move-flags";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
@ -2635,7 +2635,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
e => new FusionSpeciesFormEvolution(this.species.speciesId, e),
|
e => new FusionSpeciesFormEvolution(this.species.speciesId, e),
|
||||||
);
|
);
|
||||||
for (const fe of fusionEvolutions) {
|
for (const fe of fusionEvolutions) {
|
||||||
if (fe.validate(this)) {
|
if (fe.validate(this, true)) {
|
||||||
return fe;
|
return fe;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2774,7 +2774,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* This causes problems when there are intentional duplicates (i.e. Smeargle with Sketch)
|
* This causes problems when there are intentional duplicates (i.e. Smeargle with Sketch)
|
||||||
*/
|
*/
|
||||||
if (levelMoves) {
|
if (levelMoves) {
|
||||||
this.getUniqueMoves(levelMoves, ret);
|
Pokemon.getUniqueMoves(levelMoves, ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@ -2788,7 +2788,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param levelMoves the input array to search for non-duplicates from
|
* @param levelMoves the input array to search for non-duplicates from
|
||||||
* @param ret the output array to be pushed into.
|
* @param ret the output array to be pushed into.
|
||||||
*/
|
*/
|
||||||
private getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void {
|
private static getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void {
|
||||||
const uniqueMoves: MoveId[] = [];
|
const uniqueMoves: MoveId[] = [];
|
||||||
for (const lm of levelMoves) {
|
for (const lm of levelMoves) {
|
||||||
if (!uniqueMoves.find(m => m === lm[1])) {
|
if (!uniqueMoves.find(m => m === lm[1])) {
|
||||||
@ -3046,294 +3046,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
/** Generates a semi-random moveset for a Pokemon */
|
/** Generates a semi-random moveset for a Pokemon */
|
||||||
public generateAndPopulateMoveset(): void {
|
public generateAndPopulateMoveset(): void {
|
||||||
this.moveset = [];
|
generateMoveset(this);
|
||||||
let movePool: [MoveId, number][] = [];
|
|
||||||
const allLevelMoves = this.getLevelMoves(1, true, true, this.hasTrainer());
|
|
||||||
if (!allLevelMoves) {
|
|
||||||
console.warn("Error encountered trying to generate moveset for:", this.species.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const levelMove of allLevelMoves) {
|
|
||||||
if (this.level < levelMove[0]) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let weight = levelMove[0] + 20;
|
|
||||||
// Evolution Moves
|
|
||||||
if (levelMove[0] === EVOLVE_MOVE) {
|
|
||||||
weight = 70;
|
|
||||||
}
|
|
||||||
// Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves.
|
|
||||||
if (
|
|
||||||
(levelMove[0] === 1 && allMoves[levelMove[1]].power >= 80)
|
|
||||||
|| (levelMove[0] === RELEARN_MOVE && this.hasTrainer())
|
|
||||||
) {
|
|
||||||
weight = 60;
|
|
||||||
}
|
|
||||||
if (!movePool.some(m => m[0] === levelMove[1]) && !allMoves[levelMove[1]].name.endsWith(" (N)")) {
|
|
||||||
movePool.push([levelMove[1], weight]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasTrainer()) {
|
|
||||||
const tms = Object.keys(tmSpecies);
|
|
||||||
for (const tm of tms) {
|
|
||||||
const moveId = Number.parseInt(tm) as MoveId;
|
|
||||||
let compatible = false;
|
|
||||||
for (const p of tmSpecies[tm]) {
|
|
||||||
if (Array.isArray(p)) {
|
|
||||||
if (
|
|
||||||
p[0] === this.species.speciesId
|
|
||||||
|| (this.fusionSpecies
|
|
||||||
&& p[0] === this.fusionSpecies.speciesId
|
|
||||||
&& p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1)
|
|
||||||
) {
|
|
||||||
compatible = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) {
|
|
||||||
compatible = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
||||||
if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) {
|
|
||||||
movePool.push([moveId, 24]);
|
|
||||||
} else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) {
|
|
||||||
movePool.push([moveId, 28]);
|
|
||||||
} else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) {
|
|
||||||
movePool.push([moveId, 34]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No egg moves below level 60
|
|
||||||
if (this.level >= 60) {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i];
|
|
||||||
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
||||||
movePool.push([moveId, 60]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3];
|
|
||||||
// No rare egg moves before e4
|
|
||||||
if (
|
|
||||||
this.level >= 170
|
|
||||||
&& !movePool.some(m => m[0] === moveId)
|
|
||||||
&& !allMoves[moveId].name.endsWith(" (N)")
|
|
||||||
&& !this.isBoss()
|
|
||||||
) {
|
|
||||||
movePool.push([moveId, 50]);
|
|
||||||
}
|
|
||||||
if (this.fusionSpecies) {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i];
|
|
||||||
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
||||||
movePool.push([moveId, 60]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3];
|
|
||||||
// No rare egg moves before e4
|
|
||||||
if (
|
|
||||||
this.level >= 170
|
|
||||||
&& !movePool.some(m => m[0] === moveId)
|
|
||||||
&& !allMoves[moveId].name.endsWith(" (N)")
|
|
||||||
&& !this.isBoss()
|
|
||||||
) {
|
|
||||||
movePool.push([moveId, 50]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bosses never get self ko moves or Pain Split
|
|
||||||
if (this.isBoss()) {
|
|
||||||
movePool = movePool.filter(
|
|
||||||
m => !allMoves[m[0]].hasAttr("SacrificialAttr") && !allMoves[m[0]].hasAttr("HpSplitAttr"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// No one gets Memento or Final Gambit
|
|
||||||
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("SacrificialAttrOnHit"));
|
|
||||||
if (this.hasTrainer()) {
|
|
||||||
// Trainers never get OHKO moves
|
|
||||||
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("OneHitKOAttr"));
|
|
||||||
// Half the weight of self KO moves
|
|
||||||
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].hasAttr("SacrificialAttr") ? 0.5 : 1)]);
|
|
||||||
// Trainers get a weight bump to stat buffing moves
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1] * (allMoves[m[0]].getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1),
|
|
||||||
]);
|
|
||||||
// Trainers get a weight decrease to multiturn moves
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr("RechargeAttr") ? 0.7 : 1),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weight towards higher power moves, by reducing the power of moves below the highest power.
|
|
||||||
// Caps max power at 90 to avoid something like hyper beam ruining the stats.
|
|
||||||
// This is a pretty soft weighting factor, although it is scaled with the weight multiplier.
|
|
||||||
const maxPower = Math.min(
|
|
||||||
movePool.reduce((v, m) => Math.max(allMoves[m[0]].calculateEffectivePower(), v), 40),
|
|
||||||
90,
|
|
||||||
);
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1]
|
|
||||||
* (allMoves[m[0]].category === MoveCategory.STATUS
|
|
||||||
? 1
|
|
||||||
: Math.max(Math.min(allMoves[m[0]].calculateEffectivePower() / maxPower, 1), 0.5)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Weight damaging moves against the lower stat. This uses a non-linear relationship.
|
|
||||||
// If the higher stat is 1 - 1.09x higher, no change. At higher stat ~1.38x lower stat, off-stat moves have half weight.
|
|
||||||
// One third weight at ~1.58x higher, one quarter weight at ~1.73x higher, one fifth at ~1.87x, and one tenth at ~2.35x higher.
|
|
||||||
const atk = this.getStat(Stat.ATK);
|
|
||||||
const spAtk = this.getStat(Stat.SPATK);
|
|
||||||
const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
|
||||||
const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk;
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1] * (allMoves[m[0]].category === worseCategory ? Math.min(Math.pow(statRatio, 3) * 1.3, 1) : 1),
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */
|
|
||||||
let weightMultiplier = 1.6;
|
|
||||||
if (this.isBoss()) {
|
|
||||||
weightMultiplier += 0.4;
|
|
||||||
}
|
|
||||||
const baseWeights: [MoveId, number][] = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
Math.ceil(Math.pow(m[1], weightMultiplier) * 100),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const STAB_BLACKLIST: ReadonlySet<MoveId> = new Set([
|
|
||||||
MoveId.BEAT_UP,
|
|
||||||
MoveId.BELCH,
|
|
||||||
MoveId.BIDE,
|
|
||||||
MoveId.COMEUPPANCE,
|
|
||||||
MoveId.COUNTER,
|
|
||||||
MoveId.DOOM_DESIRE,
|
|
||||||
MoveId.DRAGON_RAGE,
|
|
||||||
MoveId.DREAM_EATER,
|
|
||||||
MoveId.ENDEAVOR,
|
|
||||||
MoveId.EXPLOSION,
|
|
||||||
MoveId.FAKE_OUT,
|
|
||||||
MoveId.FIRST_IMPRESSION,
|
|
||||||
MoveId.FISSURE,
|
|
||||||
MoveId.FLING,
|
|
||||||
MoveId.FOCUS_PUNCH,
|
|
||||||
MoveId.FUTURE_SIGHT,
|
|
||||||
MoveId.GUILLOTINE,
|
|
||||||
MoveId.HOLD_BACK,
|
|
||||||
MoveId.HORN_DRILL,
|
|
||||||
MoveId.LAST_RESORT,
|
|
||||||
MoveId.METAL_BURST,
|
|
||||||
MoveId.MIRROR_COAT,
|
|
||||||
MoveId.MISTY_EXPLOSION,
|
|
||||||
MoveId.NATURAL_GIFT,
|
|
||||||
MoveId.NATURES_MADNESS,
|
|
||||||
MoveId.NIGHT_SHADE,
|
|
||||||
MoveId.PSYWAVE,
|
|
||||||
MoveId.RUINATION,
|
|
||||||
MoveId.SELF_DESTRUCT,
|
|
||||||
MoveId.SHEER_COLD,
|
|
||||||
MoveId.SHELL_TRAP,
|
|
||||||
MoveId.SKY_DROP,
|
|
||||||
MoveId.SNORE,
|
|
||||||
MoveId.SONIC_BOOM,
|
|
||||||
MoveId.SPIT_UP,
|
|
||||||
MoveId.STEEL_BEAM,
|
|
||||||
MoveId.STEEL_ROLLER,
|
|
||||||
MoveId.SUPER_FANG,
|
|
||||||
MoveId.SYNCHRONOISE,
|
|
||||||
MoveId.UPPER_HAND,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// All Pokemon force a STAB move first
|
|
||||||
const stabMovePool = baseWeights.filter(
|
|
||||||
m =>
|
|
||||||
allMoves[m[0]].category !== MoveCategory.STATUS
|
|
||||||
&& this.isOfType(allMoves[m[0]].type)
|
|
||||||
&& !STAB_BLACKLIST.has(m[0]),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (stabMovePool.length > 0) {
|
|
||||||
const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0);
|
|
||||||
let rand = randSeedInt(totalWeight);
|
|
||||||
let index = 0;
|
|
||||||
while (rand > stabMovePool[index][1]) {
|
|
||||||
rand -= stabMovePool[index++][1];
|
|
||||||
}
|
|
||||||
this.moveset.push(new PokemonMove(stabMovePool[index][0]));
|
|
||||||
} else {
|
|
||||||
// If there are no damaging STAB moves, just force a random damaging move
|
|
||||||
const attackMovePool = baseWeights.filter(
|
|
||||||
m => allMoves[m[0]].category !== MoveCategory.STATUS && !STAB_BLACKLIST.has(m[0]),
|
|
||||||
);
|
|
||||||
if (attackMovePool.length > 0) {
|
|
||||||
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
|
|
||||||
let rand = randSeedInt(totalWeight);
|
|
||||||
let index = 0;
|
|
||||||
while (rand > attackMovePool[index][1]) {
|
|
||||||
rand -= attackMovePool[index++][1];
|
|
||||||
}
|
|
||||||
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
|
|
||||||
if (this.hasTrainer()) {
|
|
||||||
// Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier.
|
|
||||||
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB.
|
|
||||||
// Status moves remain unchanged on weight, this encourages 1-2
|
|
||||||
movePool = baseWeights
|
|
||||||
.filter(
|
|
||||||
m =>
|
|
||||||
!this.moveset.some(
|
|
||||||
mo =>
|
|
||||||
m[0] === mo.moveId
|
|
||||||
|| (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map(m => {
|
|
||||||
let ret: number;
|
|
||||||
if (
|
|
||||||
this.moveset.some(
|
|
||||||
mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[m[0]].type,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
ret = Math.ceil(Math.sqrt(m[1]));
|
|
||||||
} else if (allMoves[m[0]].category !== MoveCategory.STATUS) {
|
|
||||||
ret = Math.ceil(
|
|
||||||
(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5))
|
|
||||||
* (this.isOfType(allMoves[m[0]].type) && !STAB_BLACKLIST.has(m[0]) ? 20 : 1),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ret = m[1];
|
|
||||||
}
|
|
||||||
return [m[0], ret];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Non-trainer pokemon just use normal weights
|
|
||||||
movePool = baseWeights.filter(
|
|
||||||
m =>
|
|
||||||
!this.moveset.some(
|
|
||||||
mo =>
|
|
||||||
m[0] === mo.moveId
|
|
||||||
|| (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const totalWeight = movePool.reduce((v, m) => v + m[1], 0);
|
|
||||||
let rand = randSeedInt(totalWeight);
|
|
||||||
let index = 0;
|
|
||||||
while (rand > movePool[index][1]) {
|
|
||||||
rand -= movePool[index++][1];
|
|
||||||
}
|
|
||||||
this.moveset.push(new PokemonMove(movePool[index][0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
|
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
|
||||||
if (
|
if (
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Manager for phases used by battle scene.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* **This file must not be imported or used directly.**
|
||||||
|
* The manager is exclusively used by the Battle Scene and is NOT intended for external use.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
import { PHASE_START_COLOR } from "#app/constants/colors";
|
import { PHASE_START_COLOR } from "#app/constants/colors";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import type { Phase } from "#app/phase";
|
import type { Phase } from "#app/phase";
|
||||||
@ -103,15 +112,6 @@ import { WeatherEffectPhase } from "#phases/weather-effect-phase";
|
|||||||
import type { PhaseMap, PhaseString } from "#types/phase-types";
|
import type { PhaseMap, PhaseString } from "#types/phase-types";
|
||||||
import { type Constructor, coerceArray } from "#utils/common";
|
import { type Constructor, coerceArray } from "#utils/common";
|
||||||
|
|
||||||
/**
|
|
||||||
* @module
|
|
||||||
* Manager for phases used by battle scene.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* **This file must not be imported or used directly.**
|
|
||||||
* The manager is exclusively used by the Battle Scene and is NOT intended for external use.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object that holds all of the phase constructors.
|
* Object that holds all of the phase constructors.
|
||||||
* This is used to create new phases dynamically using the `newPhase` method in the `PhaseManager`.
|
* This is used to create new phases dynamically using the `newPhase` method in the `PhaseManager`.
|
||||||
|
@ -23,11 +23,13 @@ export function getCookie(cName: string): string {
|
|||||||
}
|
}
|
||||||
const name = `${cName}=`;
|
const name = `${cName}=`;
|
||||||
const ca = document.cookie.split(";");
|
const ca = document.cookie.split(";");
|
||||||
// Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up
|
for (let c of ca) {
|
||||||
for (const c of ca) {
|
// ⚠️ DO NOT REPLACE THIS WITH C = C.TRIM() - IT BREAKS IN NON-CHROMIUM BROWSERS ⚠️
|
||||||
const cTrimmed = c.trim();
|
while (c.charAt(0) === " ") {
|
||||||
if (cTrimmed.startsWith(name)) {
|
c = c.substring(1);
|
||||||
return c.slice(name.length, c.length);
|
}
|
||||||
|
if (c.indexOf(name) === 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Code to add markers to the beginning and end of tests.
|
||||||
|
* Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks
|
||||||
|
* (rather than as part of the reporter) to ensure Vitest waits for the log messages to be printed.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
import type CustomDefaultReporter from "#test/test-utils/reporters/custom-default-reporter";
|
import type CustomDefaultReporter from "#test/test-utils/reporters/custom-default-reporter";
|
||||||
import { basename, join, relative } from "path";
|
import { basename, join, relative } from "path";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest";
|
import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest";
|
||||||
|
|
||||||
/**
|
|
||||||
* @module
|
|
||||||
* Code to add markers to the beginning and end of tests.
|
|
||||||
* Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks
|
|
||||||
* (rather than as part of the reporter) to ensure Vitest waits for the log messages to be printed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** A long string of "="s to partition off each test from one another. */
|
/** A long string of "="s to partition off each test from one another. */
|
||||||
const TEST_END_BARRIER = chalk.bold.hex("#ff7c7cff")("==================");
|
const TEST_END_BARRIER = chalk.bold.hex("#ff7c7cff")("==================");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user