Merge branch 'pr/260'

This commit is contained in:
Chacolay 2024-04-29 11:36:09 -04:00
commit 362e42e933
3 changed files with 93 additions and 26 deletions

View File

@ -436,6 +436,7 @@ export abstract class MoveAttr {
export enum MoveEffectTrigger {
PRE_APPLY,
PRE_DAMAGE,
POST_APPLY,
HIT
}
@ -3020,7 +3021,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;
}
@ -3029,6 +3030,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);
@ -3049,6 +3053,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;
@ -5870,7 +5919,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';
@ -1220,7 +1220,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);
@ -1319,6 +1319,9 @@ 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;
@ -1326,7 +1329,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
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);
@ -1343,7 +1345,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);
@ -1362,7 +1364,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

@ -2427,7 +2427,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);
@ -2650,21 +2650,31 @@ 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();
let random = false;
@ -2683,38 +2693,44 @@ export class StatChangePhase extends PokemonPhase {
const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : this.getRandomStat()).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;
@ -2723,7 +2739,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);