mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
363 lines
17 KiB
Lua
363 lines
17 KiB
Lua
#!/usr/bin/env luajit
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
require("tests.game3_cache").mountOrSkip("game3_battle_event_seq_test")
|
|
require("tests.fixture_data.game3_items").install()
|
|
|
|
local Moves = require("src.core.game3.battle.moves")
|
|
|
|
-- pokefirered/src/data/battle_moves.h:1
|
|
local ROM = {
|
|
[18] = { effect = 28, power = 0, type = 0, accuracy = 100, pp = 20, secondaryChance = 0, target = 0, priority = -6, flags = 18 },
|
|
[19] = { effect = 155, power = 70, type = 2, accuracy = 95, pp = 15, secondaryChance = 0, target = 0, priority = 0, flags = 51 },
|
|
[33] = { effect = 0, power = 35, type = 0, accuracy = 95, pp = 35, secondaryChance = 0, target = 0, priority = 0, flags = 51 },
|
|
[45] = { effect = 18, power = 0, type = 0, accuracy = 100, pp = 40, secondaryChance = 0, target = 8, priority = 0, flags = 22 },
|
|
[46] = { effect = 28, power = 0, type = 0, accuracy = 100, pp = 20, secondaryChance = 0, target = 0, priority = -6, flags = 18 },
|
|
[73] = { effect = 84, power = 0, type = 11, accuracy = 90, pp = 10, secondaryChance = 0, target = 0, priority = 0, flags = 22 },
|
|
[164] = { effect = 79, power = 0, type = 0, accuracy = 0, pp = 10, secondaryChance = 0, target = 16, priority = 0, flags = 0 },
|
|
[226] = { effect = 127, power = 0, type = 0, accuracy = 0, pp = 40, secondaryChance = 0, target = 16, priority = 0, flags = 0 },
|
|
[264] = { effect = 170, power = 150, type = 1, accuracy = 100, pp = 20, secondaryChance = 0, target = 0, priority = -3, flags = 1 },
|
|
}
|
|
Moves._romLoaded = true
|
|
Moves._rom = ROM
|
|
|
|
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 AnimSeq = require("src.core.game3.battle.anim_seq")
|
|
local Anim = require("src.core.game3.battle.anim")
|
|
local Commands = require("src.core.game3.battle.commands")
|
|
local Types = require("src.core.game3.battle.types")
|
|
local T = Types.ID
|
|
|
|
local failed, passed = 0, 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
passed = passed + 1
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
|
|
local function mon(o)
|
|
return {
|
|
species = o.species or 1, level = o.level or 50, hp = o.hp or 100, maxHp = o.maxHp or 100,
|
|
attack = 50, defense = 50, spAtk = 50, spDef = 50, speed = o.speed or 50, ability = 0,
|
|
nickname = o.nickname, moves = o.moves or { 33 }, pp = o.pp or { 20, 20, 20, 20 },
|
|
status = o.status, item = o.item, heldItem = o.item,
|
|
}
|
|
end
|
|
|
|
local function rng(lo, hi)
|
|
if lo == 1 and hi == 100 then return 1 end
|
|
return lo
|
|
end
|
|
|
|
local function battle(p, e, extra)
|
|
extra = extra or {}
|
|
p.nickname = p.nickname or "ALPHA"
|
|
e.nickname = e.nickname or "BRAVO"
|
|
local party = { mon(p) }
|
|
for _, x in ipairs(extra.bench or {}) do party[#party + 1] = mon(x) end
|
|
local foeParty = { mon(e) }
|
|
for _, x in ipairs(extra.foeBench or {}) do foeParty[#foeParty + 1] = mon(x) end
|
|
local st = State.new({ wild = extra.wild ~= false, playerParty = party, foeParty = foeParty })
|
|
st.player.type1 = T.NORMAL
|
|
st.enemy.type1 = T.NORMAL
|
|
st.rng = rng
|
|
return st, Adapter.new(st)
|
|
end
|
|
|
|
local function use(st, ad, user, move)
|
|
local target = (user == st.player) and st.enemy or st.player
|
|
local out = {}
|
|
Engine.resolveMove(user, target, move, 1, ad, st, out)
|
|
return out
|
|
end
|
|
|
|
local function kinds(steps)
|
|
local out = {}
|
|
for _, s in ipairs(steps) do
|
|
local k = s.kind
|
|
if k == "anim" then k = s.data.anim .. ":" .. tostring(s.data.name) end
|
|
out[#out + 1] = k
|
|
end
|
|
return table.concat(out, ",")
|
|
end
|
|
|
|
local function index_of(steps, kind, from)
|
|
for i = from or 1, #steps do
|
|
local k = steps[i].kind
|
|
if k == "anim" then k = steps[i].data.anim .. ":" .. tostring(steps[i].data.name) end
|
|
if k == kind then return i end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
print("=== damaging hit: move -> effectiveness+blink -> hp ===")
|
|
do
|
|
local st, ad = battle({ moves = { 33 } }, { hp = 200, maxHp = 200 })
|
|
local out = use(st, ad, st.player, 33)
|
|
local steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
local m, fx, hp = index_of(steps, "move"), index_of(steps, "hitfx"), index_of(steps, "hp")
|
|
check(steps[1].kind == "msg" and m and fx and hp and m < fx and fx < hp, "used msg, move, hitfx, hp in order: " .. kinds(steps))
|
|
check(steps[fx].data.side == "enemy" and steps[fx].data.effectiveness == 1, "hitfx targets enemy with normal effectiveness")
|
|
end
|
|
|
|
print("=== stat change plays STATS_CHANGE with pret animArg ===")
|
|
do
|
|
local st, ad = battle({ moves = { 45 } }, {})
|
|
local out = use(st, ad, st.player, 45)
|
|
local steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
local i = index_of(steps, "general:STATS_CHANGE")
|
|
check(i ~= nil, "Growl emits STATS_CHANGE: " .. kinds(steps))
|
|
check(i and index_of(steps, "move") < i, "stat anim follows the move anim")
|
|
check(i and steps[i].data.attacker == "enemy" and tonumber(steps[i].data.arg), "stat anim on the lowered battler with arg")
|
|
end
|
|
|
|
print("=== wild Roar: move -> switch_out -> end ===")
|
|
do
|
|
local st, ad = battle({ moves = { 46 }, level = 50 }, { level = 40 })
|
|
local out = use(st, ad, st.player, 46)
|
|
local steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
local m, so, en = index_of(steps, "move"), index_of(steps, "switch_out"), index_of(steps, "end")
|
|
check(m and so and en and m < so and so < en, "roar drag-out then end: " .. kinds(steps))
|
|
check(steps[so].data.side == "enemy", "roared wild mon returns to ball")
|
|
end
|
|
|
|
print("=== trainer Roar: switch_out -> switch_in -> dragged out text ===")
|
|
do
|
|
local st, ad = battle({ moves = { 46 } }, { species = 1 }, { wild = false, foeBench = { { species = 4, nickname = "CHARLIE" } } })
|
|
local out = use(st, ad, st.player, 46)
|
|
local steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
local so, si = index_of(steps, "switch_out"), index_of(steps, "switch_in")
|
|
local drag
|
|
for i, s in ipairs(steps) do if s.kind == "msg" and tostring(s.data.text):find("dragged out") then drag = i end end
|
|
check(so and si and drag and so < si and si < drag, "drag-out order: " .. kinds(steps))
|
|
check(si and steps[si].data.slot == 2 and steps[so].data.slot == 1, "switch carries old/new party slots")
|
|
end
|
|
|
|
print("=== Baton Pass: switch_out -> Go! text -> switch_in ===")
|
|
do
|
|
local st, ad = battle({ moves = { 226 } }, {}, { bench = { { species = 4, nickname = "DELTA" } } })
|
|
local out = use(st, ad, st.player, 226)
|
|
local steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
local so, si = index_of(steps, "switch_out"), index_of(steps, "switch_in")
|
|
local go
|
|
for i, s in ipairs(steps) do if s.kind == "msg" and tostring(s.data.text):find("Go!") then go = i end end
|
|
check(so and si and go and so < go and go < si, "pret prints SWITCHINMON before switchinanim: " .. kinds(steps))
|
|
end
|
|
|
|
print("=== Substitute: doll specials wrap the next attack ===")
|
|
do
|
|
local st, ad = battle({ moves = { 164, 33 } }, { hp = 300, maxHp = 300 })
|
|
use(st, ad, st.player, 164)
|
|
check((st.player.substituteHP or 0) > 0, "substitute up")
|
|
local out = use(st, ad, st.player, 33)
|
|
local steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
local a, m, b = index_of(steps, "special:SUBSTITUTE_TO_MON"), index_of(steps, "move"), index_of(steps, "special:MON_TO_SUBSTITUTE")
|
|
check(a and m and b and a < m and m < b, "SUBSTITUTE_TO_MON, move, MON_TO_SUBSTITUTE: " .. kinds(steps))
|
|
check(steps[a].data.moveId == 33 and steps[b].data.moveId == 33, "specials know the paired move (battle scene gate)")
|
|
end
|
|
|
|
print("=== residual stream: poison status anim + hp tick ===")
|
|
do
|
|
local st, ad = battle({ moves = { 33 }, status = "PSN" }, {})
|
|
st.player.status = "PSN"
|
|
local evs = Engine.collectResidualEvents(st, ad)
|
|
local stream = {}
|
|
for _, e in ipairs(evs) do for _, x in ipairs(e.events or {}) do stream[#stream + 1] = x end end
|
|
local steps = AnimSeq.buildSteps(stream, {})
|
|
local s, h = index_of(steps, "status:POISON"), index_of(steps, "hp")
|
|
check(s and h and s < h, "poison anim precedes the hp drop: " .. kinds(steps))
|
|
check(index_of(steps, "hitfx") == nil, "residual damage does not blink")
|
|
end
|
|
|
|
print("=== Leech Seed drain anim ===")
|
|
do
|
|
local st, ad = battle({ moves = { 73 } }, {})
|
|
use(st, ad, st.player, 73)
|
|
local evs = Engine.collectResidualEvents(st, ad)
|
|
local stream = {}
|
|
for _, e in ipairs(evs) do for _, x in ipairs(e.events or {}) do stream[#stream + 1] = x end end
|
|
local steps = AnimSeq.buildSteps(stream, {})
|
|
local i = index_of(steps, "general:LEECH_SEED_DRAIN")
|
|
check(i and steps[i].data.attacker == "enemy" and steps[i].data.target == "player", "drain from seeded enemy to seeder: " .. kinds(steps))
|
|
end
|
|
|
|
print("=== Fly turn 1 carries turn index ===")
|
|
do
|
|
local st, ad = battle({ moves = { 19 } }, {})
|
|
local out = use(st, ad, st.player, 19)
|
|
local steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
local m = index_of(steps, "move")
|
|
check(m and steps[m].data.turn == 0 and st.player.semiInvulnerable, "fly charge anim is turn 0 and user is semi-invulnerable")
|
|
out = use(st, ad, st.player, 19)
|
|
steps = AnimSeq.buildSteps(out._anim.events, out._anim)
|
|
m = index_of(steps, "move")
|
|
check(m and steps[m].data.turn == 1, "fly strike anim is turn 1")
|
|
end
|
|
|
|
print("=== headless playback pushes messages in stream order ===")
|
|
do
|
|
Anim.reset({ headless = true })
|
|
local pushed = {}
|
|
AnimSeq.beginEvents({
|
|
{ kind = "msg", text = "A" },
|
|
{ kind = "anim", anim = "general", name = "RAIN_CONTINUES" },
|
|
{ kind = "msg", text = "B" },
|
|
{ kind = "hp", side = "enemy", from = 10, to = 5, maxHp = 10 },
|
|
{ kind = "end", result = "run", reason = "roar" },
|
|
}, function(t) pushed[#pushed + 1] = t end)
|
|
local guard = 0
|
|
while not AnimSeq.update() and guard < 50 do guard = guard + 1 end
|
|
check(table.concat(pushed, ",") == "A,B", "messages pushed in order")
|
|
check(AnimSeq.ended() and AnimSeq.ended().reason == "roar", "end event recorded for init")
|
|
check(Anim.present("enemy").displayHp == 5, "hp tween applied")
|
|
end
|
|
|
|
print("=== move selection restrictions (TrySetCantSelectMoveBattleScript) ===")
|
|
do
|
|
local st = battle({ moves = { 33, 45, 46, 164 } }, { moves = { 45 } })
|
|
st.player.expDisabledMove = 33
|
|
check((Commands.selectionError(st, 1) or ""):find("TACKLE\nis disabled!", 1, true), "disabled move message")
|
|
st.player.expDisabledMove = nil
|
|
st.player.expTauntedTurns = 2
|
|
check((Commands.selectionError(st, 2) or ""):find("after the TAUNT!", 1, true), "taunt blocks status moves")
|
|
check(Commands.selectionError(st, 1) == nil, "taunt allows damaging moves")
|
|
st.player.expTauntedTurns = nil
|
|
st.player.expTormented = true
|
|
st.player.lastMoveId = 33
|
|
check((Commands.selectionError(st, 1) or ""):find("TORMENT", 1, true), "torment blocks repeat")
|
|
st.player.expTormented = nil
|
|
st.enemy.expImprison = true
|
|
check((Commands.selectionError(st, 2) or ""):find("sealed", 1, true), "imprison seals shared move")
|
|
st.enemy.expImprison = nil
|
|
st.player.item = 186
|
|
st.player.choicedMove = 46
|
|
check((Commands.selectionError(st, 1) or ""):find("allows only", 1, true), "choice band lock message")
|
|
check(Commands.selectionError(st, 3) == nil, "choiced move itself is allowed")
|
|
st.player.item = nil
|
|
st.player.mon.pp[4] = 0
|
|
check((Commands.selectionError(st, 4) or ""):find("no PP left", 1, true), "no PP message")
|
|
st.player.mon.pp[4] = 10
|
|
st.player.expEncoreMove = 45
|
|
st.player.expEncoreTurns = 2
|
|
local act = Commands.fightShortcut(st)
|
|
check(act and act.slot == 2, "encore skips move selection")
|
|
st.player.expEncoreMove = nil
|
|
st.player.mon.pp = { 0, 0, 0, 0 }
|
|
local act2, msg = Commands.fightShortcut(st)
|
|
check(act2 and act2.move == "STRUGGLE" and msg and msg:find("no\nmoves left!", 1, true), "all moves unusable -> Struggle")
|
|
end
|
|
|
|
print("=== headless battle: Focus Punch text at turn start, Roar ends the battle ===")
|
|
do
|
|
package.loaded["src.core.game3.audio"] = package.loaded["src.core.game3.audio"] or setmetatable({}, {
|
|
__index = function() return function() end end,
|
|
})
|
|
Moves.loadRomPack = function()
|
|
Moves._romLoaded = true
|
|
Moves._rom = ROM
|
|
return true
|
|
end
|
|
local Battle = require("src.core.game3.battle.init")
|
|
local Damage = require("src.core.game3.battle.damage")
|
|
local pMon = Damage.ensureStats({ species = 1, level = 30, hp = 90, maxHp = 90, moves = { 33 }, pp = { 35 } })
|
|
local foe = Damage.ensureStats({ species = 16, level = 30, hp = 90, maxHp = 90, moves = { 264 }, pp = { 20 } })
|
|
Battle.start({ headless = true, autoFight = false, playerParty = { pMon }, foe = foe, wild = true })
|
|
Battle.update(0, nil)
|
|
local st = Battle.getState()
|
|
st.rng = rng
|
|
Battle._auto = true
|
|
Battle.update(0, nil)
|
|
for _ = 1, 4 do Battle.update(0, nil) end
|
|
local log = require("src.core.game3.battle.ui").log()
|
|
local fp, used
|
|
for i, t in ipairs(log) do
|
|
if not fp and t:find("tightening", 1, true) then fp = i end
|
|
if not used and t:find("TACKLE", 1, true) then used = i end
|
|
end
|
|
check(fp and used and fp < used, "focus punch setup text before the first move")
|
|
Battle.abort()
|
|
|
|
local pMon2 = Damage.ensureStats({ species = 1, level = 30, hp = 90, maxHp = 90, moves = { 46 }, pp = { 20 } })
|
|
local foe2 = Damage.ensureStats({ species = 16, level = 10, hp = 40, maxHp = 40, moves = { 33 }, pp = { 35 } })
|
|
local result
|
|
Battle.start({ headless = true, playerParty = { pMon2 }, foe = foe2, wild = true, onDone = function(r) result = r end })
|
|
check(result == "run", "roar in a wild battle ends it mid-turn with run (got " .. tostring(result) .. ")")
|
|
local sawFlee = false
|
|
for _, t in ipairs(require("src.core.game3.battle.ui").log()) do
|
|
if t:find("Got away safely", 1, true) then sawFlee = true end
|
|
end
|
|
check(not sawFlee, "no forced flee text after roar")
|
|
end
|
|
|
|
do
|
|
-- pokefirered/src/battle_script_commands.c:3113
|
|
local Experience = require("src.core.game3.battle.experience")
|
|
local Damage = require("src.core.game3.battle.damage")
|
|
local origYield = Experience.expYield
|
|
Experience.expYield = function() return 70 end
|
|
local a = Damage.ensureStats({ species = 4, level = 10, hp = 30, maxHp = 30, moves = { 33 }, pp = { 35 } })
|
|
local b = Damage.ensureStats({ species = 7, level = 10, hp = 30, maxHp = 30, moves = { 33 }, pp = { 35 }, item = 182 })
|
|
local c = Damage.ensureStats({ species = 1, level = 10, hp = 30, maxHp = 30, moves = { 33 }, pp = { 35 } })
|
|
local foe = Damage.ensureStats({ species = 16, level = 14, hp = 0, maxHp = 30, moves = { 33 }, pp = { 35 } })
|
|
local st = State.new({ wild = true, playerParty = { a, b, c }, foeParty = { foe } })
|
|
local out = Experience.awardFoe(st, st.enemy, { trainer = false, partyIndices = { 1 } })
|
|
local by = {}
|
|
for _, e in ipairs(out) do by[e.partyIndex] = e.amount end
|
|
check(by[1] == 70, "Exp. Share: the sent-in mon gets half the pool (got " .. tostring(by[1]) .. ")")
|
|
check(by[2] == 70, "Exp. Share: the holder gets the other half (got " .. tostring(by[2]) .. ")")
|
|
check(by[3] == nil, "Exp. Share: a benched mon without the item gets nothing")
|
|
Experience.expYield = origYield
|
|
end
|
|
|
|
do
|
|
-- pokefirered/src/battle_anim_status_effects.c:455
|
|
local AnimTasks = require("src.core.game3.battle.anim_tasks")
|
|
Anim.reset({ headless = true })
|
|
local vm = Anim.vm()
|
|
vm:setBattlers("player", "player")
|
|
vm.animArg = 15
|
|
local AnimVm = require("src.core.game3.battle.anim_vm")
|
|
local t = AnimVm.spawnTask(vm, "StatsChange", 5, {}, "visual")
|
|
local frames, peak = 0, 0
|
|
while t.active and frames < 200 do
|
|
AnimTasks.update(vm)
|
|
frames = frames + 1
|
|
local m = Anim.present("player").statMask
|
|
if m then peak = math.max(peak, m.eva) end
|
|
end
|
|
check(peak == 10, "ATK +1 stat mask fades to BLDALPHA 10 (got " .. tostring(peak) .. ")")
|
|
check(frames == 2 + 20 + 20 + 20 + 1, "ATK +1 stat change runs 2 setup + 20 in + 20 hold + 20 out frames (got " .. frames .. ")")
|
|
vm.animArg = 46
|
|
t = AnimVm.spawnTask(vm, "StatsChange", 5, {}, "visual")
|
|
local sharpPeak, tm = 0, nil
|
|
while t.active and frames < 1000 do
|
|
AnimTasks.update(vm)
|
|
frames = frames + 1
|
|
local m = Anim.present("player").statMask
|
|
if m then sharpPeak = math.max(sharpPeak, m.eva); tm = m.tilemap end
|
|
end
|
|
check(sharpPeak == 13 and tm == 2, "ATK -2 uses the falling tilemap and BLDALPHA 13")
|
|
end
|
|
|
|
do
|
|
-- pokefirered/src/party_menu.c:5916
|
|
local Damage = require("src.core.game3.battle.damage")
|
|
local p1 = Damage.ensureStats({ species = 4, level = 10, hp = 30, maxHp = 30, moves = { 33 }, pp = { 35 } })
|
|
local p2 = Damage.ensureStats({ species = 7, level = 10, hp = 0, maxHp = 30, moves = { 33 }, pp = { 35 } })
|
|
local p3 = Damage.ensureStats({ species = 1, level = 10, hp = 30, maxHp = 30, moves = { 33 }, pp = { 35 } })
|
|
local foe = Damage.ensureStats({ species = 16, level = 14, hp = 30, maxHp = 30, moves = { 33 }, pp = { 35 } })
|
|
local st = State.new({ wild = true, playerParty = { p1, p2, p3 }, foeParty = { foe } })
|
|
check(tostring(Commands.switchError(st, 2)):find("no energy", 1, true) ~= nil, "fainted party mon can't be sent in")
|
|
check(tostring(Commands.switchError(st, 1)):find("already", 1, true) ~= nil, "active mon is already in battle")
|
|
check(Commands.switchError(st, 3) == nil, "healthy bench mon can switch in")
|
|
end
|
|
|
|
print(string.format("%d passed, %d failed", passed, failed))
|
|
if failed > 0 then os.exit(1) end
|