Compare commits
6 Commits
e9d17f0605
...
1487d7f51c
Author | SHA1 | Date | |
---|---|---|---|
|
1487d7f51c | ||
|
0241a0a086 | ||
|
3a167610cf | ||
|
6b21a777a1 | ||
|
0e59e74197 | ||
|
cd489c6a60 |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 8.3 KiB |
@ -3784,6 +3784,30 @@ export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the power of Tera Blast if the user is Terastallized into Stellar type
|
||||
* @extends VariablePowerAttr
|
||||
*/
|
||||
export class TeraBlastPowerAttr extends VariablePowerAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} Pokemon using the move
|
||||
* @param target {@linkcode Pokemon} N/A
|
||||
* @param move {@linkcode Move} {@linkcode Move.TERA_BLAST}
|
||||
* @param {any[]} args N/A
|
||||
* @returns true or false
|
||||
*/
|
||||
const power = args[0] as Utils.NumberHolder;
|
||||
if (user.isTerastallized() && move.type === Type.STELLAR) {
|
||||
//200 instead of 100 to reflect lack of stellar being 2x dmg on any type
|
||||
power.value = 200;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the move category to status when used on the ally
|
||||
* @extends VariableMoveCategoryAttr
|
||||
@ -4037,6 +4061,28 @@ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the type of Tera Blast to match the user's tera type
|
||||
* @extends VariableMoveTypeAttr
|
||||
*/
|
||||
export class TeraBlastTypeAttr extends VariableMoveTypeAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} the user's type is checked
|
||||
* @param target {@linkcode Pokemon} N/A
|
||||
* @param move {@linkcode Move} {@linkcode Move.TeraBlastTypeAttr}
|
||||
* @param {any[]} args N/A
|
||||
* @returns true or false
|
||||
*/
|
||||
if (user.isTerastallized()) {
|
||||
move.type = user.getTeraType(); //changes move type to tera type
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class MatchUserTypeAttr extends VariableMoveTypeAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const userTypes = user.getTypes(true);
|
||||
@ -8791,7 +8837,10 @@ export function initMoves() {
|
||||
End Unused */
|
||||
new AttackMove(Moves.TERA_BLAST, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 9)
|
||||
.attr(TeraBlastCategoryAttr)
|
||||
.unimplemented(),
|
||||
.attr(TeraBlastTypeAttr)
|
||||
.attr(TeraBlastPowerAttr)
|
||||
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR))
|
||||
.partial(),
|
||||
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
|
||||
.attr(ProtectAttr, BattlerTagType.SILK_TRAP),
|
||||
new AttackMove(Moves.AXE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 90, 10, 30, 0, 9)
|
||||
|
@ -584,6 +584,10 @@ export class Arena {
|
||||
return this.getTagOnSide(tagType, ArenaTagSide.BOTH);
|
||||
}
|
||||
|
||||
hasTag(tagType: ArenaTagType) : boolean {
|
||||
return !!this.getTag(tagType);
|
||||
}
|
||||
|
||||
getTagOnSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide): ArenaTag | undefined {
|
||||
return typeof(tagType) === "string"
|
||||
? this.tags.find(t => t.tagType === tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side))
|
||||
|
@ -978,12 +978,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// this.scene potentially can be undefined for a fainted pokemon in doubles
|
||||
// use optional chaining to avoid runtime errors
|
||||
if (forDefend && (this.getTag(GroundedTag) || this.scene?.arena.getTag(ArenaTagType.GRAVITY))) {
|
||||
const flyingIndex = types.indexOf(Type.FLYING);
|
||||
if (flyingIndex > -1) {
|
||||
types.splice(flyingIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!types.length) { // become UNKNOWN if no types are present
|
||||
types.push(Type.UNKNOWN);
|
||||
@ -1272,6 +1266,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.isTerastallized() ? 2 : 1;
|
||||
}
|
||||
const types = this.getTypes(true, true);
|
||||
const arena = this.scene.arena;
|
||||
|
||||
// Handle flying v ground type immunity without removing flying type so effective types are still effective
|
||||
// Related to https://github.com/pagefaultgames/pokerogue/issues/524
|
||||
if (moveType === Type.GROUND && (this.isGrounded() || arena.hasTag(ArenaTagType.GRAVITY))) {
|
||||
const flyingIndex = types.indexOf(Type.FLYING);
|
||||
if (flyingIndex > -1) {
|
||||
types.splice(flyingIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let multiplier = types.map(defType => {
|
||||
if (source) {
|
||||
@ -1293,7 +1297,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}).reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier;
|
||||
|
||||
// Handle strong winds lowering effectiveness of types super effective against pure flying
|
||||
if (!ignoreStrongWinds && this.scene.arena.weather?.weatherType === WeatherType.STRONG_WINDS && !this.scene.arena.weather.isEffectSuppressed(this.scene) && this.isOfType(Type.FLYING) && getTypeDamageMultiplier(moveType, Type.FLYING) === 2) {
|
||||
if (!ignoreStrongWinds && arena.weather?.weatherType === WeatherType.STRONG_WINDS && !arena.weather.isEffectSuppressed(this.scene) && this.isOfType(Type.FLYING) && getTypeDamageMultiplier(moveType, Type.FLYING) === 2) {
|
||||
multiplier /= 2;
|
||||
if (!simulated) {
|
||||
this.scene.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
||||
|
@ -69,5 +69,5 @@ export const battlerTags: SimpleTranslationEntries = {
|
||||
"saltCuredLapse": "{{pokemonNameWithAffix}} wurde durch {{moveName}} verletzt!",
|
||||
"cursedOnAdd": "{{pokemonNameWithAffix}} nimmt einen Teil seiner KP und legt einen Fluch auf {{pokemonName}}!",
|
||||
"cursedLapse": "{{pokemonNameWithAffix}} wurde durch den Fluch verletzt!",
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} hortet {{stockpiledCount}}!",
|
||||
} as const;
|
||||
|
@ -7,7 +7,7 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
|
||||
*/
|
||||
export const starterSelectUiHandler: SimpleTranslationEntries = {
|
||||
"confirmStartTeam": "Mit diesen Pokémon losziehen?",
|
||||
"confirmExit": "Do you want to exit?",
|
||||
"confirmExit": "Willst du zurück?",
|
||||
"invalidParty": "Das ist kein gültiges Team!",
|
||||
"gen1": "I",
|
||||
"gen2": "II",
|
||||
@ -28,8 +28,8 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
|
||||
"toggleIVs": "DVs anzeigen/verbergen",
|
||||
"manageMoves": "Attacken ändern",
|
||||
"manageNature": "Wesen ändern",
|
||||
"addToFavorites": "Add to Favorites",
|
||||
"removeFromFavorites": "Remove from Favorites",
|
||||
"addToFavorites": "Zu Favoriten hinzufügen",
|
||||
"removeFromFavorites": "Von Favoriten entfernen",
|
||||
"useCandies": "Bonbons verwenden",
|
||||
"selectNature": "Wähle das neue Wesen.",
|
||||
"selectMoveSwapOut": "Wähle die zu ersetzende Attacke.",
|
||||
|
@ -8,7 +8,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
||||
"goingAllOutForAttack": "{{pokemonName}}[[는]]\n전력을 다하기 시작했다!",
|
||||
"regainedHealth": "{{pokemonName}}[[는]]\n기력을 회복했다!",
|
||||
"keptGoingAndCrashed": "{{pokemonName}}[[는]]\n의욕이 넘쳐서 땅에 부딪쳤다!",
|
||||
"fled": "{{pokemonName}}[[는]]\N도망쳤다!",
|
||||
"fled": "{{pokemonName}}[[는]]\n도망쳤다!",
|
||||
"cannotBeSwitchedOut": "{{pokemonName}}[[를]]\n돌아오게 할 수 없습니다!",
|
||||
"swappedAbilitiesWithTarget": "{{pokemonName}}[[는]]\n서로의 특성을 교체했다!",
|
||||
"coinsScatteredEverywhere": "돈이 주위에 흩어졌다!",
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
import { Abilities } from "#app/enums/abilities.js";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type.js";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { getMovePosition } from "#test/utils/gameManagerUtils";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { getMovePosition } from "#test/utils/gameManagerUtils";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase.js";
|
||||
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||
|
||||
describe("Arena - Gravity", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -26,14 +27,17 @@ describe("Arena - Gravity", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleType("single");
|
||||
game.override.moveset([Moves.TACKLE, Moves.GRAVITY, Moves.FISSURE]);
|
||||
game.override.ability(Abilities.UNNERVE);
|
||||
game.override.enemyAbility(Abilities.BALL_FETCH);
|
||||
game.override.enemySpecies(Species.SHUCKLE);
|
||||
game.override.enemyMoveset(new Array(4).fill(Moves.SPLASH));
|
||||
game.override
|
||||
.battleType("single")
|
||||
.moveset([Moves.TACKLE, Moves.GRAVITY, Moves.FISSURE])
|
||||
.ability(Abilities.UNNERVE)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemySpecies(Species.SHUCKLE)
|
||||
.enemyMoveset(SPLASH_ONLY);
|
||||
});
|
||||
|
||||
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
||||
|
||||
it("non-OHKO move accuracy is multiplied by 1.67", async () => {
|
||||
const moveToCheck = allMoves[Moves.TACKLE];
|
||||
|
||||
@ -77,4 +81,65 @@ describe("Arena - Gravity", () => {
|
||||
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(30);
|
||||
});
|
||||
|
||||
describe("Against flying types", () => {
|
||||
it("can be hit by ground-type moves now", async () => {
|
||||
game.override
|
||||
.startingLevel(5)
|
||||
.enemyLevel(5)
|
||||
.enemySpecies(Species.PIDGEOT)
|
||||
.moveset([Moves.GRAVITY, Moves.EARTHQUAKE]);
|
||||
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
|
||||
const pidgeot = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
|
||||
// Try earthquake on 1st turn (fails!);
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.EARTHQUAKE));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(0);
|
||||
|
||||
// Setup Gravity on 2nd turn
|
||||
await game.toNextTurn();
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.GRAVITY));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use ground move on 3rd turn
|
||||
await game.toNextTurn();
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.EARTHQUAKE));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(1);
|
||||
});
|
||||
|
||||
it("keeps super-effective moves super-effective after using gravity", async () => {
|
||||
game.override
|
||||
.startingLevel(5)
|
||||
.enemyLevel(5)
|
||||
.enemySpecies(Species.PIDGEOT)
|
||||
.moveset([Moves.GRAVITY, Moves.THUNDERBOLT]);
|
||||
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
|
||||
const pidgeot = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
|
||||
// Setup Gravity on 1st turn
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.GRAVITY));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use electric move on 2nd turn
|
||||
await game.toNextTurn();
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.THUNDERBOLT));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
106
src/test/moves/tera_blast.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { allMoves } from "#app/data/move";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||
import { Type } from "#app/data/type";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { BattleStat } from "#app/data/battle-stat";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { HitResult } from "#app/field/pokemon";
|
||||
|
||||
describe("Moves - Tera Blast", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const moveToCheck = allMoves[Moves.TERA_BLAST];
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.starterSpecies(Species.FEEBAS)
|
||||
.moveset([Moves.TERA_BLAST])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.startingHeldItems([{name: "TERA_SHARD", type: Type.FIRE}])
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyLevel(20);
|
||||
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
});
|
||||
|
||||
it("changes type to match user's tera type", async() => {
|
||||
game.override
|
||||
.enemySpecies(Species.FURRET)
|
||||
.startingHeldItems([{name: "TERA_SHARD", type: Type.FIGHTING}]);
|
||||
await game.startBattle();
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemyPokemon, "apply");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TERA_BLAST));
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
|
||||
}, 20000);
|
||||
|
||||
it("increases power if user is Stellar tera type", async() => {
|
||||
game.override.startingHeldItems([{name: "TERA_SHARD", type: Type.STELLAR}]);
|
||||
const stellarTypeMultiplier = 2;
|
||||
const stellarTypeDmgBonus = 20;
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TERA_BLAST));
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith((basePower + stellarTypeDmgBonus) * stellarTypeMultiplier);
|
||||
}, 20000);
|
||||
|
||||
// Currently abilities are bugged and can't see when a move's category is changed
|
||||
it.skip("uses the higher stat of the user's Atk and SpAtk for damage calculation", async() => {
|
||||
game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
|
||||
await game.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
playerPokemon.stats[Stat.ATK] = 100;
|
||||
playerPokemon.stats[Stat.SPATK] = 1;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TERA_BLAST));
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true);
|
||||
}, 20000);
|
||||
|
||||
it("causes stat drops if user is Stellar tera type", async() => {
|
||||
game.override.startingHeldItems([{name: "TERA_SHARD", type: Type.STELLAR}]);
|
||||
await game.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TERA_BLAST));
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(playerPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(-1);
|
||||
expect(playerPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
|
||||
}, 20000);
|
||||
});
|