diff --git a/public/audio/bgm/battle_legendary_eternatus_p1.mp3 b/public/audio/bgm/battle_legendary_eternatus_p1.mp3 new file mode 100644 index 00000000000..742a3a198c7 Binary files /dev/null and b/public/audio/bgm/battle_legendary_eternatus_p1.mp3 differ diff --git a/public/audio/bgm/battle_legendary_eternatus_p2.mp3 b/public/audio/bgm/battle_legendary_eternatus_p2.mp3 new file mode 100644 index 00000000000..2015e735840 Binary files /dev/null and b/public/audio/bgm/battle_legendary_eternatus_p2.mp3 differ diff --git a/public/images/items.json b/public/images/items.json index bde8d38e26c..7b332442f04 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -8478,6 +8478,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:0fa4b2b134eacc1b8e5cf03054124001:8eebc761c452a8a36eae96a30cd3d32b:110e074689c9edd2c54833ce2e4d9270$" + "smartupdate": "$TexturePacker:SmartUpdate:9b6fc7b241128f4f61686fe287e090cd:46e9caafcc91f3c30ff85a6e8d3f5227:110e074689c9edd2c54833ce2e4d9270$" } } diff --git a/public/images/items.png b/public/images/items.png index b8ac859102a..896bb46c7c2 100644 Binary files a/public/images/items.png and b/public/images/items.png differ diff --git a/public/images/items/berry_juice_bad.png b/public/images/items/berry_juice_bad.png index 904065ed3c1..344822c09ea 100644 Binary files a/public/images/items/berry_juice_bad.png and b/public/images/items/berry_juice_bad.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 57ca66e0dc4..721b55020f9 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2525,6 +2525,10 @@ export default class BattleScene extends SceneBase { return 10.344; case "battle_legendary_zac_zam": //SWSH Zacian & Zamazenta Battle return 11.424; + case "battle_legendary_eternatus_p1": //SWSH Eternatus Battle + return 11.102; + case "battle_legendary_eternatus_p2": //SWSH Eternamax Eternatus Battle + return 0.0; case "battle_legendary_glas_spec": //SWSH Glastrier & Spectrier Battle return 12.503; case "battle_legendary_calyrex": //SWSH Calyrex Battle diff --git a/src/battle.ts b/src/battle.ts index 878a539cecf..49f5c39e7dd 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -382,6 +382,11 @@ export default class Battle { case SpeciesId.ZACIAN: case SpeciesId.ZAMAZENTA: return "battle_legendary_zac_zam"; + case SpeciesId.ETERNATUS: + if (pokemon.getFormKey() === "eternamax") { + return "battle_legendary_eternatus_p2"; + } + return "battle_legendary_eternatus_p1"; case SpeciesId.GLASTRIER: case SpeciesId.SPECTRIER: return "battle_legendary_glas_spec"; diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 0aaf5b4b124..fa4ee2286ff 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1194,7 +1194,16 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr { } } -export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr { +/** + * Set stat stages when the user gets hit by a critical hit + * + * @privateremarks + * It is the responsibility of the caller to ensure that this ability attribute is only applied + * when the user has been hit by a critical hit; such an event is not checked here. + * + * @sealed + */ +export class PostReceiveCritStatStageChangeAbAttr extends AbAttr { private stat: BattleStat; private stages: number; @@ -1216,12 +1225,6 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr { ); } } - - override getCondition(): AbAttrCondition { - return (pokemon: Pokemon) => - pokemon.turnData.attacksReceived.length !== 0 && - pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1].critical; - } } export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { @@ -6417,7 +6420,7 @@ const AbilityAttrs = Object.freeze({ PostDefendContactApplyStatusEffectAbAttr, EffectSporeAbAttr, PostDefendContactApplyTagChanceAbAttr, - PostDefendCritStatStageChangeAbAttr, + PostReceiveCritStatStageChangeAbAttr, PostDefendContactDamageAbAttr, PostDefendPerishSongAbAttr, PostDefendWeatherChangeAbAttr, @@ -6886,7 +6889,7 @@ export function initAbilities() { new Ability(AbilityId.GLUTTONY, 4) .attr(ReduceBerryUseThresholdAbAttr), new Ability(AbilityId.ANGER_POINT, 4) - .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), + .attr(PostReceiveCritStatStageChangeAbAttr, Stat.ATK, 12), new Ability(AbilityId.UNBURDEN, 4) .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN) .bypassFaint() // Allows reviver seed to activate Unburden diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 04e64083602..f76bd66151f 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -15,7 +15,7 @@ import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { AbilityAttr } from "#enums/ability-attr"; import PokemonData from "#app/system/pokemon-data"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; -import { isNullOrUndefined, randSeedShuffle } from "#app/utils/common"; +import { getEnumValues, isNullOrUndefined, randSeedShuffle } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; @@ -30,7 +30,7 @@ import i18next from "i18next"; import { getStatKey } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import type { Nature } from "#enums/nature"; +import { Nature } from "#enums/nature"; /** The i18n namespace for the encounter */ const namespace = "mysteryEncounters/trainingSession"; @@ -184,10 +184,9 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde .withPreOptionPhase(async (): Promise => { // Open menu for selecting pokemon and Nature const encounter = globalScene.currentBattle.mysteryEncounter!; - const natures = new Array(25).fill(null).map((_val, i) => i as Nature); const onPokemonSelected = (pokemon: PlayerPokemon) => { // Return the options for nature selection - return natures.map((nature: Nature) => { + return getEnumValues(Nature).map((nature: Nature) => { const option: OptionSelectItem = { label: getNatureName(nature, true, true, true, globalScene.uiTheme), handler: () => { diff --git a/src/field/anims.ts b/src/field/anims.ts index 2fd23e4262b..f1ac87cd741 100644 --- a/src/field/anims.ts +++ b/src/field/anims.ts @@ -150,7 +150,7 @@ function doFanOutParticle( } export function addPokeballCaptureStars(pokeball: Phaser.GameObjects.Sprite): void { - const addParticle = () => { + const addParticle = (): void => { const particle = globalScene.add.sprite(pokeball.x, pokeball.y, "pb_particles", "4.png"); particle.setOrigin(pokeball.originX, pokeball.originY); particle.setAlpha(0.5); @@ -188,7 +188,9 @@ export function addPokeballCaptureStars(pokeball: Phaser.GameObjects.Sprite): vo }); }; - new Array(3).fill(null).map(() => addParticle()); + for (let i = 0; i < 3; i++) { + addParticle(); + } } export function sin(index: number, amplitude: number): number { diff --git a/src/field/arena.ts b/src/field/arena.ts index 4479748667c..c9544c97704 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -981,14 +981,15 @@ export class ArenaBase extends Phaser.GameObjects.Container { this.base = globalScene.addFieldSprite(0, 0, "plains_a", undefined, 1); this.base.setOrigin(0, 0); - this.props = !player - ? new Array(3).fill(null).map(() => { - const ret = globalScene.addFieldSprite(0, 0, "plains_b", undefined, 1); - ret.setOrigin(0, 0); - ret.setVisible(false); - return ret; - }) - : []; + this.props = []; + if (!player) { + for (let i = 0; i < 3; i++) { + const ret = globalScene.addFieldSprite(0, 0, "plains_b", undefined, 1); + ret.setOrigin(0, 0); + ret.setVisible(false); + this.props.push(ret); + } + } } setBiome(biome: BiomeId, propValue?: number): void { diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 5c60c146154..96aea699eff 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -2457,12 +2457,31 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod } export interface CustomModifierSettings { + /** If specified, will override the next X items to be the specified tier. These can upgrade with luck. */ guaranteedModifierTiers?: ModifierTier[]; + /** If specified, will override the first X items to be specific modifier options (these should be pre-genned). */ guaranteedModifierTypeOptions?: ModifierTypeOption[]; + /** If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). */ guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; + /** + * If set to `true`, will fill the remainder of shop items that were not overridden by the 3 options above, up to the `count` param value. + * @example + * ```ts + * count = 4; + * customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }; + * ``` + * The first item in the shop will be `GREAT` tier, and the remaining `3` items will be generated normally. + * + * If `fillRemaining: false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of the value of `count`). + * @defaultValue `false` + */ fillRemaining?: boolean; - /** Set to negative value to disable rerolls completely in shop */ + /** If specified, can adjust the amount of money required for a shop reroll. If set to a negative value, the shop will not allow rerolls at all. */ rerollMultiplier?: number; + /** + * If `false`, will prevent set item tiers from upgrading via luck. + * @defaultValue `true` + */ allowLuckUpgrades?: boolean; } @@ -2472,19 +2491,10 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc { /** * Generates modifier options for a {@linkcode SelectModifierPhase} - * @param count Determines the number of items to generate - * @param party Party is required for generating proper modifier pools - * @param modifierTiers (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. - * @param customModifierSettings (Optional) If specified, can customize the item shop rewards further. - * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` If specified, will override the first X items to be specific modifier options (these should be pre-genned). - * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). - * - `guaranteedModifierTiers?: ModifierTier[]` If specified, will override the next X items to be the specified tier. These can upgrade with luck. - * - `fillRemaining?: boolean` Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. - * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, - * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. - * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). - * - `rerollMultiplier?: number` If specified, can adjust the amount of money required for a shop reroll. If set to a negative value, the shop will not allow rerolls at all. - * - `allowLuckUpgrades?: boolean` Default `true`, if `false` will prevent set item tiers from upgrading via luck + * @param count - Determines the number of items to generate + * @param party - Party is required for generating proper modifier pools + * @param modifierTiers - (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. + * @param customModifierSettings - See {@linkcode CustomModifierSettings} */ export function getPlayerModifierTypeOptions( count: number, @@ -2495,16 +2505,10 @@ export function getPlayerModifierTypeOptions( const options: ModifierTypeOption[] = []; const retryCount = Math.min(count * 5, 50); if (!customModifierSettings) { - new Array(count).fill(0).map((_, i) => { - options.push( - getModifierTypeOptionWithRetry( - options, - retryCount, - party, - modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined, - ), - ); - }); + for (let i = 0; i < count; i++) { + const tier = modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined; + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier)); + } } else { // Guaranteed mod options first if ( diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 8e4300986b3..3093af40f75 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -388,7 +388,7 @@ export class EvolutionPhase extends Phase { globalScene.ui.showText( i18next.t("menu:evolutionDone", { pokemonName: this.preEvolvedPokemonName, - evolvedPokemonName: this.pokemon.species.getExpandedSpeciesName(), + evolvedPokemonName: this.pokemon.name, }), null, () => this.end(), diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 0f53561f5f3..fb421d7a25f 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -432,9 +432,15 @@ export class MoveEffectPhase extends PokemonPhase { * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - {@linkcode Pokemon} the current target of this phase's invoked move * @param hitResult - The {@linkcode HitResult} of the attempted move + * @param wasCritical - `true` if the move was a critical hit */ - protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void { - applyAbAttrs("PostDefendAbAttr", { pokemon: target, opponent: user, move: this.move, hitResult }); + protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, wasCritical = false): void { + const params = { pokemon: target, opponent: user, move: this.move, hitResult }; + applyAbAttrs("PostDefendAbAttr", params); + + if (wasCritical) { + applyAbAttrs("PostReceiveCritStatStageChangeAbAttr", params); + } target.lapseTags(BattlerTagLapseType.AFTER_HIT); } @@ -788,12 +794,12 @@ export class MoveEffectPhase extends PokemonPhase { this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target); - const hitResult = this.applyMove(user, target, effectiveness); + const [hitResult, wasCritical] = this.applyMove(user, target, effectiveness); // Apply effects to the user (always) and the target (if not blocked by substitute). this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true); if (!this.move.hitsSubstitute(user, target)) { - this.applyOnTargetEffects(user, target, hitResult, firstTarget); + this.applyOnTargetEffects(user, target, hitResult, firstTarget, wasCritical); } if (this.lastHit) { globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); @@ -813,8 +819,9 @@ export class MoveEffectPhase extends PokemonPhase { * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - The {@linkcode Pokemon} targeted by the move * @param effectiveness - The effectiveness of the move against the target + * @returns The {@linkcode HitResult} of the move against the target and a boolean indicating whether the target was crit */ - protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): HitResult { + protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] { const isCritical = target.getCriticalHitResult(user, this.move); /* @@ -845,7 +852,7 @@ export class MoveEffectPhase extends PokemonPhase { const isOneHitKo = result === HitResult.ONE_HIT_KO; if (!dmg) { - return result; + return [result, false]; } target.lapseTags(BattlerTagLapseType.HIT); @@ -873,7 +880,7 @@ export class MoveEffectPhase extends PokemonPhase { } if (damage <= 0) { - return result; + return [result, isCritical]; } if (user.isPlayer()) { @@ -902,7 +909,7 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage)); } - return result; + return [result, isCritical]; } /** @@ -956,17 +963,17 @@ export class MoveEffectPhase extends PokemonPhase { * @param target - The {@linkcode Pokemon} struck by the move * @param effectiveness - The effectiveness of the move against the target */ - protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): HitResult { + protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] { const moveCategory = user.getMoveCategory(target, this.move); if (moveCategory === MoveCategory.STATUS) { - return HitResult.STATUS; + return [HitResult.STATUS, false]; } const result = this.applyMoveDamage(user, target, effectiveness); if (user.turnData.hitsLeft === 1 || target.isFainted()) { - this.queueHitResultMessage(result); + this.queueHitResultMessage(result[0]); } if (target.isFainted()) { @@ -983,8 +990,15 @@ export class MoveEffectPhase extends PokemonPhase { * @param target - The {@linkcode Pokemon} targeted by the move * @param hitResult - The {@linkcode HitResult} obtained from applying the move * @param firstTarget - `true` if the target is the first Pokemon hit by the attack + * @param wasCritical - `true` if the move was a critical hit */ - protected applyOnTargetEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, firstTarget: boolean): void { + protected applyOnTargetEffects( + user: Pokemon, + target: Pokemon, + hitResult: HitResult, + firstTarget: boolean, + wasCritical = false, + ): void { /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ const dealsDamage = [ HitResult.EFFECTIVE, @@ -995,7 +1009,7 @@ export class MoveEffectPhase extends PokemonPhase { this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false); this.applyHeldItemFlinchCheck(user, target, dealsDamage); - this.applyOnGetHitAbEffects(user, target, hitResult); + this.applyOnGetHitAbEffects(user, target, hitResult, wasCritical); applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult }); // We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 85f7b12a2a5..28763fe970a 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -105,7 +105,7 @@ export default class PokemonData { // TODO: Can't we move some of this verification stuff to an upgrade script? this.nature = source.nature ?? Nature.HARDY; - this.moveset = source.moveset.map((m: any) => PokemonMove.loadMove(m)); + this.moveset = source.moveset?.map((m: any) => PokemonMove.loadMove(m)) ?? []; this.status = source.status ? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining) : null; diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index ca5395a5af7..dbddcb7a923 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -11,25 +11,22 @@ import { PlayerGender } from "#enums/player-gender"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { isLocal } from "#app/utils/common"; -const VOLUME_OPTIONS: SettingOption[] = new Array(11).fill(null).map((_, i) => - i - ? { - value: (i * 10).toString(), - label: (i * 10).toString(), - } - : { - value: "Mute", - label: i18next.t("settings:mute"), - }, -); +const VOLUME_OPTIONS: SettingOption[] = [ + { + value: "Mute", + label: i18next.t("settings:mute"), + }, +]; +for (let i = 1; i < 11; i++) { + const value = (i * 10).toString(); + VOLUME_OPTIONS.push({ value, label: value }); +} -const SHOP_OVERLAY_OPACITY_OPTIONS: SettingOption[] = new Array(9).fill(null).map((_, i) => { +const SHOP_OVERLAY_OPACITY_OPTIONS: SettingOption[] = []; +for (let i = 0; i < 9; i++) { const value = ((i + 1) * 10).toString(); - return { - value, - label: value, - }; -}); + SHOP_OVERLAY_OPACITY_OPTIONS.push({ value, label: value }); +} const OFF_ON: SettingOption[] = [ { @@ -183,6 +180,12 @@ export enum MusicPreference { ALLGENS, } +const windowTypeOptions: SettingOption[] = []; +for (let i = 0; i < 5; i++) { + const value = (i + 1).toString(); + windowTypeOptions.push({ value, label: value }); +} + /** * All Settings not related to controls */ @@ -432,13 +435,7 @@ export const Setting: Array = [ { key: SettingKeys.Window_Type, label: i18next.t("settings:windowType"), - options: new Array(5).fill(null).map((_, i) => { - const windowType = (i + 1).toString(); - return { - value: windowType, - label: windowType, - }; - }), + options: windowTypeOptions, default: 0, type: SettingType.DISPLAY, }, diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 7ff3a1b65ee..9e15bef0dea 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -299,8 +299,8 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.eggGachaContainer.add(this.eggGachaOptionsContainer); - new Array(getEnumKeys(VoucherType).length).fill(null).map((_, i) => { - const container = globalScene.add.container(globalScene.game.canvas.width / 6 - 56 * i, 0); + for (const voucher of getEnumValues(VoucherType)) { + const container = globalScene.add.container(globalScene.game.canvas.width / 6 - 56 * voucher, 0); const bg = addWindow(0, 0, 56, 22); bg.setOrigin(1, 0); @@ -312,7 +312,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.voucherCountLabels.push(countLabel); - const iconImage = getVoucherTypeIcon(i as VoucherType); + const iconImage = getVoucherTypeIcon(voucher); const icon = globalScene.add.sprite(-19, 2, "items", iconImage); icon.setOrigin(0, 0); @@ -320,7 +320,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { container.add(icon); this.eggGachaContainer.add(container); - }); + } this.eggGachaOverlay = globalScene.add.rectangle(0, 0, bg.displayWidth, bg.displayHeight, 0x000000); this.eggGachaOverlay.setOrigin(0, 0); diff --git a/src/ui/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts index 4213a244fdb..9bf5322a0a9 100644 --- a/src/ui/game-stats-ui-handler.ts +++ b/src/ui/game-stats-ui-handler.ts @@ -267,10 +267,10 @@ export default class GameStatsUiHandler extends UiHandler { this.statsContainer = globalScene.add.container(0, 0); - new Array(18).fill(null).map((_, s) => { + for (let i = 0; i < 18; i++) { const statLabel = addTextObject( - 8 + (s % 2 === 1 ? statsBgWidth : 0), - 28 + Math.floor(s / 2) * 16, + 8 + (i % 2 === 1 ? statsBgWidth : 0), + 28 + Math.floor(i / 2) * 16, "", TextStyle.STATS_LABEL, ); @@ -278,11 +278,11 @@ export default class GameStatsUiHandler extends UiHandler { this.statsContainer.add(statLabel); this.statLabels.push(statLabel); - const statValue = addTextObject(statsBgWidth * ((s % 2) + 1) - 8, statLabel.y, "", TextStyle.STATS_VALUE); + const statValue = addTextObject(statsBgWidth * ((i % 2) + 1) - 8, statLabel.y, "", TextStyle.STATS_VALUE); statValue.setOrigin(1, 0); this.statsContainer.add(statValue); this.statValues.push(statValue); - }); + } this.gameStatsContainer.add(headerBg); this.gameStatsContainer.add(headerText); diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index 9169ca77999..b6a0427bedf 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -483,13 +483,14 @@ export default class PokedexUiHandler extends MessageUiHandler { starterBoxContainer.add(this.starterSelectScrollBar); - this.pokerusCursorObjs = new Array(POKERUS_STARTER_COUNT).fill(null).map(() => { + this.pokerusCursorObjs = []; + for (let i = 0; i < POKERUS_STARTER_COUNT; i++) { const cursorObj = globalScene.add.image(0, 0, "select_cursor_pokerus"); cursorObj.setVisible(false); cursorObj.setOrigin(0, 0); starterBoxContainer.add(cursorObj); - return cursorObj; - }); + this.pokerusCursorObjs.push(cursorObj); + } this.cursorObj = globalScene.add.image(0, 0, "select_cursor"); this.cursorObj.setOrigin(0, 0); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 3cb6222cafa..499a99dbade 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -800,21 +800,23 @@ export default class StarterSelectUiHandler extends MessageUiHandler { starterBoxContainer.add(this.starterSelectScrollBar); - this.pokerusCursorObjs = new Array(POKERUS_STARTER_COUNT).fill(null).map(() => { + this.pokerusCursorObjs = []; + for (let i = 0; i < POKERUS_STARTER_COUNT; i++) { const cursorObj = globalScene.add.image(0, 0, "select_cursor_pokerus"); cursorObj.setVisible(false); cursorObj.setOrigin(0, 0); starterBoxContainer.add(cursorObj); - return cursorObj; - }); + this.pokerusCursorObjs.push(cursorObj); + } - this.starterCursorObjs = new Array(6).fill(null).map(() => { + this.starterCursorObjs = []; + for (let i = 0; i < 6; i++) { const cursorObj = globalScene.add.image(0, 0, "select_cursor_highlight"); cursorObj.setVisible(false); cursorObj.setOrigin(0, 0); starterBoxContainer.add(cursorObj); - return cursorObj; - }); + this.starterCursorObjs.push(cursorObj); + } this.cursorObj = globalScene.add.image(0, 0, "select_cursor"); this.cursorObj.setOrigin(0, 0); @@ -843,15 +845,16 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.starterSelectContainer.add(starterBoxContainer); - this.starterIcons = new Array(6).fill(null).map((_, i) => { + this.starterIcons = []; + for (let i = 0; i < 6; i++) { const icon = globalScene.add.sprite(teamWindowX + 7, calcStarterIconY(i), "pokemon_icons_0"); icon.setScale(0.5); icon.setOrigin(0, 0); icon.setFrame("unknown"); this.starterSelectContainer.add(icon); this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.PASSIVE); - return icon; - }); + this.starterIcons.push(icon); + } this.type1Icon = globalScene.add.sprite(8, 98, getLocalizedSpriteKey("types")); this.type1Icon.setScale(0.5); diff --git a/src/ui/stats-container.ts b/src/ui/stats-container.ts index 8cc74e64e96..4af6abdad20 100644 --- a/src/ui/stats-container.ts +++ b/src/ui/stats-container.ts @@ -19,7 +19,7 @@ const ivLabelOffset = [0, sideLabelOffset, -sideLabelOffset, sideLabelOffset, -s const ivChartLabelyOffset = [0, 5, 0, 5, 0, 0]; // doing this so attack does not overlap with (+N) const ivChartStatIndexes = [0, 1, 2, 5, 4, 3]; // swap special attack and speed -const defaultIvChartData = new Array(12).fill(null).map(() => 0); +const defaultIvChartData: number[] = new Array(12).fill(0); export class StatsContainer extends Phaser.GameObjects.Container { private showDiff: boolean; diff --git a/test/abilities/anger-point.test.ts b/test/abilities/anger-point.test.ts new file mode 100644 index 00000000000..e6f3a94d12f --- /dev/null +++ b/test/abilities/anger-point.test.ts @@ -0,0 +1,78 @@ +import { PostReceiveCritStatStageChangeAbAttr } from "#app/data/abilities/ability"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Ability - Anger Point", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .enemyLevel(100); + }); + + it("should set the user's attack stage to +6 when hit by a critical hit", async () => { + game.override.enemyAbility(AbilityId.ANGER_POINT).moveset(MoveId.FALSE_SWIPE); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + const enemy = game.scene.getEnemyPokemon()!; + + // minimize the enemy's attack stage to ensure it is always set to +6 + enemy.setStatStage(Stat.ATK, -6); + vi.spyOn(enemy, "getCriticalHitResult").mockReturnValueOnce(true); + game.move.select(MoveId.FALSE_SWIPE); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemy.getStatStage(Stat.ATK)).toBe(6); + }); + + it("should only proc once when a multi-hit move crits on the first hit", async () => { + game.override + .moveset(MoveId.BULLET_SEED) + .enemyLevel(50) + .enemyAbility(AbilityId.ANGER_POINT) + .ability(AbilityId.SKILL_LINK); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getCriticalHitResult").mockReturnValueOnce(true); + const angerPointSpy = vi.spyOn(PostReceiveCritStatStageChangeAbAttr.prototype, "apply"); + game.move.select(MoveId.BULLET_SEED); + await game.phaseInterceptor.to("BerryPhase"); + expect(angerPointSpy).toHaveBeenCalledTimes(1); + }); + + it("should set a contrary user's attack stage to -6 when hit by a critical hit", async () => { + game.override + .enemyAbility(AbilityId.ANGER_POINT) + .enemyPassiveAbility(AbilityId.CONTRARY) + .enemyHasPassiveAbility(true) + .moveset(MoveId.FALSE_SWIPE); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getCriticalHitResult").mockReturnValueOnce(true); + enemy.setStatStage(Stat.ATK, 6); + game.move.select(MoveId.FALSE_SWIPE); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemy.getStatStage(Stat.ATK)).toBe(-6); + }); +}); diff --git a/test/testUtils/helpers/overridesHelper.ts b/test/testUtils/helpers/overridesHelper.ts index a26dc883a30..8bf68489479 100644 --- a/test/testUtils/helpers/overridesHelper.ts +++ b/test/testUtils/helpers/overridesHelper.ts @@ -2,7 +2,6 @@ import type { NewArenaEvent } from "#app/events/battle-scene"; /** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */ -import { Weather } from "#app/data/weather"; import type { ModifierOverride } from "#app/modifier/modifier-type"; import type { BattleStyle, RandomTrainerOverride } from "#app/overrides"; import Overrides, { defaultOverrides } from "#app/overrides"; @@ -18,7 +17,7 @@ import { Nature } from "#enums/nature"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import type { Unlockables } from "#enums/unlockables"; -import type { WeatherType } from "#enums/weather-type"; +import { WeatherType } from "#enums/weather-type"; import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper"; import { expect, vi } from "vitest"; @@ -356,7 +355,7 @@ export class OverridesHelper extends GameManagerHelper { */ public weather(type: WeatherType): this { vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type); - this.log(`Weather set to ${Weather[type]} (=${type})!`); + this.log(`Weather set to ${WeatherType[type]} (=${type})!`); return this; }