diff --git a/src/data/ability.ts b/src/data/ability.ts index 40c3d1e5a85..ff183a28432 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2356,7 +2356,7 @@ export class IncreasePpAbAttr extends AbAttr { } export class ForceSwitchOutImmunityAbAttr extends AbAttr { apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { - pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` can't be switched out!`)) + cancelled.value = true; return true; } } diff --git a/src/data/move.ts b/src/data/move.ts index 7df69d6cf18..653ffc93c0a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -327,10 +327,12 @@ export default class Move implements Localizable { } getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { - let failedText = null; - for (let attr of this.attrs) - failedText = attr.getFailedText(user, target, move, cancelled); - return failedText; + for (let attr of this.attrs) { + let failedText = attr.getFailedText(user, target, move, cancelled); + if (failedText !== null) + return failedText; + } + return null; } getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { @@ -445,7 +447,9 @@ export abstract class MoveAttr { export enum MoveEffectTrigger { PRE_APPLY, POST_APPLY, - HIT + HIT, + /** Triggers one time after all target effects have applied */ + POST_TARGET, } export class MoveEffectAttr extends MoveAttr { @@ -738,19 +742,33 @@ export class SacrificialAttr extends MoveEffectAttr { } } +/** + * Attribute used for moves which cut the user's Max HP in half. + * Triggers using POST_TARGET. + */ export class HalfSacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, MoveEffectTrigger.POST_TARGET); } + /** + * Cut's the user's Max HP in half and displays the appropriate recoil message + * @param user Pokemon that used the move + * @param target N/A + * @param move Move with this attribute + * @param args N/A + * @returns true if the function succeeds + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) return false; const cancelled = new Utils.BooleanHolder(false); + // Check to see if the Pokemon has an ability that blocks non-direct damage applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled); if (!cancelled.value){ user.damageAndUpdate(Math.ceil(user.getMaxHp()/2), HitResult.OTHER, false, true, true); + user.scene.queueMessage(getPokemonMessage(user, ' cut its own HP to power up its move!')); // Queue recoil message } return true; } @@ -2703,26 +2721,19 @@ export class AddBattlerTagAttr extends MoveEffectAttr { export class CurseAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move:Move, args: any[]): boolean { - // Determine the correct target based on the user's type - if (!user.getTypes(true).includes(Type.GHOST)) { - // For non-Ghost types, target the user itself - target = user; - } - if (user.getTypes(true).includes(Type.GHOST)) { if (target.getTag(BattlerTagType.CURSED)) { user.scene.queueMessage('But it failed!'); return false; } - let curseRecoilDamage = Math.floor(user.getMaxHp() / 2); + let curseRecoilDamage = Math.max(1, Math.floor(user.getMaxHp() / 2)); user.damageAndUpdate(curseRecoilDamage, HitResult.OTHER, false, true, true); user.scene.queueMessage(getPokemonMessage(user, ` cut its own HP\nand laid a curse on the ${target.name}!`)); target.addTag(BattlerTagType.CURSED, 0, move.id, user.id); return true; } else { - target = user; - user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), this.selfTarget, [BattleStat.ATK, BattleStat.DEF], 1)); - user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), this.selfTarget, [BattleStat.SPD], -1)); + user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF], 1)); + user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), true, [BattleStat.SPD], -1)); return true; } } @@ -2977,7 +2988,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return new Promise(resolve => { // Check if the move category is not STATUS or if the switch out condition is not met - if (!this.getCondition()(user, target, move)) { + if (!this.getSwitchOutCondition()(user, target, move)) { //Apply effects before switch out i.e. poison point, flame body, etc applyPostDefendAbAttrs(PostDefendContactApplyStatusEffectAbAttr, target, user, new PokemonMove(move.id), null); return resolve(false); @@ -3035,8 +3046,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { - applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, cancelled); - return null; + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + return blockedByAbility.value ? getPokemonMessage(target, ` can't be switched out!`) : null; } getSwitchOutCondition(): MoveConditionFunc { @@ -3809,6 +3821,9 @@ const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.is const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const cancelled = new Utils.BooleanHolder(false); user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled)); + // Queue a message if an ability prevented usage of the move + if (cancelled.value) + user.scene.queueMessage(getPokemonMessage(user, ` cannot use ${move.name}!`)); return !cancelled.value; } @@ -4402,10 +4417,8 @@ export function initMoves() { .condition((user, target, move) => user.status?.effect === StatusEffect.SLEEP) .soundBased(), new StatusMove(Moves.CURSE, Type.GHOST, -1, 10, -1, 0, 2) - .attr(StatChangeAttr, BattleStat.SPD, -1, true) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1, true) - .target(MoveTarget.USER) - .partial(), + .attr(CurseAttr) + .ignoresProtect(true), new AttackMove(Moves.FLAIL, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) .attr(LowHpPowerAttr), new StatusMove(Moves.CONVERSION_2, Type.NORMAL, -1, 30, -1, 0, 2) @@ -6249,8 +6262,7 @@ export function initMoves() { .partial(), new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8) .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF ], 1, true) - .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) - .condition((user, target, move) => user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), + .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP), /* Unused new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.ALL_NEAR_ENEMIES) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 3d09b768c1d..83182629d05 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -54,6 +54,10 @@ export class InputsController { init(): void { this.events = new Phaser.Events.EventEmitter(); + // Handle the game losing focus + this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { + this.loseFocus() + }) if (typeof this.scene.input.gamepad !== 'undefined') { this.scene.input.gamepad.on('connected', function (thisGamepad) { @@ -78,10 +82,14 @@ export class InputsController { this.setupKeyboardControls(); } + loseFocus(): void { + this.deactivatePressedKey(); + } + update(): void { for (const b of Utils.getEnumValues(Button)) { if (!this.interactions.hasOwnProperty(b)) continue; - if (this.repeatInputDurationJustPassed(b)) { + if (this.repeatInputDurationJustPassed(b) && this.interactions[b].isPressed) { this.events.emit('input_down', { controller_type: 'repeated', button: b, @@ -166,8 +174,8 @@ export class InputsController { [Button.LEFT]: [keyCodes.LEFT, keyCodes.A], [Button.RIGHT]: [keyCodes.RIGHT, keyCodes.D], [Button.SUBMIT]: [keyCodes.ENTER], - [Button.ACTION]: [keyCodes.SPACE, this.scene.abSwapped ? keyCodes.X : keyCodes.Z], - [Button.CANCEL]: [keyCodes.BACKSPACE, this.scene.abSwapped ? keyCodes.Z : keyCodes.X], + [Button.ACTION]: [keyCodes.SPACE, keyCodes.Z], + [Button.CANCEL]: [keyCodes.BACKSPACE, keyCodes.X], [Button.MENU]: [keyCodes.ESC, keyCodes.M], [Button.STATS]: [keyCodes.SHIFT, keyCodes.C], [Button.CYCLE_SHINY]: [keyCodes.R], @@ -248,11 +256,22 @@ export class InputsController { if (!this.interactions.hasOwnProperty(button)) return; this.buttonLock = button; this.interactions[button].pressTime = this.time.now; + this.interactions[button].isPressed = true; } delLastProcessedMovementTime(button: Button): void { if (!this.interactions.hasOwnProperty(button)) return; this.buttonLock = null; this.interactions[button].pressTime = null; + this.interactions[button].isPressed = false; + } + + deactivatePressedKey(): void { + this.buttonLock = null; + for (const b of Utils.getEnumValues(Button)) { + if (!this.interactions.hasOwnProperty(b)) return; + this.interactions[b].pressTime = null; + this.interactions[b].isPressed = false; + } } } \ No newline at end of file diff --git a/src/phases.ts b/src/phases.ts index cc493ce980a..751cac166bf 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2278,7 +2278,7 @@ export class MovePhase extends BattlePhase { // Assume conditions affecting targets only apply to moves with a single target let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove()); - let cancelled = new Utils.BooleanHolder(true); + let cancelled = new Utils.BooleanHolder(false); let failedText = this.move.getMove().getFailedText(this.pokemon, targets[0], this.move.getMove(), cancelled); if (success && this.scene.arena.isMoveWeatherCancelled(this.move.getMove())) success = false; @@ -2494,6 +2494,9 @@ export class MoveEffectPhase extends PokemonPhase { }); })); } + // Trigger effect which should only apply one time after all targeted effects have already applied + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_TARGET, + user, null, this.move.getMove()) Promise.allSettled(applyAttrs).then(() => this.end()); }); }); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index b828d90f595..bf548681005 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -358,8 +358,11 @@ export class GameData { this.starterData = systemData.starterData; } - if (systemData.gameStats) + if (systemData.gameStats) { + if (systemData.gameStats.legendaryPokemonCaught !== undefined && systemData.gameStats.subLegendaryPokemonCaught === undefined) + this.fixLegendaryStats(systemData); this.gameStats = systemData.gameStats; + } if (systemData.unlocks) { for (let key of Object.keys(systemData.unlocks)) { @@ -1005,7 +1008,9 @@ export class GameData { if (incrementCount) { dexEntry.seenCount++; this.gameStats.pokemonSeen++; - if (!trainer && pokemon.species.subLegendary || pokemon.species.legendary) + if (!trainer && pokemon.species.subLegendary) + this.gameStats.subLegendaryPokemonSeen++; + else if (!trainer && pokemon.species.legendary) this.gameStats.legendaryPokemonSeen++; else if (!trainer && pokemon.species.mythical) this.gameStats.mythicalPokemonSeen++; @@ -1043,7 +1048,9 @@ export class GameData { if (!fromEgg) { dexEntry.caughtCount++; this.gameStats.pokemonCaught++; - if (pokemon.species.subLegendary || pokemon.species.legendary) + if (pokemon.species.subLegendary) + this.gameStats.subLegendaryPokemonCaught++; + else if (pokemon.species.legendary) this.gameStats.legendaryPokemonCaught++; else if (pokemon.species.mythical) this.gameStats.mythicalPokemonCaught++; @@ -1052,7 +1059,9 @@ export class GameData { } else { dexEntry.hatchedCount++; this.gameStats.pokemonHatched++; - if (pokemon.species.subLegendary || pokemon.species.legendary) + if (pokemon.species.subLegendary) + this.gameStats.subLegendaryPokemonHatched++; + else if (pokemon.species.legendary) this.gameStats.legendaryPokemonHatched++; else if (pokemon.species.mythical) this.gameStats.mythicalPokemonHatched++; @@ -1311,4 +1320,22 @@ export class GameData { for (let starterId of defaultStarterSpecies) systemData.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; } + + fixLegendaryStats(systemData: SystemSaveData): void { + systemData.gameStats.subLegendaryPokemonSeen = 0; + systemData.gameStats.subLegendaryPokemonCaught = 0; + systemData.gameStats.subLegendaryPokemonHatched = 0; + allSpecies.filter(s => s.subLegendary).forEach(s => { + const dexEntry = systemData.dexData[s.speciesId]; + systemData.gameStats.subLegendaryPokemonSeen += dexEntry.seenCount; + systemData.gameStats.legendaryPokemonSeen = Math.max(systemData.gameStats.legendaryPokemonSeen - dexEntry.seenCount, 0); + systemData.gameStats.subLegendaryPokemonCaught += dexEntry.caughtCount; + systemData.gameStats.legendaryPokemonCaught = Math.max(systemData.gameStats.legendaryPokemonCaught - dexEntry.caughtCount, 0); + systemData.gameStats.subLegendaryPokemonHatched += dexEntry.hatchedCount; + systemData.gameStats.legendaryPokemonHatched = Math.max(systemData.gameStats.legendaryPokemonHatched - dexEntry.hatchedCount, 0); + }); + systemData.gameStats.subLegendaryPokemonSeen = Math.max(systemData.gameStats.subLegendaryPokemonSeen, systemData.gameStats.subLegendaryPokemonCaught); + systemData.gameStats.legendaryPokemonSeen = Math.max(systemData.gameStats.legendaryPokemonSeen, systemData.gameStats.legendaryPokemonCaught); + systemData.gameStats.mythicalPokemonSeen = Math.max(systemData.gameStats.mythicalPokemonSeen, systemData.gameStats.mythicalPokemonCaught); + } } \ No newline at end of file diff --git a/src/system/game-stats.ts b/src/system/game-stats.ts index 9a564b3c3b8..bf25b0e881d 100644 --- a/src/system/game-stats.ts +++ b/src/system/game-stats.ts @@ -18,6 +18,9 @@ export class GameStats { public pokemonDefeated: integer; public pokemonCaught: integer; public pokemonHatched: integer; + public subLegendaryPokemonSeen: integer; + public subLegendaryPokemonCaught: integer; + public subLegendaryPokemonHatched: integer; public legendaryPokemonSeen: integer; public legendaryPokemonCaught: integer; public legendaryPokemonHatched: integer; @@ -52,6 +55,10 @@ export class GameStats { this.pokemonDefeated = source?.pokemonDefeated || 0; this.pokemonCaught = source?.pokemonCaught || 0; this.pokemonHatched = source?.pokemonHatched || 0; + // Currently handled by migration + this.subLegendaryPokemonSeen = source?.subLegendaryPokemonSeen; + this.subLegendaryPokemonCaught = source?.subLegendaryPokemonCaught; + this.subLegendaryPokemonHatched = source?.subLegendaryPokemonHatched; this.legendaryPokemonSeen = source?.legendaryPokemonSeen || 0; this.legendaryPokemonCaught = source?.legendaryPokemonCaught || 0; this.legendaryPokemonHatched = source?.legendaryPokemonHatched || 0; diff --git a/src/ui/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts index 0b97d322e02..6b067912cd8 100644 --- a/src/ui/game-stats-ui-handler.ts +++ b/src/ui/game-stats-ui-handler.ts @@ -64,13 +64,16 @@ const displayStats: DisplayStats = { pokemonDefeated: 'Pokémon Defeated', pokemonCaught: 'Pokémon Caught', pokemonHatched: 'Eggs Hatched', - legendaryPokemonSeen: 'Legendary Encounters?', - legendaryPokemonCaught: 'Legendaries Caught?', - legendaryPokemonHatched: 'Legendaries Hatched?', - mythicalPokemonSeen: 'Mythical Encounters?', + subLegendaryPokemonSeen: 'Sub-Legends Seen?', + subLegendaryPokemonCaught: 'Sub-Legends Caught?', + subLegendaryPokemonHatched: 'Sub-Legends Hatched?', + legendaryPokemonSeen: 'Legends Seen?', + legendaryPokemonCaught: 'Legends Caught?', + legendaryPokemonHatched: 'Legends Hatched?', + mythicalPokemonSeen: 'Mythicals Seen?', mythicalPokemonCaught: 'Mythicals Caught?', mythicalPokemonHatched: 'Mythicals Hatched?', - shinyPokemonSeen: 'Shiny Encounters?', + shinyPokemonSeen: 'Shinies Seen?', shinyPokemonCaught: 'Shinies Caught?', shinyPokemonHatched: 'Shinies Hatched?', pokemonFused: 'Pokémon Fused?', diff --git a/src/utils.ts b/src/utils.ts index 3ae440a0bfb..822f02f053e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -226,7 +226,7 @@ export const apiUrl = isLocal ? serverUrl : 'api'; export function setCookie(cName: string, cValue: string): void { const expiration = new Date(); - expiration.setTime(new Date().getTime() + 3600000 * 24 * 7); + expiration.setTime(new Date().getTime() + 3600000 * 24 * 30 * 3/*7*/); document.cookie = `${cName}=${cValue};SameSite=Strict;path=/;expires=${expiration.toUTCString()}`; }