Port weather, copying moves, held items and obedience (#28)
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
Complete the weather, Substitute, Transform, called-move and forced-switch effects. Add held-item behavior, Exp. Share awards and badge-based traded Pokemon obedience, and persist trainer berry consumption. Validated with 576 passing unit tests and 149 browser tests, native/reference builds, reference/vendor/release checks and 320 Metal Powder emulator comparisons per browser. Smart AI and special balls/battle types remain on #28.
This commit is contained in:
1 parent
8f5b464efc
commit
32d736ecae
13 files changed
+1509
-75
No files matched your search
+193
-2
@@ -95,7 +95,25 @@ const DOUBLESLAP = 3,
|
||||
RAPID_SPIN = 0xe5,
|
||||
PSYCH_UP = 0xf4,
|
||||
ANCIENTPOWER = 0xf6,
|
||||
FUTURE_SIGHT = 0xf8;
|
||||
FUTURE_SIGHT = 0xf8,
|
||||
RAIN_DANCE = 0xf0,
|
||||
SUNNY_DAY = 0xf1,
|
||||
SANDSTORM = 0xc9,
|
||||
SUBSTITUTE = 0xa4,
|
||||
TRANSFORM = 0x90,
|
||||
MIMIC = 0x66,
|
||||
METRONOME = 0x76,
|
||||
MIRROR_MOVE = 0x77,
|
||||
SLEEP_TALK = 0xd6,
|
||||
SNORE = 0xad,
|
||||
CONVERSION = 0xa0,
|
||||
CONVERSION2 = 0xb0,
|
||||
SKETCH = 0xa6,
|
||||
BATON_PASS = 0xe2,
|
||||
ROAR = 0x2e,
|
||||
TELEPORT = 0x64,
|
||||
WATER_GUN = 0x37,
|
||||
SYNTHESIS = 0xeb;
|
||||
// Species and items
|
||||
const PIDGEY = 16,
|
||||
RATTATA = 19,
|
||||
@@ -122,7 +140,10 @@ const PIDGEY = 16,
|
||||
NIDORAN_F = 0x1d,
|
||||
NIDORAN_M = 0x20,
|
||||
STARMIE = 0x79,
|
||||
FORRETRESS = 0xcd;
|
||||
FORRETRESS = 0xcd,
|
||||
DITTO = 0x84,
|
||||
SMEARGLE = 0xeb,
|
||||
PORYGON = 0x89;
|
||||
const AMULET_COIN = 0x5b,
|
||||
LEFTOVERS = 0x92,
|
||||
FOCUS_BAND = 0x77,
|
||||
@@ -872,6 +893,176 @@ describe.skipIf(!existsSync(romPath))("move effects with the local ROM", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("weather, substitutes and moves that copy or switch", () => {
|
||||
it("makes it rain for five turns, strengthening Water moves", () => {
|
||||
const damage = (rain: boolean) => {
|
||||
const enemy = mon(SNORLAX, 60, [SPLASH]);
|
||||
const battle = duel([mon(STARMIE, 40, [rain ? RAIN_DANCE : SPLASH, WATER_GUN])], [enemy], { seed: 3 });
|
||||
const steps = battleSteps(battle);
|
||||
steps.answer({ action: "fight", move: 0 }).answer({ action: "fight", move: 1 });
|
||||
return { taken: enemy.hp - battle.enemy.mon.hp, steps, battle };
|
||||
};
|
||||
const rain = damage(true),
|
||||
dry = damage(false);
|
||||
expect(rain.steps.texts.join(" ")).toContain("A downpour started!");
|
||||
expect(rain.steps.texts.join(" ")).toContain("Rain continues to fall.");
|
||||
expect(rain.taken).toBeGreaterThan(dry.taken * 1.3);
|
||||
for (let turn = 0; turn < 3; turn++) rain.steps.answer({ action: "fight", move: 1 });
|
||||
expect(rain.steps.texts).toContain("The rain stopped.");
|
||||
expect(rain.battle.weather).toBe(C.WEATHER_NONE);
|
||||
});
|
||||
it("skips Solarbeam's charge and heals more with Synthesis in the sun", () => {
|
||||
const player = mon(BULBASAUR, 60, [SUNNY_DAY, SOLARBEAM, SYNTHESIS]);
|
||||
player.hp = 20;
|
||||
const battle = duel([player], [mon(SNORLAX, 60, [SPLASH])]);
|
||||
const steps = battleSteps(battle);
|
||||
steps.answer({ action: "fight", move: 0 });
|
||||
expect(steps.texts.join(" ")).toContain("The sunlight got bright!");
|
||||
steps.answer({ action: "fight", move: 1 });
|
||||
expect(steps.texts).not.toContain("BULBASAUR took in sunlight!");
|
||||
expect(steps.texts).toContain("BULBASAUR used SOLARBEAM!");
|
||||
const hp = battle.player.mon.hp;
|
||||
steps.answer({ action: "fight", move: 2 });
|
||||
// Synthesis gives half the maximum HP by day, all of it in the sun.
|
||||
expect(battle.player.mon.hp).toBe(Math.min(player.stats[0], hp + player.stats[0]));
|
||||
});
|
||||
it("buffets Pokémon other than Rock, Ground and Steel types in a sandstorm", () => {
|
||||
const battle = duel([mon(GEODUDE, 40, [SANDSTORM, SPLASH])], [mon(SNORLAX, 60, [SPLASH])]);
|
||||
const steps = battleSteps(battle);
|
||||
steps.answer({ action: "fight", move: 0 }).answer({ action: "fight", move: 1 });
|
||||
expect(steps.texts).toContain("A SANDSTORM brewed!");
|
||||
expect(steps.texts).toContain("The SANDSTORM hits Enemy SNORLAX!");
|
||||
expect(steps.texts).not.toContain("The SANDSTORM hits GEODUDE!");
|
||||
// Two turns' worth: the turn it started and the next.
|
||||
expect(battle.enemy.mon.maxHp - battle.enemy.mon.hp).toBe(((battle.enemy.mon.maxHp >> 2) >> 1) * 2);
|
||||
steps.answer({ action: "fight", move: 0 });
|
||||
expect(steps.texts).toContain("But it failed!");
|
||||
});
|
||||
it("makes a substitute that takes the hits and blocks status moves", () => {
|
||||
const player = mon(SNORLAX, 60, [SUBSTITUTE, SPLASH]);
|
||||
const battle = duel([player], [mon(PIDGEY, 30, [TACKLE])]);
|
||||
const steps = battleSteps(battle);
|
||||
steps.answer({ action: "fight", move: 0 });
|
||||
const quarter = player.stats[0] >> 2;
|
||||
expect(steps.texts.join(" ")).toContain("made a SUBSTITUTE");
|
||||
expect(battle.player.mon.hp).toBe(player.stats[0] - quarter);
|
||||
expect(steps.texts.join(" ")).toContain("The SUBSTITUTE took damage");
|
||||
expect(battle.player.mon.hp).toBe(player.stats[0] - quarter);
|
||||
expect(battle.player.substituteHp).toBeLessThan(quarter);
|
||||
steps.answer({ action: "fight", move: 0 });
|
||||
expect(steps.texts).toContain("SNORLAX has a SUBSTITUTE!");
|
||||
const status = firstTurn(duel([mon(SNORLAX, 60, [SUBSTITUTE])], [mon(PIDGEY, 30, [POISONPOWDER])]));
|
||||
expect(status.texts).toContain("It didn't affect SNORLAX!");
|
||||
const weak = mon(SNORLAX, 60, [SUBSTITUTE]);
|
||||
weak.hp = weak.stats[0] >> 2;
|
||||
expect(firstTurn(duel([weak], [mon(PIDGEY, 30, [SPLASH])])).texts.join(" ")).toContain("Too weak to make a SUBSTITUTE");
|
||||
// A fast Jolteon's substitute, then a strong Tackle that breaks it.
|
||||
const breaker = duel([mon(JOLTEON, 30, [SUBSTITUTE])], [mon(SNORLAX, 60, [TACKLE])]);
|
||||
const broken = firstTurn(breaker);
|
||||
expect(broken.texts.join(" ")).toContain("SUBSTITUTE faded");
|
||||
expect(breaker.player.has(4, C.SUBSTATUS_SUBSTITUTE)).toBe(false);
|
||||
});
|
||||
it("transforms into the target with 5 PP per move", () => {
|
||||
const battle = duel([mon(DITTO, 40, [TRANSFORM])], [mon(SNORLAX, 60, [SPLASH, TACKLE])]);
|
||||
const { texts } = firstTurn(battle);
|
||||
expect(texts.join(" ")).toContain("TRANSFORMED into SNORLAX");
|
||||
expect(battle.player.mon.species).toBe(SNORLAX);
|
||||
expect(battle.player.mon.moves.slice(0, 2)).toEqual([SPLASH, TACKLE]);
|
||||
expect(battle.player.mon.pp.slice(0, 3)).toEqual([5, 5, 0]);
|
||||
});
|
||||
it("catches a transformed wild Pokémon as a Ditto with its own DVs", () => {
|
||||
const party = [mon(SNORLAX, 40, [SPLASH])];
|
||||
const bag = { items: [], balls: [{ item: C.MASTER_BALL, quantity: 1 }], keyItems: [] };
|
||||
const battle = duel(party, { wild: { species: DITTO, level: 10 } }, { bag, caughtData: () => 0 });
|
||||
const steps = battleSteps(battle);
|
||||
const dvs = battle.enemy.mon.dvs;
|
||||
steps.answer({ action: "fight", move: 0 });
|
||||
expect(battle.enemy.has(5, C.SUBSTATUS_TRANSFORMED)).toBe(true);
|
||||
steps.answer({ action: "item", item: C.MASTER_BALL, slot: 0 });
|
||||
while (!steps.done) steps.answer(steps.prompt?.kind === "yesno" ? 0 : undefined);
|
||||
expect(party[1].species).toBe(DITTO);
|
||||
expect(party[1].dvs).toBe(dvs);
|
||||
expect(party[1].moves[0]).toBe(TRANSFORM);
|
||||
});
|
||||
it("mimics the target's last move for the battle", () => {
|
||||
const player = mon(STARMIE, 60, [MIMIC]);
|
||||
const battle = duel([player], [mon(SNORLAX, 30, [TACKLE])]);
|
||||
const { texts } = turns(battle, [0, 0]);
|
||||
expect(texts.join(" ")).toContain("learned TACKLE");
|
||||
expect(battle.player.mon.moves[0]).toBe(TACKLE);
|
||||
expect(player.moves[0]).toBe(MIMIC);
|
||||
});
|
||||
it("calls a random move with Metronome", () => {
|
||||
const { texts } = firstTurn(duel([mon(STARMIE, 60, [METRONOME])], [mon(SNORLAX, 60, [SPLASH])]));
|
||||
expect(texts[0]).toBe("STARMIE used METRONOME!");
|
||||
expect(texts[1]).toMatch(/^STARMIE used /);
|
||||
expect(texts[1]).not.toBe("STARMIE used METRONOME!");
|
||||
});
|
||||
it("mirrors the target's last move", () => {
|
||||
const battle = duel([mon(SNORLAX, 30, [MIRROR_MOVE])], [mon(STARMIE, 60, [TACKLE])]);
|
||||
const { texts } = firstTurn(battle);
|
||||
const used = texts.indexOf("SNORLAX used MIRROR MOVE!");
|
||||
expect(texts[used + 1]).toBe("SNORLAX used TACKLE!");
|
||||
const first = firstTurn(duel([mon(STARMIE, 60, [MIRROR_MOVE])], [mon(SNORLAX, 30, [TACKLE])]));
|
||||
expect(first.texts.join(" ")).toContain("MIRROR MOVE failed");
|
||||
});
|
||||
it("talks in its sleep with another move, and snores only when asleep", () => {
|
||||
const sleeper = mon(SNORLAX, 60, [SLEEP_TALK, TACKLE, SNORE], { status: 7 });
|
||||
const { texts } = firstTurn(duel([sleeper], [mon(PIDGEY, 30, [SPLASH])]));
|
||||
expect(texts).toContain("SNORLAX used SLEEP TALK!");
|
||||
expect(texts.some((text) => text === "SNORLAX used TACKLE!" || text === "SNORLAX used SNORE!")).toBe(true);
|
||||
const awake = firstTurn(duel([mon(SNORLAX, 60, [SNORE])], [mon(PIDGEY, 30, [SPLASH])]));
|
||||
expect(awake.texts).toContain("But it failed!");
|
||||
});
|
||||
it("changes its type with Conversion and Conversion2", () => {
|
||||
const battle = duel([mon(PORYGON, 40, [CONVERSION, WATER_GUN])], [mon(SNORLAX, 60, [SPLASH])]);
|
||||
const { texts } = firstTurn(battle);
|
||||
expect(texts.join(" ")).toContain("transformed into the WATER-type");
|
||||
expect(battle.player.mon.types).toEqual([21, 21]);
|
||||
// Jolteon moves first, so its Tackle is the last move to resist.
|
||||
const resist = duel([mon(PORYGON, 30, [CONVERSION2])], [mon(JOLTEON, 20, [TACKLE])]);
|
||||
firstTurn(resist);
|
||||
const [type] = resist.player.mon.types;
|
||||
expect([5, 8, 9]).toContain(type); // Rock, Ghost or Steel
|
||||
});
|
||||
it("sketches the target's last move for good", () => {
|
||||
// Jolteon moves first, so Sketch has a move to copy.
|
||||
const player = mon(SMEARGLE, 20, [SKETCH]);
|
||||
const battle = duel([player], [mon(JOLTEON, 30, [TACKLE])]);
|
||||
const { texts } = firstTurn(battle);
|
||||
expect(texts.join(" ")).toContain("SKETCHED TACKLE");
|
||||
expect(player.moves[0]).toBe(TACKLE);
|
||||
expect(player.pp[0]).toBe(assets.pokemon.move(TACKLE).pp);
|
||||
});
|
||||
it("passes stat changes and the substitute along with Baton Pass", () => {
|
||||
const party = [mon(STARMIE, 60, [SWORDS_DANCE, BATON_PASS]), mon(SNORLAX, 30, [TACKLE])];
|
||||
const battle = duel(party, [mon(PIDGEY, 30, [SPLASH])]);
|
||||
const steps = battleSteps(battle);
|
||||
steps.answer({ action: "fight", move: 0 }).answer({ action: "fight", move: 1 });
|
||||
expect(steps.prompt?.kind).toBe("party");
|
||||
steps.answer(0).answer(1);
|
||||
expect(steps.texts).toContain("STARMIE is already out.");
|
||||
expect(battle.player.mon.species).toBe(SNORLAX);
|
||||
expect(battle.player.levels[C.ATTACK]).toBe(9);
|
||||
const [numerator, denominator] = [2, 1];
|
||||
expect(battle.player.mon.stats[C.ATTACK]).toBe(Math.min(999, Math.floor((party[1].stats[1] * numerator) / denominator)));
|
||||
});
|
||||
it("ends a wild battle with Roar or Teleport, and drags out a trainer's Pokémon", () => {
|
||||
const roar = duel([mon(SNORLAX, 60, [ROAR])], { wild: { species: PIDGEY, level: 5 } });
|
||||
const roared = playBattle(roar);
|
||||
expect(roared.result).toBe(C.DRAW);
|
||||
expect(roared.texts).toContain("Enemy PIDGEY fled in fear!");
|
||||
const teleport = duel([mon(STARMIE, 60, [TELEPORT])], { wild: { species: PIDGEY, level: 5 } });
|
||||
const teleported = playBattle(teleport);
|
||||
expect(teleported.result).toBe(C.DRAW);
|
||||
expect(teleported.texts.join(" ")).toContain("fled from battle");
|
||||
const trainer = duel([mon(SNORLAX, 30, [ROAR])], [mon(PIDGEY, 30, [SPLASH]), mon(RATTATA, 30, [SPLASH])]);
|
||||
const dragged = firstTurn(trainer);
|
||||
expect(dragged.texts.join(" ")).toContain("was dragged out");
|
||||
expect(trainer.enemy.mon.species).toBe(RATTATA);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Counter and Mirror Coat", () => {
|
||||
it("returns twice a physical hit taken first", () => {
|
||||
const player = mon(SNORLAX, 40, [COUNTER]);
|
||||
|
||||
Reference in new issue
Block a user