mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
912 lines
43 KiB
Lua
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!")
|