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 {
PRE_APPLY,
PRE_DAMAGE,
POST_APPLY,
HIT
}
@ -2769,7 +2770,7 @@ export class RemoveScreensAttr extends MoveEffectAttr {
private targetBothSides: boolean;
constructor(targetBothSides: boolean = false) {
super(true, MoveEffectTrigger.PRE_APPLY);
super(true, MoveEffectTrigger.PRE_DAMAGE);
this.targetBothSides = targetBothSides;
}
@ -2778,6 +2779,9 @@ export class RemoveScreensAttr extends MoveEffectAttr {
if (!super.apply(user, target, move, args))
return false;
if ((args[0] as Utils.BooleanHolder).value) // isTypeImmune
return false;
if(this.targetBothSides){
user.scene.arena.removeTagOnSide(ArenaTagType.REFLECT, 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 {
private user: 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)
.attr(StatChangeAttr, BattleStat.SPDEF, -1)
.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),
new AttackMove(Moves.FLAMETHROWER, Type.FIRE, MoveCategory.SPECIAL, 90, 100, 15, 10, 0, 1)
.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)
.attr(RechargeAttr),
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)
.ignoresAbilities()
.partial(),

View File

@ -4,7 +4,7 @@ import { Variant, VariantSet, variantColorCache } from '#app/data/variant';
import { variantData } from '#app/data/variant';
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from '../ui/battle-info';
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 * as Utils from '../utils';
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];
}
apply(source: Pokemon, battlerMove: PokemonMove): HitResult {
apply(source: Pokemon, battlerMove: PokemonMove, firstHit: boolean): HitResult {
let result: HitResult;
const move = battlerMove.getMove();
let damage = new Utils.NumberHolder(0);
@ -1298,14 +1298,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
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 targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical));
const criticalMultiplier = isCritical ? 1.5 : 1;
const screenMultiplier = new Utils.NumberHolder(1);
if (!isCritical) {
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 matchesSourceType = sourceTypes[0] === type || (sourceTypes.length > 1 && sourceTypes[1] === type);
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(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);
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
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);
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
if (!isTypeImmune && fixedDamage.value) {
if (!isTypeImmune.value && fixedDamage.value) {
damage.value = fixedDamage.value;
isCritical = false;
result = HitResult.EFFECTIVE;

View File

@ -2425,7 +2425,7 @@ export class MoveEffectPhase extends PokemonPhase {
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);
@ -2648,62 +2648,78 @@ export class ShowAbilityPhase extends PokemonPhase {
export class StatChangePhase extends PokemonPhase {
private stats: BattleStat[];
private selfTarget: boolean;
private levels: integer;
private levels: Utils.IntegerHolder;
private showMessage: 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);
this.selfTarget = selfTarget;
this.stats = stats;
this.levels = levels;
this.levels = new Utils.IntegerHolder(levels);
this.showMessage = showMessage;
this.ignoreAbilities = ignoreAbilities;
this.doChangeSynchronously = doChangeSynchronously;
if (doChangeSynchronously)
this.applyChanges()
}
start() {
applyChanges() {
const pokemon = this.getPokemon();
if (!pokemon.isActive(true))
return this.end();
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);
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);
if (!cancelled.value && !this.selfTarget && this.levels < 0)
if (!cancelled.value && !this.selfTarget && this.levels.value < 0)
applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled);
return !cancelled.value;
});
const levels = new Utils.IntegerHolder(this.levels);
if (!this.ignoreAbilities)
applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, levels);
applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, this.levels);
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 = () => {
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)
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);
applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget)
applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, this.filteredStats, levels.value, this.selfTarget)
this.end();
};
if (relLevels.filter(l => l).length && this.scene.moveAnimations) {
if (this.relLevels.filter(l => l).length && this.scene.moveAnimations) {
pokemon.enableMask();
const pokemonMaskSprite = pokemon.maskSprite;
@ -2712,7 +2728,7 @@ export class StatChangePhase extends PokemonPhase {
const tileWidth = 156 * 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.setAlpha(0);
statSprite.setScale(6);