Merge branch 'beta' into making-containers-bearable

This commit is contained in:
Wlowscha 2025-03-04 18:33:21 +01:00 committed by GitHub
commit 0f647bfd66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 368 additions and 143 deletions

17
.github/CODEOWNERS vendored
View File

@ -4,4 +4,19 @@
* @pagefaultgames/junior-dev-team * @pagefaultgames/junior-dev-team
# github actions/templates etc. - Dev Leads # 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

View File

@ -133,7 +133,7 @@
<span class="apad-label">V</span> <span class="apad-label">V</span>
</div> </div>
<!-- buttons to display battle-specific information --> <!-- buttons to display battle-specific information -->
<div id="apadInfo" class="apad-button apad-rectangle apad-small" data-key="V"> <div id="apadInfo" class="apad-button apad-rectangle apad-small" data-key="CYCLE_TERA">
<span class="apad-label">V</span> <span class="apad-label">V</span>
</div> </div>
<div id="apadStats" class="apad-button apad-rectangle apad-small" data-key="STATS"> <div id="apadStats" class="apad-button apad-rectangle apad-small" data-key="STATS">

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.7.0", "version": "1.7.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.7.0", "version": "1.7.6",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.2.7", "@material/material-color-utilities": "^0.2.7",

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 528 B

View File

@ -3,12 +3,12 @@
"838394": "4d7dc5", "838394": "4d7dc5",
"62ace6": "8363af", "62ace6": "8363af",
"7bcdff": "9c75c2", "7bcdff": "9c75c2",
"ffec8c": "ddfff9", "fdea88": "ddfff9",
"a1a1c4": "7ab7ec", "a1a1c4": "7ab7ec",
"c9b241": "97d6e2", "c9b241": "97d6e2",
"dfcf77": "bae7e8", "ccbd70": "bae7e8",
"174592": "37408c", "174592": "37408c",
"fdfdfd": "b1e5ff", "f8f8f8": "b1e5ff",
"9c9cc5": "5385c7", "9c9cc5": "5385c7",
"cdcde6": "7eb7e8", "cdcde6": "7eb7e8",
"396a83": "362864", "396a83": "362864",
@ -18,12 +18,12 @@
"838394": "cc6845", "838394": "cc6845",
"62ace6": "c44848", "62ace6": "c44848",
"7bcdff": "dd6155", "7bcdff": "dd6155",
"ffec8c": "ddfff9", "fdea88": "ddfff9",
"a1a1c4": "f7c685", "a1a1c4": "f7c685",
"c9b241": "97d6e2", "c9b241": "97d6e2",
"dfcf77": "bae7e8", "ccbd70": "bae7e8",
"174592": "198158", "174592": "198158",
"fdfdfd": "fff4bd", "f8f8f8": "fff4bd",
"9c9cc5": "c96a48", "9c9cc5": "c96a48",
"cdcde6": "f7b785", "cdcde6": "f7b785",
"396a83": "5c0d33", "396a83": "5c0d33",

View File

@ -1,17 +1,17 @@
{ {
"1": { "1": {
"838394": "4d7dc5", "848496": "4d7dc5",
"7bcdff": "9c75c2", "7bcdff": "9c75c2",
"62ace6": "8363af", "62ace6": "8363af",
"ffffff": "b1e5ff", "ffffff": "b1e5ff",
"396a83": "362864", "396a83": "362864",
"9c9cc5": "5385c7", "9c9cc5": "5385c7",
"cdcde6": "7eb7e8", "cdcde6": "7eb7e8",
"174592": "198158", "174592": "37408c",
"5a94cd": "7054a4" "5a94cd": "7054a4"
}, },
"2": { "2": {
"838394": "cc6845", "848496": "cc6845",
"7bcdff": "dd6155", "7bcdff": "dd6155",
"62ace6": "c44848", "62ace6": "c44848",
"ffffff": "fff4bd", "ffffff": "fff4bd",

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

@ -1 +1 @@
Subproject commit 5e7fc5ef1968652f2335b17c354db62d8cbec314 Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b

View File

@ -1865,6 +1865,58 @@ export default class BattleScene extends SceneBase {
this.getCurrentPhase()?.constructor.name ?? "", 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) { switch (species.speciesId) {
case Species.UNOWN: case Species.UNOWN:
case Species.SHELLOS: case Species.SHELLOS:
@ -1872,8 +1924,6 @@ export default class BattleScene extends SceneBase {
case Species.BASCULIN: case Species.BASCULIN:
case Species.DEERLING: case Species.DEERLING:
case Species.SAWSBUCK: case Species.SAWSBUCK:
case Species.FROAKIE:
case Species.FROGADIER:
case Species.SCATTERBUG: case Species.SCATTERBUG:
case Species.SPEWPA: case Species.SPEWPA:
case Species.VIVILLON: case Species.VIVILLON:
@ -1907,9 +1957,14 @@ export default class BattleScene extends SceneBase {
return 0; // No Partner Eevee for Wave 12 Preschoolers return 0; // No Partner Eevee for Wave 12 Preschoolers
} }
return Utils.randSeedInt(2); return Utils.randSeedInt(2);
case Species.FROAKIE:
case Species.FROGADIER:
case Species.GRENINJA: case Species.GRENINJA:
if (this.currentBattle?.battleType === BattleType.TRAINER) { if (
return 0; // Don't give trainers Battle Bond Greninja this.currentBattle?.battleType === BattleType.TRAINER &&
!isEggPhase
) {
return 0; // Don't give trainers Battle Bond Greninja, Froakie or Frogadier
} }
return Utils.randSeedInt(2); return Utils.randSeedInt(2);
case Species.URSHIFU: case Species.URSHIFU:

View File

@ -5054,7 +5054,7 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
const isCommandFight = turnCommand?.command === Command.FIGHT; const isCommandFight = turnCommand?.command === Command.FIGHT;
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; 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; bypassSpeed.value = false;
canCheckHeldItems.value = false; canCheckHeldItems.value = false;
return false; return false;
@ -6186,7 +6186,8 @@ export function initAbilities() {
.attr(ProtectStatAbAttr, Stat.ATK) .attr(ProtectStatAbAttr, Stat.ATK)
.ignorable(), .ignorable(),
new Ability(Abilities.PICKUP, 3) new Ability(Abilities.PICKUP, 3)
.attr(PostBattleLootAbAttr), .attr(PostBattleLootAbAttr)
.attr(UnsuppressableAbilityAbAttr),
new Ability(Abilities.TRUANT, 3) new Ability(Abilities.TRUANT, 3)
.attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false), .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false),
new Ability(Abilities.HUSTLE, 3) new Ability(Abilities.HUSTLE, 3)
@ -6378,7 +6379,8 @@ export function initAbilities() {
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SNOW) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SNOW)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SNOW), .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SNOW),
new Ability(Abilities.HONEY_GATHER, 4) new Ability(Abilities.HONEY_GATHER, 4)
.attr(MoneyAbAttr), .attr(MoneyAbAttr)
.attr(UnsuppressableAbilityAbAttr),
new Ability(Abilities.FRISK, 4) new Ability(Abilities.FRISK, 4)
.attr(FriskAbAttr), .attr(FriskAbAttr),
new Ability(Abilities.RECKLESS, 4) new Ability(Abilities.RECKLESS, 4)

View File

@ -461,7 +461,7 @@ export const speciesStarterCosts = {
[Species.GUZZLORD]: 6, [Species.GUZZLORD]: 6,
[Species.NECROZMA]: 8, [Species.NECROZMA]: 8,
[Species.MAGEARNA]: 7, [Species.MAGEARNA]: 7,
[Species.MARSHADOW]: 7, [Species.MARSHADOW]: 8,
[Species.POIPOLE]: 8, [Species.POIPOLE]: 8,
[Species.STAKATAKA]: 6, [Species.STAKATAKA]: 6,
[Species.BLACEPHALON]: 7, [Species.BLACEPHALON]: 7,

View File

@ -1,7 +1,20 @@
import { globalScene } from "#app/global-scene"; 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 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 { BattlerIndex } from "../battle";
import type { Element } from "json-stable-stringify"; import type { Element } from "json-stable-stringify";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -401,7 +414,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent {
if (Object.keys(tweenProps).length) { if (Object.keys(tweenProps).length) {
globalScene.tweens.add(Object.assign({ globalScene.tweens.add(Object.assign({
targets: moveAnim.bgSprite, targets: moveAnim.bgSprite,
duration: Utils.getFrameMs(this.duration * 3) duration: getFrameMs(this.duration * 3)
}, tweenProps)); }, tweenProps));
} }
return this.duration * 2; return this.duration * 2;
@ -437,7 +450,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent {
globalScene.tweens.add({ globalScene.tweens.add({
targets: moveAnim.bgSprite, targets: moveAnim.bgSprite,
duration: Utils.getFrameMs(this.duration * 3) duration: getFrameMs(this.duration * 3)
}); });
return this.duration * 2; return this.duration * 2;
@ -455,8 +468,8 @@ export const encounterAnims = new Map<EncounterAnim, AnimConfig>();
export function initCommonAnims(): Promise<void> { export function initCommonAnims(): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const commonAnimNames = Utils.getEnumKeys(CommonAnim); const commonAnimNames = getEnumKeys(CommonAnim);
const commonAnimIds = Utils.getEnumValues(CommonAnim); const commonAnimIds = getEnumValues(CommonAnim);
const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = []; const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
for (let ca = 0; ca < commonAnimIds.length; ca++) { for (let ca = 0; ca < commonAnimIds.length; ca++) {
const commonAnimId = commonAnimIds[ca]; const commonAnimId = commonAnimIds[ca];
@ -493,7 +506,7 @@ export function initMoveAnim(move: Moves): Promise<void> {
const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP;
const fetchAnimAndResolve = (move: Moves) => { const fetchAnimAndResolve = (move: Moves) => {
globalScene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`) globalScene.cachedFetch(`./battle-anims/${animationFileName(move)}.json`)
.then(response => { .then(response => {
const contentType = response.headers.get("content-type"); const contentType = response.headers.get("content-type");
if (!response.ok || contentType?.indexOf("application/json") === -1) { 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 * @remarks use {@linkcode useDefaultAnim} to use a default animation
*/ */
function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { 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); 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<void> { export async function initEncounterAnims(encounterAnim: EncounterAnim | EncounterAnim[]): Promise<void> {
const anims = Array.isArray(encounterAnim) ? encounterAnim : [ encounterAnim ]; const anims = Array.isArray(encounterAnim) ? encounterAnim : [ encounterAnim ];
const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); const encounterAnimNames = getEnumKeys(EncounterAnim);
const encounterAnimFetches: Promise<Map<EncounterAnim, AnimConfig>>[] = []; const encounterAnimFetches: Promise<Map<EncounterAnim, AnimConfig>>[] = [];
for (const anim of anims) { for (const anim of anims) {
if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) {
@ -922,7 +935,7 @@ export abstract class BattleAnim {
let f = 0; let f = 0;
globalScene.tweens.addCounter({ globalScene.tweens.addCounter({
duration: Utils.getFrameMs(3), duration: getFrameMs(3),
repeat: anim?.frames.length ?? 0, repeat: anim?.frames.length ?? 0,
onRepeat: () => { onRepeat: () => {
if (!f) { if (!f) {
@ -994,47 +1007,39 @@ export abstract class BattleAnim {
const moveSprite = sprites[graphicIndex]; const moveSprite = sprites[graphicIndex];
if (spritePriorities[graphicIndex] !== frame.priority) { if (spritePriorities[graphicIndex] !== frame.priority) {
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) => { const setSpritePriority = (priority: number) => {
switch (priority) { /** The sprite we are moving the moveSprite in relation to */
case 0: let targetSprite: Phaser.GameObjects.GameObject | nil;
globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false)!); // TODO: is this bang correct? /** The method that is being used to move the sprite.*/
break; let moveFunc: ((sprite: Phaser.GameObjects.GameObject, target: Phaser.GameObjects.GameObject) => void) |
case 1: ((sprite: Phaser.GameObjects.GameObject) => void) = globalScene.field.bringToTop;
globalScene.field.moveTo(moveSprite, globalScene.field.getAll().length - 1);
break; if (priority === 0) { // Place the sprite in front of the pokemon on the field.
case 2: targetSprite = globalScene.getEnemyField().find(p => p) ?? globalScene.getPlayerField().find(p => p);
switch (frame.focus) { console.log(typeof targetSprite);
case AnimFocus.USER: moveFunc = globalScene.field.moveBelow;
if (this.bgSprite) { } else if (priority === 2 && this.bgSprite) {
globalScene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.bgSprite); moveFunc = globalScene.field.moveAbove;
} else { targetSprite = this.bgSprite;
globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.user!); // TODO: is this bang correct? } else if (priority === 2 || priority === 3) {
} moveFunc = priority === 2 ? globalScene.field.moveBelow : globalScene.field.moveAbove;
break; if (frame.focus === AnimFocus.USER) {
case AnimFocus.TARGET: targetSprite = this.user;
globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.target!); // TODO: is this bang correct? } else if (frame.focus === AnimFocus.TARGET) {
break; targetSprite = this.target;
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);
} }
// 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); setSpritePriority(frame.priority);
} }
@ -1052,11 +1057,13 @@ export abstract class BattleAnim {
} }
} }
if (anim?.frameTimedEvents.has(f)) { if (anim?.frameTimedEvents.has(f)) {
for (const event of anim.frameTimedEvents.get(f)!) { // TODO: is this bang correct? const base = anim.frames.length - f;
r = Math.max((anim.frames.length - f) + event.execute(this), r); // 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) { for (const i of targets) {
const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t; const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t;
if (count < spriteCache[i].length) { if (count < spriteCache[i].length) {
@ -1084,7 +1091,7 @@ export abstract class BattleAnim {
} }
if (r) { if (r) {
globalScene.tweens.addCounter({ globalScene.tweens.addCounter({
duration: Utils.getFrameMs(r), duration: getFrameMs(r),
onComplete: () => cleanUpAndComplete() onComplete: () => cleanUpAndComplete()
}); });
} else { } else {
@ -1166,7 +1173,7 @@ export abstract class BattleAnim {
let existingFieldSprites = globalScene.field.getAll().slice(0); let existingFieldSprites = globalScene.field.getAll().slice(0);
globalScene.tweens.addCounter({ globalScene.tweens.addCounter({
duration: Utils.getFrameMs(3) * frameTimeMult, duration: getFrameMs(3) * frameTimeMult,
repeat: anim!.frames.length, repeat: anim!.frames.length,
onRepeat: () => { onRepeat: () => {
existingFieldSprites = globalScene.field.getAll().slice(0); existingFieldSprites = globalScene.field.getAll().slice(0);
@ -1215,11 +1222,12 @@ export abstract class BattleAnim {
} }
} }
if (anim?.frameTimedEvents.get(frameCount)) { if (anim?.frameTimedEvents.get(frameCount)) {
const base = anim.frames.length - frameCount;
for (const event of anim.frameTimedEvents.get(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) { for (const i of targets) {
const count = graphicFrameCount; const count = graphicFrameCount;
if (count < spriteCache[i].length) { if (count < spriteCache[i].length) {
@ -1244,7 +1252,7 @@ export abstract class BattleAnim {
} }
if (totalFrames) { if (totalFrames) {
globalScene.tweens.addCounter({ globalScene.tweens.addCounter({
duration: Utils.getFrameMs(totalFrames), duration: getFrameMs(totalFrames),
onComplete: () => cleanUpAndComplete() onComplete: () => cleanUpAndComplete()
}); });
} else { } else {
@ -1342,15 +1350,15 @@ export class EncounterBattleAnim extends BattleAnim {
} }
export async function populateAnims() { 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 commonAnimMatchNames = commonAnimNames.map(k => k.replace(/\_/g, ""));
const commonAnimIds = Utils.getEnumValues(CommonAnim) as CommonAnim[]; const commonAnimIds = getEnumValues(CommonAnim) as CommonAnim[];
const chargeAnimNames = Utils.getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase());
const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/\_/g, " ")); 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 commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
const moveNameToId = {}; 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, ""); const moveName = Moves[move].toUpperCase().replace(/\_/g, "");
moveNameToId[moveName] = move; moveNameToId[moveName] = move;
} }

View File

@ -5236,7 +5236,7 @@ export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr {
return false; return false;
} }
const combinedPledgeMove = user.turnData.combiningPledge; const combinedPledgeMove = user?.turnData?.combiningPledge;
if (!combinedPledgeMove) { if (!combinedPledgeMove) {
return false; return false;
} }
@ -9384,7 +9384,7 @@ export function initMoves() {
.attr(BypassBurnDamageReductionAttr), .attr(BypassBurnDamageReductionAttr),
new AttackMove(Moves.FOCUS_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3) 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(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(), .punchingMove(),
new AttackMove(Moves.SMELLING_SALTS, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3) 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) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)

View File

@ -607,7 +607,7 @@ export class TrainerConfig {
const shedinjaCanTera = !this.hasSpecialtyType() || this.specialtyType === Type.BUG; // Better to check one time than 6 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) 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) .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++) { for (let t = 0; t < Math.min(count(), party.length); t++) {
const randomIndex = partyMemberIndexes.indexOf(setPartySlot) > -1 ? setPartySlot : Utils.randSeedItem(partyMemberIndexes); const randomIndex = partyMemberIndexes.indexOf(setPartySlot) > -1 ? setPartySlot : Utils.randSeedItem(partyMemberIndexes);
partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1); partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1);
@ -2776,11 +2776,6 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName(); p.generateName();
p.pokeball = PokeballType.ULTRA_BALL; 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 .setInstantTera(0), // Tera Fairy Sylveon
[TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer(true) [TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer(true)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLAYDOL ], TrainerSlot.TRAINER, true, p => { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLAYDOL ], TrainerSlot.TRAINER, true, p => {

View File

@ -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. * 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 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 * @returns `true` if the pokemon is the species or is fused with it, `false` otherwise
*/ */
hasSpecies(species: Species): boolean { hasSpecies(species: Species, formKey?: string): boolean {
return this.species.speciesId === species || this.fusionSpecies?.speciesId === species; 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; 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()!)); 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 { canAddTag(tagType: BattlerTagType): boolean {
if (this.getTag(tagType)) { if (this.getTag(tagType)) {
return false; return false;

View File

@ -564,6 +564,15 @@ export class InputsController {
if (!this.configs[selectedDevice]) { if (!this.configs[selectedDevice]) {
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; this.configs[selectedDevice].custom = mappingConfigs.custom;
} }

View File

@ -101,6 +101,7 @@ export class LoadingScene extends SceneBase {
this.loadImage("icon_lock", "ui", "icon_lock.png"); this.loadImage("icon_lock", "ui", "icon_lock.png");
this.loadImage("icon_stop", "ui", "icon_stop.png"); this.loadImage("icon_stop", "ui", "icon_stop.png");
this.loadImage("icon_tera", "ui"); this.loadImage("icon_tera", "ui");
this.loadImage("cursor_tera", "ui");
this.loadImage("type_tera", "ui"); this.loadImage("type_tera", "ui");
this.loadAtlas("type_bgs", "ui"); this.loadAtlas("type_bgs", "ui");
this.loadAtlas("button_tera", "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" ]; const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ];
if (lang && availableLangs.includes(lang)) { if (lang && availableLangs.includes(lang)) {
this.loadImage("valentines2025event-" + lang, "events"); this.loadImage("pkmnday2025event-" + lang, "events");
} else { } else {
this.loadImage("valentines2025event-en", "events"); this.loadImage("pkmnday2025event-en", "events");
} }
this.loadAtlas("statuses", ""); this.loadAtlas("statuses", "");

View File

@ -2554,7 +2554,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
return DailyLuck.value; return DailyLuck.value;
} }
const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies(); 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); .reduce((total: number, value: number) => total += value, 0), 0, 14);
return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14); return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14);
} }

View File

@ -1645,11 +1645,19 @@ export class GameData {
} else if (formIndex === 3) { } else if (formIndex === 3) {
dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(1); dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(1);
} }
} } else if (pokemon.species.speciesId === Species.ZYGARDE) {
const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : []; if (formIndex === 4) {
const toCurrentFormChanges = allFormChanges.filter(f => (f.formKey === formKey)); dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(2);
if (toCurrentFormChanges.length > 0) { } else if (formIndex === 5) {
dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(0); 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);
}
} }
} }

View File

@ -1,8 +1,29 @@
import { getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import type { SessionSaveData } from "#app/system/game-data"; import { globalScene } from "#app/global-scene";
import { DexAttr, type SessionSaveData, type SystemSaveData } from "#app/system/game-data";
import * as Utils from "#app/utils"; 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; export const settingsMigrators = [] as const;

View File

@ -169,7 +169,7 @@ const timedEvents: TimedEvent[] = [
{ species: Species.WOOBAT }, { species: Species.WOOBAT },
{ species: Species.FRILLISH }, { species: Species.FRILLISH },
{ species: Species.ALOMOMOLA }, { species: Species.ALOMOMOLA },
{ species: Species.FURFROU, formIndex: 1 }, // Heart trim { species: Species.FURFROU, formIndex: 1 }, // Heart Trim
{ species: Species.ESPURR }, { species: Species.ESPURR },
{ species: Species.SPRITZEE }, { species: Species.SPRITZEE },
{ species: Species.SWIRLIX }, { species: Species.SWIRLIX },
@ -180,6 +180,33 @@ const timedEvents: TimedEvent[] = [
{ species: Species.ENAMORUS } { species: Species.ENAMORUS }
], ],
luckBoostedSpecies: [ Species.LUVDISC ] 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
]
} }
]; ];

View File

@ -147,7 +147,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
itemIcon.setScale(3 * this.scale); itemIcon.setScale(3 * this.scale);
this.optionSelectIcons.push(itemIcon); 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)); 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); itemOverlayIcon.setScale(3 * this.scale);
this.optionSelectIcons.push(itemOverlayIcon); 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)); itemOverlayIcon.setPositionRelative(this.optionSelectText, 36 * this.scale, 7 + i * (114 * this.scale - 3));

View File

@ -10,6 +10,7 @@ import { globalScene } from "#app/global-scene";
import { TerastallizeAccessModifier } from "#app/modifier/modifier"; import { TerastallizeAccessModifier } from "#app/modifier/modifier";
import { Type } from "#app/enums/type"; import { Type } from "#app/enums/type";
import { getTypeRgb } from "#app/data/type"; import { getTypeRgb } from "#app/data/type";
import { Species } from "#enums/species";
export enum Command { export enum Command {
FIGHT = 0, FIGHT = 0,
@ -180,9 +181,11 @@ export default class CommandUiHandler extends UiHandler {
canTera(): boolean { canTera(): boolean {
const hasTeraMod = !!globalScene.getModifiers(TerastallizeAccessModifier).length; 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 currentTeras = globalScene.arena.playerTerasUsed;
const plannedTera = globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0 ? 1 : 0; 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() { toggleTeraButton() {

View File

@ -226,7 +226,9 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
} }
if (!this.cursorObj) { 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); ui.add(this.cursorObj);
} }

View File

@ -422,7 +422,10 @@ export default class PartyUiHandler extends MessageUiHandler {
if (option === PartyOption.TRANSFER) { if (option === PartyOption.TRANSFER) {
if (this.transferCursor !== this.cursor) { if (this.transferCursor !== this.cursor) {
if (this.transferAll) { 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 { } else {
(this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, this.transferOptionCursor, this.transferQuantities[this.transferOptionCursor], this.cursor); (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 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 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 pokemonIcon: Phaser.GameObjects.Container;
private iconAnimHandler: PokemonIconAnimHandler; private iconAnimHandler: PokemonIconAnimHandler;
@ -1208,6 +1210,10 @@ class PartySlot extends Phaser.GameObjects.Container {
} }
setup(partyUiMode: PartyUiMode, tmMoveId: Moves) { setup(partyUiMode: PartyUiMode, tmMoveId: Moves) {
const currentLanguage = i18next.resolvedLanguage ?? "en";
const offsetJa = currentLanguage === "ja";
const battlerCount = globalScene.currentBattle.getBattlerCount(); const battlerCount = globalScene.currentBattle.getBattlerCount();
const slotKey = `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`; const slotKey = `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`;
@ -1246,15 +1252,15 @@ class PartySlot extends Phaser.GameObjects.Container {
nameSizeTest.destroy(); nameSizeTest.destroy();
this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY); 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); this.slotName.setOrigin(0, 0);
const slotLevelLabel = globalScene.add.image(0, 0, "party_slot_overlay_lv"); 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); slotLevelLabel.setOrigin(0, 0);
const slotLevelText = addTextObject(0, 0, this.pokemon.level.toString(), this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED); 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); slotLevelText.setOrigin(0, 0.25);
slotInfoContainer.add([ this.slotName, slotLevelLabel, slotLevelText ]); slotInfoContainer.add([ this.slotName, slotLevelLabel, slotLevelText ]);
@ -1331,7 +1337,7 @@ class PartySlot extends Phaser.GameObjects.Container {
this.slotHpOverlay.setVisible(false); this.slotHpOverlay.setVisible(false);
this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY); 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.setOrigin(1, 0);
this.slotHpText.setVisible(false); this.slotHpText.setVisible(false);

View File

@ -250,6 +250,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private availableVariants: number; private availableVariants: number;
private unlockedVariants: boolean[]; private unlockedVariants: boolean[];
private canUseCandies: boolean;
constructor() { constructor() {
super(Mode.POKEDEX_PAGE); super(Mode.POKEDEX_PAGE);
} }
@ -556,6 +558,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
show(args: any[]): boolean { 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") { if (args.length >= 1 && args[0] === "refresh") {
return false; return false;
} else { } else {
@ -597,6 +602,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.battleForms = []; this.battleForms = [];
const species = this.species; 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; const formIndex = this.formIndex ?? 0;
this.starterId = this.getStarterSpeciesId(this.species.speciesId); this.starterId = this.getStarterSpeciesId(this.species.speciesId);
@ -630,12 +643,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.eggMoves = speciesEggMoves[this.starterId] ?? []; this.eggMoves = speciesEggMoves[this.starterId] ?? [];
this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.starterId].eggMoves & (1 << em)) !== 0); 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) 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) ?? []; .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 : const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId) ? species.speciesId :
starterPassiveAbilities.hasOwnProperty(this.starterId) ? this.starterId : pokemonPrevolutions[this.starterId]; starterPassiveAbilities.hasOwnProperty(this.starterId) ? this.starterId : pokemonPrevolutions[this.starterId];
const passives = starterPassiveAbilities[passiveId]; const passives = starterPassiveAbilities[passiveId];
@ -779,6 +789,10 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
const formIndex = otherFormIndex !== undefined ? otherFormIndex : this.formIndex; const formIndex = otherFormIndex !== undefined ? otherFormIndex : this.formIndex;
const caughtAttr = this.isCaught(species); 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; const isFormCaught = (caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n;
return isFormCaught; return isFormCaught;
} }
@ -1570,15 +1584,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
starterAttributes.variant = newVariant; // store the selected variant starterAttributes.variant = newVariant; // store the selected variant
this.savedStarterAttributes.variant = starterAttributes.variant; this.savedStarterAttributes.variant = starterAttributes.variant;
if (newVariant > props.variant) { if ((this.isCaught() & DexAttr.NON_SHINY) && (newVariant <= props.variant)) {
this.setSpeciesDetails(this.species, { variant: newVariant as Variant });
success = true;
} else {
this.setSpeciesDetails(this.species, { shiny: false, variant: 0 }); this.setSpeciesDetails(this.species, { shiny: false, variant: 0 });
success = true; success = true;
starterAttributes.shiny = false; starterAttributes.shiny = false;
this.savedStarterAttributes.shiny = starterAttributes.shiny; 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; break;
case Button.STATS: case Button.STATS:
if (!isCaught || !isFormCaught) { if (!isCaught || !isFormCaught || !this.canUseCandies) {
error = true; error = true;
} else { } else {
const ui = this.getUi(); const ui = this.getUi();
@ -1888,7 +1901,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
if (this.isCaught()) { if (this.isCaught()) {
if (isFormCaught) { 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) { if (this.canCycleShiny) {
this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel); 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 isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY);
const isShinyCaught = !!(caughtAttr & DexAttr.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 isMaleCaught = !!(caughtAttr & DexAttr.MALE);
const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE); const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE);

View File

@ -994,7 +994,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.updateScroll(); this.updateScroll();
const proportion = this.filterBarCursor / Math.max(1, this.filterBar.numFilters - 1); 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)); 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; success = true;
} }
break; break;
@ -1116,7 +1116,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
} }
break; break;
case Button.DOWN: 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 if (currentRow - this.scrollCursor === 8) { // last row of visible pokemon
this.scrollCursor++; this.scrollCursor++;
this.updateScroll(); this.updateScroll();
@ -1585,6 +1585,37 @@ export default class PokedexUiHandler extends MessageUiHandler {
container.icon.setTint(0); 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.showDecorations) {
if (this.pokerusSpecies.includes(data.species)) { if (this.pokerusSpecies.includes(data.species)) {

View File

@ -889,7 +889,7 @@ export default class RunInfoUiHandler extends UiHandler {
/** /**
* Takes input from the user to perform a desired action. * Takes input from the user to perform a desired action.
* @param button - Button object to be processed * @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() * Button.CYCLE_FORM, Button.CYCLE_SHINY, Button.CYCLE_ABILITY - runs the function buttonCycleOption()
*/ */
override processInput(button: Button): boolean { override processInput(button: Button): boolean {
@ -900,6 +900,7 @@ export default class RunInfoUiHandler extends UiHandler {
switch (button) { switch (button) {
case Button.CANCEL: case Button.CANCEL:
case Button.LEFT:
success = true; success = true;
if (this.pageMode === RunInfoUiMode.MAIN) { if (this.pageMode === RunInfoUiMode.MAIN) {
this.runInfoContainer.removeAll(true); this.runInfoContainer.removeAll(true);

View File

@ -8,6 +8,7 @@ import i18next from "i18next";
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
import { starterColors } from "#app/battle-scene"; import { starterColors } from "#app/battle-scene";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { Ability } from "#app/data/ability";
import { allAbilities } from "#app/data/ability"; import { allAbilities } from "#app/data/ability";
import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { speciesEggMoves } from "#app/data/balance/egg-moves";
import { GrowthRate, getGrowthRateColor } from "#app/data/exp"; import { GrowthRate, getGrowthRateColor } from "#app/data/exp";
@ -2068,20 +2069,20 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
} while (newVariant !== props.variant); } while (newVariant !== props.variant);
starterAttributes.variant = newVariant; // store the selected variant starterAttributes.variant = newVariant; // store the selected variant
// If going to a higher variant, display that if ((this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY) && (newVariant <= props.variant)) {
if (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 }); this.setSpeciesDetails(this.lastSpecies, { variant: newVariant as Variant });
// Cycle tint based on current sprite tint // Cycle tint based on current sprite tint
const tint = getVariantTint(newVariant as Variant); const tint = getVariantTint(newVariant as Variant);
this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)); this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant));
this.pokemonShinyIcon.setTint(tint); this.pokemonShinyIcon.setTint(tint);
success = true; 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 isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY);
const isShinyCaught = !!(caughtAttr & DexAttr.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 isMaleCaught = !!(caughtAttr & DexAttr.MALE);
const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE); 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)) 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; .map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)).filter(f => f).length > 1;
this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).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) { if (dexEntry.caughtAttr && species.malePercent !== null) {
@ -3365,7 +3367,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
if (dexEntry.caughtAttr) { 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); this.pokemonAbilityText.setText(ability.name);
const isHidden = abilityIndex === (this.lastSpecies.ability2 ? 2 : 1); const isHidden = abilityIndex === (this.lastSpecies.ability2 ? 2 : 1);
@ -3852,12 +3859,20 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.showStats(); this.showStats();
this.statsMode = true; this.statsMode = true;
this.pokemonSprite.setVisible(false); this.pokemonSprite.setVisible(false);
this.teraIcon.setVisible(false);
this.canCycleTera = false;
this.updateInstructions();
} else { } else {
this.statsMode = false; this.statsMode = false;
this.statsContainer.setVisible(false); this.statsContainer.setVisible(false);
this.pokemonSprite.setVisible(!!this.speciesStarterDexEntry?.caughtAttr); this.pokemonSprite.setVisible(!!this.speciesStarterDexEntry?.caughtAttr);
//@ts-ignore //@ts-ignore
this.statsContainer.updateIvs(null); // TODO: resolve 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();
} }
} }

View File

@ -140,6 +140,6 @@ describe("Moves - Focus Punch", () => {
await game.phaseInterceptor.to("MessagePhase", false); await game.phaseInterceptor.to("MessagePhase", false);
const consoleSpy = vi.spyOn(console, "log"); const consoleSpy = vi.spyOn(console, "log");
await game.phaseInterceptor.to("MoveEndPhase", true); await game.phaseInterceptor.to("MoveEndPhase", true);
expect(consoleSpy).nthCalledWith(1, i18next.t("moveTriggers:lostFocus")); expect(consoleSpy).nthCalledWith(1, i18next.t("moveTriggers:lostFocus", { pokemonName: "Charizard" }));
}); });
}); });