Files
gen1recomp/tests/game3_battle_special_moves_test.lua
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

912 lines
43 KiB
Lua

#!/usr/bin/env luajit
-- Test suite for Game 3 Special Moves (OHKO, Fixed & Proportional Damage).
-- Run: luajit tests/game3_battle_special_moves_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").requireData("game3_battle_special_moves_test")
local State = require("src.core.game3.battle.state")
local Engine = require("src.core.game3.battle.engine")
local Adapter = require("src.core.game3.battle.adapter")
local Damage = require("src.core.game3.battle.damage")
local Types = require("src.core.game3.battle.types")
local Moves = require("src.core.game3.battle.moves")
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function setup_test_battle(st, rngFn)
st.rng = rngFn or function(lo, hi)
if lo == 1 and hi == 100 then return 1 end -- hit accuracy
if lo and hi then return hi end
return lo or 0
end
return Adapter.new(st)
end
print("=== 1. OHKO Moves (Horn Drill, Fissure, Guillotine, Sheer Cold) ===")
do
-- Type immunity: Fissure (Ground) against Pidgey (Normal/Flying)
local st = State.new({
wild = true,
playerParty = { { species = 95, level = 30, hp = 100, maxHp = 100, moves = { 90 }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- Fissure
foeMon = { species = 16, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 }, -- Pidgey (Flying)
})
st.player.type1 = Types.ID.ROCK
st.player.type2 = Types.ID.GROUND
st.enemy.type1 = Types.ID.NORMAL
st.enemy.type2 = Types.ID.FLYING
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 90, 1, ad, st, out)
local joined = table.concat(out, " || ")
check(joined:find("doesn't affect", 1, true) ~= nil, "Fissure fails against Flying-type")
check(ad:hp(st.enemy) == 50, "Foe takes 0 damage from immune Fissure")
end
do
-- Sturdy ability blocks OHKO
local st = State.new({
wild = true,
playerParty = { { species = 127, level = 40, hp = 100, maxHp = 100, moves = { 12 }, pp = { 5 },
attack = 60, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- Guillotine
foeMon = { species = 74, level = 30, hp = 60, maxHp = 60, moves = { 33 }, pp = { 35 },
ability = 5, attack = 30, defense = 50, spAtk = 20, spDef = 20, speed = 20 }, -- Geodude (Sturdy = 5)
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 12, 1, ad, st, out)
local joined = table.concat(out, " || ")
check(joined:find("STURDY", 1, true) ~= nil, "Sturdy blocks OHKO")
check(ad:hp(st.enemy) == 60, "Foe with Sturdy took 0 damage")
end
do
-- Level check: User level < Target level fails
local st = State.new({
wild = true,
playerParty = { { species = 127, level = 20, hp = 100, maxHp = 100, moves = { 32 }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- Horn Drill
foeMon = { species = 19, level = 25, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 32, 1, ad, st, out)
local joined = table.concat(out, " || ")
-- pokefirered/src/battle_message.c:1112
check(joined:find("is\nunaffected!", 1, true) ~= nil, "OHKO fails when user level < target level")
check(ad:hp(st.enemy) == 50, "Target undamaged when attacker is lower level")
end
do
-- OHKO Hit: Damage equal to target HP, KO message, target faints
local st = State.new({
wild = true,
playerParty = { { species = 127, level = 40, hp = 100, maxHp = 100, moves = { 32 }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- Horn Drill
foeMon = { species = 19, level = 30, hp = 75, maxHp = 75, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 32, 1, ad, st, out)
local joined = table.concat(out, " || ")
check(joined:find("It's a one-hit KO!", 1, true) ~= nil, "Prints 'It's a one-hit KO!'")
check(ad:hp(st.enemy) == 0, "Target HP reduced to 0")
check(ad:isFainted(st.enemy) == true, "Target is fainted")
end
do
-- Endure survives OHKO with 1 HP
local st = State.new({
wild = true,
playerParty = { { species = 127, level = 40, hp = 100, maxHp = 100, moves = { 32 }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 30, hp = 75, maxHp = 75, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
st.enemy.expEnduring = true
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 32, 1, ad, st, out)
local joined = table.concat(out, " || ")
check(joined:find("ENDURED\nthe hit!", 1, true) ~= nil, "Target endured OHKO")
check(ad:hp(st.enemy) == 1, "Target survives with exactly 1 HP")
check(ad:isFainted(st.enemy) == false, "Target did not faint")
end
print("\n=== 2. Fixed Damage Moves (Dragon Rage, SonicBoom) ===")
do
-- Dragon Rage deals exactly 40 HP
local st = State.new({
wild = true,
playerParty = { { species = 147, level = 15, hp = 100, maxHp = 100, moves = { 82 }, pp = { 10 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 50 } }, -- Dragon Rage
foeMon = { species = 143, level = 50, hp = 160, maxHp = 160, moves = { 33 }, pp = { 35 },
attack = 80, defense = 100, spAtk = 80, spDef = 100, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 82, 1, ad, st, out)
check(ad:hp(st.enemy) == 120, "Dragon Rage deals exactly 40 HP (160 -> 120)")
local joined = table.concat(out, " || ")
check(joined:find("critical", 1, true) == nil, "Fixed damage has no critical hit")
check(joined:find("effective", 1, true) == nil, "Fixed damage suppresses effectiveness text")
end
do
-- SonicBoom deals 20 HP to normal target, 0 to Ghost
local st = State.new({
wild = true,
playerParty = { { species = 100, level = 20, hp = 100, maxHp = 100, moves = { 49 }, pp = { 20 },
attack = 30, defense = 30, spAtk = 30, spDef = 30, speed = 50 } }, -- SonicBoom
foeMon = { species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 49, 1, ad, st, out)
check(ad:hp(st.enemy) == 30, "SonicBoom deals exactly 20 HP (50 -> 30)")
-- SonicBoom vs Ghost-type
st.enemy.type1 = Types.ID.GHOST
st.enemy.type2 = nil
local outGhost = {}
Engine.resolveMove(st.player, st.enemy, 49, 1, ad, st, outGhost)
check(ad:hp(st.enemy) == 30, "SonicBoom deals 0 HP to Ghost-type")
end
print("\n=== 3. Level-Based Damage (Seismic Toss, Night Shade) ===")
do
-- Seismic Toss deals damage equal to level
local st = State.new({
wild = true,
playerParty = { { species = 68, level = 43, hp = 100, maxHp = 100, moves = { 69 }, pp = { 20 },
attack = 80, defense = 50, spAtk = 40, spDef = 50, speed = 50 } }, -- Seismic Toss
foeMon = { species = 143, level = 30, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 69, 1, ad, st, out)
check(ad:hp(st.enemy) == 57, "Seismic Toss at level 43 deals exactly 43 HP (100 -> 57)")
-- Seismic Toss vs Ghost fails
st.enemy.type1 = Types.ID.GHOST
local outGhost = {}
Engine.resolveMove(st.player, st.enemy, 69, 1, ad, st, outGhost)
check(ad:hp(st.enemy) == 57, "Seismic Toss fails against Ghost-type")
end
do
-- Night Shade deals damage equal to level
local st = State.new({
wild = true,
playerParty = { { species = 94, level = 35, hp = 100, maxHp = 100, moves = { 101 }, pp = { 15 },
attack = 40, defense = 40, spAtk = 80, spDef = 50, speed = 80 } }, -- Night Shade
foeMon = { species = 65, level = 30, hp = 80, maxHp = 80, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
st.enemy.type1 = Types.ID.PSYCHIC
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 101, 1, ad, st, out)
check(ad:hp(st.enemy) == 45, "Night Shade at level 35 deals exactly 35 HP (80 -> 45)")
-- Night Shade vs Normal fails
st.enemy.type1 = Types.ID.NORMAL
local outNorm = {}
Engine.resolveMove(st.player, st.enemy, 101, 1, ad, st, outNorm)
check(ad:hp(st.enemy) == 45, "Night Shade fails against Normal-type")
end
print("\n=== 4. Proportional & Contextual Damage (Super Fang, Endeavor, Psywave) ===")
do
-- Super Fang cuts target current HP in half
local st = State.new({
wild = true,
playerParty = { { species = 19, level = 25, hp = 100, maxHp = 100, moves = { 162 }, pp = { 10 },
attack = 30, defense = 30, spAtk = 30, spDef = 30, speed = 50 } }, -- Super Fang
foeMon = { species = 143, level = 50, hp = 150, maxHp = 150, moves = { 33 }, pp = { 35 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 162, 1, ad, st, out)
check(ad:hp(st.enemy) == 75, "Super Fang halves 150 HP -> 75 HP")
-- Super Fang again on 75 HP -> deals floor(75 / 2) = 37 -> 38 HP remaining
Engine.resolveMove(st.player, st.enemy, 162, 1, ad, st, out)
check(ad:hp(st.enemy) == 38, "Super Fang halves 75 HP -> 38 HP remaining (37 damage)")
end
do
-- Endeavor deals target HP - user HP when user HP < target HP
local st = State.new({
wild = true,
playerParty = { { species = 19, level = 25, hp = 12, maxHp = 60, moves = { 283 }, pp = { 5 },
attack = 30, defense = 30, spAtk = 30, spDef = 30, speed = 50 } }, -- Endeavor (move 283)
foeMon = { species = 143, level = 50, hp = 80, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 283, 1, ad, st, out)
check(ad:hp(st.enemy) == 12, "Endeavor reduces foe's 80 HP down to user's 12 HP")
-- Endeavor fails when user HP >= target HP
st.player.mon.hp = 80
local outFail = {}
Engine.resolveMove(st.player, st.enemy, 283, 1, ad, st, outFail)
local joined = table.concat(outFail, " || ")
check(joined:find("But it failed!", 1, true) ~= nil, "Endeavor fails when user HP >= target HP")
check(ad:hp(st.enemy) == 12, "Foe HP unchanged on Endeavor failure")
end
do
-- Psywave deals (level * (roll + 50) / 100)
local st = State.new({
wild = true,
playerParty = { { species = 64, level = 40, hp = 100, maxHp = 100, moves = { 149 }, pp = { 15 },
attack = 30, defense = 30, spAtk = 60, spDef = 50, speed = 50 } }, -- Psywave
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
-- Test with fixed roll 0 -> floor(40 * 50 / 100) = 20 damage
local adMin = setup_test_battle(st, function(lo, hi)
if lo == 0 and hi == 10 then return 0 end
return 1
end)
Engine.resolveMove(st.player, st.enemy, 149, 1, adMin, st, {})
check(adMin:hp(st.enemy) == 80, "Psywave min roll deals 0.5x level = 20 damage (100 -> 80)")
-- Test with fixed roll 100 -> floor(40 * 150 / 100) = 60 damage
st.enemy.mon.hp = 100
local adMax = setup_test_battle(st, function(lo, hi)
if lo == 0 and hi == 10 then return 10 end
return 1
end)
Engine.resolveMove(st.player, st.enemy, 149, 1, adMax, st, {})
check(adMax:hp(st.enemy) == 40, "Psywave max roll deals 1.5x level = 60 damage (100 -> 40)")
end
print("\n=== 5. One-Off Status & Utility (Rest, Haze, Mist, Substitute, False Swipe, Pay Day, Teeter Dance) ===")
do
-- Rest: full heal, cures status, sets SLP with 2 sleep turns
local st = State.new({
wild = true,
playerParty = { { species = 143, level = 30, hp = 25, maxHp = 100, moves = { 156 }, pp = { 10 },
status = "BRN", attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 20 } }, -- Rest (156)
foeMon = { species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 156, 1, ad, st, out)
check(ad:hp(st.player) == 100, "Rest restores full HP (25 -> 100)")
check(st.player.status == "SLP", "Rest inflicts SLP")
-- pokefirered/src/battle_script_commands.c:6480
check(st.player.sleepTurns == 3, "Rest sets STATUS1_SLEEP_TURN(3)")
local joined = table.concat(out, " || ")
check(joined:find("became healthy", 1, true) ~= nil, "Rest message: went to sleep and became healthy")
-- Rest fails when already at full HP (and awake)
local stFull = State.new({
wild = true,
playerParty = { { species = 143, level = 30, hp = 100, maxHp = 100, moves = { 156 }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 20 } },
foeMon = { species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local adFull = setup_test_battle(stFull)
local outFull = {}
Engine.resolveMove(stFull.player, stFull.enemy, 156, 1, adFull, stFull, outFull)
local joinedFull = table.concat(outFull, " || ")
check(joinedFull:find("HP is full!", 1, true) ~= nil, "Rest fails when HP is full")
end
do
-- Haze resets all stat stages for both battlers
local st = State.new({
wild = true,
playerParty = { { species = 134, level = 30, hp = 100, maxHp = 100, moves = { 114 }, pp = { 30 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- Haze (114)
foeMon = { species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
st.player.stages.attack = 3
st.player.stages.speed = 2
st.enemy.stages.defense = -2
st.enemy.stages.accuracy = -1
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 114, 1, ad, st, out)
check(st.player.stages.attack == 0 and st.player.stages.speed == 0, "Player stats reset to 0")
check(st.enemy.stages.defense == 0 and st.enemy.stages.accuracy == 0, "Enemy stats reset to 0")
local joined = table.concat(out, " || ")
check(joined:find("All stat changes were\neliminated!", 1, true) ~= nil, "Haze message printed")
end
do
-- Mist protects team from stat drops
local st = State.new({
wild = true,
playerParty = { { species = 134, level = 30, hp = 100, maxHp = 100, moves = { 54 }, pp = { 30 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- Mist (54)
foeMon = { species = 19, level = 20, hp = 50, maxHp = 50, moves = { 45 }, pp = { 40 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 }, -- Growl (45)
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 54, 1, ad, st, out)
check(st.playerSide.expMistTurns == 5, "Mist sets 5 turns on side")
-- Foe uses Growl against player under Mist
local outGrowl = {}
Engine.resolveMove(st.enemy, st.player, 45, 1, ad, st, outGrowl)
check(st.player.stages.attack == 0, "Mist prevents Attack drop")
local joinedGrowl = table.concat(outGrowl, " || ")
check(joinedGrowl:find("protected\nby MIST", 1, true) ~= nil, "Mist protection message printed")
end
do
-- Substitute: costs 25% max HP, creates decoy, absorbs damage, breaks on lethal hit
local st = State.new({
wild = true,
playerParty = { { species = 25, level = 30, hp = 100, maxHp = 100, moves = { 164 }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- Substitute (164)
foeMon = { species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 40, defense = 20, spAtk = 20, spDef = 20, speed = 20 }, -- Tackle (33)
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 164, 1, ad, st, out)
check(ad:hp(st.player) == 75, "Substitute pays 25 HP (100 -> 75)")
-- pokefirered/src/battle_script_commands.c:7442
check(st.player.substituteHP == 25, "Substitute created with maxHP / 4 HP")
-- Foe attacks Substitute with non-lethal hit
-- Force damage = 10
st.player.substituteHP = 20
local outTackle = {}
Engine.resolveMove(st.enemy, st.player, 33, 1, ad, st, outTackle)
check(ad:hp(st.player) == 75, "Player takes NO HP loss while Substitute is active")
local joinedTackle = table.concat(outTackle, " || ")
check(joinedTackle:find("SUBSTITUTE took damage", 1, true) ~= nil,
"Substitute absorbed damage")
-- Foe breaks Substitute
st.player.substituteHP = 1
local outBreak = {}
Engine.resolveMove(st.enemy, st.player, 33, 1, ad, st, outBreak)
check(st.player.substituteHP == 0, "Substitute HP is 0 after break")
check(ad:hp(st.player) == 75, "Player takes NO leftover damage when Substitute breaks")
local joinedBreak = table.concat(outBreak, " || ")
check(joinedBreak:find("SUBSTITUTE faded!", 1, true) ~= nil, "Substitute faded message printed")
end
do
-- False Swipe: never KOs, leaves target with at least 1 HP
local st = State.new({
wild = true,
playerParty = { { species = 123, level = 50, hp = 100, maxHp = 100, moves = { 206 }, pp = { 40 },
attack = 100, defense = 50, spAtk = 50, spDef = 50, speed = 50 } }, -- False Swipe (206)
foeMon = { species = 19, level = 5, hp = 15, maxHp = 15, moves = { 33 }, pp = { 35 },
attack = 10, defense = 5, spAtk = 10, spDef = 5, speed = 10 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 206, 1, ad, st, out)
check(ad:hp(st.enemy) == 1, "False Swipe leaves target with exactly 1 HP")
check(ad:isFainted(st.enemy) == false, "Target did not faint from False Swipe")
-- False Swipe against target already at 1 HP
Engine.resolveMove(st.player, st.enemy, 206, 1, ad, st, {})
check(ad:hp(st.enemy) == 1, "False Swipe against 1 HP target keeps target at 1 HP")
end
do
-- Pay Day: damages foe and scatters coins = level * 5
local st = State.new({
wild = true,
playerParty = { { species = 52, level = 24, hp = 100, maxHp = 100, moves = { 6 }, pp = { 20 },
attack = 40, defense = 40, spAtk = 40, spDef = 40, speed = 50 } }, -- Pay Day (6)
foeMon = { species = 19, level = 20, hp = 80, maxHp = 80, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 6, 1, ad, st, out)
check(ad:hp(st.enemy) < 80, "Pay Day dealt damage to foe")
check(st.payDayCoins == 24 * 5, "Pay Day scattered 120 coins (level 24 * 5)")
local joined = table.concat(out, " || ")
check(joined:find("Coins scattered", 1, true) ~= nil, "Coins scattered message printed")
end
do
-- Teeter Dance confuses target
local st = State.new({
wild = true,
playerParty = { { species = 327, level = 30, hp = 100, maxHp = 100, moves = { 298 }, pp = { 20 },
attack = 40, defense = 40, spAtk = 40, spDef = 40, speed = 50 } }, -- Teeter Dance (298)
foeMon = { species = 19, level = 20, hp = 50, maxHp = 50, moves = { 33 }, pp = { 35 },
attack = 20, defense = 20, spAtk = 20, spDef = 20, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, 298, 1, ad, st, out)
check(st.enemy.confusionTurns and st.enemy.confusionTurns > 0, "Target became confused")
local joined = table.concat(out, " || ")
check(joined:find("became\nconfused!", 1, true) ~= nil, "Confusion message printed")
end
print("\n=== 6. Variable & Scaled Power Moves (Magnitude, Return, Frustration, Eruption, Flail, Facade, Revenge, Weather Ball, Smellingsalt, Low Kick) ===")
do
-- Magnitude rolls and prints magnitude level
local magId = Moves.numForName("MAGNITUDE")
-- Test roll 0 -> Magnitude 4
local st4 = State.new({
wild = true,
playerParty = { { species = 74, level = 30, hp = 100, maxHp = 100, moves = { magId }, pp = { 30 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 25, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 40, spAtk = 20, spDef = 20, speed = 20 }, -- Pikachu
})
local ad4 = setup_test_battle(st4, function(lo, hi)
if lo == 0 and hi == 99 then return 2 end -- Magnitude 4
if lo == 1 and hi == 100 then return 1 end
if lo == 85 and hi == 100 then return 100 end
return hi or lo or 0
end)
local out4 = {}
Engine.resolveMove(st4.player, st4.enemy, magId, 1, ad4, st4, out4)
local joined4 = table.concat(out4, " || ")
check(joined4:find("MAGNITUDE 4!", 1, true) ~= nil, "Magnitude 4 message printed")
-- Test roll 99 -> Magnitude 10
local st10 = State.new({
wild = true,
playerParty = { { species = 74, level = 30, hp = 100, maxHp = 100, moves = { magId }, pp = { 30 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 25, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 40, spAtk = 20, spDef = 20, speed = 20 },
})
local ad10 = setup_test_battle(st10, function(lo, hi)
if lo == 0 and hi == 99 then return 99 end -- Magnitude 10
if lo == 1 and hi == 100 then return 1 end
if lo == 85 and hi == 100 then return 100 end
return hi or lo or 0
end)
local out10 = {}
Engine.resolveMove(st10.player, st10.enemy, magId, 1, ad10, st10, out10)
local joined10 = table.concat(out10, " || ")
check(joined10:find("MAGNITUDE 10!", 1, true) ~= nil, "Magnitude 10 message printed")
check(ad4:hp(st4.enemy) > ad10:hp(st10.enemy), "Magnitude 10 dealt significantly more damage than Magnitude 4")
end
do
-- Return vs Frustration scaling with friendship
local retId = Moves.numForName("RETURN")
local frusId = Moves.numForName("FRUSTRATION")
-- Max friendship (255)
local pMonMax = { species = 143, level = 30, hp = 100, maxHp = 100, moves = { retId, frusId }, pp = { 20, 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50, friendship = 255 }
local foe = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 20, speed = 20 }
local dmgRetMax = Damage.calc({ mon = pMonMax }, { mon = foe }, retId, { forceRoll = 100, forceCrit = false })
local dmgFrusMax = Damage.calc({ mon = pMonMax }, { mon = foe }, frusId, { forceRoll = 100, forceCrit = false })
check(dmgRetMax >= dmgFrusMax * 10, "Max friendship Return (pwr 102) deals much more damage than Frustration (pwr 1)")
-- Min friendship (0)
local pMonMin = { species = 143, level = 30, hp = 100, maxHp = 100, moves = { retId, frusId }, pp = { 20, 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50, friendship = 0 }
local dmgRetMin = Damage.calc({ mon = pMonMin }, { mon = foe }, retId, { forceRoll = 100, forceCrit = false })
local dmgFrusMin = Damage.calc({ mon = pMonMin }, { mon = foe }, frusId, { forceRoll = 100, forceCrit = false })
check(dmgFrusMin >= dmgRetMin * 10, "Min friendship Frustration (pwr 102) deals much more damage than Return (pwr 1)")
end
do
-- Eruption / Water Spout scaling with HP
local erupId = Moves.numForName("ERUPTION")
local foe = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 }
local fullMon = { species = 157, level = 30, hp = 100, maxHp = 100, moves = { erupId }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50, ability = 0 }
local lowMon = { species = 157, level = 30, hp = 10, maxHp = 100, moves = { erupId }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50, ability = 0 }
local dmgFull = Damage.calc({ mon = fullMon }, { mon = foe }, erupId, { forceRoll = 100, forceCrit = false })
local dmgLow = Damage.calc({ mon = lowMon }, { mon = foe }, erupId, { forceRoll = 100, forceCrit = false })
check(dmgFull >= math.floor(dmgLow * 7), "Full HP Eruption deals ~7x-10x damage of 10% HP Eruption")
end
do
-- Flail / Reversal scaling with remaining HP
local flailId = Moves.numForName("FLAIL")
local foe = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 }
local fullMon = { species = 143, level = 30, hp = 100, maxHp = 100, moves = { flailId }, pp = { 15 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
local lowMon = { species = 143, level = 30, hp = 2, maxHp = 100, moves = { flailId }, pp = { 15 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
local dmgFull = Damage.calc({ mon = fullMon }, { mon = foe }, flailId, { forceRoll = 100, forceCrit = false })
local dmgLow = Damage.calc({ mon = lowMon }, { mon = foe }, flailId, { forceRoll = 100, forceCrit = false })
check(dmgLow >= math.floor(dmgFull * 8), "1 HP Flail (pwr 200) deals ~10x damage of full HP Flail (pwr 20)")
end
do
-- Facade doubles power under status and ignores burn drop
local facadeId = Moves.numForName("FACADE")
local foe = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 }
local normalMon = { species = 143, level = 30, hp = 100, maxHp = 100, moves = { facadeId }, pp = { 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
local burnedMon = { species = 143, level = 30, hp = 100, maxHp = 100, moves = { facadeId }, pp = { 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50, status = "BRN" }
local dmgNorm = Damage.calc({ mon = normalMon }, { mon = foe }, facadeId, { forceRoll = 100, forceCrit = false })
local dmgBurn = Damage.calc({ mon = burnedMon }, { mon = foe }, facadeId, { forceRoll = 100, forceCrit = false })
local parMon = { species = 143, level = 30, hp = 100, maxHp = 100, moves = { facadeId }, pp = { 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50, status = "PAR" }
local dmgPar = Damage.calc({ mon = parMon }, { mon = foe }, facadeId, { forceRoll = 100, forceCrit = false })
check(dmgPar >= math.floor(dmgNorm * 1.8), "Facade doubles damage under Paralysis")
-- pokefirered/src/pokemon.c:2539
check(math.abs(dmgBurn - dmgNorm) <= 2, "Facade x2 is cancelled by the Gen3 burn halving")
end
do
-- Revenge doubles power if damaged this turn
local revId = Moves.numForName("REVENGE")
local foe = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 }
local mon = { species = 66, level = 30, hp = 100, maxHp = 100, moves = { revId }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
local dmgNoDmg = Damage.calc({ mon = mon, damageTakenThisTurn = 0 }, { mon = foe }, revId, { forceRoll = 100, forceCrit = false })
local dmgHadDmg = Damage.calc({ mon = mon, damageTakenThisTurn = 25 }, { mon = foe }, revId, { forceRoll = 100, forceCrit = false })
check(dmgHadDmg >= math.floor(dmgNoDmg * 1.8), "Revenge doubles power after taking damage this turn")
end
do
-- Weather Ball doubles power and changes type with weather
local wbId = Moves.numForName("WEATHER_BALL")
local foeGrass = { species = 1, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 }
foeGrass.type1 = Types.ID.GRASS
foeGrass.type2 = Types.ID.POISON
local mon = { species = 351, level = 30, hp = 100, maxHp = 100, moves = { wbId }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
mon.type1 = Types.ID.NORMAL
local dmgClear, infoClear = Damage.calc({ mon = mon }, foeGrass, wbId, { forceRoll = 100, forceCrit = false })
local dmgSun, infoSun = Damage.calc({ mon = mon }, foeGrass, wbId, { weather = "sun", forceRoll = 100, forceCrit = false })
check(infoClear.moveType == Types.ID.NORMAL or infoClear.move.type == Types.ID.NORMAL, "Weather Ball is Normal in clear weather")
check(infoSun.moveType == Types.ID.FIRE, "Weather Ball becomes Fire type in Sunny weather")
check(infoSun.effectiveness == 2, "Weather Ball Fire type is Super Effective against Grass")
check(dmgSun > dmgClear * 2, "Weather Ball deals much higher damage in Sun due to 100 base power, Fire type, and Sun boost")
end
do
-- Smellingsalt doubles power against paralyzed target and cures paralysis
local ssId = Moves.numForName("SMELLINGSALT")
local st = State.new({
wild = true,
playerParty = { { species = 127, level = 30, hp = 100, maxHp = 100, moves = { ssId }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20, status = "PAR" },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, ssId, 1, ad, st, out)
check(ad:status(st.enemy) == nil, "Target paralysis was cured by Smellingsalt")
local joined = table.concat(out, " || ")
check(joined:find("healed of paralysis", 1, true) ~= nil, "Smellingsalt cure message printed")
end
do
-- Low Kick scales with target weight
local lkId = Moves.numForName("LOW_KICK")
local mon = { species = 66, level = 30, hp = 100, maxHp = 100, moves = { lkId }, pp = { 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
local lightFoe = { species = 19, level = 20, hp = 200, maxHp = 200, weight = 35, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 } -- 3.5kg -> power 20
local heavyFoe = { species = 143, level = 20, hp = 200, maxHp = 200, weight = 4600, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 } -- 460.0kg -> power 120
local dmgLight = Damage.calc({ mon = mon }, { mon = lightFoe }, lkId, { forceRoll = 100, forceCrit = false })
local dmgHeavy = Damage.calc({ mon = mon }, { mon = heavyFoe }, lkId, { forceRoll = 100, forceCrit = false })
check(dmgHeavy >= math.floor(dmgLight * 5), "Low Kick on Snorlax (460kg, pwr 120) deals ~6x damage of Rattata (3.5kg, pwr 20)")
end
print("\n=== 7. Retaliation, Recoil on Miss, Self-KO (Explosion, Counter, Mirror Coat, Jump Kick, Rapid Spin) ===")
do
-- Explosion / Selfdestruct causes user to faint
local exploId = Moves.numForName("EXPLOSION")
local st = State.new({
wild = true,
playerParty = { { species = 74, level = 30, hp = 100, maxHp = 100, moves = { exploId }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, exploId, 1, ad, st, out)
check(ad:hp(st.player) == 0, "User faints from Explosion")
check(ad:isFainted(st.player), "User is flagged as fainted")
check(ad:hp(st.enemy) < 100, "Enemy took damage from Explosion")
end
do
-- Counter deals 2x physical damage taken; fails on 0 physical damage
local counterId = Moves.numForName("COUNTER")
local mon = { species = 127, level = 30, hp = 100, maxHp = 100, moves = { counterId }, pp = { 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
local foe = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 }
local dmgFail, infoFail = Damage.calc({ mon = mon, lastPhysicalDamageTaken = 0 }, { mon = foe }, counterId)
check(infoFail.failed == true, "Counter fails when no physical damage taken")
local dmgSuccess, infoSuccess = Damage.calc({ mon = mon, lastPhysicalDamageTaken = 30 }, { mon = foe }, counterId)
check(dmgSuccess == 60, "Counter deals exactly 2x (60 HP) for 30 HP physical damage taken")
end
do
-- Mirror Coat deals 2x special damage taken; fails on 0 special damage
local mcId = Moves.numForName("MIRROR_COAT")
local mon = { species = 202, level = 30, hp = 100, maxHp = 100, moves = { mcId }, pp = { 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 }
local foe = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 }
local dmgFail, infoFail = Damage.calc({ mon = mon, lastSpecialDamageTaken = 0 }, { mon = foe }, mcId)
check(infoFail.failed == true, "Mirror Coat fails when no special damage taken")
local dmgSuccess, infoSuccess = Damage.calc({ mon = mon, lastSpecialDamageTaken = 25 }, { mon = foe }, mcId)
check(dmgSuccess == 50, "Mirror Coat deals exactly 2x (50 HP) for 25 HP special damage taken")
end
do
-- Jump Kick / Hi Jump Kick crash damage on miss (half max HP)
local hjkId = Moves.numForName("HI_JUMP_KICK")
local st = State.new({
wild = true,
playerParty = { { species = 106, level = 30, hp = 100, maxHp = 100, moves = { hjkId }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
-- Force miss
local ad = setup_test_battle(st, function(lo, hi)
if lo == 1 and hi == 100 then return 100 end -- miss
return hi or lo or 0
end)
local full = Damage.calc(st.player, st.enemy, hjkId, { forceRoll = 100, forceCrit = false })
-- pokefirered/src/battle_script_commands.c:6466
local crash = math.min(50, math.max(1, math.floor(full / 2)))
local out = {}
Engine.resolveMove(st.player, st.enemy, hjkId, 1, ad, st, out)
check(ad:hp(st.player) == 100 - crash, "Hi Jump Kick miss crashes for half the damage it would have dealt")
local joined = table.concat(out, " || ")
check(joined:find("kept going\nand crashed!", 1, true) ~= nil, "Crash message printed on miss")
end
do
-- pokefirered/src/battle_script_commands.c:8435
local spinId = Moves.numForName("RAPID_SPIN")
local st = State.new({
wild = true,
playerParty = { { species = 7, level = 30, hp = 100, maxHp = 100, moves = { spinId }, pp = { 40 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
st.playerSide.spikes = 1
st.player.leechSeed = true
st.player.trapped = true
st.player.expTrapTurns = 3
st.player.expTrapSource = st.enemy
st.player.expTrapMove = 35
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, spinId, 1, ad, st, out)
check(st.player.expTrapTurns == nil, "Rapid Spin freed the wrap")
check(st.player.leechSeed == nil, "Rapid Spin removed Leech Seed")
check(st.player.trapped == nil, "Rapid Spin removed trapping effect")
check(st.playerSide.spikes == 0, "Rapid Spin removed Spikes in the same use")
local joined = table.concat(out, " || ")
local iWrap = joined:find("got free of", 1, true)
check(joined:find("WRAP!", 1, true) ~= nil, "Wrap freed message names WRAP")
local iSeed = joined:find("shed\nLEECH SEED!", 1, true)
local iSpikes = joined:find("blew away\nSPIKES!", 1, true)
check(iWrap ~= nil, "Wrap freed message printed")
check(iSeed ~= nil, "Leech Seed shed message printed")
check(iSpikes ~= nil, "Spikes blown away message printed")
check(iWrap and iSeed and iSpikes and iWrap < iSeed and iSeed < iSpikes,
"Rapid Spin frees wrap, then Leech Seed, then Spikes")
end
print("\n=== 8. Two-Turn Charging, Semi-Invulnerable, and Recharge (Solar Beam, Skull Bash, Fly, Hyper Beam) ===")
do
-- Solarbeam charges turn 1 in clear weather, attacks turn 2
local sbId = Moves.numForName("SOLARBEAM") or Moves.numForName("SOLAR_BEAM")
local st = State.new({
wild = true,
playerParty = { { species = 1, level = 30, hp = 100, maxHp = 100, moves = { sbId }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
-- Turn 1
local out1 = {}
Engine.resolveMove(st.player, st.enemy, sbId, 1, ad, st, out1)
local joined1 = table.concat(out1, " || ")
check(joined1:find("took\nin sunlight!", 1, true) ~= nil, "Solarbeam charging message printed on turn 1")
check(ad:hp(st.enemy) == 100, "Foe takes 0 damage on charge turn")
check(st.player.twoTurnMove == sbId, "Player is flagged with twoTurnMove")
-- Turn 2
local out2 = {}
Engine.resolveMove(st.player, st.enemy, sbId, 1, ad, st, out2)
check(ad:hp(st.enemy) < 100, "Foe takes damage on unleash turn 2")
check(st.player.twoTurnMove == nil, "twoTurnMove is cleared after unleash")
end
do
-- Skull Bash raises defense on charge turn 1
local sbashId = Moves.numForName("SKULL_BASH")
local st = State.new({
wild = true,
playerParty = { { species = 7, level = 30, hp = 100, maxHp = 100, moves = { sbashId }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, sbashId, 1, ad, st, out)
check((st.player.stages and st.player.stages.defense or 0) == 1, "Defense stage increased by 1 during Skull Bash charge")
end
do
-- Fly makes user semi-invulnerable on turn 1 (attacks miss)
local flyId = Moves.numForName("FLY")
local tackleId = Moves.numForName("TACKLE")
local st = State.new({
wild = true,
playerParty = { { species = 16, level = 30, hp = 100, maxHp = 100, moves = { flyId }, pp = { 15 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { tackleId }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
-- Player uses Fly turn 1
local outP = {}
Engine.resolveMove(st.player, st.enemy, flyId, 1, ad, st, outP)
check(st.player.semiInvulnerable ~= nil, "Player is semi-invulnerable")
-- Enemy tries to Tackle player
local outE = {}
Engine.resolveMove(st.enemy, st.player, tackleId, 1, ad, st, outE)
check(ad:hp(st.player) == 100, "Enemy attack missed semi-invulnerable flyer")
end
do
-- Hyper Beam forces recharge turn on turn 2
local hbId = Moves.numForName("HYPER_BEAM")
local st = State.new({
wild = true,
playerParty = { { species = 143, level = 30, hp = 100, maxHp = 100, moves = { hbId }, pp = { 5 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
-- Turn 1: Hyper Beam hits
local out1 = {}
Engine.resolveMove(st.player, st.enemy, hbId, 1, ad, st, out1)
check(ad:hp(st.enemy) < 200, "Hyper Beam dealt damage to foe")
check(st.player.expMustRecharge == true, "Player flagged to recharge next turn")
-- Turn 2: Attempt move during recharge
local out2 = {}
Engine.resolveMove(st.player, st.enemy, hbId, 1, ad, st, out2)
local joined2 = table.concat(out2, " || ")
check(joined2:find("must\nrecharge!", 1, true) ~= nil, "Recharge message printed and action skipped")
check(st.player.expMustRecharge == nil, "expMustRecharge cleared after recharge turn")
end
print("\n=== 9. Move Calling & Metagame (Metronome, Sleep Talk, Mirror Move) ===")
do
-- Metronome picks a random move and executes it
local metroId = Moves.numForName("METRONOME")
local st = State.new({
wild = true,
playerParty = { { species = 35, level = 30, hp = 100, maxHp = 100, moves = { metroId }, pp = { 10 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, metroId, 1, ad, st, out)
local joined = table.concat(out, " || ")
check(joined:find("used\nMETRONOME!", 1, true) ~= nil, "Metronome user message printed")
check(#out >= 2, "Metronome resolved a secondary move")
end
do
-- Sleep Talk executes a known move while user is asleep
local sleepTalkId = Moves.numForName("SLEEP_TALK")
local emberId = Moves.numForName("EMBER")
local st = State.new({
wild = true,
playerParty = { { species = 143, level = 30, hp = 100, maxHp = 100, moves = { sleepTalkId, emberId }, pp = { 10, 25 },
status = "SLP", sleepTurns = 3,
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 19, level = 20, hp = 200, maxHp = 200, moves = { 33 }, pp = { 35 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
local out = {}
Engine.resolveMove(st.player, st.enemy, sleepTalkId, 1, ad, st, out)
local joined = table.concat(out, " || ")
check(joined:find("used\nSLEEP TALK!", 1, true) ~= nil, "Sleep Talk message printed while asleep")
check(joined:find("used\nEMBER!", 1, true) ~= nil, "Sleep Talk invoked Ember")
check(ad:hp(st.enemy) < 200, "Foe took damage from Sleep Talked move")
end
do
-- Mirror Move copies foe's last move
local mmId = Moves.numForName("MIRROR_MOVE")
local emberId = Moves.numForName("EMBER")
local st = State.new({
wild = true,
playerParty = { { species = 16, level = 30, hp = 100, maxHp = 100, moves = { mmId }, pp = { 20 },
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = 50 } },
foeMon = { species = 4, level = 20, hp = 200, maxHp = 200, moves = { emberId }, pp = { 25 },
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
})
local ad = setup_test_battle(st)
-- pokefirered/src/battle_script_commands.c:6350
Engine.resolveMove(st.enemy, st.player, emberId, 1, ad, st, {})
local out = {}
Engine.resolveMove(st.player, st.enemy, mmId, 1, ad, st, out)
local joined = table.concat(out, " || ")
check(joined:find("used\nMIRROR MOVE!", 1, true) ~= nil, "Mirror Move user message printed")
check(joined:find("used\nEMBER!", 1, true) ~= nil, "Mirror Move executed enemy's last move Ember")
check(ad:hp(st.enemy) < 200, "Foe took damage from mirrored move")
end
if failed > 0 then
print("\n[FAIL] FAILED " .. failed .. " tests")
os.exit(1)
end
print("\n[ok] All game3 special moves tests passed successfully!")