Merge remote-tracking branch 'upstream/beta' into phase-interceptor

This commit is contained in:
Bertie690 2025-08-28 10:01:56 -04:00
commit 69966719d1
21 changed files with 247 additions and 72 deletions

View File

@ -20,6 +20,7 @@ permissions:
jobs:
create-release:
if: github.repository == 'pagefaultgames/pokerogue' && (vars.BETA_DEPLOY_BRANCH == '' || ! startsWith(vars.BETA_DEPLOY_BRANCH, 'release'))
timeout-minutes: 10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed for github cli commands
runs-on: ubuntu-latest
@ -36,11 +37,13 @@ jobs:
exit 1
fi
shell: bash
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ secrets.PAGEFAULT_APP_ID }}
private-key: ${{ secrets.PAGEFAULT_APP_PRIVATE_KEY }}
- name: Check out code
uses: actions/checkout@v4
with:
@ -48,8 +51,10 @@ jobs:
# Always base off of beta branch, regardless of the branch the workflow was triggered from.
ref: beta
token: ${{ steps.app-token.outputs.token }}
- name: Create release branch
run: git checkout -b release
# In order to be able to open a PR into beta, we need the branch to have at least one change.
- name: Overwrite RELEASE file
run: |
@ -58,11 +63,14 @@ jobs:
echo "Release v${{ github.event.inputs.versionName }}" > RELEASE
git add RELEASE
git commit -m "Stage release v${{ github.event.inputs.versionName }}"
- name: Push new branch
run: git push origin release
# The repository variable is used by the deploy-beta workflow to determine whether to deploy from beta or release.
- name: Set repository variable
run: GITHUB_TOKEN="${{ steps.app-token.outputs.token }}" gh variable set BETA_DEPLOY_BRANCH --body "release"
- name: Create pull request to main
run: |
gh pr create --base main \
@ -70,6 +78,7 @@ jobs:
--title "Release v${{ github.event.inputs.versionName }} to main" \
--body "This PR is for the release of v${{ github.event.inputs.versionName }}, and was created automatically by the GitHub Actions workflow invoked by ${{ github.actor }}" \
--draft
- name: Create pull request to beta
run: |
gh pr create --base beta \

View File

@ -12,6 +12,7 @@ on:
jobs:
deploy:
if: github.repository == 'pagefaultgames/pokerogue' && github.ref_name == (vars.BETA_DEPLOY_BRANCH || 'beta')
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View File

@ -11,6 +11,7 @@ on:
jobs:
deploy:
if: github.repository == 'pagefaultgames/pokerogue'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View File

@ -20,6 +20,7 @@ jobs:
pages:
name: Github Pages
if: github.repository == 'pagefaultgames/pokerogue'
timeout-minutes: 10
runs-on: ubuntu-latest
env:
api-dir: ./

View File

@ -19,6 +19,7 @@ on:
jobs:
run-linters:
name: Run linters
timeout-minutes: 10
runs-on: ubuntu-latest
steps:

View File

@ -6,6 +6,7 @@ jobs:
# Set the BETA_DEPLOY_BRANCH variable to beta when a release branch is deleted
update-release-var:
if: github.repository == 'pagefaultgames/pokerogue' && github.event.ref_type == 'branch' && github.event.ref == 'release'
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Set BETA_DEPLOY_BRANCH to beta

View File

@ -21,6 +21,7 @@ jobs:
test:
# We can't use dynmically named jobs until https://github.com/orgs/community/discussions/13261 is implemented
name: Shard
timeout-minutes: 10
runs-on: ubuntu-latest
if: ${{ !inputs.skip }}
steps:

View File

@ -19,6 +19,7 @@ on:
jobs:
check-path-change-filter:
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
pull-requests: read
@ -37,6 +38,8 @@ jobs:
name: Run Tests
needs: check-path-change-filter
strategy:
# don't stop upon 1 shard failing
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
uses: ./.github/workflows/test-shard-template.yml

View File

@ -1,7 +1,7 @@
{
"name": "pokemon-rogue-battle",
"private": true,
"version": "1.11.0",
"version": "1.10.4",
"type": "module",
"scripts": {
"start": "vite",

View File

@ -1,3 +1,7 @@
self.addEventListener('install', function () {
console.log('Service worker installing...');
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
})

View File

@ -77,7 +77,8 @@ export enum EvolutionItem {
LEADERS_CREST
}
type TyrogueMove = MoveId.LOW_SWEEP | MoveId.MACH_PUNCH | MoveId.RAPID_SPIN;
const tyrogueMoves = [MoveId.LOW_SWEEP, MoveId.MACH_PUNCH, MoveId.RAPID_SPIN] as const;
type TyrogueMove = typeof tyrogueMoves[number];
/**
* Pokemon Evolution tuple type consisting of:
@ -192,7 +193,7 @@ export class SpeciesEvolutionCondition {
case EvoCondKey.WEATHER:
return cond.weather.includes(globalScene.arena.getWeatherType());
case EvoCondKey.TYROGUE:
return pokemon.getMoveset(true).find(m => m.moveId as TyrogueMove)?.moveId === cond.move;
return pokemon.getMoveset(true).find(m => (tyrogueMoves as readonly MoveId[]) .includes(m.moveId))?.moveId === cond.move;
case EvoCondKey.NATURE:
return cond.nature.includes(pokemon.getNature());
case EvoCondKey.RANDOM_FORM: {

View File

@ -2,6 +2,7 @@ import { globalScene } from "#app/global-scene";
import { allSpecies, modifierTypes } from "#data/data-lists";
import { getLevelTotalExp } from "#data/exp";
import type { PokemonSpecies } from "#data/pokemon-species";
import { AbilityId } from "#enums/ability-id";
import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#enums/modifier-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -10,8 +11,9 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature";
import { PartyMemberStrength } from "#enums/party-member-strength";
import { PlayerGender } from "#enums/player-gender";
import { PokemonType } from "#enums/pokemon-type";
import { MAX_POKEMON_TYPE, PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect";
import { TrainerType } from "#enums/trainer-type";
import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import type { PokemonHeldItemModifier } from "#modifiers/modifier";
@ -219,6 +221,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
await showEncounterText(`${namespace}:option.1.dreamComplete`);
await doNewTeamPostProcess(transformations);
globalScene.phaseManager.unshiftNew("PartyHealPhase", true);
setEncounterRewards({
guaranteedModifierTypeFuncs: [
modifierTypes.MEMORY_MUSHROOM,
@ -230,7 +233,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
],
fillRemaining: false,
});
leaveEncounterWithoutBattle(true);
leaveEncounterWithoutBattle(false);
})
.build(),
)
@ -431,6 +434,8 @@ function getTeamTransformations(): PokemonTransformation[] {
newAbilityIndex,
undefined,
);
transformation.newPokemon.teraType = randSeedInt(MAX_POKEMON_TYPE);
}
return pokemonTransformations;
@ -440,6 +445,8 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
let atLeastOneNewStarter = false;
for (const transformation of transformations) {
const previousPokemon = transformation.previousPokemon;
const oldHpRatio = previousPokemon.getHpRatio(true);
const oldStatus = previousPokemon.status;
const newPokemon = transformation.newPokemon;
const speciesRootForm = newPokemon.species.getRootSpeciesId();
@ -462,6 +469,19 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
}
newPokemon.calculateStats();
if (oldHpRatio > 0) {
newPokemon.hp = Math.ceil(oldHpRatio * newPokemon.getMaxHp());
// Assume that the `status` instance can always safely be transferred to the new pokemon
// This is the case (as of version 1.10.4)
// Safeguard against COMATOSE here
if (!newPokemon.hasAbility(AbilityId.COMATOSE, false, true)) {
newPokemon.status = oldStatus;
}
} else {
newPokemon.hp = 0;
newPokemon.doSetStatus(StatusEffect.FAINT);
}
await newPokemon.updateInfo();
}

View File

@ -20,3 +20,6 @@ export enum PokemonType {
FAIRY,
STELLAR
}
/** The largest legal value for a {@linkcode PokemonType} (includes Stellar) */
export const MAX_POKEMON_TYPE = PokemonType.STELLAR;

View File

@ -3070,14 +3070,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.level < levelMove[0]) {
break;
}
let weight = levelMove[0];
let weight = levelMove[0] + 20;
// Evolution Moves
if (weight === EVOLVE_MOVE) {
weight = 50;
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 ((weight === 1 && allMoves[levelMove[1]].power >= 80) || (weight === RELEARN_MOVE && this.hasTrainer())) {
weight = 40;
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]);
@ -3107,11 +3110,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) {
movePool.push([moveId, 4]);
movePool.push([moveId, 24]);
} else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) {
movePool.push([moveId, 8]);
movePool.push([moveId, 28]);
} else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) {
movePool.push([moveId, 14]);
movePool.push([moveId, 34]);
}
}
}
@ -3121,7 +3124,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
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, 40]);
movePool.push([moveId, 60]);
}
}
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3];
@ -3132,13 +3135,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
!allMoves[moveId].name.endsWith(" (N)") &&
!this.isBoss()
) {
movePool.push([moveId, 30]);
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, 40]);
movePool.push([moveId, 60]);
}
}
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3];
@ -3149,7 +3152,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
!allMoves[moveId].name.endsWith(" (N)") &&
!this.isBoss()
) {
movePool.push([moveId, 30]);
movePool.push([moveId, 50]);
}
}
}
@ -3230,6 +3233,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
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);
if (attackMovePool.length) {
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) {

View File

@ -179,12 +179,11 @@ export class TurnStartPhase extends FieldPhase {
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
phaseManager.pushNew("WeatherEffectPhase");
phaseManager.pushNew("PositionalTagPhase");
phaseManager.pushNew("BerryPhase");
/** Add a new phase to check who should be taking status damage */
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
phaseManager.pushNew("PositionalTagPhase");
phaseManager.pushNew("TurnEndPhase");
/*

View File

@ -1515,6 +1515,7 @@ export class GameData {
switch (dataType) {
case GameDataType.SYSTEM: {
dataStr = this.convertSystemDataStr(dataStr);
dataStr = dataStr.replace(/"playTime":\d+/, `"playTime":${this.gameStats.playTime + 60}`);
const systemData = this.parseSystemData(dataStr);
valid = !!systemData.dexData && !!systemData.timestamp;
break;

View File

@ -72,7 +72,7 @@ import {
rgbHexToRgba,
} from "#utils/common";
import type { StarterPreferences } from "#utils/data";
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
import { deepCopy, loadStarterPreferences, saveStarterPreferences } from "#utils/data";
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
import { toCamelCase, toTitleCase } from "#utils/strings";
import { argbFromRgba } from "@material/material-color-utilities";
@ -1148,7 +1148,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.starterSelectContainer.setVisible(true);
this.starterPreferences = loadStarterPreferences();
this.originalStarterPreferences = loadStarterPreferences();
// Deep copy the JSON (avoid re-loading from disk)
this.originalStarterPreferences = deepCopy(this.starterPreferences);
this.allSpecies.forEach((species, s) => {
const icon = this.starterContainers[s].icon;
@ -1212,6 +1213,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
preferences: StarterPreferences,
ignoreChallenge = false,
): StarterAttributes {
// if preferences for the species is undefined, set it to an empty object
preferences[species.speciesId] ??= {};
const starterAttributes = preferences[species.speciesId];
const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge);
@ -1828,9 +1831,15 @@ export class StarterSelectUiHandler extends MessageUiHandler {
// The persistent starter data to apply e.g. candy upgrades
const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId];
// The sanitized starter preferences
let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId];
// The original starter preferences
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId];
if (this.starterPreferences[this.lastSpecies.speciesId] === undefined) {
this.starterPreferences[this.lastSpecies.speciesId] = {};
}
if (this.originalStarterPreferences[this.lastSpecies.speciesId] === undefined) {
this.originalStarterPreferences[this.lastSpecies.speciesId] = {};
}
// Bangs are safe here due to the above check
const starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]!;
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId]!;
// this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons
if (!this.starterIconsCursorObj.visible) {
@ -2050,10 +2059,6 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const option: OptionSelectItem = {
label: getNatureName(n, true, true, true, globalScene.uiTheme),
handler: () => {
// update default nature in starter save data
if (!starterAttributes) {
starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] = {};
}
starterAttributes.nature = n;
originalStarterAttributes.nature = starterAttributes.nature;
this.clearText();
@ -3408,8 +3413,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
if (species) {
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
// Bang is correct due to the `?` before variant
const variant = this.starterPreferences[species.speciesId]?.variant
? (this.starterPreferences[species.speciesId].variant as Variant)
? (this.starterPreferences[species.speciesId]!.variant as Variant)
: defaultProps.variant;
const tint = getVariantTint(variant);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint);
@ -3634,7 +3640,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
if (starterIndex > -1) {
props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]);
this.setSpeciesDetails(species, {
this.setSpeciesDetails(
species,
{
shiny: props.shiny,
formIndex: props.formIndex,
female: props.female,
@ -3642,7 +3650,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: this.starterAbilityIndexes[starterIndex],
natureIndex: this.starterNatures[starterIndex],
teraType: this.starterTeras[starterIndex],
});
},
false,
);
} else {
const defaultAbilityIndex =
starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
@ -3659,7 +3669,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
props.formIndex = starterAttributes?.form ?? props.formIndex;
props.female = starterAttributes?.female ?? props.female;
this.setSpeciesDetails(species, {
this.setSpeciesDetails(
species,
{
shiny: props.shiny,
formIndex: props.formIndex,
female: props.female,
@ -3667,7 +3679,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: defaultAbilityIndex,
natureIndex: defaultNature,
teraType: starterAttributes?.tera,
});
},
false,
);
}
if (!isNullOrUndefined(props.formIndex)) {
@ -3704,7 +3718,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
this.setSpeciesDetails(species, {
this.setSpeciesDetails(
species,
{
shiny: props.shiny,
formIndex: props.formIndex,
female: props.female,
@ -3712,7 +3728,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: defaultAbilityIndex,
natureIndex: defaultNature,
forSeen: true,
});
},
false,
);
this.pokemonSprite.setTint(0x808080);
}
} else {
@ -3734,7 +3752,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonFormText.setVisible(false);
this.teraIcon.setVisible(false);
this.setSpeciesDetails(species!, {
this.setSpeciesDetails(
species!,
{
// TODO: is this bang correct?
shiny: false,
formIndex: 0,
@ -3742,7 +3762,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
variant: 0,
abilityIndex: 0,
natureIndex: 0,
});
},
false,
);
this.pokemonSprite.clearTint();
}
}
@ -3764,7 +3786,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } };
}
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void {
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, save = true): void {
let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options;
const forSeen: boolean = options.forSeen ?? false;
const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null;
@ -4176,8 +4198,10 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.updateInstructions();
if (save) {
saveStarterPreferences(this.originalStarterPreferences);
}
}
setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void {
if (type1 !== null) {

View File

@ -8,7 +8,7 @@ import { AES, enc } from "crypto-js";
* @param values - The object to be deep copied.
* @returns A new object that is a deep copy of the input.
*/
export function deepCopy(values: object): object {
export function deepCopy<T extends object>(values: T): T {
// Convert the object to a JSON string and parse it back to an object to perform a deep copy
return JSON.parse(JSON.stringify(values));
}
@ -58,13 +58,28 @@ export function decrypt(data: string, bypassLogin: boolean): string {
return AES.decrypt(data, saveKey).toString(enc.Utf8);
}
/**
* Check if an object has no properties of its own (its shape is `{}`). An empty array is considered a bare object.
* @param obj - Object to check
* @returns - Whether the object is bare
*/
export function isBareObject(obj: any): boolean {
if (typeof obj !== "object") {
return false;
}
for (const _ in obj) {
return false;
}
return true;
}
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
// if they ever add private static variables, move this into StarterPrefs
const StarterPrefers_DEFAULT: string = "{}";
let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT;
export interface StarterPreferences {
[key: number]: StarterAttributes;
[key: number]: StarterAttributes | undefined;
}
// called on starter selection show once
@ -74,11 +89,17 @@ export function loadStarterPreferences(): StarterPreferences {
localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT),
);
}
// called on starter selection clear, always
export function saveStarterPreferences(prefs: StarterPreferences): void {
const pStr: string = JSON.stringify(prefs);
// Fastest way to check if an object has any properties (does no allocation)
if (isBareObject(prefs)) {
console.warn("Refusing to save empty starter preferences");
return;
}
// no reason to store `{}` (for starters not customized)
const pStr: string = JSON.stringify(prefs, (_, value) => (isBareObject(value) ? undefined : value));
if (pStr !== StarterPrefers_private_latest) {
console.log("%cSaving starter preferences", "color: blue");
// something changed, store the update
localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr);
// update the latest prefs

View File

@ -175,4 +175,27 @@ describe("Evolution", () => {
expect(fourForm.evoFormKey).toBe("four"); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is "four"
}
});
it("tyrogue should evolve if move is not in first slot", async () => {
game.override
.moveset([MoveId.TACKLE, MoveId.RAPID_SPIN, MoveId.LOW_KICK])
.enemySpecies(SpeciesId.GOLEM)
.enemyMoveset(MoveId.SPLASH)
.startingWave(41)
.startingLevel(19)
.enemyLevel(30);
await game.classicMode.startBattle([SpeciesId.TYROGUE]);
const tyrogue = game.field.getPlayerPokemon();
const golem = game.field.getEnemyPokemon();
golem.hp = 1;
expect(golem.hp).toBe(1);
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to("EndEvolutionPhase");
expect(tyrogue.species.speciesId).toBe(SpeciesId.HITMONTOP);
});
});

39
test/utils/data.test.ts Normal file
View File

@ -0,0 +1,39 @@
import { deepCopy, isBareObject } from "#utils/data";
import { describe, expect, it } from "vitest";
describe("Utils - Data", () => {
describe("deepCopy", () => {
it("should create a deep copy of an object", () => {
const original = { a: 1, b: { c: 2 } };
const copy = deepCopy(original);
// ensure the references are different
expect(copy === original, "copied object should not compare equal").not;
expect(copy).toEqual(original);
// update copy's `a` to a different value and ensure original is unaffected
copy.a = 42;
expect(original.a, "adjusting property of copy should not affect original").toBe(1);
// update copy's nested `b.c` to a different value and ensure original is unaffected
copy.b.c = 99;
expect(original.b.c, "adjusting nested property of copy should not affect original").toBe(2);
});
});
describe("isBareObject", () => {
it("should properly identify bare objects", () => {
expect(isBareObject({}), "{} should be considered bare");
expect(isBareObject(new Object()), "new Object() should be considered bare");
expect(isBareObject(Object.create(null)));
expect(isBareObject([]), "an empty array should be considered bare");
});
it("should properly reject non-objects", () => {
expect(isBareObject(new Date())).not;
expect(isBareObject(null)).not;
expect(isBareObject(42)).not;
expect(isBareObject("")).not;
expect(isBareObject(undefined)).not;
expect(isBareObject(() => {})).not;
expect(isBareObject(new (class A {})())).not;
});
});
});

View File

@ -59,5 +59,12 @@
"outDir": "./build",
"noEmit": true
},
"exclude": ["node_modules", "dist", "vite.config.ts", "vitest.config.ts", "vitest.workspace.ts"]
"exclude": [
"node_modules",
"dist",
"vite.config.ts",
"vitest.config.ts",
"vitest.workspace.ts",
"public/service-worker.js"
]
}