Skip to content

Commit

Permalink
[Move] Update Tera Starstorm (still Partial), Readd Partial tag to Te…
Browse files Browse the repository at this point in the history
…ra Blast (#4549)

* fully implement tera starstorm

* add docs

* add tests

* add override keyword

* account for fusion

* swap party positions

* add partial tag to tera blast

* address comments
  • Loading branch information
torranx authored Oct 3, 2024
1 parent c58b5e9 commit 76e25a6
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 6 deletions.
46 changes: 40 additions & 6 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3942,7 +3942,14 @@ export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr {
}
}

export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
/**
* Attribute used for tera moves that change category based on the user's Atk and SpAtk stats
* Note: Currently, `getEffectiveStat` does not ignore all abilities that affect stats except those
* with the attribute of `StatMultiplierAbAttr`
* TODO: Remove the `.partial()` tag from Tera Blast and Tera Starstorm when the above issue is resolved
* @extends VariableMoveCategoryAttr
*/
export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder);

Expand Down Expand Up @@ -4031,6 +4038,30 @@ export class VariableMoveTypeAttr extends MoveAttr {
}
}

/**
* Attribute used for Tera Starstorm that changes the move type to Stellar
* @extends VariableMoveTypeAttr
*/
export class TeraStarstormTypeAttr extends VariableMoveTypeAttr {
/**
*
* @param user the {@linkcode Pokemon} using the move
* @param target n/a
* @param move n/a
* @param args[0] {@linkcode Utils.NumberHolder} the move type
* @returns `true` if the move type is changed to {@linkcode Type.STELLAR}, `false` otherwise
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (user.isTerastallized() && (user.hasFusionSpecies(Species.TERAPAGOS) || user.species.speciesId === Species.TERAPAGOS)) {
const moveType = args[0] as Utils.NumberHolder;

moveType.value = Type.STELLAR;
return true;
}
return false;
}
}

export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const moveType = args[0];
Expand Down Expand Up @@ -9190,7 +9221,7 @@ export function initMoves() {
.attr(HalfSacrificialAttr),
new AttackMove(Moves.EXPANDING_FORCE, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 8)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.PSYCHIC && user.isGrounded() ? 1.5 : 1)
.attr(VariableTargetAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.PSYCHIC && user.isGrounded() ? 6 : 3),
.attr(VariableTargetAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.PSYCHIC && user.isGrounded() ? MoveTarget.ALL_NEAR_ENEMIES : MoveTarget.NEAR_OTHER),
new AttackMove(Moves.STEEL_ROLLER, Type.STEEL, MoveCategory.PHYSICAL, 130, 100, 5, -1, 0, 8)
.attr(ClearTerrainAttr)
.condition((user, target, move) => !!user.scene.arena.terrain),
Expand Down Expand Up @@ -9464,10 +9495,11 @@ export function initMoves() {
.unimplemented(),
End Unused */
new AttackMove(Moves.TERA_BLAST, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 9)
.attr(TeraBlastCategoryAttr)
.attr(TeraMoveCategoryAttr)
.attr(TeraBlastTypeAttr)
.attr(TeraBlastPowerAttr)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)),
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR))
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP)
.condition(failIfLastCondition),
Expand Down Expand Up @@ -9657,8 +9689,10 @@ export function initMoves() {
.attr(ElectroShotChargeAttr)
.ignoresVirtual(),
new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(TeraBlastCategoryAttr)
.partial(),
.attr(TeraMoveCategoryAttr)
.attr(TeraStarstormTypeAttr)
.attr(VariableTargetAttr, (user, target, move) => (user.hasFusionSpecies(Species.TERAPAGOS) || user.species.speciesId === Species.TERAPAGOS) && user.isTerastallized() ? MoveTarget.ALL_NEAR_ENEMIES : MoveTarget.NEAR_OTHER)
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
new AttackMove(Moves.FICKLE_BEAM, Type.DRAGON, MoveCategory.SPECIAL, 80, 100, 5, 30, 0, 9)
.attr(PreMoveMessageAttr, doublePowerChanceMessageFunc)
.attr(DoublePowerChanceAttr),
Expand Down
9 changes: 9 additions & 0 deletions src/field/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return !!this.fusionSpecies;
}

/**
* Checks if the {@linkcode Pokemon} has a fusion with the specified {@linkcode Species}.
* @param species the pokemon {@linkcode Species} to check
* @returns `true` if the {@linkcode Pokemon} has a fusion with the specified {@linkcode Species}, `false` otherwise
*/
hasFusionSpecies(species: Species): boolean {
return this.fusionSpecies?.speciesId === species;
}

abstract isBoss(): boolean;

getMoveset(ignoreOverride?: boolean): (PokemonMove | null)[] {
Expand Down
98 changes: 98 additions & 0 deletions src/test/moves/tera_starstorm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { BattlerIndex } from "#app/battle";
import { Type } from "#app/data/type";
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, vi } from "vitest";

describe("Moves - Tera Starstorm", () => {
let phaserGame: Phaser.Game;
let game: GameManager;

beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});

afterEach(() => {
game.phaseInterceptor.restoreOg();
});

beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.TERA_STARSTORM, Moves.SPLASH])
.battleType("double")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(30)
.enemySpecies(Species.MAGIKARP)
.startingHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]);
});

it("changes type to Stellar when used by Terapagos in its Stellar Form", async () => {
game.override.battleType("single");
await game.classicMode.startBattle([Species.TERAPAGOS]);

const terapagos = game.scene.getPlayerPokemon()!;

vi.spyOn(terapagos, "getMoveType");

game.move.select(Moves.TERA_STARSTORM);
await game.phaseInterceptor.to("TurnEndPhase");

expect(terapagos.isTerastallized()).toBe(true);
expect(terapagos.getMoveType).toHaveReturnedWith(Type.STELLAR);
});

it("targets both opponents in a double battle when used by Terapagos in its Stellar Form", async () => {
await game.classicMode.startBattle([Species.MAGIKARP, Species.TERAPAGOS]);

game.move.select(Moves.TERA_STARSTORM, 0, BattlerIndex.ENEMY);
game.move.select(Moves.TERA_STARSTORM, 1);

await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);

const enemyField = game.scene.getEnemyField();

// Pokemon other than Terapagos should not be affected - only hits one target
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyField.some(pokemon => pokemon.isFullHp())).toBe(true);

// Terapagos in Stellar Form should hit both targets
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyField.every(pokemon => pokemon.isFullHp())).toBe(false);
});

it("applies the effects when Terapagos in Stellar Form is fused with another Pokemon", async () => {
await game.classicMode.startBattle([Species.TERAPAGOS, Species.CHARMANDER, Species.MAGIKARP]);

const fusionedMon = game.scene.getParty()[0];
const magikarp = game.scene.getParty()[2];

// Fuse party members (taken from PlayerPokemon.fuse(...) function)
fusionedMon.fusionSpecies = magikarp.species;
fusionedMon.fusionFormIndex = magikarp.formIndex;
fusionedMon.fusionAbilityIndex = magikarp.abilityIndex;
fusionedMon.fusionShiny = magikarp.shiny;
fusionedMon.fusionVariant = magikarp.variant;
fusionedMon.fusionGender = magikarp.gender;
fusionedMon.fusionLuck = magikarp.luck;

vi.spyOn(fusionedMon, "getMoveType");

game.move.select(Moves.TERA_STARSTORM, 0);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("TurnEndPhase");

// Fusion and terastallized
expect(fusionedMon.isFusion()).toBe(true);
expect(fusionedMon.isTerastallized()).toBe(true);
// Move effects should be applied
expect(fusionedMon.getMoveType).toHaveReturnedWith(Type.STELLAR);
expect(game.scene.getEnemyField().every(pokemon => pokemon.isFullHp())).toBe(false);
});
});

0 comments on commit 76e25a6

Please sign in to comment.