diff --git a/package.json b/package.json index a63a1ef2e5b..db07b9be8db 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.10.3", + "version": "1.10.4", "type": "module", "scripts": { "start": "vite", diff --git a/public/service-worker.js b/public/service-worker.js index b45d2484709..ff380adca73 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,3 +1,7 @@ self.addEventListener('install', function () { console.log('Service worker installing...'); }); + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}) \ No newline at end of file diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index bf90ebb7edc..bf588784f24 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -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: { diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 790bdf0dbef..240a0df9e95 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -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(); } diff --git a/src/enums/pokemon-type.ts b/src/enums/pokemon-type.ts index eca02bae275..210e3c3dcbe 100644 --- a/src/enums/pokemon-type.ts +++ b/src/enums/pokemon-type.ts @@ -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; \ No newline at end of file diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index cc7e27caae4..dab96e4090a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3243,6 +3243,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) { diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 8fc7a763c8f..59211a9eb03 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -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"); /* diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index a29a65aca80..7396608bfc4 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1828,9 +1828,9 @@ 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]; + let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] ?? {}; // The original starter preferences - const originalStarterAttributes = this.originalStarterPreferences[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) { @@ -3408,8 +3408,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); diff --git a/src/utils/data.ts b/src/utils/data.ts index 6580ecf2ee9..337aac1110b 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -64,7 +64,7 @@ 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,10 +74,27 @@ export function loadStarterPreferences(): StarterPreferences { localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT), ); } -// called on starter selection clear, always + +/** + * Check if an object has no properties of its own (its shape is `{}`) + * @param obj - Object to check + * @returns - Whether the object is bare + */ +export function isBareObject(obj: object): boolean { + for (const _ in obj) { + return false; + } + return true; +} 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) { // something changed, store the update localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr); diff --git a/test/evolution.test.ts b/test/evolution.test.ts index 3fb763e9190..7079404bdec 100644 --- a/test/evolution.test.ts +++ b/test/evolution.test.ts @@ -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); + }); }); diff --git a/tsconfig.json b/tsconfig.json index dcbf7456df8..8becb4c00ec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" + ] }