feat(mods): expose read-only battle snapshots

This commit is contained in:
AverageConsumer
2026-08-16 01:25:56 +02:00
parent d87f6b8ad1
commit fcb5d1d348
10 changed files with 583 additions and 29 deletions
+200
View File
@@ -0,0 +1,200 @@
-- Read-only Gen 1 battle state for companion UIs and accessibility mods.
local Damage = require("src.battle.Damage")
local ItemEffects = require("src.inventory.ItemEffects")
local TypeChart = require("src.battle.TypeChart")
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 {}
for i = #states, 1, -1 do
if states[i].isBattleState then return states[i], states[#states] end
end
end
local function monCopy(data, mon, active)
if not mon then return nil end
local def = 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.stats and mon.stats.hp or mon.hp,
status = mon.status, active = active and true or false,
}
end
local function visibleMessage(battle, top)
local source = top and top.isTextBox and top or top == battle and battle
local lines = source and source.visibleText and source:visibleText()
if not lines then return nil end
local copy = {}
for i, line in ipairs(lines) do copy[i] = tostring(line) end
return copy
end
local function signature(game, battle, top)
if not battle then return "none" end
local parts = { tostring(battle), tostring(top), battle.phase or "",
tostring(battle.turnCount or 0), tostring(#(battle.queue or {})),
tostring(battle.current), tostring(battle.msgWaiting),
tostring(battle.msgPrompt), tostring(battle.menuIndex),
tostring(battle.mimicIndex),
tostring(battle.safari and battle.safari.balls),
tostring(battle.ghost), tostring(battle.noCatch),
table.concat(visibleMessage(battle, top) or {}, "\n") }
for _, battler in ipairs({ battle.player, battle.enemy }) do
local mon = battler and battler.mon
parts[#parts + 1] = tostring(mon)
parts[#parts + 1] = tostring(mon and mon.hp)
parts[#parts + 1] = tostring(mon and mon.status)
parts[#parts + 1] = table.concat(battler and battler.curTypes or {}, ",")
end
for _, mon in ipairs(game.save.party or {}) do
parts[#parts + 1] = tostring(mon)
parts[#parts + 1] = tostring(mon.hp)
parts[#parts + 1] = tostring(mon.status)
end
local inventory = {}
for id, count in pairs(game.save.inventory or {}) do
if ItemEffects.isBall(id) or ItemEffects.isBattleMedicine(id) then
inventory[#inventory + 1] = id .. "=" .. tostring(count)
end
end
table.sort(inventory)
for _, item in ipairs(inventory) do parts[#parts + 1] = item end
return table.concat(parts, "|")
end
function BattleAPI:_revision(battle, top)
local nextSignature = signature(self.game, battle, top)
if nextSignature ~= self.signature then
self.signature = nextSignature
self.revision = self.revision + 1
end
return self.revision
end
local IMMUNITY_ONLY = { SPECIAL_DAMAGE_EFFECT = true,
SUPER_FANG_EFFECT = true, OHKO_EFFECT = true }
local function movePreview(battle, move, def)
local record = battle.effectRecord and battle:effectRecord(def.effect)
local power, typeMult = def.power or 0
if power > 0 and move.id ~= "COUNTER" then
local raw = TypeChart.effectiveness(def.type, battle.enemy.curTypes or {})
if IMMUNITY_ONLY[def.effect] then
typeMult = raw == 0 and 0 or 10
elseif not (record and record.chooseDamage) then
typeMult = raw
end
end
local hitChance
if record and record.neverMiss then
hitChance = 100
elseif not (record and record.gate)
and (power > 0 or (record and record.accuracyChecked)) then
hitChance = battle.enemy.invulnerable and 0
or Damage.accuracyChance(battle.ruleset, def,
battle.player, battle.enemy)
end
local displayPower = power > 0 and move.id ~= "COUNTER"
and not IMMUNITY_ONLY[def.effect]
and not (record and record.chooseDamage) and power or nil
return typeMult, hitChance, displayPower
end
local function moveCopies(game, battle)
local out = {}
for slot, move in ipairs((battle.player and battle.player.curMoves) or {}) do
local def = game.data.moves[move.id] or {}
local mult, hitChance, displayPower = movePreview(battle, move, def)
out[slot] = { slot = slot, id = move.id, name = def.name or move.id,
pp = move.pp,
maxPp = (def.pp or move.pp or 0)
+ (move.ppUps or 0) * math.floor((def.pp or 0) / 5),
type = def.type, power = def.power, accuracy = def.accuracy,
displayPower = displayPower, hitChance = hitChance,
effectiveness = mult,
disabled = battle.player.disabledSlot == slot }
end
return out
end
local function itemCopies(game, battle, catchable)
local out = {}
for id, count in pairs(game.save.inventory or {}) do
if count > 0
and (ItemEffects.isBall(id) or ItemEffects.isBattleMedicine(id)) then
local def = game.data.items[id] or {}
local ball = ItemEffects.isBall(id)
out[#out + 1] = { id = id, name = def.name or id, count = count,
ball = ball, needsTarget = not ball,
catchChance = ball and catchable and battle.catchChance
and battle:catchChance(id) or nil }
end
end
table.sort(out, function(a, b) return a.name < b.name end)
return out
end
local function mimicCopies(game, battle)
local out = {}
for i, move in ipairs(battle.mimicMoves or {}) do
local def = game.data.moves[move.id] or {}
out[i] = { index = i, slot = move.slot, id = move.id,
name = def.name or move.id }
end
return out
end
function BattleAPI:snapshot()
local game = self.game
local battle, top = activeBattle(game)
if not battle then return nil end
local kind = battle:battleKind()
local supported = kind ~= "oldman" and kind ~= "link"
local catchable = kind == "wild" and not battle.ghost and not battle.noCatch
local forcedParty = top and top.isPartyMenu and top.battle == battle
and top.forceSwitch
local canAdvance = supported and ((top == battle
and battle.phase == "messages" and battle.current
and (battle.msgWaiting or battle.msgPrompt))
or (top and top.isTextBox and not top.choice
and (top.waiting or top.done)))
local prompt = "locked"
if canAdvance then prompt = "advance"
elseif supported and forcedParty then prompt = "party"
elseif supported and top == battle and kind == "safari"
and battle.phase == "menu" then prompt = "safari"
elseif supported and top == battle and battle.phase == "mimicSelect" then
prompt = "mimic"
elseif supported and top == battle and battle.phase == "menu" then
prompt = "menu"
elseif supported and top == battle and battle.phase == "moveSelect" then
prompt = "moves"
end
local party = {}
for i, mon in ipairs(game.save.party or {}) do
party[i] = monCopy(game.data, mon,
battle.player and battle.player.mon == mon)
party[i].slot = i
end
return { revision = self:_revision(battle, top), kind = kind,
catchable = catchable, prompt = prompt,
message = visibleMessage(battle, top), turn = battle.turnCount or 0,
player = monCopy(game.data, battle.player and battle.player.mon, true),
enemy = monCopy(game.data, battle.enemy and battle.enemy.mon, true),
party = party, moves = moveCopies(game, battle),
items = itemCopies(game, battle, catchable),
safariBalls = battle.safari and battle.safari.balls or nil,
mimicMoves = mimicCopies(game, battle), mimicIndex = battle.mimicIndex }
end
return BattleAPI