[Move] Implement autotomization weight effects (#4228)
* Implement Autotomize * Another linting * Fix unit tests * Add nonnull after checking for null * Update autotomize test --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
This commit is contained in:
parent
d2ba9751b5
commit
b9b69ad834
|
@ -4296,6 +4296,10 @@ export class ReduceBerryUseThresholdAbAttr extends AbAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability attribute used for abilites that change the ability owner's weight
|
||||||
|
* Used for Heavy Metal (doubling weight) and Light Metal (halving weight)
|
||||||
|
*/
|
||||||
export class WeightMultiplierAbAttr extends AbAttr {
|
export class WeightMultiplierAbAttr extends AbAttr {
|
||||||
private multiplier: integer;
|
private multiplier: integer;
|
||||||
|
|
||||||
|
|
|
@ -2281,6 +2281,36 @@ export class TarShotTag extends BattlerTag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Battler Tag that keeps track of how many times the user has Autotomized
|
||||||
|
* Each count of Autotomization reduces the weight by 100kg
|
||||||
|
*/
|
||||||
|
export class AutotomizedTag extends BattlerTag {
|
||||||
|
public autotomizeCount: number = 0;
|
||||||
|
constructor(sourceMove: Moves = Moves.AUTOTOMIZE) {
|
||||||
|
super(BattlerTagType.AUTOTOMIZED, BattlerTagLapseType.CUSTOM, 1, sourceMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an autotomize count to the Pokemon. Each stack reduces weight by 100kg
|
||||||
|
* If the Pokemon is over 0.1kg it also displays a message.
|
||||||
|
* @param pokemon The Pokemon that is being autotomized
|
||||||
|
*/
|
||||||
|
onAdd(pokemon: Pokemon): void {
|
||||||
|
const minWeight = 0.1;
|
||||||
|
if (pokemon.getWeight() > minWeight) {
|
||||||
|
pokemon.scene.queueMessage(i18next.t("battlerTags:autotomizeOnAdd", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
this.autotomizeCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOverlap(pokemon: Pokemon): void {
|
||||||
|
this.onAdd(pokemon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SubstituteTag extends BattlerTag {
|
export class SubstituteTag extends BattlerTag {
|
||||||
/** The substitute's remaining HP. If HP is depleted, the Substitute fades. */
|
/** The substitute's remaining HP. If HP is depleted, the Substitute fades. */
|
||||||
public hp: number;
|
public hp: number;
|
||||||
|
@ -2568,6 +2598,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||||
return new GorillaTacticsTag();
|
return new GorillaTacticsTag();
|
||||||
case BattlerTagType.SUBSTITUTE:
|
case BattlerTagType.SUBSTITUTE:
|
||||||
return new SubstituteTag(sourceMove, sourceId);
|
return new SubstituteTag(sourceMove, sourceId);
|
||||||
|
case BattlerTagType.AUTOTOMIZED:
|
||||||
|
return new AutotomizedTag();
|
||||||
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
|
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
|
||||||
return new MysteryEncounterPostSummonTag();
|
return new MysteryEncounterPostSummonTag();
|
||||||
case BattlerTagType.HEAL_BLOCK:
|
case BattlerTagType.HEAL_BLOCK:
|
||||||
|
|
|
@ -8105,7 +8105,7 @@ export function initMoves() {
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status && (target.status.effect === StatusEffect.POISON || target.status.effect === StatusEffect.TOXIC) ? 2 : 1),
|
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status && (target.status.effect === StatusEffect.POISON || target.status.effect === StatusEffect.TOXIC) ? 2 : 1),
|
||||||
new SelfStatusMove(Moves.AUTOTOMIZE, Type.STEEL, -1, 15, -1, 0, 5)
|
new SelfStatusMove(Moves.AUTOTOMIZE, Type.STEEL, -1, 15, -1, 0, 5)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true)
|
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true)
|
||||||
.partial(),
|
.attr(AddBattlerTagAttr, BattlerTagType.AUTOTOMIZED, true),
|
||||||
new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5)
|
new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5)
|
||||||
.powderMove()
|
.powderMove()
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
||||||
|
|
|
@ -79,6 +79,7 @@ export enum BattlerTagType {
|
||||||
TAR_SHOT = "TAR_SHOT",
|
TAR_SHOT = "TAR_SHOT",
|
||||||
BURNED_UP = "BURNED_UP",
|
BURNED_UP = "BURNED_UP",
|
||||||
DOUBLE_SHOCKED = "DOUBLE_SHOCKED",
|
DOUBLE_SHOCKED = "DOUBLE_SHOCKED",
|
||||||
|
AUTOTOMIZED = "AUTOTOMIZED",
|
||||||
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
|
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
|
||||||
HEAL_BLOCK = "HEAL_BLOCK",
|
HEAL_BLOCK = "HEAL_BLOCK",
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
|
||||||
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
|
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
|
||||||
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
|
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
|
||||||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
||||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags";
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag } from "../data/battler-tags";
|
||||||
import { WeatherType } from "../data/weather";
|
import { WeatherType } from "../data/weather";
|
||||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability";
|
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability";
|
||||||
|
@ -1427,11 +1427,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first
|
||||||
|
* and then multiplicative modifiers happening after (Heavy Metal and Light Metal)
|
||||||
|
* @returns the kg of the Pokemon (minimum of 0.1)
|
||||||
|
*/
|
||||||
getWeight(): number {
|
getWeight(): number {
|
||||||
const weight = new Utils.NumberHolder(this.species.weight);
|
const autotomizedTag = this.getTag(AutotomizedTag);
|
||||||
|
let weightRemoved = 0;
|
||||||
|
if (!Utils.isNullOrUndefined(autotomizedTag)) {
|
||||||
|
weightRemoved = 100 * autotomizedTag!.autotomizeCount;
|
||||||
|
}
|
||||||
|
const minWeight = 0.1;
|
||||||
|
const weight = new Utils.NumberHolder(this.species.weight - weightRemoved);
|
||||||
|
|
||||||
// This will trigger the ability overlay so only call this function when necessary
|
// This will trigger the ability overlay so only call this function when necessary
|
||||||
applyAbAttrs(WeightMultiplierAbAttr, this, null, false, weight);
|
applyAbAttrs(WeightMultiplierAbAttr, this, null, false, weight);
|
||||||
return weight.value;
|
return Math.max(minWeight, weight.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -73,5 +73,6 @@
|
||||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!",
|
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!",
|
||||||
"substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!",
|
"substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!",
|
||||||
"substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!",
|
"substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!",
|
||||||
"substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!"
|
"substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!",
|
||||||
|
"autotomizeOnAdd": "{{pokemonNameWIthAffix}} became nimble!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import PartyUiHandler from "../ui/party-ui-handler";
|
||||||
import { getPokemonNameWithAffix } from "../messages";
|
import { getPokemonNameWithAffix } from "../messages";
|
||||||
import { EndEvolutionPhase } from "./end-evolution-phase";
|
import { EndEvolutionPhase } from "./end-evolution-phase";
|
||||||
import { EvolutionPhase } from "./evolution-phase";
|
import { EvolutionPhase } from "./evolution-phase";
|
||||||
|
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||||
|
|
||||||
export class FormChangePhase extends EvolutionPhase {
|
export class FormChangePhase extends EvolutionPhase {
|
||||||
private formChange: SpeciesFormChange;
|
private formChange: SpeciesFormChange;
|
||||||
|
@ -157,6 +158,7 @@ export class FormChangePhase extends EvolutionPhase {
|
||||||
}
|
}
|
||||||
|
|
||||||
end(): void {
|
end(): void {
|
||||||
|
this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED);
|
||||||
if (this.modal) {
|
if (this.modal) {
|
||||||
this.scene.ui.revertMode().then(() => {
|
this.scene.ui.revertMode().then(() => {
|
||||||
if (this.scene.ui.getMode() === Mode.PARTY) {
|
if (this.scene.ui.getMode() === Mode.PARTY) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { SemiInvulnerableTag } from "#app/data/battler-tags";
|
||||||
import { SpeciesFormChange, getSpeciesFormChangeMessage } from "#app/data/pokemon-forms";
|
import { SpeciesFormChange, getSpeciesFormChangeMessage } from "#app/data/pokemon-forms";
|
||||||
import { getTypeRgb } from "#app/data/type";
|
import { getTypeRgb } from "#app/data/type";
|
||||||
import { BattleSpec } from "#app/enums/battle-spec";
|
import { BattleSpec } from "#app/enums/battle-spec";
|
||||||
|
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||||
import Pokemon, { EnemyPokemon } from "#app/field/pokemon";
|
import Pokemon, { EnemyPokemon } from "#app/field/pokemon";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { BattlePhase } from "./battle-phase";
|
import { BattlePhase } from "./battle-phase";
|
||||||
|
@ -113,6 +114,7 @@ export class QuietFormChangePhase extends BattlePhase {
|
||||||
}
|
}
|
||||||
|
|
||||||
end(): void {
|
end(): void {
|
||||||
|
this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED);
|
||||||
if (this.pokemon.scene?.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && this.pokemon instanceof EnemyPokemon) {
|
if (this.pokemon.scene?.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && this.pokemon instanceof EnemyPokemon) {
|
||||||
this.scene.playBgm();
|
this.scene.playBgm();
|
||||||
this.scene.unshiftPhase(new PokemonHealPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getMaxHp(), null, false, false, false, true));
|
this.scene.unshiftPhase(new PokemonHealPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getMaxHp(), null, false, false, false, true));
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Autotomize", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.moveset([Moves.AUTOTOMIZE, Moves.KINGS_SHIELD, Moves.FALSE_SWIPE])
|
||||||
|
.battleType("single")
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.enemyMoveset(Moves.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Autotomize should reduce weight", async () => {
|
||||||
|
const baseDracozoltWeight = 190;
|
||||||
|
const oneAutotomizeDracozoltWeight = 90;
|
||||||
|
const twoAutotomizeDracozoltWeight = 0.1;
|
||||||
|
const threeAutotomizeDracozoltWeight = 0.1;
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([Species.DRACOZOLT]);
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
expect(playerPokemon.getWeight()).toBe(baseDracozoltWeight);
|
||||||
|
game.move.select(Moves.AUTOTOMIZE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(oneAutotomizeDracozoltWeight);
|
||||||
|
|
||||||
|
game.move.select(Moves.AUTOTOMIZE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(twoAutotomizeDracozoltWeight);
|
||||||
|
|
||||||
|
|
||||||
|
game.move.select(Moves.AUTOTOMIZE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(threeAutotomizeDracozoltWeight);
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("Changing forms should revert weight", async () => {
|
||||||
|
const baseAegislashWeight = 53;
|
||||||
|
const autotomizeAegislashWeight = 0.1;
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([Species.AEGISLASH]);
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
|
||||||
|
expect(playerPokemon.getWeight()).toBe(baseAegislashWeight);
|
||||||
|
|
||||||
|
game.move.select(Moves.AUTOTOMIZE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight);
|
||||||
|
|
||||||
|
// Transform to sword form
|
||||||
|
game.move.select(Moves.FALSE_SWIPE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(baseAegislashWeight);
|
||||||
|
|
||||||
|
game.move.select(Moves.AUTOTOMIZE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight);
|
||||||
|
|
||||||
|
// Transform to shield form
|
||||||
|
game.move.select(Moves.KINGS_SHIELD);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(baseAegislashWeight);
|
||||||
|
|
||||||
|
game.move.select(Moves.AUTOTOMIZE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight);
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
it("Autotomize should interact with light metal correctly", async () => {
|
||||||
|
const baseLightGroudonWeight = 475;
|
||||||
|
const autotomizeLightGroudonWeight = 425;
|
||||||
|
game.override.ability(Abilities.LIGHT_METAL);
|
||||||
|
await game.classicMode.startBattle([Species.GROUDON]);
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
expect(playerPokemon.getWeight()).toBe(baseLightGroudonWeight);
|
||||||
|
game.move.select(Moves.AUTOTOMIZE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(playerPokemon.getWeight()).toBe(autotomizeLightGroudonWeight);
|
||||||
|
}, TIMEOUT);
|
||||||
|
});
|
Loading…
Reference in New Issue