pre_damage MoveEffectTrigger

This commit is contained in:
cornfish 2024-04-20 12:13:41 -06:00 committed by cornfish
parent 92caf6059b
commit 5717aebe9d
3 changed files with 95 additions and 28 deletions

View File

@ -433,6 +433,7 @@ export abstract class MoveAttr {
export enum MoveEffectTrigger { export enum MoveEffectTrigger {
PRE_APPLY, PRE_APPLY,
PRE_DAMAGE,
POST_APPLY, POST_APPLY,
HIT HIT
} }
@ -2769,7 +2770,7 @@ export class RemoveScreensAttr extends MoveEffectAttr {
private targetBothSides: boolean; private targetBothSides: boolean;
constructor(targetBothSides: boolean = false) { constructor(targetBothSides: boolean = false) {
super(true, MoveEffectTrigger.PRE_APPLY); super(true, MoveEffectTrigger.PRE_DAMAGE);
this.targetBothSides = targetBothSides; this.targetBothSides = targetBothSides;
} }
@ -2778,6 +2779,9 @@ export class RemoveScreensAttr extends MoveEffectAttr {
if (!super.apply(user, target, move, args)) if (!super.apply(user, target, move, args))
return false; return false;
if ((args[0] as Utils.BooleanHolder).value) // isTypeImmune
return false;
if(this.targetBothSides){ if(this.targetBothSides){
user.scene.arena.removeTagOnSide(ArenaTagType.REFLECT, ArenaTagSide.PLAYER); user.scene.arena.removeTagOnSide(ArenaTagType.REFLECT, ArenaTagSide.PLAYER);
user.scene.arena.removeTagOnSide(ArenaTagType.LIGHT_SCREEN, ArenaTagSide.PLAYER); user.scene.arena.removeTagOnSide(ArenaTagType.LIGHT_SCREEN, ArenaTagSide.PLAYER);
@ -2798,6 +2802,51 @@ export class RemoveScreensAttr extends MoveEffectAttr {
} }
} }
export class StealStatBoostsAttr extends MoveEffectAttr {
constructor() {
super(false, MoveEffectTrigger.PRE_DAMAGE);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args))
return false;
if ((args[0] as Utils.BooleanHolder).value) // isTypeImmune
return false;
let stoleStats = false;
let stolenStatBoosts = target.summonData.battleStats.map(stat => {
if (stat > 0) {
stoleStats = true;
return stat;
}
return 0;
});
if (!stoleStats)
return false;
// directly set to 0 as opposed to "change" them to ignore abilities
for (let s = 0; s < target.summonData.battleStats.length; s++)
target.summonData.battleStats[s] = Math.min(target.summonData.battleStats[s], 0);
target.scene.queueMessage(getPokemonMessage(target, `'s stat boosts\nwere eliminated!`));
// accounting for StatChangePhase requiring a list of stats and a single amount to change by
let statsByLevel = {1: [], 2: [], 3: [], 4: [], 5: [], 6: []};
for (let [stat, boost] of stolenStatBoosts.entries()) {
statsByLevel[boost]?.push(stat);
}
for (let level = 1; level <= 6; level++) {
if(statsByLevel[level].empty)
continue
// stat change must be immediate to be ready for damage calculation
user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), true, statsByLevel[level], level, true, false, true));
}
return true;
}
}
export class ForceSwitchOutAttr extends MoveEffectAttr { export class ForceSwitchOutAttr extends MoveEffectAttr {
private user: boolean; private user: boolean;
private batonPass: boolean; private batonPass: boolean;
@ -3818,7 +3867,7 @@ export function initMoves() {
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatChangeAttr, BattleStat.SPDEF, -1) .attr(StatChangeAttr, BattleStat.SPDEF, -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.EMBER, Type.FIRE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 1) new AttackMove(Moves.EMBER, Type.FIRE, MoveCategory.SPECIAL, 40, 100, 25, 100, 0, 1)
.attr(StatusEffectAttr, StatusEffect.BURN), .attr(StatusEffectAttr, StatusEffect.BURN),
new AttackMove(Moves.FLAMETHROWER, Type.FIRE, MoveCategory.SPECIAL, 90, 100, 15, 10, 0, 1) new AttackMove(Moves.FLAMETHROWER, Type.FIRE, MoveCategory.SPECIAL, 90, 100, 15, 10, 0, 1)
.attr(StatusEffectAttr, StatusEffect.BURN), .attr(StatusEffectAttr, StatusEffect.BURN),
@ -5592,7 +5641,7 @@ export function initMoves() {
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7) new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
.attr(RechargeAttr), .attr(RechargeAttr),
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7) new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
.partial(), .attr(StealStatBoostsAttr),
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
.ignoresAbilities() .ignoresAbilities()
.partial(), .partial(),

View File

@ -4,7 +4,7 @@ import { Variant, VariantSet, variantColorCache } from '#app/data/variant';
import { variantData } from '#app/data/variant'; import { variantData } from '#app/data/variant';
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from '../ui/battle-info'; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from '../ui/battle-info';
import { Moves } from "../data/enums/moves"; import { Moves } from "../data/enums/moves";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveTypeAttr, VariableMoveCategoryAttr } from "../data/move"; import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveTypeAttr, VariableMoveCategoryAttr, MoveEffectTrigger } from "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, starterPassiveAbilities } from '../data/pokemon-species'; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, starterPassiveAbilities } from '../data/pokemon-species';
import * as Utils from '../utils'; import * as Utils from '../utils';
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from '../data/type'; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from '../data/type';
@ -1199,7 +1199,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1]; return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1];
} }
apply(source: Pokemon, battlerMove: PokemonMove): HitResult { apply(source: Pokemon, battlerMove: PokemonMove, firstHit: boolean): HitResult {
let result: HitResult; let result: HitResult;
const move = battlerMove.getMove(); const move = battlerMove.getMove();
let damage = new Utils.NumberHolder(0); let damage = new Utils.NumberHolder(0);
@ -1298,6 +1298,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
isCritical = false; isCritical = false;
} }
} }
const isTypeImmune = new Utils.BooleanHolder((typeMultiplier.value * arenaAttackTypeMultiplier) === 0);
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.PRE_DAMAGE && (!attr.firstHitOnly || firstHit), source, this, move, isTypeImmune);
const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, null, isCritical)); const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, null, isCritical));
const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical)); const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical));
const criticalMultiplier = isCritical ? 1.5 : 1; const criticalMultiplier = isCritical ? 1.5 : 1;
@ -1305,7 +1308,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!isCritical) { if (!isCritical) {
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, move.category, this.scene.currentBattle.double, screenMultiplier); this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, move.category, this.scene.currentBattle.double, screenMultiplier);
} }
const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier) === 0;
const sourceTypes = source.getTypes(); const sourceTypes = source.getTypes();
const matchesSourceType = sourceTypes[0] === type || (sourceTypes.length > 1 && sourceTypes[1] === type); const matchesSourceType = sourceTypes[0] === type || (sourceTypes.length > 1 && sourceTypes[1] === type);
let stabMultiplier = new Utils.NumberHolder(1); let stabMultiplier = new Utils.NumberHolder(1);
@ -1322,7 +1324,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
if (!isTypeImmune) { if (!isTypeImmune.value) {
damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier * screenMultiplier.value * ((this.scene.randBattleSeedInt(15) + 85) / 100) * criticalMultiplier); damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier * screenMultiplier.value * ((this.scene.randBattleSeedInt(15) + 85) / 100) * criticalMultiplier);
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) { if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
const burnDamageReductionCancelled = new Utils.BooleanHolder(false); const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
@ -1341,7 +1343,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const fixedDamage = new Utils.IntegerHolder(0); const fixedDamage = new Utils.IntegerHolder(0);
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
if (!isTypeImmune && fixedDamage.value) { if (!isTypeImmune.value && fixedDamage.value) {
damage.value = fixedDamage.value; damage.value = fixedDamage.value;
isCritical = false; isCritical = false;
result = HitResult.EFFECTIVE; result = HitResult.EFFECTIVE;

View File

@ -2425,7 +2425,7 @@ export class MoveEffectPhase extends PokemonPhase {
moveHistoryEntry.result = MoveResult.SUCCESS; moveHistoryEntry.result = MoveResult.SUCCESS;
const hitResult = !isProtected ? target.apply(user, this.move) : HitResult.NO_EFFECT; const hitResult = !isProtected ? target.apply(user, this.move, firstHit) : HitResult.NO_EFFECT;
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
@ -2648,62 +2648,78 @@ export class ShowAbilityPhase extends PokemonPhase {
export class StatChangePhase extends PokemonPhase { export class StatChangePhase extends PokemonPhase {
private stats: BattleStat[]; private stats: BattleStat[];
private selfTarget: boolean; private selfTarget: boolean;
private levels: integer; private levels: Utils.IntegerHolder;
private showMessage: boolean; private showMessage: boolean;
private ignoreAbilities: boolean; private ignoreAbilities: boolean;
private doChangeSynchronously: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false) { private relLevels: number[];
private filteredStats: BattleStat[];
/**
* @param doChangeSynchronously save the stat changes before the phase gets queued, only tested for use with Moves.SPECTRAL_THIEF
*/
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, doChangeSynchronously: boolean = false) {
super(scene, battlerIndex); super(scene, battlerIndex);
this.selfTarget = selfTarget; this.selfTarget = selfTarget;
this.stats = stats; this.stats = stats;
this.levels = levels; this.levels = new Utils.IntegerHolder(levels);
this.showMessage = showMessage; this.showMessage = showMessage;
this.ignoreAbilities = ignoreAbilities; this.ignoreAbilities = ignoreAbilities;
this.doChangeSynchronously = doChangeSynchronously;
if (doChangeSynchronously)
this.applyChanges()
} }
start() { applyChanges() {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (!pokemon.isActive(true)) if (!pokemon.isActive(true))
return this.end(); return this.end();
const allStats = Utils.getEnumValues(BattleStat); const allStats = Utils.getEnumValues(BattleStat);
const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : allStats[pokemon.randSeedInt(BattleStat.SPD + 1)]).filter(stat => { this.filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : allStats[pokemon.randSeedInt(BattleStat.SPD + 1)]).filter(stat => {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
if (!this.selfTarget && this.levels < 0) if (!this.selfTarget && this.levels.value < 0)
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
if (!cancelled.value && !this.selfTarget && this.levels < 0) if (!cancelled.value && !this.selfTarget && this.levels.value < 0)
applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled); applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled);
return !cancelled.value; return !cancelled.value;
}); });
const levels = new Utils.IntegerHolder(this.levels);
if (!this.ignoreAbilities) if (!this.ignoreAbilities)
applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, levels); applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, this.levels);
const battleStats = this.getPokemon().summonData.battleStats; const battleStats = this.getPokemon().summonData.battleStats;
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]); this.relLevels = this.filteredStats.map(stat => (this.levels.value >= 1 ? Math.min(battleStats[stat] + this.levels.value, 6) : Math.max(battleStats[stat] + this.levels.value, -6)) - battleStats[stat]);
for (let stat of this.filteredStats)
pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + this.levels.value, 6), -6);
}
start() {
const pokemon = this.getPokemon();
const levels = this.levels
if(!this.doChangeSynchronously)
this.applyChanges();
const end = () => { const end = () => {
if (this.showMessage) { if (this.showMessage) {
const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels); const messages = this.getStatChangeMessages(this.filteredStats, levels.value, this.relLevels);
for (let message of messages) for (let message of messages)
this.scene.queueMessage(message); this.scene.queueMessage(message);
} }
for (let stat of filteredStats) applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, this.filteredStats, levels.value, this.selfTarget)
pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6);
applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget)
this.end(); this.end();
}; };
if (relLevels.filter(l => l).length && this.scene.moveAnimations) { if (this.relLevels.filter(l => l).length && this.scene.moveAnimations) {
pokemon.enableMask(); pokemon.enableMask();
const pokemonMaskSprite = pokemon.maskSprite; const pokemonMaskSprite = pokemon.maskSprite;
@ -2712,7 +2728,7 @@ export class StatChangePhase extends PokemonPhase {
const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale(); const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale();
const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale(); const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale();
const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, 'battle_stats', filteredStats.length > 1 ? 'mix' : BattleStat[filteredStats[0]].toLowerCase()); const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, 'battle_stats', this.filteredStats.length > 1 ? 'mix' : BattleStat[this.filteredStats[0]].toLowerCase());
statSprite.setPipeline(this.scene.fieldSpritePipeline); statSprite.setPipeline(this.scene.fieldSpritePipeline);
statSprite.setAlpha(0); statSprite.setAlpha(0);
statSprite.setScale(6); statSprite.setScale(6);