mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
Merge pull request #1374 from AverageConsumer/codex/mod-battle-snapshot
This commit is contained in:
@@ -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
|
||||
@@ -38,6 +38,7 @@ local romText = RomText
|
||||
local BattleState = {}
|
||||
BattleState.__index = BattleState
|
||||
BattleState.isOpaque = true
|
||||
BattleState.isBattleState = true
|
||||
|
||||
-- Category identity for per-category GAME SPEED (RFC 0007), the same
|
||||
-- style OverworldController.isOverworld already uses. Every battle --
|
||||
@@ -156,6 +157,13 @@ function BattleState:caughtMarkerVisible()
|
||||
function() return false end, self) == true
|
||||
end
|
||||
|
||||
function BattleState:catchChance(ball, rateOverride)
|
||||
if Runtime.wantsHook("catch.rate") then return nil end
|
||||
return Catching.chance(ball, self.enemy.mon, self.enemy.def, rateOverride,
|
||||
{ ballDef = self:ballDef(ball), statuses = self.data.statuses,
|
||||
battle = self })
|
||||
end
|
||||
|
||||
function BattleState:moveGridNavigation()
|
||||
if self:wideLayout() then return true end
|
||||
if not Runtime.wantsHook("battle.move_grid_navigation") then return false end
|
||||
@@ -1166,7 +1174,7 @@ function BattleState:startMessage(item)
|
||||
local npos = text:find("[\n\v]", pos)
|
||||
local chunk = npos and text:sub(pos, npos - 1) or text:sub(pos)
|
||||
local codes = Font.encode(chunk)
|
||||
self.lines[#self.lines + 1] = { codes = codes, cont = cont }
|
||||
self.lines[#self.lines + 1] = { codes = codes, cont = cont, text = chunk }
|
||||
self.total = self.total + #codes
|
||||
if not npos then break end
|
||||
cont = text:sub(npos, npos) == "\v"
|
||||
@@ -1199,6 +1207,18 @@ function BattleState:beginMsgLine()
|
||||
self.shown[#self.shown + 1] = {}
|
||||
end
|
||||
|
||||
function BattleState:visibleText()
|
||||
if self.phase ~= "messages" or not (self.current or self.animPlaying) then
|
||||
return nil
|
||||
end
|
||||
local out, count = {}, #(self.shown or {})
|
||||
for i = math.max(1, self.lineIndex - count + 1), self.lineIndex do
|
||||
local line = self.lines and self.lines[i]
|
||||
if line then out[#out + 1] = line.text or "" end
|
||||
end
|
||||
return #out > 0 and out or nil
|
||||
end
|
||||
|
||||
function BattleState:updateQueue()
|
||||
if self.waitingUI then
|
||||
if self.game.stack:top() ~= self then return true end
|
||||
|
||||
+29
-14
@@ -27,6 +27,17 @@ Catching.BALLS = BALLS
|
||||
-- divisor, which is what the old per-field `or` defaults resolved to
|
||||
local DEFAULT_BALL = { randMax = 255, hpFactor = 12, wobbleFactor = 150 }
|
||||
|
||||
local function stockFactors(def, targetMon, targetDef, rateOverride, statuses)
|
||||
local rate = rateOverride or targetDef.catchRate
|
||||
local record = Status.recordFor(statuses, targetMon.status)
|
||||
local statusBonus = record and record.catchBonus or 0
|
||||
local hpQuarter = math.max(1, math.floor(targetMon.hp / 4))
|
||||
local factor = def.hpFactor or DEFAULT_BALL.hpFactor
|
||||
local f = math.min(255, math.floor(math.floor(
|
||||
targetMon.stats.hp * 255 / factor) / hpQuarter))
|
||||
return rate, statusBonus, f, record
|
||||
end
|
||||
|
||||
function Catching.registerInto(registry, _, owner)
|
||||
for id, record in pairs(BALLS) do
|
||||
registry:register(id, record, owner)
|
||||
@@ -41,21 +52,9 @@ end
|
||||
local function stockAttempt(def, targetMon, targetDef, rng, rateOverride, statuses)
|
||||
if def.autoCatch then return true, 3 end
|
||||
local randMax = def.randMax
|
||||
local rate = rateOverride or targetDef.catchRate
|
||||
|
||||
-- the status subtraction and the wobble bonus come off the merged
|
||||
-- status record (SLP/FRZ 25 and +10, the rest 12 and +5)
|
||||
local s = targetMon.status
|
||||
local record = Status.recordFor(statuses, s)
|
||||
local statusBonus = record and record.catchBonus or 0
|
||||
|
||||
-- HP factor (X)
|
||||
local maxhp = targetMon.stats.hp
|
||||
local hpQuarter = math.max(1, math.floor(targetMon.hp / 4))
|
||||
local factor = def.hpFactor or DEFAULT_BALL.hpFactor
|
||||
-- the 255 cap applies only after BOTH divisions (ItemUseBall keeps
|
||||
-- the intermediate in 16 bits); capping early collapses the value
|
||||
local f = math.min(255, math.floor(math.floor(maxhp * 255 / factor) / hpQuarter))
|
||||
local rate, statusBonus, f, record = stockFactors(
|
||||
def, targetMon, targetDef, rateOverride, statuses)
|
||||
|
||||
local function shakes()
|
||||
local ballFactor2 = def.wobbleFactor or DEFAULT_BALL.wobbleFactor
|
||||
@@ -80,6 +79,22 @@ local function stockAttempt(def, targetMon, targetDef, rng, rateOverride, status
|
||||
return false, shakes()
|
||||
end
|
||||
|
||||
-- Exact stock catch probability for read-only previews. A custom attempt
|
||||
-- function may do anything, so nil is safer than presenting a plausible lie.
|
||||
function Catching.chance(ball, targetMon, targetDef, rateOverride, opts)
|
||||
opts = opts or {}
|
||||
local def = opts.ballDef or BALLS[ball] or DEFAULT_BALL
|
||||
if def.attempt then return nil end
|
||||
if def.autoCatch then return 100 end
|
||||
local rate, statusBonus, f = stockFactors(
|
||||
def, targetMon, targetDef, rateOverride, opts.statuses)
|
||||
local outcomes = def.randMax + 1
|
||||
local automatic = math.min(outcomes, math.max(0, statusBonus))
|
||||
local passed = math.min(outcomes, math.max(0, rate + statusBonus + 1))
|
||||
return (automatic + (passed - automatic) * (f + 1) / 256)
|
||||
* 100 / outcomes
|
||||
end
|
||||
|
||||
-- Returns caught, shakes (0-3). rateOverride replaces the species catch
|
||||
-- rate (the Safari game's BAIT/ROCK-modified wEnemyMonActualCatchRate).
|
||||
-- opts (all optional): ballDef = the merged ball record, statuses = the
|
||||
|
||||
+22
-12
@@ -83,25 +83,35 @@ function Damage.critRoll(ruleset, attacker, moveId, rng, highCrit)
|
||||
return rng(0, 255) < b
|
||||
end
|
||||
|
||||
-- Accuracy test: rand(0..255) < floor(accuracy * 255 / 100) adjusted by
|
||||
-- accuracy/evasion stages. With oneIn256Miss a max-accuracy move still
|
||||
-- misses on 255.
|
||||
function Damage.accuracyRoll(ruleset, move, attacker, defender, rng)
|
||||
rng = rng or love.math.random
|
||||
-- Exact number of the 256 RNG outcomes that pass MoveHitTest. Keeping the
|
||||
-- threshold public lets read-only UIs preview the same rules the roll uses.
|
||||
function Damage.accuracyThreshold(ruleset, move, attacker, defender)
|
||||
-- X ACCURACY sets USING_X_ACCURACY: the move simply never misses
|
||||
-- (MoveHitTest returns before any accuracy math, 1/256 included)
|
||||
if attacker.xAccuracy then return true end
|
||||
if attacker.xAccuracy then return 256 end
|
||||
local acc = math.floor(move.accuracy * 255 / 100)
|
||||
local accuracyStage = attacker.stages and attacker.stages.accuracy or 0
|
||||
local evasionStage = defender.stages and defender.stages.evasion or 0
|
||||
-- CalcHitChance scales by the accuracy stage and the evasion stage as
|
||||
-- two separate ratio multiplications, clamping each result
|
||||
acc = math.min(255, Stats.applyStage(acc,
|
||||
attacker.stages and attacker.stages.accuracy or 0))
|
||||
acc = math.min(255, Stats.applyStage(acc,
|
||||
-(defender.stages and defender.stages.evasion or 0)))
|
||||
acc = math.min(255, Stats.applyStage(acc, accuracyStage))
|
||||
acc = math.min(255, Stats.applyStage(acc, -evasionStage))
|
||||
if not ruleset.oneIn256Miss and move.accuracy >= 100
|
||||
and (attacker.stages.accuracy or 0) >= (defender.stages.evasion or 0) then
|
||||
return true
|
||||
and accuracyStage >= evasionStage then
|
||||
return 256
|
||||
end
|
||||
return acc
|
||||
end
|
||||
|
||||
function Damage.accuracyChance(ruleset, move, attacker, defender)
|
||||
return Damage.accuracyThreshold(ruleset, move, attacker, defender) * 100 / 256
|
||||
end
|
||||
|
||||
-- Accuracy test: rand(0..255) < the shared ruleset-aware threshold.
|
||||
function Damage.accuracyRoll(ruleset, move, attacker, defender, rng)
|
||||
rng = rng or love.math.random
|
||||
local acc = Damage.accuracyThreshold(ruleset, move, attacker, defender)
|
||||
if acc == 256 then return true end
|
||||
return rng(0, 255) < acc
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
-- 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 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
|
||||
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),
|
||||
-- Gold's PACK is pocketed and target selection is screen-owned. Omit it
|
||||
-- until the engine can expose the same semantic item records as Gen 1.
|
||||
items = {} }
|
||||
end
|
||||
|
||||
return BattleAPI
|
||||
@@ -83,6 +83,12 @@ function ItemEffects.healsHP(id)
|
||||
or id == "REVIVE" or id == "MAX_REVIVE"
|
||||
end
|
||||
|
||||
function ItemEffects.isBattleMedicine(id)
|
||||
return HEAL_AMOUNT[id] ~= nil or STATUS_HEAL[id] ~= nil
|
||||
or id == "MAX_POTION" or id == "FULL_RESTORE"
|
||||
or id == "REVIVE" or id == "MAX_REVIVE"
|
||||
end
|
||||
|
||||
-- Does this item need a party-member target?
|
||||
-- 'data' is optional for compat purposes; targeting falls back to itemDef/vanilla detection
|
||||
function ItemEffects.needsTarget(id, itemDef, data)
|
||||
|
||||
+10
-2
@@ -1308,7 +1308,7 @@ function Loader:_api(mod)
|
||||
-- mod.world materializes on first touch, like the image helper above: a
|
||||
-- headless load must not drag the world stack in, and the Game the facade
|
||||
-- acts on is still being wired when the entry chunk runs
|
||||
local world
|
||||
local world, battle
|
||||
setmetatable(api, { __index = function(_, key)
|
||||
-- mod.game is the live service owner, resolved per generation the way
|
||||
-- mod.world is: src/core/Game.lua's singleton under Gen 1, the Game2
|
||||
@@ -1317,9 +1317,17 @@ function Loader:_api(mod)
|
||||
-- entry chunk runs. This is what a mod should hold instead of requiring
|
||||
-- src.core.Game, which under Gold hands back a table nothing instantiated.
|
||||
if key == "game" then return loader:_game() end
|
||||
local game = loader:_game()
|
||||
if key == "battle" then
|
||||
if battle then return battle end
|
||||
local module = game and engineRequire(loader.generation == 2
|
||||
and "src.battle.gen2.BattleAPI" or "src.battle.BattleAPI")
|
||||
if not module then return nil end
|
||||
battle = module.new(game)
|
||||
return battle
|
||||
end
|
||||
if key ~= "world" then return nil end
|
||||
if world then return world end
|
||||
local game = loader:_game()
|
||||
-- one facade name, one arm per generation: Gold's world is not a stack
|
||||
-- state and its flags are a bitfield, so the resolution differs even
|
||||
-- where the method set does not (src/world/gen2/WorldAPI.lua)
|
||||
|
||||
@@ -253,6 +253,16 @@ function TextBox:beginLine()
|
||||
table.insert(self.shown, {})
|
||||
end
|
||||
|
||||
function TextBox:visibleText()
|
||||
local page = self.pages[self.pageIndex]
|
||||
if not page then return nil end
|
||||
local out, count = {}, #(self.shown or {})
|
||||
for i = math.max(1, self.lineIndex - count + 1), self.lineIndex do
|
||||
if page[i] ~= nil then out[#out + 1] = page[i] end
|
||||
end
|
||||
return #out > 0 and out or nil
|
||||
end
|
||||
|
||||
function TextBox:update(dt)
|
||||
local input = self.game.input
|
||||
self.blink = (self.blink + 1) % 60
|
||||
|
||||
Reference in New Issue
Block a user