mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 06:55:29 +02:00
191 lines
6.8 KiB
Lua
191 lines
6.8 KiB
Lua
-- Read-only Gen 2 battle state with the same shape as mod.battle on Gen 1.
|
|
|
|
local BattleAPI = {}
|
|
BattleAPI.__index = BattleAPI
|
|
|
|
function BattleAPI.new(game)
|
|
return setmetatable({ game = game, revision = 0, signature = nil }, BattleAPI)
|
|
end
|
|
|
|
local function activeBattle(game)
|
|
local states = game and game.stack and game.stack.states or {}
|
|
local battle
|
|
for i = #states, 1, -1 do
|
|
local state = states[i]
|
|
if state.screenId == "Gen2BattleState" or state.isGen2BattleState then
|
|
battle = state
|
|
break
|
|
end
|
|
end
|
|
return battle, states[#states]
|
|
end
|
|
|
|
local function monCopy(data, mon, active)
|
|
if not mon then return nil end
|
|
local def = data and data.pokemon and data.pokemon[mon.species]
|
|
return { species = mon.species,
|
|
name = mon.nickname or (def and def.name) or mon.species,
|
|
level = mon.level, hp = mon.hp,
|
|
maxHp = mon.maxHp or (mon.stats and mon.stats.hp) or mon.hp,
|
|
status = mon.status, active = active and true or false }
|
|
end
|
|
|
|
local function messageCopy(screen)
|
|
if not screen.message then return nil end
|
|
local lines = {}
|
|
for line in tostring(screen.message):gmatch("[^\n]+") do
|
|
lines[#lines + 1] = line
|
|
end
|
|
return #lines > 0 and lines or nil
|
|
end
|
|
|
|
local function itemCopies(game, screen, catchable)
|
|
local out = {}
|
|
for id, count in pairs((game.save and game.save.inventory) or {}) do
|
|
local def = game.data.items and game.data.items[id]
|
|
if count > 0 and def and def.pocket == "BALL" then
|
|
out[#out + 1] = { id = id, name = def.name or id, count = count,
|
|
ball = true, needsTarget = false,
|
|
catchChance = catchable and screen.catchChance
|
|
and screen:catchChance(id) or nil }
|
|
end
|
|
end
|
|
table.sort(out, function(a, b) return a.name < b.name end)
|
|
return out
|
|
end
|
|
|
|
local function signature(game, screen, top)
|
|
if not screen then return "none" end
|
|
local battle = screen.battle or {}
|
|
local parts = { tostring(screen), tostring(top), tostring(screen.phase),
|
|
tostring(screen.message), tostring(screen.messageTimer),
|
|
tostring(screen.menuIndex), tostring(screen.moveIndex),
|
|
tostring(battle.turn), tostring(battle.over), tostring(battle.outcome) }
|
|
for _, mon in ipairs({ battle.player, battle.enemy }) do
|
|
parts[#parts + 1] = tostring(mon)
|
|
parts[#parts + 1] = tostring(mon and mon.hp)
|
|
parts[#parts + 1] = tostring(mon and mon.status)
|
|
end
|
|
for _, mon in ipairs((game.save and game.save.party) or battle.party or {}) do
|
|
parts[#parts + 1] = tostring(mon)
|
|
parts[#parts + 1] = tostring(mon.hp)
|
|
parts[#parts + 1] = tostring(mon.status)
|
|
end
|
|
for _, item in ipairs(itemCopies(game, screen, false)) do
|
|
parts[#parts + 1] = item.id .. "=" .. tostring(item.count)
|
|
end
|
|
return table.concat(parts, "|")
|
|
end
|
|
|
|
function BattleAPI:_revision(screen, top)
|
|
local nextSignature = signature(self.game, screen, top)
|
|
if nextSignature ~= self.signature then
|
|
self.signature = nextSignature
|
|
self.revision = self.revision + 1
|
|
end
|
|
return self.revision
|
|
end
|
|
|
|
local function moveCopies(game, battle)
|
|
local out = {}
|
|
for slot, move in ipairs((battle.player and battle.player.moves) or {}) do
|
|
local def = (game.data.moves or {})[move.id] or {}
|
|
out[slot] = { slot = slot, id = move.id, name = def.name or move.id,
|
|
pp = move.pp, maxPp = move.maxPp or def.pp or move.pp,
|
|
type = def.type, power = def.power, accuracy = def.accuracy,
|
|
disabled = battle:moveDisabled(battle.player, move.id) }
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function battleKind(screen)
|
|
if screen.tutorial then return "oldman" end
|
|
return screen.battle and screen.battle.wild and "wild" or "trainer"
|
|
end
|
|
|
|
function BattleAPI:snapshot()
|
|
local game = self.game
|
|
local screen, top = activeBattle(game)
|
|
if not screen or not screen.battle then return nil end
|
|
local battle = screen.battle
|
|
local prompt = "locked"
|
|
if top == screen and screen.phase == "menu" then
|
|
prompt = "menu"
|
|
elseif top == screen and screen.phase == "moves" then
|
|
prompt = "moves"
|
|
elseif top == screen and screen.message then
|
|
prompt = "advance"
|
|
elseif top and top.screenId == "Gen2PartyMenu" then
|
|
prompt = "party"
|
|
end
|
|
local party = {}
|
|
for i, mon in ipairs((game.save and game.save.party) or battle.party or {}) do
|
|
party[i] = monCopy(game.data, mon, mon == battle.player)
|
|
party[i].slot = i
|
|
end
|
|
return { revision = self:_revision(screen, top), kind = battleKind(screen),
|
|
catchable = battle.wild and not screen.tutorial, prompt = prompt,
|
|
message = messageCopy(screen), turn = battle.turn or 0,
|
|
player = monCopy(game.data, battle.player, true),
|
|
enemy = monCopy(game.data, battle.enemy, true),
|
|
party = party, moves = moveCopies(game, battle),
|
|
-- Targeted medicine remains screen-owned, but balls are complete semantic
|
|
-- records and can safely expose the same read-only preview as Gen 1.
|
|
items = itemCopies(game, screen, battle.wild and not screen.tutorial) }
|
|
end
|
|
|
|
local MENU_CHOICES = { fight = true, party = true, item = true, run = true }
|
|
|
|
local function validSlot(slot)
|
|
return type(slot) == "number" and slot % 1 == 0 and slot >= 1
|
|
end
|
|
|
|
function BattleAPI:submit(intent)
|
|
if type(intent) ~= "table" then return nil, "intent must be a table" end
|
|
if type(intent.id) ~= "number" or intent.id % 1 ~= 0 or intent.id < 1 then
|
|
return nil, "intent id must be a positive integer"
|
|
end
|
|
if self.lastIntentId and intent.id <= self.lastIntentId then
|
|
return nil, "replayed intent"
|
|
end
|
|
|
|
local screen, top = activeBattle(self.game)
|
|
if not screen or not screen.battle then return nil, "no battle" end
|
|
if intent.revision ~= self:_revision(screen, top) then
|
|
return nil, "stale battle context"
|
|
end
|
|
if screen.tutorial then return nil, "battle kind is not controllable" end
|
|
if top ~= screen then return nil, "battle menu is covered" end
|
|
|
|
local battle = screen.battle
|
|
local ok, err
|
|
if intent.kind == "menu" then
|
|
if screen.phase ~= "menu" then return nil, "battle menu is not active" end
|
|
if not MENU_CHOICES[intent.choice] then
|
|
return nil, "unknown battle menu choice"
|
|
end
|
|
ok, err = screen:chooseMenu(intent.choice)
|
|
elseif intent.kind == "move" then
|
|
if screen.phase ~= "moves" then return nil, "move menu is not active" end
|
|
if screen.moveSwapIndex then return nil, "move reorder is active" end
|
|
local move = validSlot(intent.slot) and battle.player
|
|
and battle.player.moves and battle.player.moves[intent.slot]
|
|
if not move then return nil, "invalid move slot" end
|
|
if (move.pp or 0) <= 0 then return nil, "move has no PP" end
|
|
if battle:moveDisabled(battle.player, move.id) then
|
|
return nil, "move is disabled"
|
|
end
|
|
ok, err = screen:chooseMove(intent.slot)
|
|
elseif intent.kind == "back" then
|
|
ok, err = screen:cancelMove()
|
|
else
|
|
return nil, "unknown battle intent"
|
|
end
|
|
if not ok then return nil, err end
|
|
self.lastIntentId = intent.id
|
|
self.signature = nil
|
|
return true
|
|
end
|
|
|
|
return BattleAPI
|