mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 03:35:56 +02:00
265 lines
9.7 KiB
Lua
265 lines
9.7 KiB
Lua
-- 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
|
|
|
|
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 battle, top = activeBattle(self.game)
|
|
if not battle then return nil, "no battle" end
|
|
if intent.revision ~= self:_revision(battle, top) then
|
|
return nil, "stale battle context"
|
|
end
|
|
local kind = battle:battleKind()
|
|
if kind == "oldman" or kind == "link" then
|
|
return nil, "battle kind is not controllable"
|
|
end
|
|
if top ~= battle then return nil, "battle menu is covered" end
|
|
|
|
local ok, err
|
|
if intent.kind == "safari" then
|
|
if kind ~= "safari" then return nil, "safari menu is not active" end
|
|
ok, err = battle:chooseSafari(intent.action)
|
|
elseif kind == "safari" then
|
|
return nil, "battle kind is not controllable"
|
|
elseif intent.kind == "mimic" then
|
|
ok, err = battle:chooseMimic(intent.index)
|
|
elseif intent.kind == "menu" then
|
|
if battle.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 = battle:chooseMenu(intent.choice)
|
|
elseif intent.kind == "move" then
|
|
if battle.phase ~= "moveSelect" then
|
|
return nil, "move menu is not active"
|
|
end
|
|
if battle.moveSwapIndex then return nil, "move reorder is active" end
|
|
local move = validSlot(intent.slot) and battle.player
|
|
and battle.player.curMoves[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.player.disabledSlot == intent.slot then
|
|
return nil, "move is disabled"
|
|
end
|
|
ok, err = battle:chooseMove(intent.slot)
|
|
elseif intent.kind == "back" then
|
|
ok, err = battle: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
|