diff --git a/README.md b/README.md index 53ebbc17e11..a63d7da6c29 100644 --- a/README.md +++ b/README.md @@ -14,26 +14,15 @@ - Add IV screen - Capture logic - Critical capture -- Save data - - Update dex format to share attributes - Modifiers - - PP Up - Various mainline game items for various enhancements - - IV scanner - Valuable items for money - Trainers - Finish party pools - Add dialogue - Add reward for gym leader victories -- Encounters - - Add extremely rare chance of Arceus available anywhere with type change -- Balancing - - Biome pools - Battle animations - Fix broken battle animations (mostly ones with backgrounds) - - Add common animations for modifier effects -- Achievements - - Add more achievements - Modes - Add random mode - Add Nuzlocke mode diff --git a/src/arena.ts b/src/arena.ts index 69ae007edec..f17f17490a2 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -9,7 +9,8 @@ import { CommonAnimPhase } from "./battle-phases"; import { CommonAnim } from "./data/battle-anims"; import { Type } from "./data/type"; import Move from "./data/move"; -import { ArenaTag, ArenaTagType, getArenaTag } from "./data/arena-tag"; +import { ArenaTag, ArenaTagSide, getArenaTag } from "./data/arena-tag"; +import { ArenaTagType } from "./data/enums/arena-tag-type"; import { GameMode } from "./game-mode"; import { TrainerType } from "./data/enums/trainer-type"; import { BattlerIndex } from "./battle"; @@ -387,21 +388,27 @@ export class Arena { } } - applyTags(tagType: ArenaTagType | { new(...args: any[]): ArenaTag }, ...args: any[]): void { - const tags = typeof tagType === 'number' + applyTagsForSide(tagType: ArenaTagType | { new(...args: any[]): ArenaTag }, side: ArenaTagSide, ...args: any[]): void { + let tags = typeof tagType === 'string' ? this.tags.filter(t => t.tagType === tagType) : this.tags.filter(t => t instanceof tagType); - tags.forEach(t => t.apply(args)); + if (side !== ArenaTagSide.BOTH) + tags = tags.filter(t => t.side === side); + tags.forEach(t => t.apply(this, args)); + } + + applyTags(tagType: ArenaTagType | { new(...args: any[]): ArenaTag }, ...args: any[]): void { + this.applyTagsForSide(tagType, ArenaTagSide.BOTH, args); } - addTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, targetIndex?: BattlerIndex): boolean { + addTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide = ArenaTagSide.BOTH, targetIndex?: BattlerIndex): boolean { const existingTag = this.getTag(tagType); if (existingTag) { existingTag.onOverlap(this); return false; } - const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, targetIndex); + const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, targetIndex, side); this.tags.push(newTag); newTag.onAdd(this); @@ -409,9 +416,13 @@ export class Arena { } getTag(tagType: ArenaTagType | { new(...args: any[]): ArenaTag }): ArenaTag { - return typeof(tagType) === 'number' - ? this.tags.find(t => t.tagType === tagType) - : this.tags.find(t => t instanceof tagType); + return this.getTagOnSide(tagType, ArenaTagSide.BOTH); + } + + getTagOnSide(tagType: ArenaTagType | { new(...args: any[]): ArenaTag }, side: ArenaTagSide): ArenaTag { + return typeof(tagType) === 'string' + ? this.tags.find(t => t.tagType === tagType && (t.side === ArenaTagSide.BOTH || t.side === side)) + : this.tags.find(t => t instanceof tagType && (t.side === ArenaTagSide.BOTH || t.side === side)); } lapseTags(): void { diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 20f3be907e5..58907179c02 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -28,7 +28,8 @@ import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { TempBattleStat } from "./data/temp-battle-stat"; -import { ArenaTagType, ArenaTrapTag, TrickRoomTag } from "./data/arena-tag"; +import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; +import { ArenaTagType } from "./data/enums/arena-tag-type"; import { Abilities, CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, PostAttackAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreWeatherEffectAbAttrs } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./arena"; @@ -2072,7 +2073,9 @@ export class MoveEndPhase extends PokemonPhase { start() { super.start(); - this.getPokemon().lapseTags(BattlerTagLapseType.AFTER_MOVE); + const pokemon = this.getPokemon(); + if (pokemon.isActive(true)) + pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); this.end(); } @@ -2132,13 +2135,15 @@ export class StatChangePhase extends PokemonPhase { private stats: BattleStat[]; private selfTarget: boolean; private levels: integer; + private showMessage: boolean; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true) { super(scene, battlerIndex); this.selfTarget = selfTarget; this.stats = stats; this.levels = levels; + this.showMessage = showMessage; } start() { @@ -2152,6 +2157,9 @@ export class StatChangePhase extends PokemonPhase { const cancelled = new Utils.BooleanHolder(false); if (!this.selfTarget && this.levels < 0) + this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); + + if (!cancelled.value && !this.selfTarget && this.levels < 0) applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled); return !cancelled.value; @@ -2164,9 +2172,11 @@ export class StatChangePhase extends PokemonPhase { const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]); const end = () => { - const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels); - for (let message of messages) - this.scene.queueMessage(message); + if (this.showMessage) { + const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels); + for (let message of messages) + this.scene.queueMessage(message); + } for (let stat of filteredStats) pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6); diff --git a/src/data/ability.ts b/src/data/ability.ts index ad322ca0a64..e1e5019140f 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -9,7 +9,7 @@ import { BattlerTag } from "./battler-tags"; import { BattlerTagType } from "./enums/battler-tag-type"; import { StatusEffect, getStatusEffectDescriptor } from "./status-effect"; import Move, { MoveCategory, MoveFlags, RecoilAttr } from "./move"; -import { ArenaTagType } from "./arena-tag"; +import { ArenaTagType } from "./enums/arena-tag-type"; import { Stat } from "./pokemon-stat"; import { PokemonHeldItemModifier } from "../modifier/modifier"; import { Moves } from "./enums/moves"; diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 71bc7cdf1d4..30cc1da7d14 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -9,18 +9,12 @@ import { StatusEffect } from "./status-effect"; import { BattlerTagType } from "./enums/battler-tag-type"; import { BattlerIndex } from "../battle"; import { Moves } from "./enums/moves"; +import { ArenaTagType } from "./enums/arena-tag-type"; -export enum ArenaTagType { - NONE, - MUD_SPORT, - WATER_SPORT, - SPIKES, - TOXIC_SPIKES, - FUTURE_SIGHT, - DOOM_DESIRE, - STEALTH_ROCK, - TRICK_ROOM, - GRAVITY +export enum ArenaTagSide { + BOTH, + PLAYER, + ENEMY } export abstract class ArenaTag { @@ -28,22 +22,24 @@ export abstract class ArenaTag { public turnCount: integer; public sourceMove: Moves; public sourceId: integer; + public side: ArenaTagSide; - constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId?: integer) { + constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId?: integer, side: ArenaTagSide = ArenaTagSide.BOTH) { this.tagType = tagType; this.turnCount = turnCount; this.sourceMove = sourceMove; this.sourceId = sourceId; + this.side = side; } - apply(args: any[]): boolean { + apply(arena: Arena, args: any[]): boolean { return true; } onAdd(arena: Arena): void { } onRemove(arena: Arena): void { - arena.scene.queueMessage(`${this.getMoveName()}\'s effect wore off.`); + arena.scene.queueMessage(`${this.getMoveName()}\'s effect wore off${this.side === ArenaTagSide.PLAYER ? '\non your side' : this.side === ArenaTagSide.ENEMY ? '\non the foe\'s side' : ''}.`); } onOverlap(arena: Arena): void { } @@ -59,6 +55,27 @@ export abstract class ArenaTag { } } +export class MistTag extends ArenaTag { + constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + super(ArenaTagType.MIST, turnCount, Moves.MIST, sourceId, side); + } + + onAdd(arena: Arena): void { + super.onAdd(arena); + + const source = arena.scene.getPokemonById(this.sourceId); + arena.scene.queueMessage(getPokemonMessage(source, `'s team became\nshrowded in mist!`)); + } + + apply(arena: Arena, args: any[]): boolean { + (args[0] as Utils.BooleanHolder).value = true; + + arena.scene.queueMessage('The mist prevented\nthe lowering of stats!'); + + return true; + } +} + export class WeakenMoveTypeTag extends ArenaTag { private weakenedType: Type; @@ -68,7 +85,7 @@ export class WeakenMoveTypeTag extends ArenaTag { this.weakenedType = type; } - apply(args: any[]): boolean { + apply(arena: Arena, args: any[]): boolean { if ((args[0] as Type) === this.weakenedType) { (args[1] as Utils.NumberHolder).value *= 0.33; return true; @@ -110,8 +127,8 @@ export class ArenaTrapTag extends ArenaTag { public layers: integer; public maxLayers: integer; - constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, maxLayers: integer) { - super(tagType, 0, sourceMove, sourceId); + constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, maxLayers: integer) { + super(tagType, 0, sourceMove, sourceId, side); this.layers = 1; this.maxLayers = maxLayers; @@ -125,9 +142,9 @@ export class ArenaTrapTag extends ArenaTag { } } - apply(args: any[]): boolean { + apply(arena: Arena, args: any[]): boolean { const pokemon = args[0] as Pokemon; - if (this.sourceId === pokemon.id || !!(pokemon.scene.getPokemonById(this.sourceId)?.isPlayer()) === pokemon.isPlayer()) + if (this.sourceId === pokemon.id || (this.side === ArenaTagSide.PLAYER) === pokemon.isPlayer()) return false; return this.activateTrap(pokemon); @@ -139,8 +156,8 @@ export class ArenaTrapTag extends ArenaTag { } class SpikesTag extends ArenaTrapTag { - constructor(sourceId: integer) { - super(ArenaTagType.SPIKES, Moves.SPIKES, sourceId, 3); + constructor(sourceId: integer, side: ArenaTagSide) { + super(ArenaTagType.SPIKES, Moves.SPIKES, sourceId, side, 3); } onAdd(arena: Arena): void { @@ -165,8 +182,8 @@ class SpikesTag extends ArenaTrapTag { } class ToxicSpikesTag extends ArenaTrapTag { - constructor(sourceId: integer) { - super(ArenaTagType.TOXIC_SPIKES, Moves.TOXIC_SPIKES, sourceId, 2); + constructor(sourceId: integer, side: ArenaTagSide) { + super(ArenaTagType.TOXIC_SPIKES, Moves.TOXIC_SPIKES, sourceId, side, 2); } onAdd(arena: Arena): void { @@ -211,8 +228,8 @@ class DelayedAttackTag extends ArenaTag { } class StealthRockTag extends ArenaTrapTag { - constructor(sourceId: integer) { - super(ArenaTagType.STEALTH_ROCK, Moves.STEALTH_ROCK, sourceId, 1); + constructor(sourceId: integer, side: ArenaTagSide) { + super(ArenaTagType.STEALTH_ROCK, Moves.STEALTH_ROCK, sourceId, side, 1); } onAdd(arena: Arena): void { @@ -263,7 +280,7 @@ export class TrickRoomTag extends ArenaTag { super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId); } - apply(args: any[]): boolean { + apply(arena: Arena, args: any[]): boolean { const speedReversed = args[0] as Utils.BooleanHolder; speedReversed.value = !speedReversed.value; return true; @@ -292,21 +309,23 @@ export class GravityTag extends ArenaTag { } } -export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, targetIndex?: BattlerIndex): ArenaTag { +export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag { switch (tagType) { + case ArenaTagType.MIST: + return new MistTag(turnCount, sourceId, side); case ArenaTagType.MUD_SPORT: return new MudSportTag(turnCount, sourceId); case ArenaTagType.WATER_SPORT: return new WaterSportTag(turnCount, sourceId); case ArenaTagType.SPIKES: - return new SpikesTag(sourceId); + return new SpikesTag(sourceId, side); case ArenaTagType.TOXIC_SPIKES: - return new ToxicSpikesTag(sourceId); + return new ToxicSpikesTag(sourceId, side); case ArenaTagType.FUTURE_SIGHT: case ArenaTagType.DOOM_DESIRE: return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex); case ArenaTagType.STEALTH_ROCK: - return new StealthRockTag(sourceId); + return new StealthRockTag(sourceId, side); case ArenaTagType.TRICK_ROOM: return new TrickRoomTag(turnCount, sourceId); case ArenaTagType.GRAVITY: diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 5d7ce0d9ad6..31412db13da 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -564,6 +564,27 @@ export class ProtectedTag extends BattlerTag { } } +export class EnduringTag extends BattlerTag { + constructor(sourceMove: Moves) { + super(BattlerTagType.ENDURING, BattlerTagLapseType.CUSTOM, 0, sourceMove); + } + + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' braced\nitself!')); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType === BattlerTagLapseType.CUSTOM) { + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' endured\nthe hit!')); + return true; + } + + return super.lapse(pokemon, lapseType); + } +} + export class PerishSongTag extends BattlerTag { constructor(turnCount: integer) { super(BattlerTagType.PERISH_SONG, BattlerTagLapseType.TURN_END, turnCount, Moves.PERISH_SONG); @@ -743,6 +764,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc return new MagmaStormTag(turnCount, sourceId); case BattlerTagType.PROTECTED: return new ProtectedTag(sourceMove); + case BattlerTagType.ENDURING: + return new EnduringTag(sourceMove); case BattlerTagType.PERISH_SONG: return new PerishSongTag(turnCount); case BattlerTagType.TRUANT: diff --git a/src/data/enums/arena-tag-type.ts b/src/data/enums/arena-tag-type.ts new file mode 100644 index 00000000000..90c5b1dc554 --- /dev/null +++ b/src/data/enums/arena-tag-type.ts @@ -0,0 +1,14 @@ + +export enum ArenaTagType { + NONE = "NONE", + MUD_SPORT = "MUD_SPORT", + WATER_SPORT = "WATER_SPORT", + SPIKES = "SPIKES", + TOXIC_SPIKES = "TOXIC_SPIKES", + MIST = "MIST", + FUTURE_SIGHT = "FUTURE_SIGHT", + DOOM_DESIRE = "DOOM_DESIRE", + STEALTH_ROCK = "STEALTH_ROCK", + TRICK_ROOM = "TRICK_ROOM", + GRAVITY = "GRAVITY" +} diff --git a/src/data/enums/battler-tag-type.ts b/src/data/enums/battler-tag-type.ts index 3e5b30b6236..06e3c4e8186 100644 --- a/src/data/enums/battler-tag-type.ts +++ b/src/data/enums/battler-tag-type.ts @@ -21,6 +21,7 @@ export enum BattlerTagType { SAND_TOMB = "SAND_TOMB", MAGMA_STORM = "MAGMA_STORM", PROTECTED = "PROTECTED", + ENDURING = "ENDURING", PERISH_SONG = "PERISH_SONG", TRUANT = "TRUANT", SLOW_START = "SLOW_START", diff --git a/src/data/move.ts b/src/data/move.ts index ee89447280f..def9f6344a4 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,7 +1,7 @@ import { Moves } from "./enums/moves"; import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; import { BattleEndPhase, DamagePhase, MovePhase, NewBattlePhase, ObtainStatusEffectPhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../battle-phases"; -import { BattleStat } from "./battle-stat"; +import { BattleStat, getBattleStatName } from "./battle-stat"; import { EncoreTag } from "./battler-tags"; import { BattlerTagType } from "./enums/battler-tag-type"; import { getPokemonMessage } from "../messages"; @@ -10,11 +10,12 @@ import { StatusEffect, getStatusEffectDescriptor } from "./status-effect"; import { Type } from "./type"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; -import { ArenaTagType, ArenaTrapTag } from "./arena-tag"; +import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; +import { ArenaTagType } from "./enums/arena-tag-type"; import { Abilities, BlockRecoilDamageAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs } from "./ability"; import { PokemonHeldItemModifier } from "../modifier/modifier"; import { BattlerIndex } from "../battle"; -import { Stat } from "./pokemon-stat"; +import { Stat, getStatName } from "./pokemon-stat"; export enum MoveCategory { PHYSICAL, @@ -998,7 +999,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr { (args[0] as Utils.BooleanHolder).value = true; user.scene.queueMessage(getPokemonMessage(user, ` ${this.chargeText.replace('{TARGET}', target.name)}`)); user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); - user.scene.arena.addTag(this.tagType, 3, move.id, user.id, target.getBattlerIndex()); + user.scene.arena.addTag(this.tagType, 3, move.id, user.id, ArenaTagSide.BOTH, target.getBattlerIndex()); resolve(true); }); @@ -1012,23 +1013,25 @@ export class StatChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; public levels: integer; private condition: MoveConditionFunc; + private showMessage: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc) { + constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true) { super(selfTarget, MoveEffectTrigger.HIT); this.stats = typeof(stats) === 'number' ? [ stats as BattleStat ] : stats as BattleStat[]; this.levels = levels; this.condition = condition || null; + this.showMessage = showMessage; } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) return false; if (move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) { const levels = this.getLevels(user); - user.scene.unshiftPhase(new StatChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, levels)); + user.scene.unshiftPhase(new StatChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, levels, this.showMessage)); return true; } @@ -1061,6 +1064,27 @@ export class GrowthStatChangeAttr extends StatChangeAttr { } } +export class HalfHpStatMaxAttr extends StatChangeAttr { + constructor(stat: BattleStat) { + super(stat, 12, true, null, false); + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + return new Promise(resolve => { + user.damage(Math.floor(user.getMaxHp() / 2)); + user.updateInfo().then(() => { + const ret = super.apply(user, target, move, args); + user.scene.queueMessage(getPokemonMessage(user, ` cut its own hp\nand maximized its ${getBattleStatName(this.stats[0])}!`)); + resolve(ret); + }); + }); + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => user.getHpRatio() > 0.5 || user.summonData.battleStats[this.stats[0]] >= 6; + } +} + export class HpSplitAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { @@ -1568,8 +1592,8 @@ export class TrapAttr extends AddBattlerTagAttr { } export class ProtectAttr extends AddBattlerTagAttr { - constructor() { - super(BattlerTagType.PROTECTED, true); + constructor(tagType: BattlerTagType = BattlerTagType.PROTECTED) { + super(tagType, true); } getCondition(): MoveConditionFunc { @@ -1577,7 +1601,7 @@ export class ProtectAttr extends AddBattlerTagAttr { let timesUsed = 0; const moveHistory = user.getLastXMoves(); let turnMove: TurnMove; - while (moveHistory.length && (turnMove = moveHistory.shift()).move === move.id && turnMove.result === MoveResult.SUCCESS) + while (moveHistory.length && allMoves[(turnMove = moveHistory.shift()).move].getAttrs(ProtectAttr).find(pa => (pa as ProtectAttr).tagType === this.tagType) && turnMove.result === MoveResult.SUCCESS) timesUsed++; if (timesUsed) return !user.randSeedInt(Math.pow(2, timesUsed)); @@ -1586,6 +1610,12 @@ export class ProtectAttr extends AddBattlerTagAttr { } } +export class EndureAttr extends ProtectAttr { + constructor() { + super(BattlerTagType.ENDURING); + } +} + export class IgnoreAccuracyAttr extends AddBattlerTagAttr { constructor() { super(BattlerTagType.IGNORE_ACCURACY, true, false, 1); @@ -1635,12 +1665,14 @@ export class HitsTagAttr extends MoveAttr { export class AddArenaTagAttr extends MoveEffectAttr { public tagType: ArenaTagType; public turnCount: integer; + private failOnOverlap: boolean; - constructor(tagType: ArenaTagType, turnCount?: integer) { + constructor(tagType: ArenaTagType, turnCount?: integer, failOnOverlap: boolean = false) { super(true); this.tagType = tagType; this.turnCount = turnCount; + this.failOnOverlap = failOnOverlap; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -1648,12 +1680,18 @@ export class AddArenaTagAttr extends MoveEffectAttr { return false; if (move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) { - user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id); + user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); return true; } return false; } + + getCondition(): MoveConditionFunc { + return this.failOnOverlap + ? (user, target, move) => !user.scene.arena.getTagOnSide(this.tagType, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY) + : null; + } } export class AddArenaTrapTagAttr extends AddArenaTagAttr { @@ -1738,8 +1776,10 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { }; } - getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return -100; // Overridden in switch logic + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + if (this.batonPass) + return -100; // Overridden in switch logic + return this.user ? Math.floor(user.getHpRatio() * 20) : super.getUserBenefitScore(user, target, move); } } @@ -2249,7 +2289,8 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.BURN), new AttackMove(Moves.FLAMETHROWER, "Flamethrower", Type.FIRE, MoveCategory.SPECIAL, 90, 100, 15, 125, "The target is scorched with an intense blast of fire. This may also leave the target with a burn.", 10, 0, 1) .attr(StatusEffectAttr, StatusEffect.BURN), - new StatusMove(Moves.MIST, "Mist (N)", Type.ICE, -1, 30, -1, "The user cloaks itself and its allies in a white mist that prevents any of their stats from being lowered for five turns.", -1, 0, 1) + new StatusMove(Moves.MIST, "Mist", Type.ICE, -1, 30, -1, "The user cloaks itself and its allies in a white mist that prevents any of their stats from being lowered for five turns.", -1, 0, 1) + .attr(AddArenaTagAttr, ArenaTagType.MIST, 5, true) .target(MoveTarget.USER_SIDE), new AttackMove(Moves.WATER_GUN, "Water Gun", Type.WATER, MoveCategory.SPECIAL, 40, 100, 25, -1, "The target is blasted with a forceful shot of water.", -1, 0, 1), new AttackMove(Moves.HYDRO_PUMP, "Hydro Pump", Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, 142, "The target is blasted by a huge volume of water launched under great pressure.", -1, 0, 1), @@ -2582,7 +2623,8 @@ export function initMoves() { new AttackMove(Moves.FEINT_ATTACK, "Feint Attack", Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, "The user approaches the target disarmingly, then throws a sucker punch. This attack never misses.", -1, 0, 2), new StatusMove(Moves.SWEET_KISS, "Sweet Kiss", Type.FAIRY, 75, 10, -1, "The user kisses the target with a sweet, angelic cuteness that causes confusion.", -1, 0, 2) .attr(ConfuseAttr), - new SelfStatusMove(Moves.BELLY_DRUM, "Belly Drum (N)", Type.NORMAL, -1, 10, -1, "The user maximizes its Attack stat in exchange for HP equal to half its max HP.", -1, 0, 2), + new SelfStatusMove(Moves.BELLY_DRUM, "Belly Drum", Type.NORMAL, -1, 10, -1, "The user maximizes its Attack stat in exchange for HP equal to half its max HP.", -1, 0, 2) + .attr(HalfHpStatMaxAttr, BattleStat.ATK), new AttackMove(Moves.SLUDGE_BOMB, "Sludge Bomb", Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 148, "Unsanitary sludge is hurled at the target. This may also poison the target.", 30, 0, 2) .attr(StatusEffectAttr, StatusEffect.POISON) .ballBombMove(), @@ -2627,7 +2669,8 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.GIGA_DRAIN, "Giga Drain", Type.GRASS, MoveCategory.SPECIAL, 75, 100, 10, 111, "A nutrient-draining attack. The user's HP is restored by half the damage taken by the target.", -1, 0, 2) .attr(HitHealAttr), - new SelfStatusMove(Moves.ENDURE, "Endure (N)", Type.NORMAL, -1, 10, 47, "The user endures any attack with at least 1 HP. Its chance of failing rises if it is used in succession.", -1, 4, 2), + new SelfStatusMove(Moves.ENDURE, "Endure", Type.NORMAL, -1, 10, 47, "The user endures any attack with at least 1 HP. Its chance of failing rises if it is used in succession.", -1, 4, 2) + .attr(EndureAttr), new StatusMove(Moves.CHARM, "Charm", Type.FAIRY, 100, 20, 2, "The user gazes at the target rather charmingly, making it less wary. This harshly lowers the target's Attack stat.", -1, 0, 2) .attr(StatChangeAttr, BattleStat.ATK, -2), new AttackMove(Moves.ROLLOUT, "Rollout", Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, "The user continually rolls into the target over five turns. It becomes more powerful each time it hits.", -1, 0, 2) diff --git a/src/pokemon.ts b/src/pokemon.ts index 3584cb6130f..b7c58d596e9 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -22,7 +22,8 @@ import { BattlerTagType } from "./data/enums/battler-tag-type"; import { Species } from './data/enums/species'; import { WeatherType } from './data/weather'; import { TempBattleStat } from './data/temp-battle-stat'; -import { ArenaTagType, WeakenMoveTypeTag } from './data/arena-tag'; +import { WeakenMoveTypeTag } from './data/arena-tag'; +import { ArenaTagType } from "./data/enums/arena-tag-type"; import { Biome } from "./data/enums/biome"; import { Abilities, Ability, BattleStatMultiplierAbAttr, BlockCritAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, NonSuperEffectiveImmunityAbAttr, PreApplyBattlerTagAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPostDefendAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs } from './data/ability'; import PokemonData from './system/pokemon-data'; @@ -1115,7 +1116,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.hp > 1 && this.hp - damage <= 0 && !preventEndure) { const surviveDamage = new Utils.BooleanHolder(false); - this.scene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); + if (this.lapseTag(BattlerTagType.ENDURING)) + surviveDamage.value = true; + if (!surviveDamage.value) + this.scene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); if (surviveDamage.value) damage = this.hp - 1; }