diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8e7606f0a48..79ab1bdc38a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,4 +4,19 @@ * @pagefaultgames/junior-dev-team # github actions/templates etc. - Dev Leads -/.github @pagefaultgames/dev-leads +/.github @pagefaultgames/senior-dev-team + +# Art Team +/public/**/*.png @pagefaultgames/art-team +/public/**/*.json @pagefaultgames/art-team +/public/images @pagefaultgames/art-team +/public/battle-anims @pagefaultgames/art-team + +# Audio files +*.mp3 @pagefaultgames/composer-team +*.wav @pagefaultgames/composer-team +*.ogg @pagefaultgames/composer-team +/public/audio @pagefaultgames/composer-team + +# Balance Files; contain actual code logic and must also be owned by dev team +/src/data/balance @pagefaultgames/balance-team @pagefaultgames/junior-dev-team \ No newline at end of file diff --git a/index.html b/index.html index 91367cf73ec..111464b5e5c 100644 --- a/index.html +++ b/index.html @@ -133,7 +133,7 @@ V -
+
V
diff --git a/package-lock.json b/package-lock.json index 739ce18496d..453a525581b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pokemon-rogue-battle", - "version": "1.7.0", + "version": "1.7.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pokemon-rogue-battle", - "version": "1.7.0", + "version": "1.7.6", "hasInstallScript": true, "dependencies": { "@material/material-color-utilities": "^0.2.7", diff --git a/package.json b/package.json index 5a191b3ec99..4c9204f60f9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.7.0", + "version": "1.7.6", "type": "module", "scripts": { "start": "vite", diff --git a/public/images/events/pkmnday2025event-de.png b/public/images/events/pkmnday2025event-de.png new file mode 100644 index 00000000000..4cc53546752 Binary files /dev/null and b/public/images/events/pkmnday2025event-de.png differ diff --git a/public/images/events/pkmnday2025event-en.png b/public/images/events/pkmnday2025event-en.png new file mode 100644 index 00000000000..e9caa9e19d6 Binary files /dev/null and b/public/images/events/pkmnday2025event-en.png differ diff --git a/public/images/events/pkmnday2025event-es-ES.png b/public/images/events/pkmnday2025event-es-ES.png new file mode 100644 index 00000000000..e1ab096dffc Binary files /dev/null and b/public/images/events/pkmnday2025event-es-ES.png differ diff --git a/public/images/events/pkmnday2025event-fr.png b/public/images/events/pkmnday2025event-fr.png new file mode 100644 index 00000000000..037d1e06e61 Binary files /dev/null and b/public/images/events/pkmnday2025event-fr.png differ diff --git a/public/images/events/pkmnday2025event-it.png b/public/images/events/pkmnday2025event-it.png new file mode 100644 index 00000000000..f38a60330fa Binary files /dev/null and b/public/images/events/pkmnday2025event-it.png differ diff --git a/public/images/events/pkmnday2025event-ja.png b/public/images/events/pkmnday2025event-ja.png new file mode 100644 index 00000000000..94b02ad93a0 Binary files /dev/null and b/public/images/events/pkmnday2025event-ja.png differ diff --git a/public/images/events/pkmnday2025event-ko.png b/public/images/events/pkmnday2025event-ko.png new file mode 100644 index 00000000000..aed9ee3fb28 Binary files /dev/null and b/public/images/events/pkmnday2025event-ko.png differ diff --git a/public/images/events/pkmnday2025event-pt-BR.png b/public/images/events/pkmnday2025event-pt-BR.png new file mode 100644 index 00000000000..2190bbac535 Binary files /dev/null and b/public/images/events/pkmnday2025event-pt-BR.png differ diff --git a/public/images/events/pkmnday2025event-zh-CN.png b/public/images/events/pkmnday2025event-zh-CN.png new file mode 100644 index 00000000000..a3430482dd0 Binary files /dev/null and b/public/images/events/pkmnday2025event-zh-CN.png differ diff --git a/public/images/pokemon/656.png b/public/images/pokemon/656.png index 6acfe282dca..06a9cd58268 100644 Binary files a/public/images/pokemon/656.png and b/public/images/pokemon/656.png differ diff --git a/public/images/pokemon/variant/656.json b/public/images/pokemon/variant/656.json index 68743a4c9f1..5037f86f22a 100644 --- a/public/images/pokemon/variant/656.json +++ b/public/images/pokemon/variant/656.json @@ -3,12 +3,12 @@ "838394": "4d7dc5", "62ace6": "8363af", "7bcdff": "9c75c2", - "ffec8c": "ddfff9", + "fdea88": "ddfff9", "a1a1c4": "7ab7ec", "c9b241": "97d6e2", - "dfcf77": "bae7e8", + "ccbd70": "bae7e8", "174592": "37408c", - "fdfdfd": "b1e5ff", + "f8f8f8": "b1e5ff", "9c9cc5": "5385c7", "cdcde6": "7eb7e8", "396a83": "362864", @@ -18,12 +18,12 @@ "838394": "cc6845", "62ace6": "c44848", "7bcdff": "dd6155", - "ffec8c": "ddfff9", + "fdea88": "ddfff9", "a1a1c4": "f7c685", "c9b241": "97d6e2", - "dfcf77": "bae7e8", + "ccbd70": "bae7e8", "174592": "198158", - "fdfdfd": "fff4bd", + "f8f8f8": "fff4bd", "9c9cc5": "c96a48", "cdcde6": "f7b785", "396a83": "5c0d33", diff --git a/public/images/pokemon/variant/back/656.json b/public/images/pokemon/variant/back/656.json index 34b11bfab78..f41398f3154 100644 --- a/public/images/pokemon/variant/back/656.json +++ b/public/images/pokemon/variant/back/656.json @@ -1,17 +1,17 @@ { "1": { - "838394": "4d7dc5", + "848496": "4d7dc5", "7bcdff": "9c75c2", "62ace6": "8363af", "ffffff": "b1e5ff", "396a83": "362864", "9c9cc5": "5385c7", "cdcde6": "7eb7e8", - "174592": "198158", + "174592": "37408c", "5a94cd": "7054a4" }, "2": { - "838394": "cc6845", + "848496": "cc6845", "7bcdff": "dd6155", "62ace6": "c44848", "ffffff": "fff4bd", diff --git a/public/images/ui/cursor_tera.png b/public/images/ui/cursor_tera.png new file mode 100644 index 00000000000..34cbe095895 Binary files /dev/null and b/public/images/ui/cursor_tera.png differ diff --git a/public/images/ui/legacy/cursor_tera.png b/public/images/ui/legacy/cursor_tera.png new file mode 100644 index 00000000000..f2e77046137 Binary files /dev/null and b/public/images/ui/legacy/cursor_tera.png differ diff --git a/public/locales b/public/locales index 5e7fc5ef196..0e5c6096ba2 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 5e7fc5ef1968652f2335b17c354db62d8cbec314 +Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8205c1fcebc..996c3b0de87 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1865,6 +1865,58 @@ export default class BattleScene extends SceneBase { this.getCurrentPhase()?.constructor.name ?? "", ); + if ( // Give trainers with specialty types an appropriately-typed form for Wormadam, Rotom, Arceus, Oricorio, Silvally, or Paldean Tauros. + !isEggPhase && + this.currentBattle?.battleType === BattleType.TRAINER && !isNullOrUndefined(this.currentBattle.trainer) && + this.currentBattle.trainer.config.hasSpecialtyType() + ) { + if (species.speciesId === Species.WORMADAM) { + switch (this.currentBattle.trainer.config.specialtyType) { + case Type.GROUND: + return 1; // Sandy Cloak + case Type.STEEL: + return 2; // Trash Cloak + case Type.GRASS: + return 0; // Plant Cloak + } + } else if (species.speciesId === Species.ROTOM) { + switch (this.currentBattle.trainer.config.specialtyType) { + case Type.FLYING: + return 4; // Fan Rotom + case Type.GHOST: + return 0; // Lightbulb Rotom + case Type.FIRE: + return 1; // Heat Rotom + case Type.GRASS: + return 5; // Mow Rotom + case Type.WATER: + return 2; // Wash Rotom + case Type.ICE: + return 3; // Frost Rotom + } + } else if (species.speciesId === Species.ORICORIO) { + switch (this.currentBattle.trainer.config.specialtyType) { + case Type.GHOST: + return 3; // Sensu Style + case Type.FIRE: + return 0; // Baile Style + case Type.ELECTRIC: + return 1; // Pom-Pom Style + case Type.PSYCHIC: + return 2; // Pa'u Style + } + } else if (species.speciesId === Species.PALDEA_TAUROS) { + switch (this.currentBattle.trainer.config.specialtyType) { + case Type.FIRE: + return 1; // Blaze Breed + case Type.WATER: + return 2; // Aqua Breed + } + } else if (species.speciesId === Species.SILVALLY || species.speciesId === Species.ARCEUS) { // Would probably never happen, but might as well + return this.currentBattle.trainer.config.specialtyType; + } + } + switch (species.speciesId) { case Species.UNOWN: case Species.SHELLOS: @@ -1872,8 +1924,6 @@ export default class BattleScene extends SceneBase { case Species.BASCULIN: case Species.DEERLING: case Species.SAWSBUCK: - case Species.FROAKIE: - case Species.FROGADIER: case Species.SCATTERBUG: case Species.SPEWPA: case Species.VIVILLON: @@ -1907,9 +1957,14 @@ export default class BattleScene extends SceneBase { return 0; // No Partner Eevee for Wave 12 Preschoolers } return Utils.randSeedInt(2); + case Species.FROAKIE: + case Species.FROGADIER: case Species.GRENINJA: - if (this.currentBattle?.battleType === BattleType.TRAINER) { - return 0; // Don't give trainers Battle Bond Greninja + if ( + this.currentBattle?.battleType === BattleType.TRAINER && + !isEggPhase + ) { + return 0; // Don't give trainers Battle Bond Greninja, Froakie or Frogadier } return Utils.randSeedInt(2); case Species.URSHIFU: diff --git a/src/data/ability.ts b/src/data/ability.ts index 8c4b2ba380a..37b97ffb5e6 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5054,7 +5054,7 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; const isCommandFight = turnCommand?.command === Command.FIGHT; const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; - if (this.condition(pokemon, move!) && isCommandFight) { + if (isCommandFight && this.condition(pokemon, move!)) { bypassSpeed.value = false; canCheckHeldItems.value = false; return false; @@ -6186,7 +6186,8 @@ export function initAbilities() { .attr(ProtectStatAbAttr, Stat.ATK) .ignorable(), new Ability(Abilities.PICKUP, 3) - .attr(PostBattleLootAbAttr), + .attr(PostBattleLootAbAttr) + .attr(UnsuppressableAbilityAbAttr), new Ability(Abilities.TRUANT, 3) .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false), new Ability(Abilities.HUSTLE, 3) @@ -6378,7 +6379,8 @@ export function initAbilities() { .attr(PostSummonWeatherChangeAbAttr, WeatherType.SNOW) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SNOW), new Ability(Abilities.HONEY_GATHER, 4) - .attr(MoneyAbAttr), + .attr(MoneyAbAttr) + .attr(UnsuppressableAbilityAbAttr), new Ability(Abilities.FRISK, 4) .attr(FriskAbAttr), new Ability(Abilities.RECKLESS, 4) diff --git a/src/data/balance/starters.ts b/src/data/balance/starters.ts index 10263f895b3..3468163c988 100644 --- a/src/data/balance/starters.ts +++ b/src/data/balance/starters.ts @@ -461,7 +461,7 @@ export const speciesStarterCosts = { [Species.GUZZLORD]: 6, [Species.NECROZMA]: 8, [Species.MAGEARNA]: 7, - [Species.MARSHADOW]: 7, + [Species.MARSHADOW]: 8, [Species.POIPOLE]: 8, [Species.STAKATAKA]: 6, [Species.BLACEPHALON]: 7, diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index a179f3a3e9b..a42779563f2 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,7 +1,20 @@ import { globalScene } from "#app/global-scene"; -import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; +import { + AttackMove, + BeakBlastHeaderAttr, + DelayedAttackAttr, + MoveFlags, + SelfStatusMove, + allMoves, +} from "./move"; import type Pokemon from "../field/pokemon"; -import * as Utils from "../utils"; +import { + type nil, + getFrameMs, + getEnumKeys, + getEnumValues, + animationFileName, +} from "../utils"; import type { BattlerIndex } from "../battle"; import type { Element } from "json-stable-stringify"; import { Moves } from "#enums/moves"; @@ -401,7 +414,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent { if (Object.keys(tweenProps).length) { globalScene.tweens.add(Object.assign({ targets: moveAnim.bgSprite, - duration: Utils.getFrameMs(this.duration * 3) + duration: getFrameMs(this.duration * 3) }, tweenProps)); } return this.duration * 2; @@ -437,7 +450,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { globalScene.tweens.add({ targets: moveAnim.bgSprite, - duration: Utils.getFrameMs(this.duration * 3) + duration: getFrameMs(this.duration * 3) }); return this.duration * 2; @@ -455,8 +468,8 @@ export const encounterAnims = new Map(); export function initCommonAnims(): Promise { return new Promise(resolve => { - const commonAnimNames = Utils.getEnumKeys(CommonAnim); - const commonAnimIds = Utils.getEnumValues(CommonAnim); + const commonAnimNames = getEnumKeys(CommonAnim); + const commonAnimIds = getEnumValues(CommonAnim); const commonAnimFetches: Promise>[] = []; for (let ca = 0; ca < commonAnimIds.length; ca++) { const commonAnimId = commonAnimIds[ca]; @@ -493,7 +506,7 @@ export function initMoveAnim(move: Moves): Promise { const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; const fetchAnimAndResolve = (move: Moves) => { - globalScene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`) + globalScene.cachedFetch(`./battle-anims/${animationFileName(move)}.json`) .then(response => { const contentType = response.headers.get("content-type"); if (!response.ok || contentType?.indexOf("application/json") === -1) { @@ -550,7 +563,7 @@ function useDefaultAnim(move: Moves, defaultMoveAnim: Moves) { * @remarks use {@linkcode useDefaultAnim} to use a default animation */ function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { - const moveName = Utils.animationFileName(move); + const moveName = animationFileName(move); console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams); } @@ -560,7 +573,7 @@ function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { */ export async function initEncounterAnims(encounterAnim: EncounterAnim | EncounterAnim[]): Promise { const anims = Array.isArray(encounterAnim) ? encounterAnim : [ encounterAnim ]; - const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); + const encounterAnimNames = getEnumKeys(EncounterAnim); const encounterAnimFetches: Promise>[] = []; for (const anim of anims) { if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { @@ -922,7 +935,7 @@ export abstract class BattleAnim { let f = 0; globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(3), + duration: getFrameMs(3), repeat: anim?.frames.length ?? 0, onRepeat: () => { if (!f) { @@ -994,47 +1007,39 @@ export abstract class BattleAnim { const moveSprite = sprites[graphicIndex]; if (spritePriorities[graphicIndex] !== frame.priority) { spritePriorities[graphicIndex] = frame.priority; + /** Move the position that the moveSprite is rendered in based on the priority. + * @param priority The priority level to draw the sprite. + * - 0: Draw the sprite in front of the pokemon on the field. + * - 1: Draw the sprite in front of the user pokemon. + * - 2: Draw the sprite in front of its `bgSprite` (if it has one), or its + * `AnimFocus` (if that is user/target), otherwise behind everything. + * - 3: Draw the sprite behind its `AnimFocus` (if that is user/target), otherwise in front of everything. + */ const setSpritePriority = (priority: number) => { - switch (priority) { - case 0: - globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false)!); // TODO: is this bang correct? - break; - case 1: - globalScene.field.moveTo(moveSprite, globalScene.field.getAll().length - 1); - break; - case 2: - switch (frame.focus) { - case AnimFocus.USER: - if (this.bgSprite) { - globalScene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.bgSprite); - } else { - globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.user!); // TODO: is this bang correct? - } - break; - case AnimFocus.TARGET: - globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.target!); // TODO: is this bang correct? - break; - default: - setSpritePriority(1); - break; - } - break; - case 3: - switch (frame.focus) { - case AnimFocus.USER: - globalScene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.user!); // TODO: is this bang correct? - break; - case AnimFocus.TARGET: - globalScene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.target!); // TODO: is this bang correct? - break; - default: - setSpritePriority(1); - break; - } - break; - default: - setSpritePriority(1); + /** The sprite we are moving the moveSprite in relation to */ + let targetSprite: Phaser.GameObjects.GameObject | nil; + /** The method that is being used to move the sprite.*/ + let moveFunc: ((sprite: Phaser.GameObjects.GameObject, target: Phaser.GameObjects.GameObject) => void) | + ((sprite: Phaser.GameObjects.GameObject) => void) = globalScene.field.bringToTop; + + if (priority === 0) { // Place the sprite in front of the pokemon on the field. + targetSprite = globalScene.getEnemyField().find(p => p) ?? globalScene.getPlayerField().find(p => p); + console.log(typeof targetSprite); + moveFunc = globalScene.field.moveBelow; + } else if (priority === 2 && this.bgSprite) { + moveFunc = globalScene.field.moveAbove; + targetSprite = this.bgSprite; + } else if (priority === 2 || priority === 3) { + moveFunc = priority === 2 ? globalScene.field.moveBelow : globalScene.field.moveAbove; + if (frame.focus === AnimFocus.USER) { + targetSprite = this.user; + } else if (frame.focus === AnimFocus.TARGET) { + targetSprite = this.target; + } } + // If target sprite is not undefined and exists in the field container, then move the sprite using the moveFunc. + // Otherwise, default to just bringing it to the top. + targetSprite && globalScene.field.exists(targetSprite) ? moveFunc.bind(globalScene.field)(moveSprite as Phaser.GameObjects.GameObject, targetSprite) : globalScene.field.bringToTop(moveSprite as Phaser.GameObjects.GameObject); }; setSpritePriority(frame.priority); } @@ -1052,11 +1057,13 @@ export abstract class BattleAnim { } } if (anim?.frameTimedEvents.has(f)) { - for (const event of anim.frameTimedEvents.get(f)!) { // TODO: is this bang correct? - r = Math.max((anim.frames.length - f) + event.execute(this), r); + const base = anim.frames.length - f; + // Bang is correct due to `has` check above, which cannot return true for an undefined / null `f` + for (const event of anim.frameTimedEvents.get(f)!) { + r = Math.max(base + event.execute(this), r); } } - const targets = Utils.getEnumValues(AnimFrameTarget); + const targets = getEnumValues(AnimFrameTarget); for (const i of targets) { const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t; if (count < spriteCache[i].length) { @@ -1084,7 +1091,7 @@ export abstract class BattleAnim { } if (r) { globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(r), + duration: getFrameMs(r), onComplete: () => cleanUpAndComplete() }); } else { @@ -1166,7 +1173,7 @@ export abstract class BattleAnim { let existingFieldSprites = globalScene.field.getAll().slice(0); globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(3) * frameTimeMult, + duration: getFrameMs(3) * frameTimeMult, repeat: anim!.frames.length, onRepeat: () => { existingFieldSprites = globalScene.field.getAll().slice(0); @@ -1215,11 +1222,12 @@ export abstract class BattleAnim { } } if (anim?.frameTimedEvents.get(frameCount)) { + const base = anim.frames.length - frameCount; for (const event of anim.frameTimedEvents.get(frameCount)!) { - totalFrames = Math.max((anim.frames.length - frameCount) + event.execute(this, frameTimedEventPriority), totalFrames); + totalFrames = Math.max(base + event.execute(this, frameTimedEventPriority), totalFrames); } } - const targets = Utils.getEnumValues(AnimFrameTarget); + const targets = getEnumValues(AnimFrameTarget); for (const i of targets) { const count = graphicFrameCount; if (count < spriteCache[i].length) { @@ -1244,7 +1252,7 @@ export abstract class BattleAnim { } if (totalFrames) { globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(totalFrames), + duration: getFrameMs(totalFrames), onComplete: () => cleanUpAndComplete() }); } else { @@ -1342,15 +1350,15 @@ export class EncounterBattleAnim extends BattleAnim { } export async function populateAnims() { - const commonAnimNames = Utils.getEnumKeys(CommonAnim).map(k => k.toLowerCase()); + const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase()); const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/\_/g, "")); - const commonAnimIds = Utils.getEnumValues(CommonAnim) as CommonAnim[]; - const chargeAnimNames = Utils.getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); + const commonAnimIds = getEnumValues(CommonAnim) as CommonAnim[]; + const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/\_/g, " ")); - const chargeAnimIds = Utils.getEnumValues(ChargeAnim) as ChargeAnim[]; + const chargeAnimIds = getEnumValues(ChargeAnim) as ChargeAnim[]; const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/; const moveNameToId = {}; - for (const move of Utils.getEnumValues(Moves).slice(1)) { + for (const move of getEnumValues(Moves).slice(1)) { const moveName = Moves[move].toUpperCase().replace(/\_/g, ""); moveNameToId[moveName] = move; } diff --git a/src/data/move.ts b/src/data/move.ts index 18f4b220911..677ad9f0ebc 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5236,7 +5236,7 @@ export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr { return false; } - const combinedPledgeMove = user.turnData.combiningPledge; + const combinedPledgeMove = user?.turnData?.combiningPledge; if (!combinedPledgeMove) { return false; } @@ -9384,7 +9384,7 @@ export function initMoves() { .attr(BypassBurnDamageReductionAttr), new AttackMove(Moves.FOCUS_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3) .attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) - .attr(PreUseInterruptAttr, i18next.t("moveTriggers:lostFocus"), user => !!user.turnData.attacksReceived.find(r => r.damage)) + .attr(PreUseInterruptAttr, (user, target, move) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => !!user.turnData.attacksReceived.find(r => r.damage)) .punchingMove(), new AttackMove(Moves.SMELLING_SALTS, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1) diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index b7d99ad7ef3..21b04c182e6 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -607,7 +607,7 @@ export class TrainerConfig { const shedinjaCanTera = !this.hasSpecialtyType() || this.specialtyType === Type.BUG; // Better to check one time than 6 const partyMemberIndexes = new Array(party.length).fill(null).map((_, i) => i) .filter(i => shedinjaCanTera || party[i].species.speciesId !== Species.SHEDINJA); // Shedinja can only Tera on Bug specialty type (or no specialty type) - const setPartySlot = !Utils.isNullOrUndefined(slot) ? Phaser.Math.Wrap(slot, 0, party.length - 1) : -1; // If we have a tera slot defined, wrap it to party size. + const setPartySlot = !Utils.isNullOrUndefined(slot) ? Phaser.Math.Wrap(slot, 0, party.length) : -1; // If we have a tera slot defined, wrap it to party size. for (let t = 0; t < Math.min(count(), party.length); t++) { const randomIndex = partyMemberIndexes.indexOf(setPartySlot) > -1 ? setPartySlot : Utils.randSeedItem(partyMemberIndexes); partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1); @@ -2776,11 +2776,6 @@ export const trainerConfigs: TrainerConfigs = { p.generateName(); p.pokeball = PokeballType.ULTRA_BALL; })) - .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.ZAMAZENTA ], TrainerSlot.TRAINER, true, p => { - p.setBoss(true, 2); - p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; - })) .setInstantTera(0), // Tera Fairy Sylveon [TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer(true) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLAYDOL ], TrainerSlot.TRAINER, true, p => { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f0486a8f111..53d4b6c54d2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1226,10 +1226,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Checks if the {@linkcode Pokemon} has is the specified {@linkcode Species} or is fused with it. * @param species the pokemon {@linkcode Species} to check + * @param formKey If provided, requires the species to be in that form * @returns `true` if the pokemon is the species or is fused with it, `false` otherwise */ - hasSpecies(species: Species): boolean { - return this.species.speciesId === species || this.fusionSpecies?.speciesId === species; + hasSpecies(species: Species, formKey?: string): boolean { + if (Utils.isNullOrUndefined(formKey)) { + return this.species.speciesId === species || this.fusionSpecies?.speciesId === species; + } + + return (this.species.speciesId === species && this.getFormKey() === formKey) || (this.fusionSpecies?.speciesId === species && this.getFusionFormKey() === formKey); } abstract isBoss(): boolean; @@ -3204,6 +3209,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return maxForms.includes(this.getFormKey()) || (!!this.getFusionFormKey() && maxForms.includes(this.getFusionFormKey()!)); } + isMega(): boolean { + const megaForms = [ SpeciesFormKey.MEGA, SpeciesFormKey.MEGA_X, SpeciesFormKey.MEGA_Y, SpeciesFormKey.PRIMAL ] as string[]; + return megaForms.includes(this.getFormKey()) || (!!this.getFusionFormKey() && megaForms.includes(this.getFusionFormKey()!)); + } + canAddTag(tagType: BattlerTagType): boolean { if (this.getTag(tagType)) { return false; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index d382caf6cb6..392761cf8e4 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -564,6 +564,15 @@ export class InputsController { if (!this.configs[selectedDevice]) { this.configs[selectedDevice] = {}; } + // A proper way of handling migrating keybinds would be much better + const mappingOverrides = { + "BUTTON_CYCLE_VARIANT": "BUTTON_CYCLE_TERA", + }; + for (const key in mappingConfigs.custom) { + if (mappingConfigs.custom[key] in mappingOverrides) { + mappingConfigs.custom[key] = mappingOverrides[mappingConfigs.custom[key]]; + } + } this.configs[selectedDevice].custom = mappingConfigs.custom; } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index fc685fc2332..183b38c49e5 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -101,6 +101,7 @@ export class LoadingScene extends SceneBase { this.loadImage("icon_lock", "ui", "icon_lock.png"); this.loadImage("icon_stop", "ui", "icon_stop.png"); this.loadImage("icon_tera", "ui"); + this.loadImage("cursor_tera", "ui"); this.loadImage("type_tera", "ui"); this.loadAtlas("type_bgs", "ui"); this.loadAtlas("button_tera", "ui"); @@ -250,9 +251,9 @@ export class LoadingScene extends SceneBase { } const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ]; if (lang && availableLangs.includes(lang)) { - this.loadImage("valentines2025event-" + lang, "events"); + this.loadImage("pkmnday2025event-" + lang, "events"); } else { - this.loadImage("valentines2025event-en", "events"); + this.loadImage("pkmnday2025event-en", "events"); } this.loadAtlas("statuses", ""); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 112a3c9aa35..ae8b9a45c0d 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -2554,7 +2554,7 @@ export function getPartyLuckValue(party: Pokemon[]): number { return DailyLuck.value; } const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies(); - const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 3 : 0) : 0) + const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0) .reduce((total: number, value: number) => total += value, 0), 0, 14); return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 57e25325ba4..aefc583a98a 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1645,11 +1645,19 @@ export class GameData { } else if (formIndex === 3) { dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(1); } - } - const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : []; - const toCurrentFormChanges = allFormChanges.filter(f => (f.formKey === formKey)); - if (toCurrentFormChanges.length > 0) { - dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(0); + } else if (pokemon.species.speciesId === Species.ZYGARDE) { + if (formIndex === 4) { + dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(2); + } else if (formIndex === 5) { + dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(3); + } + } else { + const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : []; + const toCurrentFormChanges = allFormChanges.filter(f => (f.formKey === formKey)); + if (toCurrentFormChanges.length > 0) { + // Needs to do this or Castform can unlock the wrong form, etc. + dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(0); + } } } diff --git a/src/system/version_migration/versions/v1_7_0.ts b/src/system/version_migration/versions/v1_7_0.ts index 2acb9d8151a..bdb9e6aab9f 100644 --- a/src/system/version_migration/versions/v1_7_0.ts +++ b/src/system/version_migration/versions/v1_7_0.ts @@ -1,8 +1,29 @@ -import { getPokemonSpeciesForm } from "#app/data/pokemon-species"; -import type { SessionSaveData } from "#app/system/game-data"; +import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; +import { globalScene } from "#app/global-scene"; +import { DexAttr, type SessionSaveData, type SystemSaveData } from "#app/system/game-data"; import * as Utils from "#app/utils"; -export const systemMigrators = [] as const; +export const systemMigrators = [ + /** + * If a starter is caught, but the only forms registered as caught are not starterSelectable, + * unlock the default form. + * @param data {@linkcode SystemSaveData} + */ + function migrateUnselectableForms(data: SystemSaveData) { + if (data.starterData && data.dexData) { + Object.keys(data.starterData).forEach(sd => { + const caughtAttr = data.dexData[sd]?.caughtAttr; + const species = getPokemonSpecies(Number(sd)); + if (caughtAttr && species.forms?.length > 1) { + const selectableForms = species.forms.filter((form, formIndex) => form.isStarterSelectable && (caughtAttr & globalScene.gameData.getFormAttr(formIndex))); + if (selectableForms.length === 0) { + data.dexData[sd].caughtAttr += DexAttr.DEFAULT_FORM; + } + } + }); + } + }, +] as const; export const settingsMigrators = [] as const; diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index bebacf87ebc..c12f9d569c0 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -169,7 +169,7 @@ const timedEvents: TimedEvent[] = [ { species: Species.WOOBAT }, { species: Species.FRILLISH }, { species: Species.ALOMOMOLA }, - { species: Species.FURFROU, formIndex: 1 }, // Heart trim + { species: Species.FURFROU, formIndex: 1 }, // Heart Trim { species: Species.ESPURR }, { species: Species.SPRITZEE }, { species: Species.SWIRLIX }, @@ -180,6 +180,33 @@ const timedEvents: TimedEvent[] = [ { species: Species.ENAMORUS } ], luckBoostedSpecies: [ Species.LUVDISC ] + }, + { + name: "PKMNDAY2025", + eventType: EventType.LUCK, + startDate: new Date(Date.UTC(2025, 1, 27)), + endDate: new Date(Date.UTC(2025, 2, 4)), + classicFriendshipMultiplier: 4, + bannerKey: "pkmnday2025event-", + scale: 0.21, + availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ], + eventEncounters: [ + { species: Species.PIKACHU, formIndex: 1, blockEvolution: true }, // Partner Form + { species: Species.EEVEE, formIndex: 1, blockEvolution: true }, // Partner Form + { species: Species.CHIKORITA }, + { species: Species.TOTODILE }, + { species: Species.TEPIG } + ], + luckBoostedSpecies: [ + Species.PICHU, Species.PIKACHU, Species.RAICHU, Species.ALOLA_RAICHU, + Species.PSYDUCK, Species.GOLDUCK, + Species.EEVEE, Species.FLAREON, Species.JOLTEON, Species.VAPOREON, Species.ESPEON, Species.UMBREON, Species.LEAFEON, Species.GLACEON, Species.SYLVEON, + Species.CHIKORITA, Species.BAYLEEF, Species.MEGANIUM, + Species.TOTODILE, Species.CROCONAW, Species.FERALIGATR, + Species.TEPIG, Species.PIGNITE, Species.EMBOAR, + Species.ZYGARDE, + Species.ETERNAL_FLOETTE + ] } ]; diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 07e43a344dd..a462ed158cb 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -147,7 +147,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { itemIcon.setScale(3 * this.scale); this.optionSelectIcons.push(itemIcon); - this.optionSelectContainer.add(itemIcon); + this.optionSelectTextContainer.add(itemIcon); itemIcon.setPositionRelative(this.optionSelectText, 36 * this.scale, 7 + i * (114 * this.scale - 3)); @@ -156,7 +156,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { itemOverlayIcon.setScale(3 * this.scale); this.optionSelectIcons.push(itemOverlayIcon); - this.optionSelectContainer.add(itemOverlayIcon); + this.optionSelectTextContainer.add(itemOverlayIcon); itemOverlayIcon.setPositionRelative(this.optionSelectText, 36 * this.scale, 7 + i * (114 * this.scale - 3)); diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index f23cc78c9f7..20cffbbe30a 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -10,6 +10,7 @@ import { globalScene } from "#app/global-scene"; import { TerastallizeAccessModifier } from "#app/modifier/modifier"; import { Type } from "#app/enums/type"; import { getTypeRgb } from "#app/data/type"; +import { Species } from "#enums/species"; export enum Command { FIGHT = 0, @@ -180,9 +181,11 @@ export default class CommandUiHandler extends UiHandler { canTera(): boolean { const hasTeraMod = !!globalScene.getModifiers(TerastallizeAccessModifier).length; + const activePokemon = globalScene.getField()[this.fieldIndex]; + const isBlockedForm = activePokemon.isMega() || activePokemon.isMax() || activePokemon.hasSpecies(Species.NECROZMA, "ultra"); const currentTeras = globalScene.arena.playerTerasUsed; const plannedTera = globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0 ? 1 : 0; - return hasTeraMod && (currentTeras + plannedTera) < 1; + return hasTeraMod && !isBlockedForm && (currentTeras + plannedTera) < 1; } toggleTeraButton() { diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 1c1dceb24a5..8e8b197117c 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -226,7 +226,9 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { } if (!this.cursorObj) { - this.cursorObj = globalScene.add.image(0, 0, "cursor"); + const isTera = this.fromCommand === Command.TERA; + this.cursorObj = globalScene.add.image(0, 0, isTera ? "cursor_tera" : "cursor"); + this.cursorObj.setScale(isTera ? 0.7 : 1); ui.add(this.cursorObj); } diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 136f098df7e..0af94053ceb 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -422,7 +422,10 @@ export default class PartyUiHandler extends MessageUiHandler { if (option === PartyOption.TRANSFER) { if (this.transferCursor !== this.cursor) { if (this.transferAll) { - getTransferrableItemsFromPokemon(globalScene.getPlayerParty()[this.transferCursor]).forEach((_, i) => (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, i, this.transferQuantitiesMax[i], this.cursor)); + getTransferrableItemsFromPokemon(globalScene.getPlayerParty()[this.transferCursor]).forEach((_, i, array) => { + const invertedIndex = array.length - 1 - i; + (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, invertedIndex, this.transferQuantitiesMax[invertedIndex], this.cursor); + }); } else { (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, this.transferOptionCursor, this.transferQuantities[this.transferOptionCursor], this.cursor); } @@ -1187,7 +1190,6 @@ class PartySlot extends Phaser.GameObjects.Container { public slotHpText: Phaser.GameObjects.Text; public slotDescriptionLabel: Phaser.GameObjects.Text; // this is used to show text instead of the HP bar i.e. for showing "Able"/"Not Able" for TMs when you try to learn them - private pokemonIcon: Phaser.GameObjects.Container; private iconAnimHandler: PokemonIconAnimHandler; @@ -1208,6 +1210,10 @@ class PartySlot extends Phaser.GameObjects.Container { } setup(partyUiMode: PartyUiMode, tmMoveId: Moves) { + + const currentLanguage = i18next.resolvedLanguage ?? "en"; + const offsetJa = currentLanguage === "ja"; + const battlerCount = globalScene.currentBattle.getBattlerCount(); const slotKey = `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`; @@ -1246,15 +1252,15 @@ class PartySlot extends Phaser.GameObjects.Container { nameSizeTest.destroy(); this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY); - this.slotName.setPositionRelative(slotBg, this.slotIndex >= battlerCount ? 21 : 24, this.slotIndex >= battlerCount ? 2 : 10); + this.slotName.setPositionRelative(slotBg, this.slotIndex >= battlerCount ? 21 : 24, (this.slotIndex >= battlerCount ? 2 : 10) + (offsetJa ? 2 : 0)); this.slotName.setOrigin(0, 0); const slotLevelLabel = globalScene.add.image(0, 0, "party_slot_overlay_lv"); - slotLevelLabel.setPositionRelative(this.slotName, 8, 12); + slotLevelLabel.setPositionRelative(slotBg, (this.slotIndex >= battlerCount ? 21 : 24) + 8, (this.slotIndex >= battlerCount ? 2 : 10) + 12); slotLevelLabel.setOrigin(0, 0); const slotLevelText = addTextObject(0, 0, this.pokemon.level.toString(), this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED); - slotLevelText.setPositionRelative(slotLevelLabel, 9, 0); + slotLevelText.setPositionRelative(slotLevelLabel, 9, offsetJa ? 1.5 : 0); slotLevelText.setOrigin(0, 0.25); slotInfoContainer.add([ this.slotName, slotLevelLabel, slotLevelText ]); @@ -1331,7 +1337,7 @@ class PartySlot extends Phaser.GameObjects.Container { this.slotHpOverlay.setVisible(false); this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY); - this.slotHpText.setPositionRelative(this.slotHpBar, this.slotHpBar.width - 3, this.slotHpBar.height - 2); + this.slotHpText.setPositionRelative(this.slotHpBar, this.slotHpBar.width - 3, this.slotHpBar.height - 2 + (offsetJa ? 2 : 0)); this.slotHpText.setOrigin(1, 0); this.slotHpText.setVisible(false); diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 99b25c3c383..eee900d411e 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -250,6 +250,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler { private availableVariants: number; private unlockedVariants: boolean[]; + private canUseCandies: boolean; + constructor() { super(Mode.POKEDEX_PAGE); } @@ -556,6 +558,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { show(args: any[]): boolean { + // Allow the use of candies if we are in one of the whitelisted phases + this.canUseCandies = [ "TitlePhase", "SelectStarterPhase", "CommandPhase" ].includes(globalScene.getCurrentPhase()?.constructor.name ?? ""); + if (args.length >= 1 && args[0] === "refresh") { return false; } else { @@ -597,6 +602,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.battleForms = []; const species = this.species; + + let formKey = this.species?.forms.length > 0 ? this.species.forms[this.formIndex].formKey : ""; + this.isFormGender = formKey === "male" || formKey === "female"; + if (this.isFormGender && ((this.savedStarterAttributes.female === true && formKey === "male") || (this.savedStarterAttributes.female === false && formKey === "female"))) { + this.formIndex = (this.formIndex + 1) % 2; + formKey = this.species.forms[this.formIndex].formKey; + } + const formIndex = this.formIndex ?? 0; this.starterId = this.getStarterSpeciesId(this.species.speciesId); @@ -630,12 +643,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.eggMoves = speciesEggMoves[this.starterId] ?? []; this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.starterId].eggMoves & (1 << em)) !== 0); - const formKey = this.species?.forms.length > 0 ? this.species.forms[this.formIndex].formKey : ""; this.tmMoves = speciesTmMoves[species.speciesId]?.filter(m => Array.isArray(m) ? (m[0] === formKey ? true : false ) : true) .map(m => Array.isArray(m) ? m[1] : m).sort((a, b) => allMoves[a].name > allMoves[b].name ? 1 : -1) ?? []; - this.isFormGender = formKey === "male" || formKey === "female"; - const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId) ? species.speciesId : starterPassiveAbilities.hasOwnProperty(this.starterId) ? this.starterId : pokemonPrevolutions[this.starterId]; const passives = starterPassiveAbilities[passiveId]; @@ -779,6 +789,10 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const formIndex = otherFormIndex !== undefined ? otherFormIndex : this.formIndex; const caughtAttr = this.isCaught(species); + if (caughtAttr && (!species.forms.length || species.forms.length === 1)) { + return true; + } + const isFormCaught = (caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n; return isFormCaught; } @@ -1570,15 +1584,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler { starterAttributes.variant = newVariant; // store the selected variant this.savedStarterAttributes.variant = starterAttributes.variant; - if (newVariant > props.variant) { - this.setSpeciesDetails(this.species, { variant: newVariant as Variant }); - success = true; - } else { + if ((this.isCaught() & DexAttr.NON_SHINY) && (newVariant <= props.variant)) { this.setSpeciesDetails(this.species, { shiny: false, variant: 0 }); success = true; - starterAttributes.shiny = false; this.savedStarterAttributes.shiny = starterAttributes.shiny; + } else { + this.setSpeciesDetails(this.species, { variant: newVariant as Variant }); + success = true; } } } @@ -1626,7 +1639,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } break; case Button.STATS: - if (!isCaught || !isFormCaught) { + if (!isCaught || !isFormCaught || !this.canUseCandies) { error = true; } else { const ui = this.getUi(); @@ -1888,7 +1901,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (this.isCaught()) { if (isFormCaught) { - this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel); + if (this.canUseCandies) { + this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel); + } if (this.canCycleShiny) { this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel); } @@ -2189,7 +2204,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); const isShinyCaught = !!(caughtAttr & DexAttr.SHINY); - this.canCycleShiny = isNonShinyCaught && isShinyCaught; + const caughtVariants = [ DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3 ].filter(v => caughtAttr & v); + this.canCycleShiny = (isNonShinyCaught && isShinyCaught) || (isShinyCaught && caughtVariants.length > 1); const isMaleCaught = !!(caughtAttr & DexAttr.MALE); const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE); diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index d336ae4012b..67d491317dc 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -994,7 +994,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.updateScroll(); const proportion = this.filterBarCursor / Math.max(1, this.filterBar.numFilters - 1); const targetCol = Math.min(8, proportion < 0.5 ? Math.floor(proportion * 8) : Math.ceil(proportion * 8)); - this.setCursor(Math.min(targetCol, numberOfStarters)); + this.setCursor(Math.min(targetCol, numberOfStarters - 1)); success = true; } break; @@ -1116,7 +1116,7 @@ export default class PokedexUiHandler extends MessageUiHandler { } break; case Button.DOWN: - if (currentRow < numOfRows - 1) { // not last row + if ((currentRow < numOfRows - 1) && (this.cursor + 9 < this.filteredPokemonData.length)) { // not last row if (currentRow - this.scrollCursor === 8) { // last row of visible pokemon this.scrollCursor++; this.updateScroll(); @@ -1585,6 +1585,37 @@ export default class PokedexUiHandler extends MessageUiHandler { container.icon.setTint(0); } + if (data.eggMove1) { + container.eggMove1Icon.setVisible(true); + } else { + container.eggMove1Icon.setVisible(false); + } + if (data.eggMove2) { + container.eggMove2Icon.setVisible(true); + } else { + container.eggMove2Icon.setVisible(false); + } + if (data.tmMove1) { + container.tmMove1Icon.setVisible(true); + } else { + container.tmMove1Icon.setVisible(false); + } + if (data.tmMove2) { + container.tmMove2Icon.setVisible(true); + } else { + container.tmMove2Icon.setVisible(false); + } + if (data.passive1) { + container.passive1Icon.setVisible(true); + } else { + container.passive1Icon.setVisible(false); + } + if (data.passive2) { + container.passive2Icon.setVisible(true); + } else { + container.passive2Icon.setVisible(false); + } + if (this.showDecorations) { if (this.pokerusSpecies.includes(data.species)) { diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index bf07374e21a..f1fe9ac8194 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -889,7 +889,7 @@ export default class RunInfoUiHandler extends UiHandler { /** * Takes input from the user to perform a desired action. * @param button - Button object to be processed - * Button.CANCEL - removes all containers related to RunInfo and returns the user to Run History + * Button.CANCEL, Button.LEFT - removes all containers related to RunInfo and returns the user to Run History * Button.CYCLE_FORM, Button.CYCLE_SHINY, Button.CYCLE_ABILITY - runs the function buttonCycleOption() */ override processInput(button: Button): boolean { @@ -900,6 +900,7 @@ export default class RunInfoUiHandler extends UiHandler { switch (button) { case Button.CANCEL: + case Button.LEFT: success = true; if (this.pageMode === RunInfoUiMode.MAIN) { this.runInfoContainer.removeAll(true); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 229dcf7fded..6a83c0526de 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -8,6 +8,7 @@ import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { starterColors } from "#app/battle-scene"; import { globalScene } from "#app/global-scene"; +import type { Ability } from "#app/data/ability"; import { allAbilities } from "#app/data/ability"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { GrowthRate, getGrowthRateColor } from "#app/data/exp"; @@ -2068,20 +2069,20 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } while (newVariant !== props.variant); starterAttributes.variant = newVariant; // store the selected variant - // If going to a higher variant, display that - if (newVariant > props.variant) { + if ((this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY) && (newVariant <= props.variant)) { + // If we have run out of variants, go back to non shiny + this.setSpeciesDetails(this.lastSpecies, { shiny: false, variant: 0 }); + this.pokemonShinyIcon.setVisible(false); + success = true; + starterAttributes.shiny = false; + } else { + // If going to a higher variant, or only shiny forms are caught, go to next variant this.setSpeciesDetails(this.lastSpecies, { variant: newVariant as Variant }); // Cycle tint based on current sprite tint const tint = getVariantTint(newVariant as Variant); this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)); this.pokemonShinyIcon.setTint(tint); success = true; - // If we have run out of variants, go back to non shiny - } else { - this.setSpeciesDetails(this.lastSpecies, { shiny: false, variant: 0 }); - this.pokemonShinyIcon.setVisible(false); - success = true; - starterAttributes.shiny = false; } } } @@ -3328,7 +3329,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); const isShinyCaught = !!(caughtAttr & DexAttr.SHINY); - this.canCycleShiny = isNonShinyCaught && isShinyCaught; + const caughtVariants = [ DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3 ].filter(v => caughtAttr & v); + this.canCycleShiny = (isNonShinyCaught && isShinyCaught) || (isShinyCaught && caughtVariants.length > 1); const isMaleCaught = !!(caughtAttr & DexAttr.MALE); const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE); @@ -3352,7 +3354,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.canCycleForm = species.forms.filter(f => f.isStarterSelectable || !pokemonFormChanges[species.speciesId]?.find(fc => fc.formKey)) .map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)).filter(f => f).length > 1; this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1; - this.canCycleTera = globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && !Utils.isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2); + this.canCycleTera = !this.statsMode && globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && !Utils.isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2); } if (dexEntry.caughtAttr && species.malePercent !== null) { @@ -3365,7 +3367,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } if (dexEntry.caughtAttr) { - const ability = allAbilities[this.lastSpecies.getAbility(abilityIndex!)]; // TODO: is this bang correct? + let ability: Ability; + if (this.lastSpecies.forms?.length > 1) { + ability = allAbilities[this.lastSpecies.forms[formIndex ?? 0].getAbility(abilityIndex!)]; + } else { + ability = allAbilities[this.lastSpecies.getAbility(abilityIndex!)]; // TODO: is this bang correct? + } this.pokemonAbilityText.setText(ability.name); const isHidden = abilityIndex === (this.lastSpecies.ability2 ? 2 : 1); @@ -3852,12 +3859,20 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.showStats(); this.statsMode = true; this.pokemonSprite.setVisible(false); + this.teraIcon.setVisible(false); + this.canCycleTera = false; + this.updateInstructions(); } else { this.statsMode = false; this.statsContainer.setVisible(false); this.pokemonSprite.setVisible(!!this.speciesStarterDexEntry?.caughtAttr); //@ts-ignore this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. !?!? + this.teraIcon.setVisible(globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id)); + const props = globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.getCurrentDexProps(this.lastSpecies.speciesId)); + const formIndex = props.formIndex; + this.canCycleTera = !this.statsMode && globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && !Utils.isNullOrUndefined(getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2); + this.updateInstructions(); } } diff --git a/test/moves/focus_punch.test.ts b/test/moves/focus_punch.test.ts index 9bf858dfda5..1f14a19fbd7 100644 --- a/test/moves/focus_punch.test.ts +++ b/test/moves/focus_punch.test.ts @@ -140,6 +140,6 @@ describe("Moves - Focus Punch", () => { await game.phaseInterceptor.to("MessagePhase", false); const consoleSpy = vi.spyOn(console, "log"); await game.phaseInterceptor.to("MoveEndPhase", true); - expect(consoleSpy).nthCalledWith(1, i18next.t("moveTriggers:lostFocus")); + expect(consoleSpy).nthCalledWith(1, i18next.t("moveTriggers:lostFocus", { pokemonName: "Charizard" })); }); });