mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-24 07:23:24 +02:00
Merge remote-tracking branch 'upstream/beta' into phase-interceptor
This commit is contained in:
commit
69966719d1
9
.github/workflows/create-release.yml
vendored
9
.github/workflows/create-release.yml
vendored
@ -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 \
|
||||
|
1
.github/workflows/deploy-beta.yml
vendored
1
.github/workflows/deploy-beta.yml
vendored
@ -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
|
||||
|
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@ -11,6 +11,7 @@ on:
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'pagefaultgames/pokerogue'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
1
.github/workflows/github-pages.yml
vendored
1
.github/workflows/github-pages.yml
vendored
@ -20,6 +20,7 @@ jobs:
|
||||
pages:
|
||||
name: Github Pages
|
||||
if: github.repository == 'pagefaultgames/pokerogue'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
api-dir: ./
|
||||
|
1
.github/workflows/linting.yml
vendored
1
.github/workflows/linting.yml
vendored
@ -19,6 +19,7 @@ on:
|
||||
jobs:
|
||||
run-linters:
|
||||
name: Run linters
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
1
.github/workflows/post-release-deleted.yml
vendored
1
.github/workflows/post-release-deleted.yml
vendored
@ -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
|
||||
|
1
.github/workflows/test-shard-template.yml
vendored
1
.github/workflows/test-shard-template.yml
vendored
@ -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:
|
||||
|
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pokemon-rogue-battle",
|
||||
"private": true,
|
||||
"version": "1.11.0",
|
||||
"version": "1.10.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -1,3 +1,7 @@
|
||||
self.addEventListener('install', function () {
|
||||
console.log('Service worker installing...');
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
})
|
@ -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: {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
||||
/*
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
39
test/utils/data.test.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user