mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-06 17:10:55 -04:00
bing bang bong
This commit is contained in:
@@ -1 +1 @@
|
||||
{"mods":[],"schema_version":1}
|
||||
{"schema_version":1,"mods":[]}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -4,19 +4,57 @@ return function(mod)
|
||||
local mew = mod.content.pokemon:get("MEW")
|
||||
assert(mew, "Mew is missing from the imported base data")
|
||||
|
||||
-- Keep every vanilla Mew field, changing only the two battle sprite paths.
|
||||
-- Keep every vanilla Mew field, changing only the battle sprite paths.
|
||||
local invertedMew = {}
|
||||
for key, value in pairs(mew) do invertedMew[key] = value end
|
||||
invertedMew.spriteFront = mod.path .. "/assets/mew_front_inverted.png"
|
||||
invertedMew.spriteBack = mod.path .. "/assets/mew_back_inverted.png"
|
||||
invertedMew.spriteFront64 = mod.path .. "/assets/mew_front_inverted_64.png"
|
||||
invertedMew.spriteBack64 = mod.path .. "/assets/mew_back_inverted_64.png"
|
||||
mod.content.pokemon:override("MEW", invertedMew)
|
||||
|
||||
local function firstStarter(ctx)
|
||||
local map = ctx.overworld and ctx.overworld.map
|
||||
local save = ctx.save
|
||||
return map and map.id == "OAKS_LAB" and not save.flags.EVENT_GOT_STARTER
|
||||
and #(save.party or {}) == 0
|
||||
end
|
||||
|
||||
if mod.generation == 3 then
|
||||
local PLAYER_STARTER_SPECIES, CHARMANDER, MEW = 0x4002, 4, 151
|
||||
mod.hooks:wrap("script.command", function(next, ctx, op, row)
|
||||
if op == "setvar" and (row.var or row[1]) == PLAYER_STARTER_SPECIES
|
||||
and (row.value or row[2]) == CHARMANDER and firstStarter(ctx) then
|
||||
local copy = {}
|
||||
for k, v in pairs(row) do copy[k] = v end
|
||||
copy.value, copy[2] = MEW, MEW
|
||||
return next(ctx, op, copy)
|
||||
end
|
||||
return next(ctx, op, row)
|
||||
end)
|
||||
|
||||
local confirm = mod.content.text:get("g3:0818e194")
|
||||
if type(confirm) == "table" then
|
||||
local hoghead = {}
|
||||
for i, token in ipairs(confirm) do
|
||||
local t = {}
|
||||
for k, v in pairs(token) do t[k] = v end
|
||||
if type(t.s) == "string" then
|
||||
t.s = t.s:gsub("FIRE POKéMON CHARMANDER", "PSYCHIC POKéMON HOGHEAD")
|
||||
:gsub("CHARMANDER", "HOGHEAD")
|
||||
end
|
||||
hoghead[i] = t
|
||||
end
|
||||
mod.content.text:override("g3:0818e194", hoghead)
|
||||
end
|
||||
end
|
||||
|
||||
mod.events:on("pokemon.before_give", function(gift)
|
||||
-- Only change the Oak's Lab Charmander gift; wild encounters and other
|
||||
-- story gifts remain vanilla.
|
||||
local map = gift.ctx.overworld and gift.ctx.overworld.map
|
||||
if gift.species == "CHARMANDER" and map and map.id == "OAKS_LAB"
|
||||
and not gift.ctx.save.flags.EVENT_GOT_STARTER then
|
||||
local starter = gift.species == "CHARMANDER"
|
||||
or (mod.generation == 3 and gift.species == "MEW")
|
||||
if starter and firstStarter(gift.ctx) then
|
||||
gift.species = "MEW"
|
||||
gift.level = 20
|
||||
gift.nickname = "HOGHEAD"
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
"dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"conflicts": [],
|
||||
"games": ["gen1", "gen3"],
|
||||
"force_enable_env": "POKEPORT_MEW_STARTER",
|
||||
"description": "Turns the Oak's Lab Charmander gift into a level 20 Mew with inverted battle sprites."
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ return {
|
||||
differences = {
|
||||
changed = {
|
||||
"the Oak's Lab starter gift becomes a level 20 Mew nicknamed HOGHEAD",
|
||||
"MEW's front and back battle sprites point at inverted copies",
|
||||
"MEW's front and back battle sprites point at inverted copies "
|
||||
.. "(64x64 on FireRed)",
|
||||
},
|
||||
added = {},
|
||||
known = {
|
||||
@@ -24,6 +25,7 @@ return {
|
||||
},
|
||||
credits = {
|
||||
{ who = "pret/pokered", for_ = "the Mew sprites the local generator inverts" },
|
||||
{ who = "pret/pokefirered", for_ = "the FireRed Mew pics the local generator inverts" },
|
||||
},
|
||||
compat = { engine = ">=1.0.0 <2.0.0", modApi = 1 },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
-- Read-only FireRed battle state with the same shape as mod.battle on Gen 1 / Gen 2.
|
||||
|
||||
local Gen3Compat = require("src.mods.Gen3Compat")
|
||||
|
||||
local BattleAPI = {}
|
||||
BattleAPI.__index = BattleAPI
|
||||
|
||||
function BattleAPI.new(game)
|
||||
return setmetatable({ game = game, revision = 0, signature = nil }, BattleAPI)
|
||||
end
|
||||
|
||||
local function battleModule()
|
||||
return package.loaded["src.core.game3.battle.init"]
|
||||
or package.loaded["src.core.game3.battle"]
|
||||
end
|
||||
|
||||
local function ui()
|
||||
return package.loaded["src.core.game3.battle.ui"]
|
||||
end
|
||||
|
||||
local function message()
|
||||
return package.loaded["src.ui.game3.message"]
|
||||
end
|
||||
|
||||
local function activeBattle()
|
||||
local B = battleModule()
|
||||
if not (B and B.isActive and B.isActive()) then return nil end
|
||||
local st = B.getState and B.getState()
|
||||
if not st then return nil end
|
||||
return st, B
|
||||
end
|
||||
|
||||
local function session()
|
||||
local R = package.loaded["src.core.game3.runtime"]
|
||||
return R and R.getSession and R.getSession() or nil
|
||||
end
|
||||
|
||||
local function displayName(mon, species)
|
||||
if mon and mon.nickname and mon.nickname ~= "" then return mon.nickname end
|
||||
local P = package.loaded["src.core.game3.pokemon"]
|
||||
if P and P.name then return P.name(species) end
|
||||
return mon and mon.name or tostring(species)
|
||||
end
|
||||
|
||||
local function monCopy(mon, active)
|
||||
if not mon then return nil end
|
||||
local P = package.loaded["src.core.game3.pokemon"]
|
||||
local species = P and P.speciesOf and P.speciesOf(mon) or mon.species
|
||||
return { species = Gen3Compat.speciesName(species) or species,
|
||||
gen3Species = species, name = displayName(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()
|
||||
local M = message()
|
||||
if not (M and M.isOpen and M.isOpen() and M.currentPage) then return nil end
|
||||
local lines = {}
|
||||
for line in tostring(M.currentPage() or ""):gmatch("[^\n]+") do
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
return #lines > 0 and lines or nil
|
||||
end
|
||||
|
||||
local function ballCopies(catchable)
|
||||
local out = {}
|
||||
local s = session()
|
||||
local Bag = package.loaded["src.core.game3.bag"]
|
||||
if not (s and s.bag and Bag and Bag.listPocket) then return out end
|
||||
local ok, rows = pcall(Bag.listPocket, s.bag, "POKE_BALLS")
|
||||
if not ok or type(rows) ~= "table" then return out end
|
||||
for _, row in ipairs(rows) do
|
||||
out[#out + 1] = { id = row.id, name = row.name or tostring(row.id),
|
||||
count = row.qty, ball = true, needsTarget = false,
|
||||
catchable = catchable and true or false }
|
||||
end
|
||||
table.sort(out, function(a, b) return tostring(a.name) < tostring(b.name) end)
|
||||
return out
|
||||
end
|
||||
|
||||
local function slot(st, id)
|
||||
if id == 0 then return st.player end
|
||||
if id == 1 then return st.enemy end
|
||||
return st.battlers and st.battlers[id] or nil
|
||||
end
|
||||
|
||||
local function activeId(st)
|
||||
if not st.double then return 0 end
|
||||
local U = ui()
|
||||
local id = (U and tonumber(U._active)) or tonumber(st.activeBattler) or 0
|
||||
if id ~= 0 and id ~= 2 then id = 0 end
|
||||
return id
|
||||
end
|
||||
|
||||
local function battlerCopies(st)
|
||||
local out = {}
|
||||
for id = 0, (st.double and 3 or 1) do
|
||||
local b = slot(st, id)
|
||||
local absent = st.absent and st.absent[id] or false
|
||||
if b then
|
||||
local copy = monCopy(b.mon, not absent) or {}
|
||||
copy.id, copy.side = id, b.side or (id % 2 == 0 and "player" or "enemy")
|
||||
copy.absent = absent and true or false
|
||||
copy.partyIndex = b.partyIndex
|
||||
out[#out + 1] = copy
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function moveCopies(st)
|
||||
local out = {}
|
||||
local b = slot(st, activeId(st))
|
||||
local mon = b and b.mon
|
||||
if not mon then return out end
|
||||
local Moves = package.loaded["src.core.game3.battle.moves"]
|
||||
for slot = 1, 4 do
|
||||
local id = mon.moves and mon.moves[slot]
|
||||
if id and id ~= 0 and id ~= "" then
|
||||
local def = Moves and Moves.get and Moves.get(id) or {}
|
||||
out[#out + 1] = { slot = slot, id = id,
|
||||
name = Moves and Moves.displayName and Moves.displayName(id) or tostring(id),
|
||||
pp = mon.pp and mon.pp[slot],
|
||||
maxPp = (mon.maxPp and mon.maxPp[slot]) or def.pp,
|
||||
type = def.type, power = def.power, accuracy = def.accuracy }
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function prompt(B)
|
||||
local U = ui()
|
||||
if B._phase == "command" and U then
|
||||
if U._mode == "menu" then return "menu" end
|
||||
if U._mode == "moves" then return "moves" end
|
||||
if U._mode == "target" then return "target" end
|
||||
if U._mode == "party" then return "party" end
|
||||
end
|
||||
local M = message()
|
||||
if M and M.isWaiting and M.isWaiting() then return "advance" end
|
||||
return "locked"
|
||||
end
|
||||
|
||||
local function signature(st, B)
|
||||
if not st then return "none" end
|
||||
local U = ui() or {}
|
||||
local parts = { tostring(st), tostring(B._phase), tostring(U._mode),
|
||||
tostring(U._menuIndex), tostring(U._moveIndex), tostring(st.turn),
|
||||
tostring(st.over), tostring(st.result), tostring(U._active),
|
||||
tostring(U._target and U._target.cursor),
|
||||
table.concat(messageCopy() or {}, "\n") }
|
||||
for id = 0, (st.double and 3 or 1) do
|
||||
local battler = slot(st, id)
|
||||
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] = tostring(st.absent and st.absent[id])
|
||||
end
|
||||
for _, mon in ipairs(st.playerParty or {}) do
|
||||
parts[#parts + 1] = tostring(mon)
|
||||
parts[#parts + 1] = tostring(mon.hp)
|
||||
parts[#parts + 1] = tostring(mon.status)
|
||||
end
|
||||
for _, item in ipairs(ballCopies(false)) do
|
||||
parts[#parts + 1] = tostring(item.id) .. "=" .. tostring(item.count)
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
function BattleAPI:_revision(st, B)
|
||||
local nextSignature = signature(st, B)
|
||||
if nextSignature ~= self.signature then
|
||||
self.signature = nextSignature
|
||||
self.revision = self.revision + 1
|
||||
end
|
||||
return self.revision
|
||||
end
|
||||
|
||||
function BattleAPI:snapshot()
|
||||
local st, B = activeBattle()
|
||||
if not st then return nil end
|
||||
local catchable = st.wild and not st.ghost and not st.noCatch
|
||||
local party = {}
|
||||
local b2 = st.double and slot(st, 2) or nil
|
||||
for i, mon in ipairs(st.playerParty or {}) do
|
||||
party[i] = monCopy(mon, (st.player and st.player.mon == mon) or (b2 and b2.mon == mon))
|
||||
party[i].slot = i
|
||||
end
|
||||
local active = activeId(st)
|
||||
local U = ui()
|
||||
local targets
|
||||
if U and U._mode == "target" then
|
||||
targets = {}
|
||||
for id = 0, 3 do
|
||||
local b = slot(st, id)
|
||||
if b and not (st.absent and st.absent[id]) then targets[#targets + 1] = id end
|
||||
end
|
||||
end
|
||||
local snap = { revision = self:_revision(st, B), kind = st.kind or (st.wild and "wild" or "trainer"),
|
||||
catchable = catchable and true or false, prompt = prompt(B),
|
||||
message = messageCopy(), turn = st.turn or 0,
|
||||
double = st.double and true or false, active = active,
|
||||
player = monCopy(slot(st, active) and slot(st, active).mon, true),
|
||||
enemy = monCopy(st.enemy and st.enemy.mon, true),
|
||||
battlers = battlerCopies(st), targets = targets,
|
||||
target = U and U._target and U._target.cursor or nil,
|
||||
party = party, moves = moveCopies(st), items = ballCopies(catchable) }
|
||||
return snap
|
||||
end
|
||||
|
||||
local MENU_INDEX = { fight = 1, item = 2, party = 3, run = 4 }
|
||||
|
||||
local function validSlot(slot)
|
||||
return type(slot) == "number" and slot % 1 == 0 and slot >= 1
|
||||
end
|
||||
|
||||
local function press(key)
|
||||
return { wasPressed = function(_, k) return k == key end,
|
||||
isDown = function() return false end }
|
||||
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 st, B = activeBattle()
|
||||
if not st then return nil, "no battle" end
|
||||
if intent.revision ~= self:_revision(st, B) then
|
||||
return nil, "stale battle context"
|
||||
end
|
||||
if B._auto then return nil, "battle kind is not controllable" end
|
||||
local U = ui()
|
||||
if not (U and U.handleInput) or B._phase ~= "command" then
|
||||
return nil, "battle menu is covered"
|
||||
end
|
||||
if intent.kind == "menu" then
|
||||
if U._mode ~= "menu" then return nil, "battle menu is not active" end
|
||||
local index = MENU_INDEX[intent.choice]
|
||||
if not index then return nil, "unknown battle menu choice" end
|
||||
U._menuIndex = index
|
||||
U.handleInput(press("a"))
|
||||
elseif intent.kind == "move" then
|
||||
if U._mode ~= "moves" then return nil, "move menu is not active" end
|
||||
local user = slot(st, activeId(st))
|
||||
local mon = user and user.mon
|
||||
local move = validSlot(intent.slot) and mon and mon.moves
|
||||
and mon.moves[intent.slot]
|
||||
if not move or move == 0 or move == "" then return nil, "invalid move slot" end
|
||||
local pp = mon.pp and tonumber(mon.pp[intent.slot])
|
||||
if pp and pp <= 0 then return nil, "move has no PP" end
|
||||
U._moveIndex = intent.slot
|
||||
U.handleInput(press("a"))
|
||||
elseif intent.kind == "target" then
|
||||
if U._mode ~= "target" or not U._target then return nil, "target cursor is not active" end
|
||||
local id = intent.target
|
||||
if type(id) ~= "number" or id % 1 ~= 0 or id < 0 or id > 3 then
|
||||
return nil, "target must be a battler id 0-3"
|
||||
end
|
||||
if not slot(st, id) or (st.absent and st.absent[id]) then
|
||||
return nil, "no battler in that slot"
|
||||
end
|
||||
if id == U._target.battler then
|
||||
local mon = slot(st, id).mon
|
||||
local mv = mon and mon.moves and mon.moves[U._target.slot]
|
||||
local Moves = package.loaded["src.core.game3.battle.moves"]
|
||||
local def = Moves and mv and Moves.get(mv)
|
||||
if math.floor((tonumber(def and def.target) or 0) / 2) % 2 ~= 1 then
|
||||
return nil, "that move cannot target its user"
|
||||
end
|
||||
end
|
||||
U._target.cursor = id
|
||||
U.handleInput(press("a"))
|
||||
elseif intent.kind == "back" then
|
||||
if U._mode ~= "moves" and U._mode ~= "target" then return nil, "move menu is not active" end
|
||||
U.handleInput(press("b"))
|
||||
else
|
||||
return nil, "unknown battle intent"
|
||||
end
|
||||
self.lastIntentId = intent.id
|
||||
self.signature = nil
|
||||
return true
|
||||
end
|
||||
|
||||
return BattleAPI
|
||||
+189
-25
@@ -15,10 +15,13 @@ local Help = require("src.ui.game3.help_system")
|
||||
|
||||
local QuestLog = require("src.ui.game3.quest_log")
|
||||
local QuestRecorder = require("src.core.game3.quest_log_recorder")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Game3 = {}
|
||||
Game3.__index = Game3
|
||||
|
||||
local function noop() end
|
||||
|
||||
Game3.SKIN_FAST_FORWARD = 4
|
||||
|
||||
function Game3.new()
|
||||
@@ -75,6 +78,7 @@ function Game3:_enterField(session, reason)
|
||||
end
|
||||
session._questNewScene=true
|
||||
if reason == "continue" then session._questMap=session.map end
|
||||
require("src.core.game3.map")._announced = nil
|
||||
Runtime.start(nil, self, session, { reason = reason or "new_game" })
|
||||
-- Runtime.start already Map.loads unless alreadyOnMap; keep explicit reload for
|
||||
-- session x/y/facing in case start opts change.
|
||||
@@ -156,6 +160,9 @@ function Game3:load(opts)
|
||||
self.options = options
|
||||
self:applyOptions(options)
|
||||
|
||||
self:_exposeModData()
|
||||
self:_loadMods(opts)
|
||||
|
||||
-- Never auto-skip boot into a legacy Sevii sidecar.
|
||||
local continueOk = self:_hasContinueSave()
|
||||
require("src.ui.game3.start_menu").resetCursor() -- pokefirered/src/main.c:134
|
||||
@@ -175,6 +182,67 @@ function Game3:load(opts)
|
||||
pcall(function()
|
||||
require("src.core.PresentSync").applyFixedStepPeriod()
|
||||
end)
|
||||
if ModRuntime.wants("game.ready") then
|
||||
ModRuntime.emit("game.ready", { game = self })
|
||||
end
|
||||
end
|
||||
|
||||
function Game3:_exposeModData()
|
||||
local data = self.data
|
||||
if type(data) ~= "table" then return end
|
||||
data.gen3Pokemon = require("src.core.game3.pokemon")
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
if not Moves._romLoaded then pcall(Moves.loadRomPack, Dataset.cache()) end
|
||||
data.gen3Moves = Moves
|
||||
local ItemsData = require("src.core.game3.items_data")
|
||||
pcall(ItemsData.ensureLoaded)
|
||||
data.gen3Items = ItemsData
|
||||
local Encounters = require("src.core.game3.encounters")
|
||||
data.gen3Encounters = Encounters._tables
|
||||
local Trainers = require("src.core.game3.scripting.trainers")
|
||||
local okT, pack = pcall(Trainers.pack)
|
||||
data.gen3Trainers = okT and type(pack) == "table" and pack or nil
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local bundle = Space.bundle
|
||||
data.gen3Text = bundle and bundle.text or nil
|
||||
data.gen3Scripts = bundle and bundle.scripts or nil
|
||||
end
|
||||
|
||||
function Game3:_loadMods(opts)
|
||||
local modOpts = opts and opts.modOpts or nil
|
||||
local ok, loader = pcall(function()
|
||||
local mods = require("src.mods.Loader").new()
|
||||
mods.game = self
|
||||
mods:load(self.data, modOpts)
|
||||
return mods
|
||||
end)
|
||||
if ok and loader then
|
||||
self.mods = loader
|
||||
self.modStatus = loader:status()
|
||||
else
|
||||
require("src.core.Logger").error(
|
||||
"mods failed to load, continuing without them: %s", tostring(loader))
|
||||
end
|
||||
local okC, Gen3Compat = pcall(require, "src.mods.Gen3Compat")
|
||||
if okC and type(Gen3Compat) == "table" and Gen3Compat.applyMerged then
|
||||
local okA, err = pcall(Gen3Compat.applyMerged, self)
|
||||
if not okA then
|
||||
require("src.core.Logger").error("Gen3Compat.applyMerged failed: %s", tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Game3:adoptSave(session, seedBuckets)
|
||||
if type(session) ~= "table" then return end
|
||||
if type(session.modData) ~= "table" then session.modData = {} end
|
||||
local loader = self.mods
|
||||
if not loader then return end
|
||||
if seedBuckets then
|
||||
for id, bucket in pairs(loader.modSave or {}) do
|
||||
if session.modData[id] == nil then session.modData[id] = bucket end
|
||||
end
|
||||
end
|
||||
loader.modSave = session.modData
|
||||
end
|
||||
|
||||
function Game3:writeOptions()
|
||||
@@ -279,6 +347,14 @@ function Game3:_handleBootAction(action)
|
||||
if action.action == "continue" then
|
||||
local ok, save = pcall(SaveData.load)
|
||||
if ok and save and save.engine == "game3" then
|
||||
if ModRuntime.wants("save.loading") then
|
||||
ModRuntime.emit("save.loading", { raw = save })
|
||||
end
|
||||
local activeMods = self.modStatus and self.modStatus.loaded
|
||||
if SaveData.runMigrations then
|
||||
SaveData.runMigrations(save, self.mods and self.mods.migrations, activeMods)
|
||||
end
|
||||
local modsDiff = SaveData.modsDiff and SaveData.modsDiff(save, activeMods) or nil
|
||||
local session = Schema.fromSaveTable(save)
|
||||
Options.bind(session, self.options)
|
||||
-- Refuse Sevii leftovers.
|
||||
@@ -286,6 +362,9 @@ function Game3:_handleBootAction(action)
|
||||
print("[game3] ignoring legacy Sevii save map " .. session.map)
|
||||
session = Schema.newGame({ gender = 0 })
|
||||
end
|
||||
self:adoptSave(session, not self._modSaveAdopted)
|
||||
self._modSaveAdopted = true
|
||||
self.sessionStartedAt = os.time()
|
||||
self.questPlayback = QuestLog.begin(session)
|
||||
if self.questPlayback then
|
||||
self.session=session
|
||||
@@ -294,6 +373,13 @@ function Game3:_handleBootAction(action)
|
||||
else
|
||||
self:_enterField(session, "continue")
|
||||
end
|
||||
if modsDiff and SaveData.modsDiffNotice then
|
||||
local notice = SaveData.modsDiffNotice(modsDiff, save.meta)
|
||||
if notice then require("src.core.Logger").warn("%s", notice) end
|
||||
end
|
||||
if ModRuntime.wants("save.loaded") then
|
||||
ModRuntime.emit("save.loaded", { save = session, meta = session.meta, modsDiff = modsDiff })
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -304,6 +390,12 @@ function Game3:_handleBootAction(action)
|
||||
gender = action.gender or 0,
|
||||
start = action.start,
|
||||
})
|
||||
self:adoptSave(session, not self._modSaveAdopted)
|
||||
self._modSaveAdopted = true
|
||||
self.sessionStartedAt = os.time()
|
||||
if ModRuntime.wants("save.created") then
|
||||
ModRuntime.emit("save.created", { save = session })
|
||||
end
|
||||
self:_enterField(session, "new_game")
|
||||
return
|
||||
end
|
||||
@@ -322,6 +414,9 @@ end
|
||||
|
||||
function Game3:fixedUpdate(dt)
|
||||
self:_aliasLA()
|
||||
if ModRuntime.wantsHook("input.step") then
|
||||
ModRuntime.call("input.step", noop, self, dt or FixedStep.STEP)
|
||||
end
|
||||
if self.input and self.input.step then self.input:step() end
|
||||
if self.input and self.input.softResetStep and self.input:softResetStep() then
|
||||
self.input:reset()
|
||||
@@ -441,6 +536,20 @@ function Game3:update(dt)
|
||||
pcall(function() require("src.render.Tilt").update(dt) end)
|
||||
end
|
||||
|
||||
function Game3:_drawHud(w, h)
|
||||
if not ModRuntime.wantsHook("render.hud") then return end
|
||||
local scale, ox, oy, _, _, scaleY = Display.fit(w, h)
|
||||
local viewport = {
|
||||
width = w, height = h,
|
||||
gameX = ox, gameY = oy,
|
||||
gameWidth = Display.W * scale, gameHeight = Display.H * (scaleY or scale),
|
||||
scale = scale,
|
||||
}
|
||||
love.graphics.push("all")
|
||||
pcall(function() ModRuntime.call("render.hud", noop, self, viewport) end)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
function Game3:draw()
|
||||
local w = love.graphics.getWidth()
|
||||
local h = love.graphics.getHeight()
|
||||
@@ -469,6 +578,7 @@ function Game3:draw()
|
||||
drawBootFrame()
|
||||
end
|
||||
end
|
||||
self:_drawHud(w, h)
|
||||
if self.touchControls then
|
||||
self.touchControls:draw()
|
||||
end
|
||||
@@ -477,6 +587,7 @@ function Game3:draw()
|
||||
|
||||
if Runtime.isActive() then
|
||||
if Display.present(self, w, h) then
|
||||
self:_drawHud(w, h)
|
||||
if self.touchControls then
|
||||
self.touchControls:draw()
|
||||
end
|
||||
@@ -488,6 +599,7 @@ function Game3:draw()
|
||||
love.graphics.setColor(0.4, 0.8, 0.4)
|
||||
local map = self.session and self.session.map or "?"
|
||||
love.graphics.printf("Fire Red field: " .. tostring(map), 0, h * 0.45, w, "center")
|
||||
self:_drawHud(w, h)
|
||||
if self.touchControls then
|
||||
self.touchControls:draw()
|
||||
end
|
||||
@@ -543,11 +655,19 @@ function Game3:_hotkey(key)
|
||||
end
|
||||
|
||||
function Game3:keypressed(key)
|
||||
if self:_hotkey(key) then return end
|
||||
if self.input and self.input.keypressed then self.input:keypressed(key) end
|
||||
local function vanilla()
|
||||
if self:_hotkey(key) then return end
|
||||
if self.input and self.input.keypressed then self.input:keypressed(key) end
|
||||
end
|
||||
if not ModRuntime.wantsHook("input.key") then return vanilla() end
|
||||
return ModRuntime.call("input.key", vanilla, self, { phase = "pressed", key = key })
|
||||
end
|
||||
function Game3:keyreleased(key)
|
||||
if self.input and self.input.keyreleased then self.input:keyreleased(key) end
|
||||
local function vanilla()
|
||||
if self.input and self.input.keyreleased then self.input:keyreleased(key) end
|
||||
end
|
||||
if not ModRuntime.wantsHook("input.key") then return vanilla() end
|
||||
return ModRuntime.call("input.key", vanilla, self, { phase = "released", key = key })
|
||||
end
|
||||
|
||||
function Game3:_padPressedBody(joystick, button)
|
||||
@@ -585,21 +705,45 @@ function Game3:_padReleasedBody(joystick, button)
|
||||
end
|
||||
|
||||
function Game3:gamepadpressed(joystick, button)
|
||||
self:_padPressedBody(joystick, button)
|
||||
local function vanilla() self:_padPressedBody(joystick, button) end
|
||||
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
|
||||
return ModRuntime.call("input.gamepad", vanilla, self,
|
||||
{ phase = "pressed", joystick = joystick, button = button })
|
||||
end
|
||||
function Game3:gamepadreleased(joystick, button)
|
||||
self:_padReleasedBody(joystick, button)
|
||||
local function vanilla() self:_padReleasedBody(joystick, button) end
|
||||
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
|
||||
return ModRuntime.call("input.gamepad", vanilla, self,
|
||||
{ phase = "released", joystick = joystick, button = button })
|
||||
end
|
||||
|
||||
function Game3:saveGame()
|
||||
if not self.session or self.phase == "quest_log" then return end
|
||||
if ModRuntime.wantsHook("save.write")
|
||||
and ModRuntime.call("save.write", function() return true end, self) == false then
|
||||
return false
|
||||
end
|
||||
if Runtime.getSession then
|
||||
local s = Runtime.getSession()
|
||||
if s then self.session = s end
|
||||
end
|
||||
pcall(function()
|
||||
require("src.core.game3.scripting.space").persistSession(nil, self)
|
||||
end)
|
||||
QuestRecorder.save(self)
|
||||
self.save = Schema.toSaveTable(self.session)
|
||||
if SaveData.save then pcall(SaveData.save, self.save) end
|
||||
local save = Schema.toSaveTable(self.session)
|
||||
if SaveData.buildMeta then
|
||||
save.meta = SaveData.buildMeta(
|
||||
self.modStatus and self.modStatus.loaded, save.meta, self.sessionStartedAt)
|
||||
self.session.meta = save.meta
|
||||
end
|
||||
self.save = save
|
||||
if ModRuntime.wants("save.writing") then
|
||||
ModRuntime.emit("save.writing", { save = save, meta = save.meta })
|
||||
end
|
||||
if not SaveData.save then return false end
|
||||
local ok, written = pcall(SaveData.save, save)
|
||||
return ok and written ~= false
|
||||
end
|
||||
|
||||
function Game3:resize() end
|
||||
@@ -627,11 +771,15 @@ end
|
||||
|
||||
function Game3:wheelmoved(_, dy)
|
||||
if type(dy) ~= "number" then return end
|
||||
if dy > 0 then
|
||||
self:zoomStep(1)
|
||||
elseif dy < 0 then
|
||||
self:zoomStep(-1)
|
||||
local function vanilla()
|
||||
if dy > 0 then
|
||||
self:zoomStep(1)
|
||||
elseif dy < 0 then
|
||||
self:zoomStep(-1)
|
||||
end
|
||||
end
|
||||
if not ModRuntime.wantsHook("input.wheel") then return vanilla() end
|
||||
return ModRuntime.call("input.wheel", vanilla, self, dy)
|
||||
end
|
||||
function Game3:textinput() end
|
||||
function Game3:filedropped() end
|
||||
@@ -649,21 +797,26 @@ function Game3:touchreleased(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function Game3:gamepadaxis(joystick, axis, value)
|
||||
if math.abs(value) > 0.5 and self.touchControls then
|
||||
self.touchControls:noteGamepad()
|
||||
end
|
||||
if self.input and self.input.triggerAxis then
|
||||
local trigger, phase = self.input:triggerAxis(axis, value)
|
||||
if trigger then
|
||||
if phase == "pressed" then
|
||||
self:_padPressedBody(joystick, trigger)
|
||||
elseif phase == "released" then
|
||||
self:_padReleasedBody(joystick, trigger)
|
||||
end
|
||||
return
|
||||
local function vanilla()
|
||||
if math.abs(value) > 0.5 and self.touchControls then
|
||||
self.touchControls:noteGamepad()
|
||||
end
|
||||
if self.input and self.input.triggerAxis then
|
||||
local trigger, phase = self.input:triggerAxis(axis, value)
|
||||
if trigger then
|
||||
if phase == "pressed" then
|
||||
self:_padPressedBody(joystick, trigger)
|
||||
elseif phase == "released" then
|
||||
self:_padReleasedBody(joystick, trigger)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
if self.input and self.input.gamepadaxis then self.input:gamepadaxis(joystick, axis, value) end
|
||||
end
|
||||
if self.input and self.input.gamepadaxis then self.input:gamepadaxis(joystick, axis, value) end
|
||||
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
|
||||
return ModRuntime.call("input.gamepad", vanilla, self,
|
||||
{ phase = "axis", joystick = joystick, axis = axis, value = value })
|
||||
end
|
||||
|
||||
function Game3:joystickpressed(joystick, button)
|
||||
@@ -701,15 +854,23 @@ function Game3:joystickhat(joystick, hat, direction)
|
||||
if self.input and self.input.joystickhat then self.input:joystickhat(joystick, hat, direction) end
|
||||
end
|
||||
|
||||
function Game3:joystickadded() end
|
||||
function Game3:_releaseModInput()
|
||||
if self.mods and self.mods.releaseModInput then self.mods:releaseModInput() end
|
||||
end
|
||||
|
||||
function Game3:joystickadded()
|
||||
self:_releaseModInput()
|
||||
end
|
||||
|
||||
function Game3:joystickremoved(joystick)
|
||||
self:_releaseModInput()
|
||||
if self.touchControls then self.touchControls:joystickremoved() end
|
||||
end
|
||||
|
||||
function Game3:focus(f)
|
||||
if self.input then self.input:reset() end
|
||||
if self.touchControls then self.touchControls:reset() end
|
||||
self:_releaseModInput()
|
||||
if f then
|
||||
if self.input then self.input:reconcile() end
|
||||
Audio.onFocusGained()
|
||||
@@ -796,6 +957,9 @@ function Game3:reset()
|
||||
self.boot = nil
|
||||
self.session = nil
|
||||
self.data = nil
|
||||
self.mods = nil
|
||||
self.modStatus = nil
|
||||
self._modSaveAdopted = nil
|
||||
self.phase = "boot"
|
||||
self.returnToLauncher = nil
|
||||
self.onExit = nil
|
||||
|
||||
@@ -1,530 +0,0 @@
|
||||
# Battle intro + trainer presentation — implementation plan
|
||||
|
||||
Target: FRLG `BeginBattleIntro` presentation for the owned game3 battle engine, built the
|
||||
same way move anims were (ROM/pret-derived assets → cache → host present state → runtime
|
||||
sequencer). Reference tree: `/home/autumn/src/pokefirered` (`$POKEFIRERED`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope
|
||||
|
||||
### Goals
|
||||
|
||||
1. **In-battle intro** for wild and single trainer battles:
|
||||
BG intro slide → sprite slide-in → party summary (trainer) → intro message →
|
||||
opponent send-out → player throw + send-out → healthbox slide-in → `command`.
|
||||
2. **Trainer front pics** and **player back pics** resolved from the ROM (lazy decode with
|
||||
cache write-through, mirroring `Pokemon.frontPic`), plus a small `trainers.lua` data
|
||||
extract for trainer class / name / pic id.
|
||||
3. **Soft field fade** into the battle scene (`src/ui/game3/fade.lua`), which already ticks
|
||||
and draws around `Battle.draw` for free.
|
||||
4. **Party status summary** bar + balls during the trainer intro. Cheap: the balls are
|
||||
already in the extracted healthbox element sheet; only the 128×8 bar needs a new offset.
|
||||
5. Fix the trainer intro string, which currently prints the **lead mon's** name instead of
|
||||
the trainer class + name.
|
||||
|
||||
### Non-goals (explicitly deferred)
|
||||
|
||||
- `battle_transition.c` mugshots / VS wipe / all 15 `BattleTransition_*` effects. The field
|
||||
side keeps a plain fade.
|
||||
- Double battles, link/multi, Safari, Ghost, Old Man tutorial, Pokédude intro variants.
|
||||
- Shiny sparkle anim (`TryShinyAnimation`), `Intro_WaitForShinyAnimAndHealthbox`.
|
||||
- Real GBA scanline/window effects in the BG slide (`gScanlineEffectRegBuffers`, `WIN0V`).
|
||||
Approximated — see §7.
|
||||
- Non-Poké Ball ball graphics (`ItemIdToBallId`); MVP always uses the Poké Ball sheet.
|
||||
- Real cries (`Audio.playCry` is already a timing stub; the sequencer only waits on it).
|
||||
|
||||
---
|
||||
|
||||
## 2. Why a new sequencer, not `AnimSeq`
|
||||
|
||||
`anim_seq.lua` is the per-hit loop (`anim → wait VM → tweenHp → wait`) driven from
|
||||
`Engine.resolveMove` output. The intro is a different shape: a fixed script of
|
||||
presentation beats with no hit list, exactly like `exp_seq.lua` (award → bar → level →
|
||||
learn) and `evo_seq.lua`. So: **new `intro_seq.lua`, same module contract**
|
||||
(`reset` / `busy` / `begin(...) -> bool` / `update() -> done`).
|
||||
|
||||
Likewise the intro send-out is **not** an `AnimVm` script. In pret, `Special_BallThrow` /
|
||||
`Special_BallThrowWithTrainer` are the *catch* throw (`AnimTask_ThrowBall`,
|
||||
`AnimTask_IsBallBlockedByTrainerOrDodged`) — they are the wrong animation and they are also
|
||||
tasks the VM currently stubs. The intro send-out is `DoPokeballSendOutAnimation` in
|
||||
`src/pokeball.c`: pure C sprite callbacks with no battle-anim script at all. `pack.special`
|
||||
has no send-out entry to wire up. **Therefore the send-out is implemented as host tweens in
|
||||
`anim.lua` driven by `intro_seq.lua`**, using pret's exact frame counts (§5). This is the
|
||||
same call the repo already made for `HorizontalLunge` (a `noGfx` template routed to a host
|
||||
visual task).
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
```
|
||||
ROM (FireRed USA 1.0, sha1 41cb23d8…)
|
||||
│
|
||||
┌──────────────────────────────────┴────────────────────────────────────┐
|
||||
│ src/import/gba/trainer_extract.lua (NEW) │
|
||||
│ gTrainers / gTrainerClassNames → data/generated/gba/trainers.lua │
|
||||
│ gTrainerBackPicTable[0..1] → …/trainers/back_<0|1>.rgba │
|
||||
│ src/import/gba/battle_chrome_extract.lua (EXTEND) │
|
||||
│ gBattleInterface_PartySummaryBar_Gfx → …/pokemon/battle/ │
|
||||
│ party_summary_bar.rgba │
|
||||
└──────────────────────────────────┬────────────────────────────────────┘
|
||||
│ CacheFs (firered/ prefix)
|
||||
┌──────────────────────────────────┴────────────────────────────────────┐
|
||||
│ src/core/game3/trainer_pic.lua (NEW) │
|
||||
│ TrainerPic.front(picId) – lazy ROM LZ decode + cache write-through │
|
||||
│ TrainerPic.back(gender) – 5-frame 64×320 strip │
|
||||
│ src/core/game3/scripting/trainers.lua (REWRITE) │
|
||||
│ Trainers.info(id) -> { class, className, name, picId, partySize } │
|
||||
└──────────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────────────┴────────────────────────────────────┐
|
||||
│ src/core/game3/battle/anim.lua (EXTEND) = the HOST │
|
||||
│ Anim._stage : { slide, trainer[side], ball, healthbox[side], │
|
||||
│ partyBar[side] } │
|
||||
│ Anim.stage() / Anim.introSlideDone() / Anim.tweenStage(...) │
|
||||
│ Anim.present(side).visible / .darken (mon hidden until release) │
|
||||
│ Anim._introTweening feeds Anim.busy() │
|
||||
└──────────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────────────┴────────────────────────────────────┐
|
||||
│ src/core/game3/battle/intro_seq.lua (NEW) = SEQUENCER │
|
||||
│ step table (wild | trainer), one beat per entry (§5) │
|
||||
└──────────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────────────┴────────────────────────────────────┐
|
||||
│ src/core/game3/battle/init.lua phase "intro" delegates to IntroSeq │
|
||||
│ src/core/game3/battle/ui.lua draws trainers / ball / gated boxes │
|
||||
│ src/core/game3/battle/healthbox.lua accepts visible + ox/oy │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Nothing new is required in `anim_vm.lua` / `anim_sprites.lua` / `anim_tasks.lua`. The intro
|
||||
draws from `Anim._stage` in `ui.lua`, in the same z-order slot the mon sprites already use.
|
||||
|
||||
---
|
||||
|
||||
## 4. Verified ROM offsets (FireRed USA 1.0, file offsets)
|
||||
|
||||
All of these were confirmed against the local dump (sha1
|
||||
`41cb23d8dccc8ebd7c649cd8fbb58eeace6e2fdc`) — structural scan plus byte-exact comparison
|
||||
against pret data where a reference file exists.
|
||||
|
||||
| pret symbol | file offset | layout | notes |
|
||||
|---|---|---|---|
|
||||
| `gTrainerFrontPicTable` | `0x23957C` | 148 × 8B `{u32 ptr, u16 size, u16 tag}` | `tag == index`; size `0x800` mostly, `0x1000` for a few (2-frame sheets — use frame 0 only) |
|
||||
| `gTrainerFrontPicPaletteTable` | `0x239A1C` | 148 × 8B `{u32 ptr, u16 tag, u16 pad}` | LZ → 32B (16 colors) |
|
||||
| `gTrainerBackPicTable` | `0x239FA4` | 6 × 8B | `[0]`=Red `0x2800`, `[1]`=Leaf `0x2800` → **5 frames** of 64×64 4bpp |
|
||||
| `gTrainerBackPicPaletteTable` | `0x239FD4` | 6 × 8B | |
|
||||
| `gTrainers` | `0x23EAC8` | stride `0x28` | `+1` trainerClass, `+3` trainerPic, `+4` trainerName[12], `+0x20` partySize (u32), `+0x24` party ptr |
|
||||
| `gTrainerClassNames` | `0x23E558` | 107 × 13B | GBA charmap, `0xFF`-terminated; `0x53 0x54` = `{PKMN}` |
|
||||
| `gBattleInterface_PartySummaryBar_Gfx` | `0xE7BB04` | LZ → 512B = 128×8 4bpp | byte-exact vs `graphics/battle_interface/party_summary_bar.png`; palette = `healthbox_pal` @ `0xD11B84` (already extracted) |
|
||||
|
||||
Spot checks that passed: trainer `326/327/328` (Oak's Lab rival) → class `81`, pic `106`,
|
||||
name `"TERRY"`, partySize `1`; class `81` = `"RIVAL"`, `86` = `"COOLTRAINER"`, `88` =
|
||||
`"GENTLEMAN"`; pic `106` LZ-decodes to exactly 2048 tile bytes + 32 palette bytes.
|
||||
|
||||
Two things that **need no extract at all**:
|
||||
|
||||
- **Party summary balls** — `sPartySummaryBallSpriteSheets` is
|
||||
`gBattleInterface_Gfx + B_INTERFACE_GFX_BALL_PARTY_SUMMARY` where the constant is `66`
|
||||
(`src/battle_interface.c:46`). That is tile 66 of the already-extracted
|
||||
`pokemon/battle/elements.rgba` (320×24), and `BattleChrome`'s existing
|
||||
`elements_tile_quad(ti)` indexes it directly. Four tiles: 66..69.
|
||||
- **The Poké Ball sprite** — `graphics/interface/ball/poke.png` is 16×48 (3 frames of
|
||||
16×16) and is already extracted as `ball_poke.png` from
|
||||
`Versions.OAK_SPEECH.ball_poke_tiles = 0xD01724` / `ball_poke_pal = 0xD017E0`, loaded by
|
||||
`src/ui/game3/boot.lua` as `assets.ballPoke` and already drawn frame-by-frame in
|
||||
`src/ui/game3/oak_scene.lua`. Reuse that loader; do not add a new one.
|
||||
|
||||
### Trainer pic placement (no coords table needed)
|
||||
|
||||
`OpponentHandleDrawTrainerPic` computes `y = (8 - gTrainerFrontPicCoords[id].size) * 4 + 40`
|
||||
and every front-pic entry has `size == 8`, so the opponent trainer sprite **center is always
|
||||
(176, 40)**. Same for `PlayerHandleDrawTrainerPic`:
|
||||
`(8 - gTrainerBackPicCoords[id].size) * 4 + 80` with `size == 8` for Red/Leaf → **player back
|
||||
center (80, 80)** (note: `80`, not the mon's `72`). So `gTrainerFrontPicCoords` /
|
||||
`gTrainerBackPicCoords` do not need extracting. Sprites are 64×64, so top-left = center − 32.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sequencer step tables
|
||||
|
||||
Frame counts are pret's, at 60 Hz. `f` = frames.
|
||||
|
||||
### Shared beat vocabulary (`intro_seq.lua` step kinds)
|
||||
|
||||
| kind | data | effect |
|
||||
|---|---|---|
|
||||
| `fade` | `mode, speed` | `Fade.begin`; wait on `Fade.isActive()` |
|
||||
| `bgslide` | `frames` | tween `stage.slide` 0→1; unlock sprite motion partway (`gIntroSlideFlags`) |
|
||||
| `slidein` | `who, from, to, frames` | tween `stage.trainer[side].ox` or `Anim.present(side).ox` |
|
||||
| `undarken` | `side, frames` | tween `Anim.present(side).darken` 10/16 → 0 |
|
||||
| `partybar` | `side, dir` | tween `stage.partyBar[side].ox`, set `.balls` from party |
|
||||
| `msg` | `text` | `pushMsg`; the `Ui.pump()` gate in `Battle.update` blocks until drained |
|
||||
| `trainerexit` | `side, dx, frames` | linear translate the trainer sprite off-screen; hide its party bar |
|
||||
| `throwpose` | `frames[]` | drive `stage.trainer.player.frame` through the back-pic anim |
|
||||
| `ballarc` | `from, to, frames, arcH` | tween `stage.ball.{x,y}`; parabolic `y` |
|
||||
| `ballwait` | `frames` | plain delay (opponent ball hangs before opening) |
|
||||
| `release` | `side` | ball frame 1, `present.visible = true`, `oy` 16→0 over 14f, cry |
|
||||
| `cry` | `side` | `Audio.playCry`; wait `Audio.isCryFinished()` |
|
||||
| `healthbox` | `side` | tween `stage.healthbox[side].ox` ±115 → 0 over 23f, `visible = true` |
|
||||
| `wait` | `frames` | delay |
|
||||
|
||||
### Wild
|
||||
|
||||
| # | step | pret source | frames |
|
||||
|---|---|---|---|
|
||||
| 1 | `fade` FROM_BLACK | (repo-side; not pret) | 16 |
|
||||
| 2 | `bgslide` | `BattleIntroSlide1/2/3` by terrain | 24 (unlock sprites at 8) |
|
||||
| 3 | `slidein` enemy mon `ox −240 → 0` @ +2/f, `darken = 10/16` | `OpponentHandleLoadMonSprite` sets `x2 = -DISPLAY_WIDTH`; `SpriteCB_MoveWildMonToRight` does `x2 += 2`; `SpriteCB_EnemyMon` does `BeginNormalPaletteFade(…, 10, 10, RGB(8,8,8))` | 120 |
|
||||
| 4 | `cry` enemy (pan 25) | `PlayCry_Normal` at `x2 == 0` | — |
|
||||
| 5 | `undarken` enemy | `BeginNormalPaletteFade(…, 10, 0, …)` in `SpriteCB_WildMonShowHealthbox` | 10 |
|
||||
| 6 | `healthbox` enemy | `StartHealthboxSlideIn` | 23 |
|
||||
| 7 | `msg` `Wild {MON} appeared!` | `STRINGID_INTROMSG` → `sText_WildPkmnAppeared` | — |
|
||||
| 8 | `msg` `Go! {MON}!` | `STRINGID_INTROSENDOUT` → `sText_GoPkmn` | — |
|
||||
| 9 | player throw block (§5.1) | `PlayerHandleIntroTrainerBallThrow` | ~120 |
|
||||
| 10 | `healthbox` player | `Intro_TryShinyAnimShowHealthbox` | 23 |
|
||||
|
||||
Note the ordering: in pret the wild mon's healthbox appears **before** the "appeared!"
|
||||
message, because the sprite callback chain and `gBattleMainFunc` advance in parallel. The
|
||||
sequencer is serial, so slotting the healthbox at step 6 reproduces the visible result.
|
||||
|
||||
### Trainer
|
||||
|
||||
| # | step | pret source | frames |
|
||||
|---|---|---|---|
|
||||
| 1 | `fade` FROM_BLACK | — | 16 |
|
||||
| 2 | `bgslide` | `BattleIntroPrepareBackgroundSlide` | 24 |
|
||||
| 3 | `slidein` **both**: enemy trainer `ox −240 → 0` @ +2/f, player back `ox +240 → 0` @ −2/f | `SpriteCB_TrainerSlideIn`, both `DrawTrainerPic` handlers | 120 |
|
||||
| 4 | `partybar` enemy `(104, 40)`, `ox −100 → 0` @ +5/f, and player `(136, 96)`, `ox +100 → 0` @ −5/f | `CreatePartyStatusSummarySprites` (`battle_interface.c:1080`) | 20 |
|
||||
| 5 | `msg` `{CLASS} {NAME}\nwould like to battle!` | `sText_Trainer1WantsToBattle` | — |
|
||||
| 6 | `msg` `{CLASS} {NAME} sent\nout {MON}!` | `sText_Trainer1SentOutPkmn` | — |
|
||||
| 7 | `trainerexit` enemy `176 → 280`, hide enemy party bar | `OpponentHandleIntroTrainerBallThrow` (`data[0]=35`, `data[2]=280`) + `Task_HidePartyStatusSummary` | 35 |
|
||||
| 8 | `ballwait` then `release` enemy | `SpriteCB_OpponentMonSendOut` waits `data[0] > 15`; ball spawns at `(enemy cx, enemy cy + 24)` | 16 + 14 |
|
||||
| 9 | `cry` enemy, `healthbox` enemy | `Intro_TryShinyAnimShowHealthbox` | 23 |
|
||||
| 10 | `msg` `Go! {MON}!` | `sText_GoPkmn` | — |
|
||||
| 11 | player throw block (§5.1), hide player party bar | `PlayerHandleIntroTrainerBallThrow` | ~120 |
|
||||
| 12 | `healthbox` player | | 23 |
|
||||
|
||||
### 5.1 Player throw block (shared by wild and trainer)
|
||||
|
||||
From `PlayerHandleIntroTrainerBallThrow` + `Task_StartSendOutAnim` +
|
||||
`SpriteCB_PlayerMonSendOut_1/2` + `SpriteCB_ReleaseMonFromBall` + `HandleBallAnimEnd`:
|
||||
|
||||
1. `throwpose` — player back pic anim 1, verified byte-exact against
|
||||
`src/data/trainer_graphics/back_pic_anims.h` (`sAnimCmd_Red_1` / `sAnimCmd_Leaf_1`, and
|
||||
the same bytes read back from the ROM anim table at `0x239EBC`):
|
||||
frame 1 (20f) → 2 (6f) → 3 (6f) → 4 (24f) → 0 (1f) = **57f total**.
|
||||
Concurrently `trainerexit` player `80 → −40` over **50f** (`data[0]=50`, `data[2]=-40`).
|
||||
2. After **31f** (`Task_StartSendOutAnim` gate `data[1] < 31`), the ball spawns at
|
||||
**(48, 70)** and arcs to `(player cx, player cy_pic_offset + 24)` over **25f** with arc
|
||||
height **−30** (`SpriteCB_PlayerMonSendOut_1`: `data[0]=25`, `data[5]=-30`).
|
||||
3. `release`: ball anim frame 1, ball-open particles, mon `visible = true`, mon `oy` 16 → 0
|
||||
over ~**14f** (`data[1] = 0x1000`, `−288`/f, `y2 = data[1] >> 8`), affine
|
||||
`BATTLER_AFFINE_EMERGE` scale-up, cry (pan −25).
|
||||
|
||||
For MVP the "affine emerge" can be a `Task.tween` on a new `present.scale` (0.2 → 1.0 over
|
||||
14f) rather than a real affine matrix; the `oy` rise is already 1:1.
|
||||
|
||||
### 5.2 Exact strings (fixing the current bug)
|
||||
|
||||
`Battle.start` currently pushes `State.displayName(st.enemy) .. " wants\nto battle!"` — the
|
||||
**mon** name, and Gen-1 wording. FRLG (`src/battle_message.c`):
|
||||
|
||||
| case | pret string |
|
||||
|---|---|
|
||||
| wild intro | `Wild {B_OPPONENT_MON1_NAME} appeared!` |
|
||||
| trainer intro | `{B_TRAINER1_CLASS} {B_TRAINER1_NAME}\nwould like to battle!` |
|
||||
| trainer send-out | `{B_TRAINER1_CLASS} {B_TRAINER1_NAME} sent\nout {B_OPPONENT_MON1_NAME}!` |
|
||||
| player send-out | `Go! {B_PLAYER_MON1_NAME}!` |
|
||||
|
||||
`{B_TRAINER1_CLASS}` = `gTrainerClassNames[gTrainers[id].trainerClass]`,
|
||||
`{B_TRAINER1_NAME}` = `gTrainers[id].trainerName`. When `trainerId` is absent, fall back to
|
||||
`"{PKMN} TRAINER"` (class 0) and no name, collapsing the double space.
|
||||
|
||||
Rival trainers (class `81` = `RIVAL`) carry the placeholder name `"TERRY"` in the struct; the
|
||||
real name lives in the save. If the session has a rival name, prefer it — otherwise the
|
||||
struct name is the correct display fallback.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ordered work slices
|
||||
|
||||
Each slice is independently landable and leaves the game runnable.
|
||||
|
||||
### Slice 1 — trainer data (no visuals)
|
||||
|
||||
- `src/import/gba/versions.lua`: add
|
||||
```lua
|
||||
Versions.TRAINER_FRONT_PIC_TABLE = 0x23957C
|
||||
Versions.TRAINER_FRONT_PIC_PAL_TABLE = 0x239A1C
|
||||
Versions.TRAINER_BACK_PIC_TABLE = 0x239FA4
|
||||
Versions.TRAINER_BACK_PIC_PAL_TABLE = 0x239FD4
|
||||
Versions.TRAINERS_TABLE = 0x23EAC8
|
||||
Versions.TRAINER_STRIDE = 0x28
|
||||
Versions.TRAINER_CLASS_NAMES = 0x23E558
|
||||
Versions.TRAINER_CLASS_NAME_STRIDE = 13
|
||||
Versions.TRAINER_CLASS_COUNT = 107
|
||||
Versions.TRAINER_PIC_COUNT = 148
|
||||
Versions.TRAINERS_COUNT = 743 -- pret NUM_TRAINERS
|
||||
```
|
||||
and bump the format-version comment block at the top like the other entries.
|
||||
- **New** `src/import/gba/trainer_extract.lua`, modelled on
|
||||
`battle_moves_extract.lua` / `battle_chrome_extract.lua`
|
||||
(`FORMAT_VERSION`, `CACHE_SUB = "trainers"`, `run(rom, cache, opts)`, `ready(cache, root)`):
|
||||
- decode names via `TextIR.CHARMAP` exactly as `pokemon_extract.lua`'s `decode_name` does
|
||||
(it already handles the `0xBB`/`0xD5` letter ranges and `0xFF` EOS);
|
||||
- write `data/generated/gba/trainers.lua` → `{ classNames = {...}, trainers = { [id] = { class, pic, name, partySize } } }`;
|
||||
- write `data/generated/gba/trainers/manifest.lua`.
|
||||
- Wire into `PokemonExtract.run` next to the existing sub-extracts
|
||||
(`src/import/gba/pokemon_extract.lua:567-591` — same `progress("trainers", 0, 1)` shape),
|
||||
and return it in the detail table so `RomExtractorGen3:runPokemonExtract` reports it.
|
||||
- Add `--trainers` to `src/import/gba/cli_extract.lua` following the `--battle-anims` branch.
|
||||
- **Rewrite** `src/core/game3/scripting/trainers.lua`: keep `Trainers.foeFromId` (the
|
||||
hardcoded Oak's Lab parties stay as the party source until a `gTrainers` party extract
|
||||
lands) and add `Trainers.info(id)` reading the generated pack, with the current
|
||||
hardcoded table as the fallback when the cache is missing.
|
||||
- Thread `trainerId` through: `ops_a.lua:718-751` already has it →
|
||||
`adapters.lua:980` `startTrainerBattle(foe, done, battleOpts)` → `battle_bridge.lua:181`
|
||||
`Battle.start{...}`. Add `trainerId = opts.trainerId` and let `Battle.start` resolve
|
||||
class/name/pic through `Trainers.info`. Store on `st` as
|
||||
`st.trainerId / st.trainerClassName / st.trainerName / st.trainerPicId`.
|
||||
- **Fix the strings now** (§5.2), still pushed synchronously. This alone is a visible win
|
||||
and keeps the diff reviewable.
|
||||
|
||||
### Slice 2 — trainer pics
|
||||
|
||||
- **New** `src/core/game3/trainer_pic.lua`. Copy the shape of
|
||||
`src/core/game3/pokemon.lua:761-880`:
|
||||
- `decode_pic_rgba(index, picTable, palTable, cacheRel, frames)` — the mon version is
|
||||
hardcoded to 8×8 tiles; generalise to `frames` so the back pic decodes as a 64×320
|
||||
vertical strip (5 × 64×64);
|
||||
- `TrainerPic.front(picId)` → `{ image, w = 64, h = 64 }`, cache
|
||||
`data/generated/gba/trainers/front/<picId>.rgba`;
|
||||
- `TrainerPic.back(gender)` → `{ image, w = 64, h = 320, frames = 5 }`, cache
|
||||
`data/generated/gba/trainers/back/<gender>.rgba`;
|
||||
- reuse `Versions.gbaToFile` and `src/import/gba/lz77.lua` as the mon path does;
|
||||
- for `0x1000`-sized front entries take the first 2048 bytes only.
|
||||
- The 2 player back sheets are needed in **every** battle, so also bake them eagerly in
|
||||
`trainer_extract.run` (2 × 5 × 16 KB = 160 KB) so a shipped cache never needs the ROM.
|
||||
Front pics stay lazy, matching the existing mon-pic tradeoff.
|
||||
- `src/import/CacheContract.lua`: add to `VERSION_REQUIRED_FILES_OVERRIDE.firered`
|
||||
(after the existing `pokemon/party/slot_main.rgba` line):
|
||||
```
|
||||
"data/generated/gba/trainers.lua",
|
||||
"data/generated/gba/trainers/back_0.rgba",
|
||||
"data/generated/gba/trainers/back_1.rgba",
|
||||
```
|
||||
Adding required files invalidates existing caches by design, so bump
|
||||
`CacheContract.VERSION_FORMAT.firered` from `"rom-cache-v1-firered:"` to
|
||||
`"rom-cache-v2-firered:"` in the same commit.
|
||||
|
||||
### Slice 3 — host stage state (`anim.lua`)
|
||||
|
||||
- Add `Anim._stage` and `Anim.resetStage()` (called from `Anim.reset`):
|
||||
```lua
|
||||
Anim._stage = {
|
||||
slide = 0, slideDone = false,
|
||||
trainer = { player = { visible=false, ox=0, oy=0, frame=0, gender=0 },
|
||||
enemy = { visible=false, ox=0, oy=0, picId=nil } },
|
||||
ball = { visible=false, x=0, y=0, frame=0, side=nil },
|
||||
healthbox = { player = { visible=false, ox=0 }, enemy = { visible=false, ox=0 } },
|
||||
partyBar = { player = { visible=false, ox=0, balls={} },
|
||||
enemy = { visible=false, ox=0, balls={} } },
|
||||
}
|
||||
```
|
||||
- `Anim.stage()`, `Anim.introSlideDone()`.
|
||||
- `Anim.tweenStage(frames, onStep, onComplete)` — thin wrapper over `Task.tween` that
|
||||
raises/lowers a new `Anim._introTweening` counter.
|
||||
- Add `Anim._introTweening > 0` to `Anim.busy()`, next to `_hpTweening` / `_expTweening`.
|
||||
Keep the existing comment's warning in mind: this must gate *in-flight tweens only*, not
|
||||
"the sequencer has steps left", or `Battle.update` soft-locks.
|
||||
- Extend `default_present(side)` with `darken = 0` and `scale = 1`; both must be honoured in
|
||||
`ui.lua`'s `draw_mon_sprite`.
|
||||
- Headless: `Anim.reset({ headless = true })` must leave `slideDone = true` and everything
|
||||
`visible = true` so `runToEnd` never sees a hidden battler.
|
||||
|
||||
### Slice 4 — sequencer (`intro_seq.lua`)
|
||||
|
||||
- **New** `src/core/game3/battle/intro_seq.lua`, structurally a sibling of `exp_seq.lua`:
|
||||
`IntroSeq.reset()`, `IntroSeq.busy()`, `IntroSeq.begin(st, opts) -> bool`,
|
||||
`IntroSeq.update() -> done`, module-local `_steps/_i/_waiting/advance()/finish()`.
|
||||
- `begin` builds the step list from `st.wild` (§5) and returns `false` when
|
||||
`opts.headless` — the caller then pushes the strings directly.
|
||||
- `update` follows `ExpSeq.update`'s orphan-wait guard: if `_waiting` and
|
||||
`not Anim.busy()`, `advance()`.
|
||||
- `opts`: `{ pushMsg, headless, playerGender, trainerClassName, trainerName, trainerPicId, playerParty, foeParty }`.
|
||||
|
||||
### Slice 5 — engine phase (`init.lua`)
|
||||
|
||||
- `require` `IntroSeq`; add `IntroSeq.reset()` to `finish()` and `Battle.runToEnd()`
|
||||
alongside the other `*.reset()` calls.
|
||||
- In `Battle.start`, replace the `if not opts.headless then … Ui.push(…) end` block
|
||||
(`init.lua:175-183`) with `IntroSeq.begin(st, {...})`. Keep the headless branch pushing
|
||||
the four strings in the same order so text-log assertions stay stable.
|
||||
- Replace the `Battle._phase == "intro"` branch (`init.lua:469-477`). It currently sits
|
||||
*after* the shared `if Anim.busy() then return end; if not Ui.pump() then return end`
|
||||
gate, which is exactly the gate the sequencer wants, so the body becomes:
|
||||
```lua
|
||||
if Battle._phase == "intro" then
|
||||
if not IntroSeq.update() then return end
|
||||
Battle._phase = "command"
|
||||
if Battle._auto then
|
||||
begin_turn_with(Commands.playerAction(Battle._st, 1, 1))
|
||||
else
|
||||
Ui.openMenu()
|
||||
end
|
||||
return
|
||||
end
|
||||
```
|
||||
- Accept and store the new `opts` (`trainerId`, `trainerClass`, `trainerName`,
|
||||
`trainerPicId`, `playerGender`) on `st`.
|
||||
|
||||
### Slice 6 — drawing (`ui.lua`, `healthbox.lua`)
|
||||
|
||||
- `ui.lua` `draw_mon_sprite`: honour `pres.darken` (lerp toward `RGB(8,8,8)` = `(8/255)`)
|
||||
and `pres.scale` (scale about the sprite centre). `pres.visible == false` is already
|
||||
respected at line 271.
|
||||
- New locals in `ui.lua`:
|
||||
- `draw_trainer_sprite(side)` — `TrainerPic.front(stage.trainer.enemy.picId)` at centre
|
||||
`(176 + ox, 40)`; `TrainerPic.back(gender)` quad-clipped to frame `stage.trainer.player.frame`
|
||||
at centre `(80 + ox, 80)`.
|
||||
- `draw_intro_ball()` — `assets.ballPoke` quad `(0, frame*16, 16, 16)` at
|
||||
`(stage.ball.x − 8, stage.ball.y − 8)`. Pull the image through
|
||||
`src/ui/game3/boot.lua`'s `ballPoke` rather than adding a second loader.
|
||||
- `draw_party_bar(side)` — `party_summary_bar.rgba` (128×8) at `(x + ox, y)` plus up to 6
|
||||
balls from `BattleChrome` element tile 66 at `(x + 8*i, y − 4)`.
|
||||
- Insert into `Ui.draw` between the terrain and the mon sprites, matching pret subpriority:
|
||||
BG → enemy trainer → enemy mon → particles → player mon → player trainer → ball →
|
||||
healthboxes → party bars → panel.
|
||||
- `Healthbox.draw(side, battler)` gains an `opts` or reads `Anim.stage().healthbox[side]`:
|
||||
skip entirely when `visible == false`, and offset both the box and its text/bars by `ox`.
|
||||
Today `Ui.draw` calls it unconditionally at lines 398-399.
|
||||
- `BattleChrome.drawTerrain` / `BattleBg.draw` take an optional `x` offset so `bgslide` can
|
||||
scroll the terrain (pret scrolls `gBattle_BG1_X += 6`/frame).
|
||||
|
||||
### Slice 7 — party summary asset
|
||||
|
||||
- `src/import/gba/versions.lua`: `Versions.BATTLE_CHROME.party_summary_bar = 0xE7BB04`.
|
||||
- `battle_chrome_extract.lua`: LZ → 512 B, decode as 16 tiles → 128×8, palette
|
||||
`healthbox_pal`, write `pokemon/battle/party_summary_bar.rgba`; add
|
||||
`partySummaryBar = { file = ..., w = 128, h = 8 }` plus
|
||||
`partyBarPlayer = { x = 136, y = 96 }`, `partyBarOpponent = { x = 104, y = 40 }`
|
||||
to the generated `manifest.lua`. Bump `BattleChromeExtract.FORMAT_VERSION` 3 → 4.
|
||||
- `battle_chrome.lua`: load it in `install`, expose `BattleChrome.drawPartyBar(x, y)` and
|
||||
`BattleChrome.drawPartyBall(x, y, kind)` (element tiles 66..69).
|
||||
|
||||
### Slice 8 — field fade + docs
|
||||
|
||||
- `BattleBridge.start` (`battle_bridge.lua:149`): before `Battle.start`, run
|
||||
`Fade.begin(Fade.MODE.TO_BLACK, 1, cb)` and start the battle in the callback. `Runtime`
|
||||
already ticks `Fade` (`runtime.lua:149`) and `Gfx.drawUi` already draws it after
|
||||
`Battle.draw` (`display.lua:132-141`, `gfx.lua:121-124`), so the FROM_BLACK half needs
|
||||
nothing beyond being step 1 of the sequencer.
|
||||
- Update `src/core/game3/battle/PARITY.md`:
|
||||
- Layers table: new row **Battle intro / send-out** — *Owned MVP: BG slide approximation,
|
||||
trainer slide-in, party summary, ROM trainer pics, pret-timed ball send-out. Deferred:
|
||||
battle_transition mugshot/VS, shiny anim, doubles.*
|
||||
- "Battle anim architecture" list: add
|
||||
`**Intro sequencer** intro_seq.lua: bgslide → slide-in → party summary → msgs →
|
||||
send-out → healthbox (pret BeginBattleIntro + DoPokeballSendOutAnimation)`.
|
||||
- "Pret 1:1 (later pass)": add battle transitions, shiny anim, real affine emerge,
|
||||
scanline BG slide.
|
||||
- Note that the send-out is deliberately **not** an `AnimVm` script, with the reason.
|
||||
|
||||
---
|
||||
|
||||
## 7. Risks and accepted MVP shortcuts
|
||||
|
||||
| # | risk / shortcut | mitigation |
|
||||
|---|---|---|
|
||||
| 1 | **BG intro slide is not reproducible.** `BattleIntroSlide1/2/3` drive `WIN0V`, `BLDCNT`, and per-scanline `gScanlineEffectRegBuffers` — no equivalent in the LÖVE compositor. | Approximate: horizontal terrain scroll (pret's `gBattle_BG1_X += 6`) plus a vertical wipe. What actually matters for parity is the *gate*: sprites must not move until the slide releases them, which `stage.slideDone` reproduces exactly. Flag it in `PARITY.md`. |
|
||||
| 2 | **Total intro is long** (~7 s for a trainer battle at pret timings). | Keep the frame counts (they are the real game), but let `A`/`B` skip the remaining tweens by fast-forwarding the sequencer — the same affordance `Message` already gives. |
|
||||
| 3 | **Lazy ROM decode needs the `.gba` in cwd.** `Pokemon.load_rom_bytes` scans three hardcoded filenames. A shipped cache with no ROM gets no trainer front pic. | Eager-bake the 2 player back sheets (always needed). For fronts, `TrainerPic.front` returning `nil` must degrade to *no trainer sprite* (skip straight to the send-out), never a placeholder rectangle — the "don't invent FX" rule. A follow-up slice can bake all 148 (≈2.4 MB). |
|
||||
| 4 | **`Anim.busy()` soft-lock.** The comment at `anim.lua:99-101` documents that folding "sequencer has work left" into `busy()` deadlocks `Battle.update`. | `_introTweening` must be incremented only for in-flight `Task.tween`s and decremented in their `onComplete`. Cover with a test that drives 2000 frames and asserts the phase reaches `command`. |
|
||||
| 5 | **Affine emerge** (`BATTLER_AFFINE_EMERGE`) is a real OAM matrix anim. | `present.scale` linear 0.2 → 1.0 over 14f. Visually close; note in `PARITY.md`. |
|
||||
| 6 | **Trainer party for the summary bar** is unknown — `scripting/trainers.lua` only hardcodes three Oak's Lab parties and `st.foeParty` holds one mon. | `gTrainers[id].partySize` (offset `+0x20`) is in the extract, so the bar can show the right *count* of full balls without a party extract. Fall back to 1 ball. |
|
||||
| 7 | **Cries are stubs.** `Audio.playCry` just sets a 64-frame timer. | Fine — the sequencer only needs the timing. |
|
||||
| 8 | **`{PKMN}` in class names** is the 2-byte `0x53 0x54` charmap pair. | Decode to the literal `"POKéMON"` that `FrlgFont` can render, as the rest of the repo's strings do. |
|
||||
| 9 | **Rival name.** Class 81 trainers carry the placeholder `"TERRY"`. | Prefer a session rival name when present; the struct name is the correct fallback. |
|
||||
| 10 | **Cache invalidation.** New `CacheContract` required files reject every existing FireRed cache. | Intended; bump `VERSION_FORMAT.firered` in the same commit so the launcher explains the re-import instead of reporting a corrupt cache. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Test plan
|
||||
|
||||
### Automated (`luajit tests/<name>.lua`, the CI convention)
|
||||
|
||||
**New `tests/game3_battle_intro_test.lua`**, following the plain
|
||||
`check(cond, msg)`-and-`print("[test] N. …")` style of `tests/game3_intro_test.lua`:
|
||||
|
||||
1. **Offsets** — assert every `Versions.TRAINER_*` constant equals the value in §4. Cheap
|
||||
regression guard; `tests/game3_intro_test.lua` already does exactly this for
|
||||
`Versions.INTRO_MOVIE`.
|
||||
2. **Step table shape** — `IntroSeq.begin` on a synthetic wild `st` produces steps in the
|
||||
§5 order; same for a trainer `st`, asserting the trainer path has `partybar` and
|
||||
`trainerexit` and the wild path does not.
|
||||
3. **Strings** — a trainer battle with `trainerClassName = "RIVAL"`, `trainerName = "TERRY"`
|
||||
yields `"RIVAL TERRY\nwould like to battle!"` and
|
||||
`"RIVAL TERRY sent\nout SQUIRTLE!"`; a wild battle yields
|
||||
`"Wild PIDGEY appeared!"`. This is the bug-fix regression test.
|
||||
4. **Headless skip** — `Battle.start{ headless = true }` leaves `IntroSeq.busy() == false`
|
||||
and `Ui.log()` contains the intro strings in order; `Battle.runToEnd()` still terminates
|
||||
within the existing 800-iteration guard.
|
||||
5. **No soft-lock** — non-headless start, then drive `Battle.update(1/60)` up to 2000 times
|
||||
with a stub input; assert `Battle._phase == "command"` and
|
||||
`Anim.stage().healthbox.player.visible == true`.
|
||||
6. **Visibility gating** — immediately after a non-headless `Battle.start`,
|
||||
`Anim.present("player").visible == false` and
|
||||
`Anim.stage().healthbox.enemy.visible == false`.
|
||||
|
||||
Also re-run the neighbours that touch the same modules: `tests/game3_summary_test.lua`
|
||||
(shares `Pokemon` pic decode) and the `parity_battle_intro_chrome.lua` /
|
||||
`parity_battle_intro_cry.lua` suites (Gen-2 paths — they must stay green, i.e. untouched).
|
||||
|
||||
### Manual
|
||||
|
||||
1. `luajit src/import/gba/cli_extract.lua --trainers` → verify
|
||||
`data/generated/gba/trainers.lua` has 743 rows, `classNames[81] == "RIVAL"`,
|
||||
`trainers[326] == { class = 81, pic = 106, name = "TERRY", partySize = 1 }`.
|
||||
2. `luajit src/import/gba/cli_extract.lua --pokemon` → confirm
|
||||
`pokemon/battle/party_summary_bar.rgba` is 128×8×4 = 4096 bytes.
|
||||
3. In-game **wild**: walk into Route 1 grass. Expect fade → BG slide → darkened Pidgey
|
||||
sliding in from the left → cry → un-darken → enemy healthbox → "Wild PIDGEY appeared!"
|
||||
→ "Go! …!" → back-pic throw pose + slide-off-left → ball arc → mon rises → player
|
||||
healthbox → FIGHT menu.
|
||||
4. In-game **trainer**: Oak's Lab rival battle. Expect both trainer sprites sliding in from
|
||||
opposite edges, both party bars, "RIVAL TERRY would like to battle!", trainer exiting
|
||||
right, opponent ball opening, then the player throw. Confirm the message says the
|
||||
trainer's class and name, **not** "SQUIRTLE".
|
||||
5. **Degraded path** — rename the `.gba` out of cwd with a warm cache and start a trainer
|
||||
battle: no trainer sprite, no crash, intro still reaches `command`.
|
||||
6. **Battle end** — win, lose, and run to confirm `finish()` clears the stage and the field
|
||||
redraws with no leftover trainer sprite or ball.
|
||||
|
||||
---
|
||||
|
||||
## 9. File-by-file summary
|
||||
|
||||
| file | action |
|
||||
|---|---|
|
||||
| `src/import/gba/versions.lua` | + `TRAINER_*` offsets, + `BATTLE_CHROME.party_summary_bar` |
|
||||
| `src/import/gba/trainer_extract.lua` | **new** — trainers.lua + eager player back sheets |
|
||||
| `src/import/gba/pokemon_extract.lua` | wire `TrainerExtract.run` into the sub-extract chain |
|
||||
| `src/import/gba/battle_chrome_extract.lua` | + party summary bar; `FORMAT_VERSION` 3 → 4 |
|
||||
| `src/import/gba/cli_extract.lua` | + `--trainers` |
|
||||
| `src/import/CacheContract.lua` | + 3 firered required files; `VERSION_FORMAT.firered` v1 → v2 |
|
||||
| `src/core/game3/trainer_pic.lua` | **new** — lazy front / eager back pic decode |
|
||||
| `src/core/game3/scripting/trainers.lua` | + `Trainers.info(id)` from the generated pack |
|
||||
| `src/core/game3/battle/anim.lua` | + `_stage`, `stage()`, `tweenStage`, `_introTweening`, `present.darken/.scale` |
|
||||
| `src/core/game3/battle/intro_seq.lua` | **new** — the sequencer |
|
||||
| `src/core/game3/battle/init.lua` | `Battle.start` builds the intro; `"intro"` phase delegates; resets |
|
||||
| `src/core/game3/battle/ui.lua` | + trainer / ball / party-bar draws; gate healthboxes; honour darken/scale |
|
||||
| `src/core/game3/battle/healthbox.lua` | + visibility and `ox` offset |
|
||||
| `src/core/game3/battle/bg.lua` | + slide `x` offset passthrough |
|
||||
| `src/ui/game3/battle_chrome.lua` | + `drawPartyBar` / `drawPartyBall` |
|
||||
| `src/core/game3/battle_bridge.lua` | + `trainerId` passthrough, TO_BLACK fade before start |
|
||||
| `src/core/game3/scripting/adapters.lua` | + `trainerId` in `startTrainerBattle` opts |
|
||||
| `src/core/game3/battle/PARITY.md` | intro row + architecture bullet + deferred list |
|
||||
| `tests/game3_battle_intro_test.lua` | **new** |
|
||||
@@ -1,60 +0,0 @@
|
||||
# Game3 battle core — ownership & pret target
|
||||
|
||||
Owned under `src/core/game3/battle/`. **No** live `require` of KR `battle/core` or host `Battle` / `BattleState`.
|
||||
|
||||
## Goal
|
||||
|
||||
Replicate pret FireRed/LeafGreen battle behavior. KR `mods/Kanto-Reforged/battle/core/effects/*` is a **port farm** for adapter-pure handler bodies only. **ROM `gBattleMoves[].effect`** (extracted pack) is the dispatch key via [`effect_ids.lua`](effect_ids.lua) `STATUS_SETUP` / `AFTER_HIT` / `STAT_CHANGES`.
|
||||
|
||||
## Layers
|
||||
|
||||
| Layer | Status |
|
||||
|-------|--------|
|
||||
| Rules / capabilities / type chart / damage | Owned MVP (Gen3-shaped) |
|
||||
| Residuals scheduler + status/weather/seed/trap/perish/wish | Owned |
|
||||
| Effect registry (FRLG-legal; KR-sourced bodies) | Growing; ROM effect-byte driven |
|
||||
| Turn / commands (FIGHT/BAG/POKéMON/RUN) | Owned; BAG/switch placeholder |
|
||||
| UI | ROM chrome extract + pret layout (healthboxes, textbox panels, 2×2 cursors) |
|
||||
| Battle anims | **Owned MVP**: pret IR extract + ANIM_TAG PNGs → VM; template callbacks (HitSplat, RoarNoiseLine, …); stub rare AnimTask_* |
|
||||
| Battle intro / send-out | **Owned MVP**: `intro_seq.lua` (BeginBattleIntro); ROM trainer pics + class/name; pret-timed ball send-out host tweens; party summary bar. Deferred: battle_transition mugshot/VS, shiny anim, doubles, real affine emerge / scanline BG slide |
|
||||
| EXP / level-up | **Owned MVP**: ROM `expYield` + `growthRate`; pret tables; Cmd_getexp; EXP bar; **learn-move** (ROM learnsets, YES/NO forget, HM block); **EVO_LEVEL** after win (Everstone). Exp.Share split per pret Cmd_getexp. Deferred: stones/trade/friendship evo, evo animation, Summary forget UI |
|
||||
| ROM `gBattleMoves` extract | Owned (`data/generated/gba/pokemon/battle_moves.lua`) |
|
||||
| Base stats / abilities on battlers | From species pack; ability effects in [`abilities.lua`](abilities.lua) |
|
||||
| Held items | [`held_items.lua`](held_items.lua): pret hold effects (berries, Leftovers, Focus Band, Choice Band, type boosts, King's Rock, Shell Bell, Quick Claw, Smoke Ball) |
|
||||
|
||||
## Battle anim architecture
|
||||
|
||||
- **Host** [`anim.lua`](anim.lua): present state (ox/oy/alpha/z), HP display tween + `onComplete`, composite `busy()`
|
||||
- **Event sequencer** [`anim_seq.lua`](anim_seq.lua): plays the adapter event stream (msg / move / anim general·status·special / hit / hp / faint / switch / end) in pret battle-script order; Battle Scene OFF follows Cmd_attackanimation / Cmd_playanimation / Cmd_statusanimation
|
||||
- **VM context** [`anim_ctx.lua`](anim_ctx.lua): gAnimMovePower / gAnimMoveDmg / gAnimFriendship / gWeatherMoveAnim / gAnimDisableStructPtr / behindSubstitute filled at every launch
|
||||
- **Intro sequencer** [`intro_seq.lua`](intro_seq.lua): bgslide → slide-in → party summary → msgs → send-out → healthbox (pret `BeginBattleIntro` + `DoPokeballSendOutAnimation` host tweens — **not** catch `Special_BallThrow`)
|
||||
- **EXP sequencer** [`exp_seq.lua`](exp_seq.lua) + [`experience.lua`](experience.lua): faint → award → gained text → bar fill → level-up → [`learn_move.lua`](learn_move.lua) (ROM learnset)
|
||||
- **Evo sequencer** [`evo_seq.lua`](evo_seq.lua) + [`../evolution.lua`](../evolution.lua): after win, `EVO_LEVEL` from ROM evolutions.lua → learn moves at level for new species
|
||||
- **VM** [`anim_vm.lua`](anim_vm.lua): portable IR opcodes; **`isReversed`** on enemy attackers; palette shader draw
|
||||
- **Pools** [`anim_sprites.lua`](anim_sprites.lua) / [`anim_tasks.lua`](anim_tasks.lua): fixed 128 sprites + 48 tasks (no GC churn)
|
||||
- **Extract** → `pokemon/battle_anims/pack.lua` (named template/task IDs, not live ROM pointers)
|
||||
- **Palettes** [`anim_pal.lua`](anim_pal.lua): every ANIM_TAG sheet also ships as a 4bpp index PNG + its gBattleAnimPaletteTable colours (`tags[T].pal`, `tagPals`); per-tag unfaded/faded palettes live in `AnimPal`, and rotations / LoadPalette / greyscale / allocated palettes write real indices; sprites and anim BGs draw through a palette-lookup shader (RGBA PNGs remain for other consumers)
|
||||
- **Anim BGs** `pack.animBgs`: the 27 gBattleAnimBackgroundTable entries plus task-loaded BG1/BG2 layers (ATTRACT, SCARY_FACE_*, MORNING_SUN, METAL_SHINE, CURE_BUBBLES, CURSE, FOG, SANDSTORM, SURF_*; `bgPals.MUDDY_WATER`), drawn with exact BLDALPHA eva/evb; mon-window masks go through `present.statMask`
|
||||
- Unknown `AnimTask_*` stub to 1-frame finish so scripts reach `end`
|
||||
- Headless / `runToEnd`: VM + tweens instant-complete
|
||||
|
||||
## Dispatch
|
||||
|
||||
1. `Moves.get(id)` merges ROM row (power/type/accuracy/pp/**effect**/…).
|
||||
2. Status/setup: `EffectIds.STATUS_SETUP[effect]` → registry handler.
|
||||
3. Stat ups/downs: `STATUS_SETUP` → `EXP_STAT_FROM_EFFECT` + `STAT_CHANGES[effect]`.
|
||||
4. Damaging secondaries: `AFTER_HIT[effect]`.
|
||||
|
||||
Gen4+ MODERN leftovers (Stealth Rock, Toxic Spikes, Trick Room, Aqua Ring, Tailwind) are **not** registered.
|
||||
|
||||
## Pret 1:1 (later pass)
|
||||
|
||||
- Exact damage formula / crit / accuracy / damage variance from pret
|
||||
- Remaining move effects (two-turn, OHKO, multi-hit polish, Metronome/Mirror Move/Sleep Talk, …)
|
||||
- Fill visual-task long tail; full ANIM_TAG bank from ROM; BG affine / monbg fidelity
|
||||
- Trainer AI scripts
|
||||
- Double battles out of scope until singles match
|
||||
|
||||
## Wire-in
|
||||
|
||||
`BattleBridge` → `Battle.start` (session party only). Scripts wait on `nativePoll`.
|
||||
@@ -168,29 +168,36 @@ function Abilities.switchIn(ad, b)
|
||||
end
|
||||
|
||||
-- pokefirered/data/battle_scripts_1.s:3983
|
||||
local function intimidate_one(ad, b, foe)
|
||||
if not (foe and not ad:isFainted(foe) and (foe.substituteHP or 0) <= 0) then return end
|
||||
local fab = ad:abilityOf(foe)
|
||||
if fab == "CLEAR_BODY" or fab == "HYPER_CUTTER" or fab == "WHITE_SMOKE" then
|
||||
ad:say(name(ad, foe) .. "'s " .. Abilities.name(fab) .. "\nprevented " .. name(ad, b)
|
||||
.. "'s\nINTIMIDATE from working!")
|
||||
else
|
||||
local side = ad:ownSide(foe)
|
||||
if side and (side.expMistTurns or 0) > 0 then
|
||||
if not foe._statLoweredMsg then
|
||||
foe._statLoweredMsg = true
|
||||
ad:say(name(ad, foe) .. " is protected\nby MIST!")
|
||||
end
|
||||
elseif (foe.stages.attack or 0) > -6 then
|
||||
foe.stages.attack = foe.stages.attack - 1
|
||||
ad:playAnim("general", "STATS_CHANGE", foe, foe, Secondary.statAnimArg("attack", -1))
|
||||
ad:say(name(ad, b) .. "'s INTIMIDATE\ncuts " .. name(ad, foe) .. "'s ATTACK!")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Abilities.runIntimidate(ad)
|
||||
for _, b in ipairs(ad:activeBattlers()) do
|
||||
if b.expIntimidatePending and ad:abilityOf(b) == "INTIMIDATE" then
|
||||
b.expIntimidatePending = nil
|
||||
local foe = ad:foeOf(b)
|
||||
if foe and not ad:isFainted(foe) and (foe.substituteHP or 0) <= 0 then
|
||||
local fab = ad:abilityOf(foe)
|
||||
if fab == "CLEAR_BODY" or fab == "HYPER_CUTTER" or fab == "WHITE_SMOKE" then
|
||||
ad:say(name(ad, foe) .. "'s " .. Abilities.name(fab) .. "\nprevented " .. name(ad, b)
|
||||
.. "'s\nINTIMIDATE from working!")
|
||||
else
|
||||
local side = ad:ownSide(foe)
|
||||
if side and (side.expMistTurns or 0) > 0 then
|
||||
if not foe._statLoweredMsg then
|
||||
foe._statLoweredMsg = true
|
||||
ad:say(name(ad, foe) .. " is protected\nby MIST!")
|
||||
end
|
||||
elseif (foe.stages.attack or 0) > -6 then
|
||||
foe.stages.attack = foe.stages.attack - 1
|
||||
ad:playAnim("general", "STATS_CHANGE", foe, foe, Secondary.statAnimArg("attack", -1))
|
||||
ad:say(name(ad, b) .. "'s INTIMIDATE\ncuts " .. name(ad, foe) .. "'s ATTACK!")
|
||||
end
|
||||
end
|
||||
if ad._st and ad._st.double then
|
||||
-- pokefirered/src/battle_script_commands.c:9174
|
||||
for _, foe in ipairs(ad:foesOf(b)) do intimidate_one(ad, b, foe) end
|
||||
else
|
||||
intimidate_one(ad, b, ad:foeOf(b))
|
||||
end
|
||||
return true
|
||||
end
|
||||
@@ -203,6 +210,24 @@ function Abilities.runTrace(ad)
|
||||
for _, b in ipairs(ad:activeBattlers()) do
|
||||
if b.expTracePending and ad:abilityOf(b) == "TRACE" then
|
||||
local foe = ad:foeOf(b)
|
||||
local st = ad._st
|
||||
if st and st.double then
|
||||
-- pokefirered/src/battle_util.c:2243
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local side = (b.id % 2 == 0) and 1 or 0
|
||||
local t1, t2 = State.battler(st, side), State.battler(st, side + 2)
|
||||
local ok1 = t1 and ad:abilityOf(t1) and ad:hp(t1) > 0
|
||||
local ok2 = t2 and ad:abilityOf(t2) and ad:hp(t2) > 0
|
||||
if ok1 and ok2 then
|
||||
foe = State.battler(st, ad:roll(0, 1) * 2 + side)
|
||||
elseif ok1 then
|
||||
foe = t1
|
||||
elseif ok2 then
|
||||
foe = t2
|
||||
else
|
||||
foe = nil
|
||||
end
|
||||
end
|
||||
local fab = foe and ad:abilityOf(foe)
|
||||
if fab and ad:hp(foe) > 0 then
|
||||
b.expTracePending = nil
|
||||
@@ -467,6 +492,24 @@ end
|
||||
|
||||
-- pokefirered/src/battle_main.c:3002
|
||||
function Abilities.escapeBlocker(ad, b)
|
||||
if ad._st and ad._st.double then
|
||||
for _, foe in ipairs(ad:foesOf(b)) do
|
||||
if not ad:isFainted(foe) then
|
||||
local fab = ad:abilityOf(foe)
|
||||
if fab == "SHADOW_TAG" then return foe, fab end
|
||||
if fab == "ARENA_TRAP" and ad:abilityOf(b) ~= "LEVITATE" and not is_type(b, Types.ID.FLYING) then
|
||||
return foe, fab
|
||||
end
|
||||
end
|
||||
end
|
||||
-- pokefirered/src/battle_main.c:3036
|
||||
if is_type(b, Types.ID.STEEL) then
|
||||
for _, o in ipairs(ad:activeBattlers()) do
|
||||
if o ~= b and not ad:isFainted(o) and ad:abilityOf(o) == "MAGNET_PULL" then return o, "MAGNET_PULL" end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local foe = ad:foeOf(b)
|
||||
if not foe or ad:isFainted(foe) then return nil end
|
||||
local fab = ad:abilityOf(foe)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local Rules = require("src.core.game3.battle.rules")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Adapter = {}
|
||||
|
||||
@@ -45,9 +46,16 @@ Adapter.ABILITY_BY_ID = ABILITY_BY_ID
|
||||
local function side_of(battler)
|
||||
if type(battler) == "table" then return battler.side end
|
||||
if type(battler) == "string" then return battler end
|
||||
if type(battler) == "number" then return State.sideOf(battler) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function id_of(battler)
|
||||
if battler == nil then return nil end
|
||||
return State.idOf(battler)
|
||||
end
|
||||
Adapter.idOf = id_of
|
||||
|
||||
function Adapter.new(battleState, sayFn)
|
||||
local a = {
|
||||
_st = battleState,
|
||||
@@ -77,6 +85,8 @@ function Adapter.new(battleState, sayFn)
|
||||
name = name,
|
||||
attacker = side_of(attacker),
|
||||
target = side_of(target),
|
||||
attackerId = id_of(attacker),
|
||||
targetId = id_of(target),
|
||||
arg = arg,
|
||||
})
|
||||
end
|
||||
@@ -175,6 +185,14 @@ function Adapter.new(battleState, sayFn)
|
||||
local Engine = package.loaded["src.core.game3.battle.engine"]
|
||||
if Engine and Engine.cancelMultiTurnMoves then Engine.cancelMultiTurnMoves(battler) end
|
||||
end
|
||||
-- pokefirered/src/battle_script_commands.c:2110
|
||||
if ModRuntime.wants("battle.status_inflicted") then
|
||||
ModRuntime.emit("battle.status_inflicted", {
|
||||
battle = self._st, target = battler, status = status, source = source,
|
||||
side = battler.side, battlerId = id_of(battler),
|
||||
sourceId = type(source) == "table" and id_of(source) or nil,
|
||||
})
|
||||
end
|
||||
return true
|
||||
end
|
||||
function a:clearStatus(battler)
|
||||
@@ -210,6 +228,7 @@ function Adapter.new(battleState, sayFn)
|
||||
return self:pushEvent({
|
||||
kind = kind or "hp",
|
||||
side = battler.side,
|
||||
battler = id_of(battler),
|
||||
from = from,
|
||||
to = to,
|
||||
maxHp = self:maxHp(battler),
|
||||
@@ -246,6 +265,14 @@ function Adapter.new(battleState, sayFn)
|
||||
if battler then
|
||||
battler.fainted = true
|
||||
if battler.mon then battler.mon.hp = 0 end
|
||||
-- pokefirered/src/battle_script_commands.c:2831
|
||||
if ModRuntime.wants("battle.fainted") and battler._modFainted ~= (battler.mon or true) then
|
||||
battler._modFainted = battler.mon or true
|
||||
ModRuntime.emit("battle.fainted", {
|
||||
battle = self._st, battler = battler, side = self:ownSide(battler),
|
||||
sideName = battler.side, battlerId = id_of(battler),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
function a:displayName(battler) return State.displayName(battler) end
|
||||
@@ -264,17 +291,33 @@ function Adapter.new(battleState, sayFn)
|
||||
if ok and type(v) == "number" then return v end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
function a:activeBattlers()
|
||||
function a:battlers() return State.present(self._st) end
|
||||
function a:activeBattlers() return State.present(self._st) end
|
||||
function a:aliveBattlers()
|
||||
local out = {}
|
||||
if self._st.player then out[#out + 1] = self._st.player end
|
||||
if self._st.enemy then out[#out + 1] = self._st.enemy end
|
||||
for _, b in ipairs(State.present(self._st)) do
|
||||
if not State.isFainted(b) then out[#out + 1] = b end
|
||||
end
|
||||
return out
|
||||
end
|
||||
function a:battler(id) return State.battler(self._st, id) end
|
||||
function a:isDouble() return self._st.double == true end
|
||||
function a:foeOf(battler)
|
||||
if not battler then return nil end
|
||||
if battler.side == "player" then return self._st.enemy end
|
||||
return self._st.player
|
||||
local st = self._st
|
||||
if not st.double then
|
||||
if battler.side == "player" then return st.enemy end
|
||||
return st.player
|
||||
end
|
||||
local opp = State.OPPOSITE(State.idOf(battler))
|
||||
if State.isPresent(st, opp) then return State.battler(st, opp) end
|
||||
local alt = State.PARTNER(opp)
|
||||
if State.isPresent(st, alt) then return State.battler(st, alt) end
|
||||
return State.battler(st, opp)
|
||||
end
|
||||
function a:foesOf(battler) return State.foes(self._st, battler) end
|
||||
function a:alliesOf(battler) return State.allies(self._st, battler) end
|
||||
function a:partnerOf(battler) return State.partner(self._st, battler) end
|
||||
function a:ownSide(battler)
|
||||
if not battler then return nil end
|
||||
if battler.side == "player" then return self._st.playerSide end
|
||||
|
||||
+259
-114
@@ -2,6 +2,8 @@
|
||||
|
||||
local AiVm = require("src.core.game3.battle.ai_vm")
|
||||
|
||||
local choose_move_core
|
||||
|
||||
local Ai = {}
|
||||
|
||||
Ai._pack = nil
|
||||
@@ -146,36 +148,6 @@ local function roll(rng, lo, hi)
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
local function first_usable(mon)
|
||||
if not mon or not mon.moves then return nil end
|
||||
for i = 1, 4 do
|
||||
local mv = mon.moves[i]
|
||||
local p = mon.pp and mon.pp[i]
|
||||
if mv and mv ~= 0 and mv ~= "" and (p == nil or tonumber(p) > 0) then
|
||||
return { kind = "move", move = mv, slot = i, user = "enemy" }
|
||||
end
|
||||
end
|
||||
return { kind = "move", move = "STRUGGLE", slot = nil, user = "enemy" }
|
||||
end
|
||||
|
||||
local function random_usable(mon, rng)
|
||||
local usable = {}
|
||||
if mon and mon.moves then
|
||||
for i = 1, 4 do
|
||||
local mv = mon.moves[i]
|
||||
local p = mon.pp and mon.pp[i]
|
||||
if mv and mv ~= 0 and mv ~= "" and (p == nil or tonumber(p) > 0) then
|
||||
usable[#usable + 1] = { move = mv, slot = i }
|
||||
end
|
||||
end
|
||||
end
|
||||
if #usable == 0 then
|
||||
return { kind = "move", move = "STRUGGLE", slot = nil, user = "enemy" }
|
||||
end
|
||||
local pick = usable[roll(rng, 1, #usable)]
|
||||
return { kind = "move", move = pick.move, slot = pick.slot, user = "enemy" }
|
||||
end
|
||||
|
||||
local function bit_and_flags(a, b)
|
||||
a = math.floor(a or 0)
|
||||
b = math.floor(b or 0)
|
||||
@@ -187,59 +159,12 @@ local function bit_and_flags(a, b)
|
||||
return r
|
||||
end
|
||||
|
||||
--- Choose enemy move via pret AI scripts.
|
||||
-- @return { kind="move", move=..., slot=i, user="enemy", scores=... }
|
||||
function Ai.chooseMove(st, opts)
|
||||
opts = opts or {}
|
||||
local mon = st and st.enemy and st.enemy.mon
|
||||
local rng = rng_fn(st, opts)
|
||||
|
||||
local aiFlags = opts.aiFlags
|
||||
if aiFlags == nil then
|
||||
if st and st.aiFlags ~= nil then
|
||||
aiFlags = st.aiFlags
|
||||
elseif st and st.wild then
|
||||
aiFlags = 0
|
||||
else
|
||||
aiFlags = st and st.aiFlags or 0
|
||||
end
|
||||
end
|
||||
aiFlags = tonumber(aiFlags) or 0
|
||||
|
||||
if aiFlags == 0 then
|
||||
-- Wild / no scripts: match prior fallback (first usable). Random also acceptable.
|
||||
local act = first_usable(mon)
|
||||
act.scores = { 0, 0, 0, 0 }
|
||||
return act
|
||||
end
|
||||
|
||||
local pack = opts.pack or Ai.loadPack()
|
||||
if not pack or not pack.table or not pack.scripts then
|
||||
return first_usable(mon)
|
||||
end
|
||||
|
||||
local scores = { 100, 100, 100, 100 }
|
||||
local simulatedRNG = {}
|
||||
for i = 1, 4 do
|
||||
local mv = mon and mon.moves and mon.moves[i]
|
||||
local pp = mon and mon.pp and mon.pp[i]
|
||||
if not mv or mv == 0 or mv == "" or (pp ~= nil and tonumber(pp) <= 0) then
|
||||
scores[i] = 0
|
||||
end
|
||||
simulatedRNG[i] = 100 - (roll(rng, 0, 15))
|
||||
end
|
||||
-- pokefirered/src/battle_ai_script_commands.c:302
|
||||
local Engine = package.loaded["src.core.game3.battle.engine"]
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
if Engine and Engine.moveLimitations and st and st.enemy then
|
||||
local okL, bad = pcall(Engine.moveLimitations, st.enemy, opts.adapter or (Battle and Battle._adapter))
|
||||
if okL and type(bad) == "table" then
|
||||
for i = 1, 4 do
|
||||
if bad[i] then scores[i] = 0 end
|
||||
end
|
||||
end
|
||||
end
|
||||
local function bit_or_flags(a, b)
|
||||
return a + b - bit_and_flags(a, b)
|
||||
end
|
||||
|
||||
local function run_scripts(pack, aiFlags, st, user, target, userSide, targetSide, scores, simulatedRNG, rng)
|
||||
local aiAction = 0
|
||||
local logicId = 0
|
||||
local flags = aiFlags
|
||||
while flags ~= 0 do
|
||||
@@ -247,25 +172,21 @@ function Ai.chooseMove(st, opts)
|
||||
local scriptName = pack.table[logicId + 1] -- Lua 1-based; pret index 0
|
||||
if scriptName and pack.scripts[scriptName] then
|
||||
for movesetIndex = 1, 4 do
|
||||
if scores[movesetIndex] ~= 0 or true then
|
||||
-- Still run; empty/no-PP moves get score 0 inside VM
|
||||
local vm = AiVm.new({
|
||||
pack = pack,
|
||||
st = st,
|
||||
user = st.enemy,
|
||||
target = st.player,
|
||||
userSide = st.enemySide,
|
||||
targetSide = st.playerSide,
|
||||
scores = scores,
|
||||
simulatedRNG = simulatedRNG,
|
||||
movesetIndex = movesetIndex,
|
||||
rng = rng,
|
||||
})
|
||||
AiVm.run(vm, scriptName)
|
||||
if vm.aiAction and vm.aiAction ~= 0 then
|
||||
-- flee/watch: ignore for MVP move choice
|
||||
end
|
||||
end
|
||||
local vm = AiVm.new({
|
||||
pack = pack,
|
||||
st = st,
|
||||
user = user,
|
||||
target = target,
|
||||
userSide = userSide,
|
||||
targetSide = targetSide,
|
||||
scores = scores,
|
||||
simulatedRNG = simulatedRNG,
|
||||
movesetIndex = movesetIndex,
|
||||
rng = rng,
|
||||
})
|
||||
AiVm.run(vm, scriptName)
|
||||
aiAction = bit_or_flags(aiAction, vm.aiAction or 0)
|
||||
if bit_and_flags(aiAction, 0x8) ~= 0 then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -273,33 +194,257 @@ function Ai.chooseMove(st, opts)
|
||||
logicId = logicId + 1
|
||||
if logicId > 31 then break end
|
||||
end
|
||||
return aiAction
|
||||
end
|
||||
|
||||
-- Pick max score; ties → Random() % numBest
|
||||
local MOVE_TARGET_BOTH = 0x08
|
||||
local MOVE_TARGET_SELF = 0x12
|
||||
|
||||
local function move_num(mv)
|
||||
local n = tonumber(mv)
|
||||
if n then return n end
|
||||
if mv == nil or mv == "" then return 0 end
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
return Moves.numForName and Moves.numForName(mv) or 0
|
||||
end
|
||||
|
||||
local function move_target_byte(mv)
|
||||
if move_num(mv) == 0 then return 0 end
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
local m = Moves.get(mv)
|
||||
return tonumber(m and m.target) or 0
|
||||
end
|
||||
|
||||
local function random_u16(rng)
|
||||
return roll(rng, 0, 65535)
|
||||
end
|
||||
|
||||
local ad_cache = setmetatable({}, { __mode = "k" })
|
||||
local function adapter_for(st, opts)
|
||||
if opts and opts.adapter then return opts.adapter end
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local ad = Battle and Battle._adapter
|
||||
if ad and ad._st == st then return ad end
|
||||
ad = ad_cache[st]
|
||||
if not ad then
|
||||
ad = require("src.core.game3.battle.adapter").new(st, function() end)
|
||||
ad_cache[st] = ad
|
||||
end
|
||||
return ad
|
||||
end
|
||||
|
||||
-- src/battle_ai_script_commands.c:370
|
||||
local function pret_pick(scores, rng)
|
||||
local best = scores[1] or 0
|
||||
local considered = { 1 }
|
||||
for i = 2, 4 do
|
||||
local s = scores[i] or 0
|
||||
if s > best then
|
||||
if best < s then
|
||||
best = s
|
||||
considered = { i }
|
||||
elseif s == best then
|
||||
considered[#considered + 1] = i
|
||||
end
|
||||
if best == s then considered[#considered + 1] = i end
|
||||
end
|
||||
return considered[roll(rng, 1, #considered)], best
|
||||
end
|
||||
|
||||
local function double_first_usable(mon, id, bad)
|
||||
for i = 1, 4 do
|
||||
local mv = mon and mon.moves and mon.moves[i]
|
||||
if move_num(mv) ~= 0 and not (bad and bad[i]) then
|
||||
return { kind = "move", move = mv, slot = i, user = "enemy", battler = id }
|
||||
end
|
||||
end
|
||||
local pickSlot = considered[roll(rng, 1, #considered)]
|
||||
local mv = mon and mon.moves and mon.moves[pickSlot]
|
||||
if not mv or mv == 0 or mv == "" or best <= 0 then
|
||||
local fallback = first_usable(mon)
|
||||
fallback.scores = scores
|
||||
return fallback
|
||||
return { kind = "move", move = "STRUGGLE", slot = nil, user = "enemy", battler = id }
|
||||
end
|
||||
|
||||
local AI_SCRIPT_ROAMING = 0x20000000
|
||||
local AI_SCRIPT_SAFARI = 0x40000000
|
||||
|
||||
local function uses_ai(st)
|
||||
return not st.wild or st.roamer or st.safari or st.firstBattle
|
||||
end
|
||||
|
||||
-- src/battle_controller_opponent.c:1350
|
||||
function choose_move_core(st, id, opts)
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local Engine = require("src.core.game3.battle.engine")
|
||||
local b = State.battler(st, id)
|
||||
local mon = b and b.mon
|
||||
if not mon then return nil end
|
||||
local rng = rng_fn(st, opts)
|
||||
local ad = adapter_for(st, opts)
|
||||
local double = st.double and true or false
|
||||
local function shape(act)
|
||||
if not double then
|
||||
act.target = nil
|
||||
if opts.battler == nil then act.battler = nil end
|
||||
end
|
||||
return act
|
||||
end
|
||||
return {
|
||||
|
||||
local bad = {}
|
||||
local okL, lim = pcall(Engine.moveLimitations, b, ad)
|
||||
if okL and type(lim) == "table" then bad = lim end
|
||||
-- src/battle_main.c:3147
|
||||
if bad[1] and bad[2] and bad[3] and bad[4] then
|
||||
return shape({ kind = "move", move = "STRUGGLE", slot = nil, user = "enemy", battler = id })
|
||||
end
|
||||
|
||||
if not uses_ai(st) then
|
||||
-- src/battle_controller_opponent.c:1389
|
||||
local slot, mv
|
||||
for _ = 1, 1000 do
|
||||
slot = roll(rng, 0, 3) + 1
|
||||
mv = mon.moves and mon.moves[slot]
|
||||
if move_num(mv) ~= 0 then break end
|
||||
end
|
||||
if move_num(mv) == 0 then return shape(double_first_usable(mon, id, bad)) end
|
||||
local tid
|
||||
if bit_and_flags(move_target_byte(mv), MOVE_TARGET_SELF) ~= 0 then
|
||||
tid = id
|
||||
elseif double then
|
||||
tid = bit_and_flags(random_u16(rng), 2)
|
||||
else
|
||||
tid = State.OPPOSITE(id)
|
||||
end
|
||||
return shape({ kind = "move", move = mv, slot = slot, user = "enemy", battler = id, target = tid,
|
||||
scores = { 0, 0, 0, 0 } })
|
||||
end
|
||||
|
||||
-- src/battle_ai_script_commands.c:331
|
||||
local aiFlags
|
||||
if st.safari then
|
||||
aiFlags = AI_SCRIPT_SAFARI
|
||||
elseif st.roamer then
|
||||
aiFlags = AI_SCRIPT_ROAMING
|
||||
else
|
||||
aiFlags = tonumber(opts.aiFlags or st.aiFlags) or 0
|
||||
end
|
||||
local pack = opts.pack
|
||||
if aiFlags ~= 0 and not pack then pack = Ai.loadPack() end
|
||||
|
||||
-- src/battle_ai_script_commands.c:301
|
||||
local scores = { 100, 100, 100, 100 }
|
||||
local simulatedRNG = {}
|
||||
for i = 1, 4 do
|
||||
if bad[i] then scores[i] = 0 end
|
||||
simulatedRNG[i] = 100 - roll(rng, 0, 15)
|
||||
end
|
||||
-- src/battle_ai_script_commands.c:317
|
||||
local tid
|
||||
if double then
|
||||
tid = bit_and_flags(random_u16(rng), 2)
|
||||
if State.isAbsent(st, tid) then tid = 2 - tid end
|
||||
else
|
||||
tid = State.OPPOSITE(id)
|
||||
end
|
||||
local target = State.battler(st, tid)
|
||||
local userSide = (b.side == "player") and st.playerSide or st.enemySide
|
||||
local targetSide = (b.side == "player") and st.enemySide or st.playerSide
|
||||
|
||||
local aiAction = 0
|
||||
if aiFlags ~= 0 and pack and pack.table and pack.scripts and target then
|
||||
aiAction = run_scripts(pack, aiFlags, st, b, target, userSide, targetSide, scores, simulatedRNG, rng)
|
||||
end
|
||||
-- src/battle_ai_script_commands.c:383
|
||||
if bit_and_flags(aiAction, 0x2) ~= 0 then
|
||||
return shape({ kind = "run", user = "enemy", battler = id, scores = scores })
|
||||
end
|
||||
if bit_and_flags(aiAction, 0x4) ~= 0 then
|
||||
return shape({ kind = "watch", user = "enemy", battler = id, scores = scores })
|
||||
end
|
||||
|
||||
local slot = pret_pick(scores, rng)
|
||||
local mv = mon.moves and mon.moves[slot]
|
||||
if move_num(mv) == 0 then
|
||||
local fb = double_first_usable(mon, id, bad)
|
||||
fb.scores = scores
|
||||
return shape(fb)
|
||||
end
|
||||
-- src/battle_controller_opponent.c:1370
|
||||
local tt = move_target_byte(mv)
|
||||
if bit_and_flags(tt, MOVE_TARGET_SELF) ~= 0 then tid = id end
|
||||
if bit_and_flags(tt, MOVE_TARGET_BOTH) ~= 0 then
|
||||
tid = double and 0 or State.OPPOSITE(id)
|
||||
if double and State.isAbsent(st, tid) then tid = 2 end
|
||||
end
|
||||
return shape({
|
||||
kind = "move",
|
||||
move = mv,
|
||||
slot = pickSlot,
|
||||
slot = slot,
|
||||
user = "enemy",
|
||||
battler = id,
|
||||
target = tid,
|
||||
scores = scores,
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
--- Choose enemy move via pret AI scripts.
|
||||
-- @return { kind="move", move=..., slot=i, user="enemy", scores=... }
|
||||
function Ai.chooseMove(st, opts)
|
||||
opts = opts or {}
|
||||
if not st then return nil end
|
||||
local id = opts.battler or 1
|
||||
return choose_move_core(st, id, opts)
|
||||
end
|
||||
|
||||
-- src/battle_main.c:3125
|
||||
function Ai.chooseAction(st, id, opts)
|
||||
opts = opts or {}
|
||||
id = id or 1
|
||||
if not st then return nil end
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local b = State.battler(st, id)
|
||||
if not b or not b.mon then return nil end
|
||||
if b.expLockedMove or b.expMustRecharge then
|
||||
local act = { kind = "move", move = b.expLockedMove, slot = b.expLockedSlot, user = "enemy", battler = id }
|
||||
if not act.move then act = double_first_usable(b.mon, id) end
|
||||
return act
|
||||
end
|
||||
local rng = rng_fn(st, opts)
|
||||
local ad = adapter_for(st, opts)
|
||||
-- src/battle_ai_switch_items.c:358
|
||||
if not st.wild and not st.pokedude then
|
||||
local AiSwitch = require("src.core.game3.battle.ai_switch")
|
||||
local pick = AiSwitch.trySwitch(st, ad, id, rng)
|
||||
if pick then
|
||||
st.monToSwitchInto = st.monToSwitchInto or {}
|
||||
st.monToSwitchInto[id] = pick
|
||||
return { kind = "switch", slot = pick, user = "enemy", battler = id }
|
||||
end
|
||||
local AiItems = require("src.core.game3.battle.ai_items")
|
||||
local use = AiItems.shouldUseItem(st, id)
|
||||
if use then
|
||||
return {
|
||||
kind = "item",
|
||||
item = use.item,
|
||||
aiItemType = use.aiItemType,
|
||||
aiItemFlags = use.aiItemFlags,
|
||||
user = "enemy",
|
||||
battler = id,
|
||||
target = id,
|
||||
}
|
||||
end
|
||||
end
|
||||
return choose_move_core(st, id, {
|
||||
battler = id,
|
||||
rng = rng,
|
||||
adapter = ad,
|
||||
pack = opts.pack,
|
||||
aiFlags = opts.aiFlags,
|
||||
})
|
||||
end
|
||||
|
||||
-- src/battle_controllers.c:59
|
||||
function Ai.battleStart(st, opts)
|
||||
opts = opts or {}
|
||||
if not st then return end
|
||||
st._aiHistory = nil
|
||||
require("src.core.game3.battle.ai_items").history(st)
|
||||
local rng = rng_fn(st, opts)
|
||||
for _ = 1, 4 do roll(rng, 0, 15) end
|
||||
if st.double then random_u16(rng) end
|
||||
end
|
||||
|
||||
return Ai
|
||||
|
||||
@@ -272,12 +272,25 @@ local function pret_stat_level(battler, statId)
|
||||
return s + 6 -- pret 0..12 with 6 neutral
|
||||
end
|
||||
|
||||
-- src/pokemon.c:2552
|
||||
local function spread_hit(vm, moveId)
|
||||
local st = vm.st
|
||||
if not (st and st.double) then return nil end
|
||||
local m = Moves.get(moveId)
|
||||
if bit_and_local(tonumber(m and m.target) or 0, 0x08) == 0 then return nil end
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local id = State.idOf(vm.target)
|
||||
if id == nil then return nil end
|
||||
return State.countPresentOnSide(st, State.sideOf(id)) == 2 or nil
|
||||
end
|
||||
|
||||
local function ai_damage(vm, moveId, movesetIndex)
|
||||
local dmg = Damage.calc(vm.user, vm.target, moveId, {
|
||||
forceCrit = false,
|
||||
forceRoll = 100,
|
||||
weather = vm.st and vm.st.weather,
|
||||
rng = function() return 100 end,
|
||||
spread = spread_hit(vm, moveId),
|
||||
})
|
||||
local sim = vm.simulatedRNG and vm.simulatedRNG[movesetIndex] or 100
|
||||
dmg = math.floor(dmg * sim / 100)
|
||||
@@ -605,12 +618,21 @@ function CMD.count_alive_pokemon(vm, op)
|
||||
local b = AiCmds.battler(vm, op.battler)
|
||||
local party = (b == vm.user) and (vm.st and vm.st.foeParty) or (vm.st and vm.st.playerParty)
|
||||
local onField = b and b.partyIndex or 1
|
||||
local onField2 = onField
|
||||
local n = 0
|
||||
if party then
|
||||
for i, mon in ipairs(party) do
|
||||
if i ~= onField and mon and (tonumber(mon.hp) or 0) > 0 and (mon.species or mon.id) then
|
||||
n = n + 1
|
||||
end
|
||||
-- src/battle_ai_script_commands.c:1099
|
||||
if vm.st and vm.st.double then
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local id = State.idOf(b)
|
||||
local partner = id and State.battler(vm.st, State.PARTNER(id))
|
||||
onField2 = partner and partner.partyIndex or onField
|
||||
end
|
||||
for i = 1, 6 do
|
||||
local mon = party and party[i]
|
||||
local sp = mon and (mon.species or mon.id)
|
||||
if i ~= onField and i ~= onField2 and (tonumber(mon and mon.hp) or 0) ~= 0
|
||||
and sp and sp ~= 0 and not mon.isEgg then
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
vm.funcResult = n
|
||||
@@ -854,8 +876,9 @@ function CMD.get_stockpile_count(vm, op)
|
||||
next_ip(vm)
|
||||
end
|
||||
|
||||
-- src/battle_ai_script_commands.c:1806
|
||||
function CMD.is_double_battle(vm, op)
|
||||
vm.funcResult = 0
|
||||
vm.funcResult = (vm.st and vm.st.double) and 1 or 0
|
||||
next_ip(vm)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
-- FireRed battle AI trainer item use (port of battle_ai_switch_items.c ShouldUseItem).
|
||||
|
||||
local State = require("src.core.game3.battle.state")
|
||||
|
||||
local AiItems = {}
|
||||
|
||||
AiItems.TYPE = {
|
||||
FULL_RESTORE = 1, HEAL_HP = 2, CURE_CONDITION = 3, X_STAT = 4, GUARD_SPECS = 5, NOT_RECOGNIZABLE = 6,
|
||||
}
|
||||
local T = AiItems.TYPE
|
||||
|
||||
AiItems.ITEM_FULL_RESTORE = 19
|
||||
AiItems.HEAL_HP_FULL = 0xFF
|
||||
AiItems.HEAL_HP_HALF = 0xFE
|
||||
AiItems.HEAL_HP_LVL_UP = 0xFD
|
||||
|
||||
-- src/data/pokemon/item_effects.h:338
|
||||
local EFFECTS = {
|
||||
[13] = { 0, 0, 0, 0, 0x04, 20 },
|
||||
[14] = { 0, 0, 0, 0x10, 0 },
|
||||
[15] = { 0, 0, 0, 0x08, 0 },
|
||||
[16] = { 0, 0, 0, 0x04, 0 },
|
||||
[17] = { 0, 0, 0, 0x20, 0 },
|
||||
[18] = { 0, 0, 0, 0x02, 0 },
|
||||
[19] = { 0, 0, 0, 0x3F, 0x04, 0xFF },
|
||||
[20] = { 0, 0, 0, 0, 0x04, 0xFF },
|
||||
[21] = { 0, 0, 0, 0, 0x04, 200 },
|
||||
[22] = { 0, 0, 0, 0, 0x04, 50 },
|
||||
[23] = { 0, 0, 0, 0x3F, 0 },
|
||||
[24] = { 0, 0, 0, 0, 0x44, 0xFE },
|
||||
[25] = { 0, 0, 0, 0, 0x44, 0xFF },
|
||||
[26] = { 0, 0, 0, 0, 0x04, 50 },
|
||||
[27] = { 0, 0, 0, 0, 0x04, 60 },
|
||||
[28] = { 0, 0, 0, 0, 0x04, 80 },
|
||||
[29] = { 0, 0, 0, 0, 0x04, 100 },
|
||||
[30] = { 0, 0, 0, 0, 0x04, 50 },
|
||||
[31] = { 0, 0, 0, 0, 0x04, 200 },
|
||||
[32] = { 0, 0, 0, 0x3F, 0 },
|
||||
[33] = { 0, 0, 0, 0, 0x44, 0xFF },
|
||||
[34] = { 0, 0, 0, 0, 0x18, 10 },
|
||||
[35] = { 0, 0, 0, 0, 0x18, 0x7F },
|
||||
[36] = { 0, 0, 0, 0, 0x08, 10 },
|
||||
[37] = { 0, 0, 0, 0, 0x08, 0x7F },
|
||||
[38] = { 0, 0, 0, 0x3F, 0 },
|
||||
[39] = { 0, 0, 0, 0x20, 0 },
|
||||
[40] = { 0, 0, 0, 0x01, 0 },
|
||||
[41] = { 0x80, 0, 0, 0, 0 },
|
||||
[44] = { 0, 0, 0, 0, 0x04, 20 },
|
||||
[45] = { 0x40, 0, 0, 0, 0x44, 0xFF },
|
||||
[63] = { 0, 0, 0, 0, 0x01 },
|
||||
[64] = { 0, 0, 0, 0, 0x02 },
|
||||
[65] = { 0, 0, 0, 0, 0 },
|
||||
[66] = { 0, 0, 0, 0, 0 },
|
||||
[67] = { 0, 0, 0, 0, 0 },
|
||||
[68] = { 0, 0, 0, 0x40, 0x44, 0xFD },
|
||||
[69] = { 0, 0, 0, 0, 0x20 },
|
||||
[70] = { 0, 0, 0, 0, 0 },
|
||||
[71] = { 0, 0, 0, 0, 0 },
|
||||
[73] = { 0, 0, 0, 0x80, 0 },
|
||||
[74] = { 0x20, 0, 0, 0, 0 },
|
||||
[75] = { 0x01, 0, 0, 0, 0 },
|
||||
[76] = { 0, 0x10, 0, 0, 0 },
|
||||
[77] = { 0, 0x01, 0, 0, 0 },
|
||||
[78] = { 0, 0, 0x10, 0, 0 },
|
||||
[79] = { 0, 0, 0x01, 0, 0 },
|
||||
[93] = { 0, 0, 0, 0, 0x80 },
|
||||
[94] = { 0, 0, 0, 0, 0x80 },
|
||||
[95] = { 0, 0, 0, 0, 0x80 },
|
||||
[96] = { 0, 0, 0, 0, 0x80 },
|
||||
[97] = { 0, 0, 0, 0, 0x80 },
|
||||
[98] = { 0, 0, 0, 0, 0x80 },
|
||||
[133] = { 0, 0, 0, 0x02, 0 },
|
||||
[134] = { 0, 0, 0, 0x20, 0 },
|
||||
[135] = { 0, 0, 0, 0x10, 0 },
|
||||
[136] = { 0, 0, 0, 0x08, 0 },
|
||||
[137] = { 0, 0, 0, 0x04, 0 },
|
||||
[138] = { 0, 0, 0, 0, 0x18, 10 },
|
||||
[139] = { 0, 0, 0, 0, 0x04, 10 },
|
||||
[140] = { 0, 0, 0, 0x01, 0 },
|
||||
[141] = { 0, 0, 0, 0x3F, 0 },
|
||||
[142] = { 0, 0, 0, 0, 0x04, 30 },
|
||||
}
|
||||
|
||||
local function band(a, b)
|
||||
a, b = math.floor(tonumber(a) or 0), math.floor(tonumber(b) or 0)
|
||||
local r, bit = 0, 1
|
||||
while a > 0 and b > 0 do
|
||||
if a % 2 == 1 and b % 2 == 1 then r = r + bit end
|
||||
a, b, bit = math.floor(a / 2), math.floor(b / 2), bit * 2
|
||||
end
|
||||
return r
|
||||
end
|
||||
AiItems.band = band
|
||||
|
||||
local function item_num(item)
|
||||
local n = tonumber(item)
|
||||
if n then return n end
|
||||
if item == nil or item == "" then return 0 end
|
||||
local ok, ItemsData = pcall(require, "src.core.game3.items_data")
|
||||
if ok and ItemsData.toNumericId then return ItemsData.toNumericId(item) or 0 end
|
||||
return 0
|
||||
end
|
||||
AiItems.itemNum = item_num
|
||||
|
||||
function AiItems.effect(item)
|
||||
local e = EFFECTS[item_num(item)]
|
||||
if not e then return nil end
|
||||
return { e[1], e[2], e[3], e[4], e[5], hp = e[6] }
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:546
|
||||
function AiItems.itemType(item, e)
|
||||
if item_num(item) == AiItems.ITEM_FULL_RESTORE then return T.FULL_RESTORE end
|
||||
if band(e[5], 0x04) ~= 0 then return T.HEAL_HP end
|
||||
if band(e[4], 0x3F) ~= 0 then return T.CURE_CONDITION end
|
||||
if band(e[1], 0x3F) ~= 0 or e[2] ~= 0 or e[3] ~= 0 then return T.X_STAT end
|
||||
if band(e[4], 0x80) ~= 0 then return T.GUARD_SPECS end
|
||||
return T.NOT_RECOGNIZABLE
|
||||
end
|
||||
|
||||
-- src/pokemon.c:4843
|
||||
function AiItems.hpParam(e)
|
||||
if band(e[5], 0x04) == 0 then return 0 end
|
||||
return e.hp or 0
|
||||
end
|
||||
|
||||
-- src/battle_ai_script_commands.c:262
|
||||
function AiItems.history(st)
|
||||
if st._aiHistory then return st._aiHistory end
|
||||
local h = { items = {}, itemsNo = 0 }
|
||||
if st and not st.wild and not st.safari then
|
||||
for i = 1, 4 do
|
||||
local it = item_num(st.trainerItems and st.trainerItems[i])
|
||||
if it ~= 0 then
|
||||
h.itemsNo = h.itemsNo + 1
|
||||
h.items[h.itemsNo] = it
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = h.itemsNo + 1, 4 do h.items[i] = 0 end
|
||||
st._aiHistory = h
|
||||
return h
|
||||
end
|
||||
|
||||
local function status_name(b)
|
||||
local s = b and (b.status or (b.mon and b.mon.status))
|
||||
if not s or s == 0 then return nil end
|
||||
s = tostring(s):upper()
|
||||
if s == "SLEEP" then return "SLP" end
|
||||
if s == "POISON" then return "PSN" end
|
||||
if s == "TOXIC" then return "TOX" end
|
||||
if s == "BURN" then return "BRN" end
|
||||
if s == "FREEZE" then return "FRZ" end
|
||||
if s == "PARALYSIS" then return "PAR" end
|
||||
return s
|
||||
end
|
||||
AiItems.statusName = status_name
|
||||
|
||||
local function mon_valid(mon)
|
||||
if not mon or mon.isEgg then return false end
|
||||
local sp = mon.species or mon.speciesId or mon.id
|
||||
return (tonumber(mon.hp) or 0) ~= 0 and sp ~= nil and sp ~= 0
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:562
|
||||
function AiItems.shouldUseItem(st, id)
|
||||
local b = State.battler(st, id)
|
||||
if not b or not b.mon then return nil end
|
||||
local h = AiItems.history(st)
|
||||
local validMons = 0
|
||||
for i = 1, 6 do
|
||||
if mon_valid(st.foeParty and st.foeParty[i]) then validMons = validMons + 1 end
|
||||
end
|
||||
local hp = tonumber(b.mon.hp) or 0
|
||||
local maxHp = tonumber(b.mon.maxHp) or 0
|
||||
for i = 0, 3 do
|
||||
if not (i > 0 and validMons > (h.itemsNo - i) + 1) then
|
||||
local item = h.items[i + 1] or 0
|
||||
local e = item ~= 0 and AiItems.effect(item) or nil
|
||||
if e then
|
||||
local kind = AiItems.itemType(item, e)
|
||||
st.aiItemType = st.aiItemType or {}
|
||||
st.aiItemType[id] = kind
|
||||
local flags = 0
|
||||
local shouldUse = false
|
||||
if kind == T.FULL_RESTORE then
|
||||
if hp < math.floor(maxHp / 4) and hp ~= 0 then shouldUse = true end
|
||||
elseif kind == T.HEAL_HP then
|
||||
local param = AiItems.hpParam(e)
|
||||
if param ~= 0 and hp ~= 0 then
|
||||
if hp < math.floor(maxHp / 4) or maxHp - hp > param then shouldUse = true end
|
||||
end
|
||||
elseif kind == T.CURE_CONDITION then
|
||||
local s = status_name(b)
|
||||
if band(e[4], 0x20) ~= 0 and s == "SLP" then flags = flags + 0x20; shouldUse = true end
|
||||
if band(e[4], 0x10) ~= 0 and (s == "PSN" or s == "TOX") then flags = flags + 0x10; shouldUse = true end
|
||||
if band(e[4], 0x08) ~= 0 and s == "BRN" then flags = flags + 0x08; shouldUse = true end
|
||||
if band(e[4], 0x04) ~= 0 and s == "FRZ" then flags = flags + 0x04; shouldUse = true end
|
||||
if band(e[4], 0x02) ~= 0 and s == "PAR" then flags = flags + 0x02; shouldUse = true end
|
||||
if band(e[4], 0x01) ~= 0 and (tonumber(b.confusionTurns) or 0) > 0 then flags = flags + 0x01; shouldUse = true end
|
||||
elseif kind == T.X_STAT then
|
||||
if (tonumber(b.isFirstTurn) or 0) ~= 0 then
|
||||
if band(e[1], 0x0F) ~= 0 then flags = flags + 0x01 end
|
||||
if band(e[2], 0xF0) ~= 0 then flags = flags + 0x02 end
|
||||
if band(e[2], 0x0F) ~= 0 then flags = flags + 0x04 end
|
||||
if band(e[3], 0x0F) ~= 0 then flags = flags + 0x08 end
|
||||
if band(e[3], 0xF0) ~= 0 then flags = flags + 0x20 end
|
||||
if band(e[1], 0x30) ~= 0 then flags = flags + 0x80 end
|
||||
shouldUse = true
|
||||
end
|
||||
elseif kind == T.GUARD_SPECS then
|
||||
local side = (b.side == "player") and st.playerSide or st.enemySide
|
||||
if (tonumber(b.isFirstTurn) or 0) ~= 0 and (tonumber(side and side.expMistTurns) or 0) == 0 then
|
||||
shouldUse = true
|
||||
end
|
||||
else
|
||||
return nil
|
||||
end
|
||||
if shouldUse then
|
||||
st.aiItemFlags = st.aiItemFlags or {}
|
||||
st.aiItemFlags[id] = flags
|
||||
h.items[i + 1] = 0
|
||||
return { item = item, aiItemType = kind, aiItemFlags = flags }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return AiItems
|
||||
@@ -0,0 +1,377 @@
|
||||
-- FireRed battle AI switch decision (port of battle_ai_switch_items.c).
|
||||
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
local Types = require("src.core.game3.battle.types")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
|
||||
local AiSwitch = {}
|
||||
|
||||
local AB = {
|
||||
VOLT_ABSORB = 10, WATER_ABSORB = 11, FLASH_FIRE = 18, SHADOW_TAG = 23,
|
||||
WONDER_GUARD = 25, LEVITATE = 26, NATURAL_CURE = 30, MAGNET_PULL = 42, ARENA_TRAP = 71,
|
||||
}
|
||||
local MOVE_STRUGGLE = 165
|
||||
local LAST_HIT_NONE = 0xFF
|
||||
|
||||
local ABILITY_BY_NAME
|
||||
local function ability_id(v)
|
||||
if v == nil then return 0 end
|
||||
local n = tonumber(v)
|
||||
if n then return n end
|
||||
if type(v) ~= "string" or v == "" then return 0 end
|
||||
if not ABILITY_BY_NAME then
|
||||
ABILITY_BY_NAME = {}
|
||||
local ok, Adapter = pcall(require, "src.core.game3.battle.adapter")
|
||||
if ok and Adapter and Adapter.ABILITY_BY_ID then
|
||||
for id, name in pairs(Adapter.ABILITY_BY_ID) do ABILITY_BY_NAME[name] = id end
|
||||
end
|
||||
end
|
||||
return ABILITY_BY_NAME[(v:upper():gsub("%s+", "_"))] or 0
|
||||
end
|
||||
|
||||
local function battler_ability(b)
|
||||
if not b then return 0 end
|
||||
if b.expTracedAbility then return ability_id(b.expTracedAbility) end
|
||||
local a = b.ability
|
||||
if a == nil and b.mon then a = b.mon.ability or b.mon.abilityId end
|
||||
return ability_id(a)
|
||||
end
|
||||
|
||||
local function move_num(mv)
|
||||
local n = tonumber(mv)
|
||||
if n then return n end
|
||||
if mv == nil or mv == "" then return 0 end
|
||||
return Moves.numForName and Moves.numForName(mv) or 0
|
||||
end
|
||||
|
||||
local function move_def(mv)
|
||||
local n = move_num(mv)
|
||||
if n == 0 then return nil end
|
||||
return Moves.get(mv)
|
||||
end
|
||||
|
||||
local function move_power(mv)
|
||||
local m = move_def(mv)
|
||||
return m and tonumber(m.power) or 0
|
||||
end
|
||||
|
||||
local function move_type(mv)
|
||||
local m = move_def(mv)
|
||||
return m and tonumber(m.type) or 0
|
||||
end
|
||||
|
||||
local function species_of(mon)
|
||||
if not mon then return 0 end
|
||||
local s = mon.species or mon.speciesId or mon.id
|
||||
if type(s) == "number" then return s end
|
||||
if type(s) == "string" then
|
||||
return (Pokemon.speciesFromName and Pokemon.speciesFromName(s)) or tonumber(s) or 0
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function mon_usable(mon)
|
||||
return mon ~= nil and (tonumber(mon.hp) or 0) ~= 0 and species_of(mon) ~= 0 and not mon.isEgg
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:112
|
||||
local function party_ability(mon)
|
||||
local explicit = mon and (mon.ability or mon.abilityId)
|
||||
if explicit ~= nil then return ability_id(explicit) end
|
||||
local pair = Pokemon.abilities(species_of(mon))
|
||||
local num = mon and mon.abilityNum
|
||||
if num == nil then
|
||||
num = ((pair[2] or 0) ~= 0) and ((tonumber(mon and mon.personality) or 0) % 2) or 0
|
||||
end
|
||||
if num ~= 0 then return pair[2] or 0 end
|
||||
return pair[1] or 0
|
||||
end
|
||||
|
||||
-- src/battle_script_commands.c:1513
|
||||
local function ai_type_calc(mv, species, ability)
|
||||
local f = { super = false, notVery = false, noEffect = false }
|
||||
if move_num(mv) == MOVE_STRUGGLE or move_num(mv) == 0 then return f end
|
||||
local mt = move_type(mv)
|
||||
local power = move_power(mv)
|
||||
if ability == AB.LEVITATE and mt == Types.ID.GROUND then
|
||||
f.noEffect = true
|
||||
else
|
||||
local ty = Pokemon.types(species)
|
||||
local t1, t2 = ty[1] or 0, ty[2] or 0
|
||||
local function modulate(mult)
|
||||
if mult == 0 then
|
||||
f.noEffect, f.notVery, f.super = true, false, false
|
||||
elseif mult == 5 then
|
||||
if power ~= 0 and not f.noEffect then
|
||||
if f.super then f.super = false else f.notVery = true end
|
||||
end
|
||||
elseif mult == 20 then
|
||||
if power ~= 0 and not f.noEffect then
|
||||
if f.notVery then f.notVery = false else f.super = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
local t = Types.TABLE
|
||||
for i = 1, #t, 3 do
|
||||
local a, d, m = t[i], t[i + 1], t[i + 2]
|
||||
if a ~= -1 and a == mt then
|
||||
if d == t1 then modulate(m) end
|
||||
if d == t2 and t1 ~= t2 then modulate(m) end
|
||||
end
|
||||
end
|
||||
end
|
||||
if ability == AB.WONDER_GUARD and (not f.super or (f.super and f.notVery)) and power ~= 0 then
|
||||
f.noEffect = true
|
||||
end
|
||||
return f
|
||||
end
|
||||
AiSwitch.aiTypeCalc = ai_type_calc
|
||||
|
||||
local function roll(rng, lo, hi)
|
||||
local ok, v = pcall(rng, lo, hi)
|
||||
if ok and type(v) == "number" then return v end
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
local function status_bits(b)
|
||||
local s = b and (b.status or (b.mon and b.mon.status))
|
||||
if type(s) == "number" then return s end
|
||||
s = s and tostring(s):upper() or ""
|
||||
if s == "SLP" or s == "SLEEP" then return 0x7 end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function moves_of(b)
|
||||
return (b and b.mon and b.mon.moves) or {}
|
||||
end
|
||||
|
||||
local function last_landed(b)
|
||||
return move_num(b and b.expLastLandedMove)
|
||||
end
|
||||
|
||||
local function last_hit_by(b)
|
||||
local id = b and b.expLastHitById
|
||||
if id == nil then return LAST_HIT_NONE end
|
||||
return id
|
||||
end
|
||||
|
||||
local function battlers_in(st, id)
|
||||
local in2 = id
|
||||
if st.double then
|
||||
local pid = State.PARTNER(id)
|
||||
if not State.isAbsent(st, pid) and State.battler(st, pid) then in2 = pid end
|
||||
end
|
||||
return id, in2
|
||||
end
|
||||
|
||||
local function excluded(st, i, in1, in2)
|
||||
local b1, b2 = State.battler(st, in1), State.battler(st, in2)
|
||||
local pend = st.monToSwitchInto or {}
|
||||
return (b1 and b1.partyIndex == i) or (b2 and b2.partyIndex == i)
|
||||
or pend[in1] == i or pend[in2] == i
|
||||
end
|
||||
|
||||
local function num_battlers(st)
|
||||
return st.double and 4 or 2
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:17
|
||||
local function if_perish_song(st, b)
|
||||
if b.perishSong and tonumber(b.expPerishTurns) == 0 then return true, nil end
|
||||
return false
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:32
|
||||
local function if_wonder_guard(st, b, id, rng)
|
||||
if st.double then return false end
|
||||
local opp = State.battler(st, 0)
|
||||
if not opp or battler_ability(opp) ~= AB.WONDER_GUARD then return false end
|
||||
for i = 1, 4 do
|
||||
local mv = moves_of(b)[i]
|
||||
if move_num(mv) ~= 0 and ai_type_calc(mv, opp.species, battler_ability(opp)).super then return false end
|
||||
end
|
||||
for i = 1, 6 do
|
||||
local mon = st.foeParty and st.foeParty[i]
|
||||
if mon_usable(mon) and i ~= b.partyIndex then
|
||||
for j = 1, 4 do
|
||||
local mv = mon.moves and mon.moves[j]
|
||||
if move_num(mv) ~= 0 and ai_type_calc(mv, opp.species, battler_ability(opp)).super
|
||||
and roll(rng, 0, 2) < 2 then
|
||||
return true, i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:176
|
||||
local function has_super_effective(st, b, noRng, rng)
|
||||
local function scan(oid)
|
||||
local opp = State.battler(st, oid)
|
||||
if State.isAbsent(st, oid) or not opp then return false end
|
||||
for i = 1, 4 do
|
||||
local mv = moves_of(b)[i]
|
||||
if move_num(mv) ~= 0 and ai_type_calc(mv, opp.species, battler_ability(opp)).super then
|
||||
if noRng or roll(rng, 0, 9) ~= 0 then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
if scan(0) then return true end
|
||||
if not st.double then return false end
|
||||
return scan(2)
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:82
|
||||
local function absorbs_opponents_move(st, b, id, rng)
|
||||
if (has_super_effective(st, b, true, rng) and roll(rng, 0, 2) ~= 0) or last_landed(b) == 0 then
|
||||
return false
|
||||
end
|
||||
local last = b.expLastLandedMove
|
||||
if last_landed(b) == 0xFFFF or move_power(last) == 0 then return false end
|
||||
local mt = move_type(last)
|
||||
local absorb
|
||||
if mt == Types.ID.FIRE then absorb = AB.FLASH_FIRE
|
||||
elseif mt == Types.ID.WATER then absorb = AB.WATER_ABSORB
|
||||
elseif mt == Types.ID.ELECTRIC then absorb = AB.VOLT_ABSORB
|
||||
else return false end
|
||||
if battler_ability(b) == absorb then return false end
|
||||
local in1, in2 = battlers_in(st, id)
|
||||
for i = 1, 6 do
|
||||
local mon = st.foeParty and st.foeParty[i]
|
||||
if mon_usable(mon) and not excluded(st, i, in1, in2) then
|
||||
if absorb == party_ability(mon) and roll(rng, 0, 1) == 1 then return true, i end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:236
|
||||
local function find_mon_with_flags(st, b, id, flag, modulo, rng)
|
||||
local last = last_landed(b)
|
||||
if last == 0 then return false end
|
||||
if last == 0xFFFF or last_hit_by(b) == LAST_HIT_NONE or move_power(b.expLastLandedMove) == 0 then
|
||||
return false
|
||||
end
|
||||
local in1, in2 = battlers_in(st, id)
|
||||
for i = 1, 6 do
|
||||
local mon = st.foeParty and st.foeParty[i]
|
||||
if mon_usable(mon) and not excluded(st, i, in1, in2) then
|
||||
local f = ai_type_calc(b.expLastLandedMove, species_of(mon), party_ability(mon))
|
||||
if f[flag] then
|
||||
local opp = State.battler(st, last_hit_by(b))
|
||||
for j = 1, 4 do
|
||||
local mv = mon.moves and mon.moves[j]
|
||||
if move_num(mv) ~= 0 and opp
|
||||
and ai_type_calc(mv, opp.species, battler_ability(opp)).super
|
||||
and roll(rng, 0, modulo - 1) == 0 then
|
||||
return true, i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:146
|
||||
local function if_natural_cure(st, b, id, rng)
|
||||
local hp = tonumber(b.mon and b.mon.hp) or 0
|
||||
local maxHp = tonumber(b.mon and b.mon.maxHp) or 1
|
||||
if status_bits(b) % 8 == 0 or battler_ability(b) ~= AB.NATURAL_CURE or hp < math.floor(maxHp / 2) then
|
||||
return false
|
||||
end
|
||||
local last = last_landed(b)
|
||||
if (last == 0 or last == 0xFFFF) and roll(rng, 0, 1) == 1 then
|
||||
return true, nil
|
||||
elseif move_power(last) == 0 and roll(rng, 0, 1) == 1 then
|
||||
return true, nil
|
||||
end
|
||||
local ok, pick = find_mon_with_flags(st, b, id, "noEffect", 1, rng)
|
||||
if ok then return true, pick end
|
||||
ok, pick = find_mon_with_flags(st, b, id, "notVery", 1, rng)
|
||||
if ok then return true, pick end
|
||||
if roll(rng, 0, 1) == 1 then return true, nil end
|
||||
return false
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:223
|
||||
local function stats_raised(b)
|
||||
local total = 0
|
||||
for _, v in pairs(b.stages or {}) do
|
||||
v = tonumber(v) or 0
|
||||
if v > 0 then total = total + v end
|
||||
end
|
||||
return total > 3
|
||||
end
|
||||
|
||||
local function is_steel(b)
|
||||
return b.type1 == Types.ID.STEEL or b.type2 == Types.ID.STEEL
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:302
|
||||
function AiSwitch.shouldSwitch(st, id, rng)
|
||||
local b = State.battler(st, id)
|
||||
if not b or not b.mon then return false end
|
||||
if b.expTrapped or b.escapePrevention or (tonumber(b.expTrapTurns) or 0) > 0 or b.expIngrain then
|
||||
return false
|
||||
end
|
||||
for oid = 0, num_battlers(st) - 1 do
|
||||
local o = State.battler(st, oid)
|
||||
if o and State.sideOf(oid) ~= State.sideOf(id) then
|
||||
local a = battler_ability(o)
|
||||
if a == AB.SHADOW_TAG or a == AB.ARENA_TRAP then return false end
|
||||
end
|
||||
end
|
||||
for oid = 0, num_battlers(st) - 1 do
|
||||
local o = State.battler(st, oid)
|
||||
if o and battler_ability(o) == AB.MAGNET_PULL and is_steel(b) then return false end
|
||||
end
|
||||
local in1, in2 = battlers_in(st, id)
|
||||
local available = 0
|
||||
for i = 1, 6 do
|
||||
local mon = st.foeParty and st.foeParty[i]
|
||||
if mon_usable(mon) and not excluded(st, i, in1, in2) then available = available + 1 end
|
||||
end
|
||||
if available == 0 then return false end
|
||||
|
||||
local ok, pick = if_perish_song(st, b)
|
||||
if ok then return true, pick end
|
||||
ok, pick = if_wonder_guard(st, b, id, rng)
|
||||
if ok then return true, pick end
|
||||
ok, pick = absorbs_opponents_move(st, b, id, rng)
|
||||
if ok then return true, pick end
|
||||
ok, pick = if_natural_cure(st, b, id, rng)
|
||||
if ok then return true, pick end
|
||||
if has_super_effective(st, b, false, rng) or stats_raised(b) then return false end
|
||||
ok, pick = find_mon_with_flags(st, b, id, "noEffect", 2, rng)
|
||||
if ok then return true, pick end
|
||||
ok, pick = find_mon_with_flags(st, b, id, "notVery", 3, rng)
|
||||
if ok then return true, pick end
|
||||
return false
|
||||
end
|
||||
|
||||
-- src/battle_ai_switch_items.c:358
|
||||
function AiSwitch.trySwitch(st, ad, id, rng)
|
||||
local ok, pick = AiSwitch.shouldSwitch(st, id, rng)
|
||||
if not ok then return nil end
|
||||
if pick == nil then
|
||||
local Engine = require("src.core.game3.battle.engine")
|
||||
if Engine.mostSuitableMon then pick = Engine.mostSuitableMon(st, ad, id) end
|
||||
if pick == nil then
|
||||
local in1, in2 = 1, st.double and 3 or 1
|
||||
for i = 1, 6 do
|
||||
local mon = st.foeParty and st.foeParty[i]
|
||||
if mon and (tonumber(mon.hp) or 0) ~= 0 and not excluded(st, i, in1, in2) then
|
||||
pick = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return pick
|
||||
end
|
||||
|
||||
return AiSwitch
|
||||
+175
-44
@@ -6,10 +6,12 @@ local AnimVm = require("src.core.game3.battle.anim_vm")
|
||||
local AnimSprites = require("src.core.game3.battle.anim_sprites")
|
||||
local BallOpen = require("src.core.game3.battle.ball_open")
|
||||
local AnimPal = require("src.core.game3.battle.anim_pal")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local Anim = {}
|
||||
|
||||
Anim.Z = AnimVm.Z
|
||||
Anim.Coords = AnimCoords
|
||||
|
||||
-- pret sBattlerCoords (singles) — CreateSprite CENTER
|
||||
Anim.ENEMY_MON = { x = 176, y = 40 }
|
||||
@@ -24,10 +26,7 @@ Anim._expTweening = false
|
||||
Anim._introTweening = 0
|
||||
Anim._seqBusy = false
|
||||
Anim._statusQueue = {}
|
||||
Anim._present = {
|
||||
player = nil,
|
||||
enemy = nil,
|
||||
}
|
||||
Anim._present = AnimCoords.idTable()
|
||||
Anim._stage = nil
|
||||
Anim._screenEffect = {
|
||||
type = "none",
|
||||
@@ -112,9 +111,11 @@ function Anim.endScreenEffect()
|
||||
end
|
||||
end
|
||||
|
||||
local function default_present(side)
|
||||
local function default_present(id)
|
||||
local side = AnimCoords.sideOf(id)
|
||||
local z = (side == "player") and Anim.Z.PLAYER or Anim.Z.ENEMY
|
||||
return {
|
||||
id = id,
|
||||
side = side,
|
||||
ox = 0,
|
||||
oy = 0,
|
||||
@@ -144,10 +145,13 @@ local function default_stage(headless)
|
||||
enemy = { visible = false, ox = 0, oy = 0, picId = nil },
|
||||
},
|
||||
ball = { visible = false, x = 0, y = 0, frame = 0, side = nil },
|
||||
healthbox = {
|
||||
player = { visible = headless and true or false, ox = 0 },
|
||||
enemy = { visible = headless and true or false, ox = 0 },
|
||||
},
|
||||
balls = {},
|
||||
healthbox = AnimCoords.idTable({
|
||||
[0] = { visible = headless and true or false, ox = 0 },
|
||||
[1] = { visible = headless and true or false, ox = 0 },
|
||||
[2] = { visible = headless and true or false, ox = 0 },
|
||||
[3] = { visible = headless and true or false, ox = 0 },
|
||||
}),
|
||||
partyBar = {
|
||||
player = { visible = false, ox = 0, balls = {} },
|
||||
enemy = { visible = false, ox = 0, balls = {} },
|
||||
@@ -165,20 +169,69 @@ function Anim.x(v)
|
||||
return tonumber(v) or 0
|
||||
end
|
||||
|
||||
function Anim.present(side)
|
||||
if side == "attacker_side" and Anim._vm then
|
||||
side = Anim._vm:attackerSide()
|
||||
function Anim.idOf(key)
|
||||
if key == "attacker_side" then
|
||||
local vm = Anim._vm
|
||||
return vm and vm.attackerId and vm:attackerId() or 0
|
||||
end
|
||||
if side ~= "player" and side ~= "enemy" then return nil end
|
||||
if not Anim._present[side] then
|
||||
Anim._present[side] = default_present(side)
|
||||
end
|
||||
return Anim._present[side]
|
||||
return AnimCoords.idOf(key)
|
||||
end
|
||||
|
||||
function Anim.battlerCenter(side)
|
||||
local base = (side == "player") and Anim.PLAYER_MON or Anim.ENEMY_MON
|
||||
local p = Anim.present(side)
|
||||
function Anim.sideOf(id)
|
||||
return AnimCoords.sideOf(id)
|
||||
end
|
||||
|
||||
function Anim.isDouble(st)
|
||||
return AnimCoords.isDouble(st)
|
||||
end
|
||||
|
||||
function Anim.setDouble(v)
|
||||
AnimCoords.setDouble(v)
|
||||
end
|
||||
|
||||
function Anim.present(key)
|
||||
local id = Anim.idOf(key)
|
||||
if id == nil then return nil end
|
||||
local p = rawget(Anim._present, id)
|
||||
if not p then
|
||||
if id >= 2 and not AnimCoords.isDouble() then return nil end
|
||||
p = default_present(id)
|
||||
if id >= 2 and not Anim._headless then p.visible = false end
|
||||
rawset(Anim._present, id, p)
|
||||
end
|
||||
return p
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:105
|
||||
function Anim.coords(st, key)
|
||||
return AnimCoords.coords(st, key)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1908
|
||||
function Anim.subpriority(key)
|
||||
return AnimCoords.subpriority(key)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1934
|
||||
function Anim.bgPriorityRank(key)
|
||||
return AnimCoords.bgPriorityRank(key)
|
||||
end
|
||||
|
||||
function Anim.monDrawOrder(st)
|
||||
return AnimCoords.monDrawOrder(st)
|
||||
end
|
||||
|
||||
function Anim.particleBand(k, st)
|
||||
return AnimCoords.particleBand(k, st)
|
||||
end
|
||||
|
||||
function Anim.battlerIds(st)
|
||||
return AnimCoords.ids(st)
|
||||
end
|
||||
|
||||
function Anim.battlerCenter(key)
|
||||
local base = Anim.coords(nil, key) or Anim.ENEMY_MON
|
||||
local p = Anim.present(key)
|
||||
local cx = base.x + (p and p.ox or 0)
|
||||
local cy = base.y + (p and p.oy or 0)
|
||||
return cx, cy
|
||||
@@ -192,13 +245,15 @@ function Anim.reset(opts)
|
||||
Anim._introTweening = 0
|
||||
Anim._seqBusy = false
|
||||
Anim._statusQueue = {}
|
||||
Anim._present.player = default_present("player")
|
||||
Anim._present.enemy = default_present("enemy")
|
||||
Anim._stage = default_stage(Anim._headless)
|
||||
if not Anim._headless then
|
||||
Anim._present.player.visible = false
|
||||
Anim._present.enemy.visible = false
|
||||
AnimCoords.setDouble(opts.double)
|
||||
AnimCoords.bind(nil)
|
||||
for id = 0, 3 do rawset(Anim._present, id, nil) end
|
||||
for id = 0, AnimCoords.isDouble() and 3 or 1 do
|
||||
local p = default_present(id)
|
||||
if not Anim._headless then p.visible = false end
|
||||
rawset(Anim._present, id, p)
|
||||
end
|
||||
Anim._stage = default_stage(Anim._headless)
|
||||
if not Anim._vm then
|
||||
Anim._vm = AnimVm.new()
|
||||
end
|
||||
@@ -220,11 +275,81 @@ function Anim.reset(opts)
|
||||
end
|
||||
|
||||
-- pokefirered/src/pokeball.c:769
|
||||
function Anim.ballOpen(side, x, y)
|
||||
function Anim.ballOpen(key, x, y)
|
||||
if Anim._headless then return nil end
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local b = Battle and Battle._st and Battle._st[side]
|
||||
return BallOpen.start(side, x, y, b and b.mon and b.mon.pokeball)
|
||||
local id = Anim.idOf(key) or 1
|
||||
local b = AnimCoords.battler(nil, id)
|
||||
return BallOpen.start(id, x, y, b and b.mon and b.mon.pokeball)
|
||||
end
|
||||
|
||||
local function play_se(name, pan)
|
||||
pcall(function()
|
||||
local SE = require("src.core.game3.se_ids")
|
||||
require("src.core.game3.audio").playSe(SE[name], { pan = pan })
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokefirered/src/pokeball.c:349
|
||||
function Anim.sendOutMon(key, opts)
|
||||
opts = opts or {}
|
||||
local id = Anim.idOf(key) or 1
|
||||
local side = AnimCoords.sideOf(id)
|
||||
local p = Anim.present(id)
|
||||
local done = opts.onComplete
|
||||
if Anim._headless or not p then
|
||||
if p then
|
||||
p.visible = true
|
||||
p.ox, p.oy, p.scale = 0, 0, 1
|
||||
end
|
||||
if done then done() end
|
||||
return nil
|
||||
end
|
||||
local base = Anim.coords(nil, id) or Anim.ENEMY_MON
|
||||
local stage = Anim.stage()
|
||||
stage.balls = stage.balls or {}
|
||||
local ball = { visible = true, frame = 0, rot = 0, side = side, battler = id, x = 0, y = 0 }
|
||||
stage.balls[id] = ball
|
||||
local pan = (side == "player") and -64 or 63
|
||||
local function reveal()
|
||||
ball.frame = 1
|
||||
ball.rot = 0
|
||||
play_se("SE_BALL_OPEN", pan)
|
||||
Anim.ballOpen(id, ball.x, ball.y)
|
||||
p.visible = true
|
||||
p.ox = 0
|
||||
p.oy = 16
|
||||
p.scale = 0.16
|
||||
p.darken = 0
|
||||
Anim.tweenStage(12, function(u)
|
||||
p.oy = 16 * (1 - u)
|
||||
p.scale = 0.16 + 0.84 * u
|
||||
ball.frame = (u < 0.5) and 1 or 2
|
||||
end, function()
|
||||
p.oy = 0
|
||||
p.scale = 1
|
||||
ball.visible = false
|
||||
if stage.balls[id] == ball then stage.balls[id] = nil end
|
||||
if done then done() end
|
||||
end)
|
||||
end
|
||||
if side == "player" then
|
||||
-- pokefirered/src/pokeball.c:912
|
||||
local sx, sy = 48, 70
|
||||
local tx, ty = base.x, base.y + 24
|
||||
ball.x, ball.y = sx, sy
|
||||
play_se("SE_BALL_THROW", pan)
|
||||
Anim.tweenStage(25, function(u, t)
|
||||
local f = t and t.frames or (u * 25)
|
||||
ball.x = sx + (tx - sx) * u
|
||||
ball.y = sy + (ty - sy) * u + (-30 * 4 * u * (1 - u))
|
||||
ball.rot = f * ((25 / 256) * math.pi * 2)
|
||||
end, reveal)
|
||||
else
|
||||
-- pokefirered/src/pokeball.c:406
|
||||
ball.x, ball.y = base.x, base.y + 24
|
||||
Anim.tweenStage(16, function() end, reveal)
|
||||
end
|
||||
return ball
|
||||
end
|
||||
|
||||
function Anim.setSeqBusy(v)
|
||||
@@ -261,14 +386,15 @@ end
|
||||
--- Pret faint presentation: SE_FAINT + sink/slide off, then hide mon + healthbox.
|
||||
-- Opponent: SpriteCB_AnimFaintOpponent — +8px every 2 frames, ~8 steps.
|
||||
-- Player: SpriteCB_FaintSlideAnim — +5px/frame until below screen.
|
||||
function Anim.faintMon(side, opts)
|
||||
function Anim.faintMon(key, opts)
|
||||
opts = opts or {}
|
||||
side = side or "enemy"
|
||||
local p = Anim.present(side)
|
||||
local id = Anim.idOf(key or "enemy") or 1
|
||||
local side = AnimCoords.sideOf(id)
|
||||
local p = Anim.present(id)
|
||||
local stage = Anim.stage()
|
||||
local hb = stage and stage.healthbox and stage.healthbox[side]
|
||||
local hb = stage and stage.healthbox and stage.healthbox[id]
|
||||
local AnimSprites = require("src.core.game3.battle.anim_sprites")
|
||||
AnimSprites.clearHost(side)
|
||||
AnimSprites.clearHost(id)
|
||||
|
||||
local function hide_all()
|
||||
if p then
|
||||
@@ -311,7 +437,8 @@ function Anim.faintMon(side, opts)
|
||||
local ok, Display = pcall(require, "src.core.game3.display")
|
||||
if ok and Display and Display.H then screenH = Display.H end
|
||||
end
|
||||
local need = math.max(1, math.ceil((screenH - (Anim.PLAYER_MON.y or 80) + 32) / 5))
|
||||
local base = Anim.coords(nil, id) or Anim.PLAYER_MON
|
||||
local need = math.max(1, math.ceil((screenH - (base.y or 80) + 32) / 5))
|
||||
local frames = math.max(16, need + 2)
|
||||
Anim.tweenStage(frames, function(_, t)
|
||||
p.oy = fromOy + 5 * (t.frames or 1)
|
||||
@@ -348,9 +475,9 @@ end
|
||||
function Anim.syncDisplayFromState(st)
|
||||
if not st then return end
|
||||
local Experience = require("src.core.game3.battle.experience")
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
local b = st[side]
|
||||
local p = Anim.present(side)
|
||||
for _, id in ipairs(AnimCoords.ids(st)) do
|
||||
local b = AnimCoords.battler(st, id)
|
||||
local p = Anim.present(id)
|
||||
if b and b.mon and p then
|
||||
p.displayHp = tonumber(b.mon.hp) or 0
|
||||
p.displayMaxHp = tonumber(b.mon.maxHp) or 1
|
||||
@@ -671,6 +798,7 @@ local function launch_table(kind, name, opts)
|
||||
local o = {}
|
||||
for k, v in pairs(opts) do o[k] = v end
|
||||
if o.targetSide == nil then o.targetSide = o.attackerSide end
|
||||
if o.targetId == nil then o.targetId = o.attackerId end
|
||||
if kind == "status" and o.statusAnim == nil then o.statusAnim = true end
|
||||
if Anim._vm.launchScript then return Anim._vm:launchScript(script, o) end
|
||||
return Anim._vm:launch(script, o)
|
||||
@@ -726,16 +854,19 @@ function Anim.shownBattler(side, battler)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:286
|
||||
function Anim.substituteY(side)
|
||||
if side == "player" then return Anim.PLAYER_MON.y + 17 end
|
||||
return Anim.ENEMY_MON.y + 16
|
||||
function Anim.substituteY(key)
|
||||
local id = Anim.idOf(key) or 1
|
||||
local base = Anim.coords(nil, id) or Anim.ENEMY_MON
|
||||
if AnimCoords.sideOf(id) == "player" then return base.y + 17 end
|
||||
return base.y + 16
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_gfx_sfx_util.c:762
|
||||
function Anim.substituteImage(side)
|
||||
function Anim.substituteImage(key)
|
||||
local pack = load_pack()
|
||||
local tags = pack and pack.tags
|
||||
local info = tags and tags[(side == "player") and "SUBSTITUTE_DOLL_BACK" or "SUBSTITUTE_DOLL_FRONT"]
|
||||
local id = Anim.idOf(key) or 1
|
||||
local info = tags and tags[(AnimCoords.sideOf(id) == "player") and "SUBSTITUTE_DOLL_BACK" or "SUBSTITUTE_DOLL_FRONT"]
|
||||
return info and info.image or nil
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
local bit = require("bit")
|
||||
|
||||
local AnimCoords = {}
|
||||
|
||||
local function xy(x, y) return { x = x, y = y, x, y } end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:31
|
||||
AnimCoords.SINGLES = { [0] = xy(72, 80), xy(176, 40), xy(48, 40), xy(112, 80) }
|
||||
AnimCoords.DOUBLES = { [0] = xy(32, 80), xy(200, 40), xy(90, 88), xy(152, 32) }
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1908
|
||||
AnimCoords.SUBPRIORITY = { [0] = 30, 40, 20, 50 }
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1934
|
||||
AnimCoords.BG_PRIORITY_RANK = { [0] = 2, 1, 1, 2 }
|
||||
|
||||
AnimCoords.DRAW_ORDER_SINGLES = { 1, 0 }
|
||||
AnimCoords.DRAW_ORDER_DOUBLES = { 3, 1, 0, 2 }
|
||||
|
||||
AnimCoords._double = nil
|
||||
AnimCoords._bind = nil
|
||||
|
||||
local function battle_state()
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
return Battle and Battle._st
|
||||
end
|
||||
AnimCoords.battleState = battle_state
|
||||
|
||||
function AnimCoords.setDouble(v)
|
||||
if v == nil then AnimCoords._double = nil else AnimCoords._double = v and true or false end
|
||||
end
|
||||
|
||||
function AnimCoords.isDouble(st)
|
||||
if AnimCoords._double ~= nil then return AnimCoords._double end
|
||||
st = st or battle_state()
|
||||
return type(st) == "table" and st.double == true
|
||||
end
|
||||
|
||||
function AnimCoords.sideOf(id)
|
||||
id = tonumber(id) or 0
|
||||
return (id % 2 == 0) and "player" or "enemy"
|
||||
end
|
||||
|
||||
function AnimCoords.partner(id)
|
||||
return bit.bxor(tonumber(id) or 0, 2)
|
||||
end
|
||||
|
||||
function AnimCoords.fixedId(key)
|
||||
if type(key) == "number" then return key end
|
||||
if key == "enemy" then return 1 end
|
||||
if key == "player" then return 0 end
|
||||
return nil
|
||||
end
|
||||
|
||||
function AnimCoords.idOf(key)
|
||||
local t = type(key)
|
||||
if t == "number" then
|
||||
if key >= 0 and key <= 3 then return math.floor(key) end
|
||||
return nil
|
||||
end
|
||||
if key == "player" then
|
||||
local b = AnimCoords._bind
|
||||
return (b and b.player) or 0
|
||||
end
|
||||
if key == "enemy" then
|
||||
local b = AnimCoords._bind
|
||||
return (b and b.enemy) or 1
|
||||
end
|
||||
if t == "table" then
|
||||
if type(key.id) == "number" then return key.id end
|
||||
if key.side then return AnimCoords.idOf(key.side) end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function AnimCoords.bind(atkId, tgtId)
|
||||
if atkId == nil then
|
||||
AnimCoords._bind = nil
|
||||
return
|
||||
end
|
||||
tgtId = tgtId or atkId
|
||||
local b = {}
|
||||
b[AnimCoords.sideOf(tgtId)] = tgtId
|
||||
b[AnimCoords.sideOf(atkId)] = atkId
|
||||
if b.player == nil then b.player = 0 end
|
||||
if b.enemy == nil then b.enemy = 1 end
|
||||
AnimCoords._bind = b
|
||||
end
|
||||
|
||||
function AnimCoords.withBattler(id, fn, ...)
|
||||
local prev = AnimCoords._bind
|
||||
local b = { player = (prev and prev.player) or 0, enemy = (prev and prev.enemy) or 1 }
|
||||
b[AnimCoords.sideOf(id)] = id
|
||||
AnimCoords._bind = b
|
||||
local r = { pcall(fn, AnimCoords.sideOf(id), ...) }
|
||||
AnimCoords._bind = prev
|
||||
if not r[1] then error(r[2], 0) end
|
||||
return unpack(r, 2)
|
||||
end
|
||||
|
||||
function AnimCoords.sideArg(fn, idx)
|
||||
return function(...)
|
||||
local args = { n = select("#", ...), ... }
|
||||
local v = args[idx]
|
||||
if type(v) ~= "number" or v < 0 or v > 3 then return fn(...) end
|
||||
return AnimCoords.withBattler(v, function(side)
|
||||
args[idx] = side
|
||||
return fn(unpack(args, 1, args.n))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function AnimCoords.coords(st, key)
|
||||
local id = AnimCoords.idOf(key)
|
||||
if id == nil then return nil end
|
||||
local t = AnimCoords.isDouble(st) and AnimCoords.DOUBLES or AnimCoords.SINGLES
|
||||
return t[id]
|
||||
end
|
||||
|
||||
function AnimCoords.subpriority(key)
|
||||
local id = AnimCoords.idOf(key)
|
||||
return AnimCoords.SUBPRIORITY[id or 1]
|
||||
end
|
||||
|
||||
function AnimCoords.bgPriorityRank(key)
|
||||
local id = AnimCoords.idOf(key)
|
||||
return AnimCoords.BG_PRIORITY_RANK[id or 1]
|
||||
end
|
||||
|
||||
function AnimCoords.battler(st, id)
|
||||
st = st or battle_state()
|
||||
if type(st) ~= "table" or id == nil then return nil end
|
||||
local bs = rawget(st, "battlers") or st.battlers
|
||||
local b = bs and bs[id]
|
||||
if b == nil and id == 0 then b = st.player end
|
||||
if b == nil and id == 1 then b = st.enemy end
|
||||
return b
|
||||
end
|
||||
|
||||
function AnimCoords.ids(st)
|
||||
if AnimCoords.isDouble(st) then return { 0, 1, 2, 3 } end
|
||||
return { 0, 1 }
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:841
|
||||
function AnimCoords.spritePresent(st, id)
|
||||
if id == nil then return false end
|
||||
st = st or battle_state()
|
||||
if id >= 2 and not AnimCoords.isDouble(st) then return false end
|
||||
if type(st) ~= "table" then return id < 2 end
|
||||
if st.absent and st.absent[id] then return false end
|
||||
local b = AnimCoords.battler(st, id)
|
||||
if not b then return id < 2 end
|
||||
local mon = b.mon
|
||||
if mon and (tonumber(mon.hp) or 0) <= 0 then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function AnimCoords.monDrawOrder(st)
|
||||
if AnimCoords.isDouble(st) then return AnimCoords.DRAW_ORDER_DOUBLES end
|
||||
return AnimCoords.DRAW_ORDER_SINGLES
|
||||
end
|
||||
|
||||
function AnimCoords.monBehindZ(key, st)
|
||||
local id = AnimCoords.idOf(key) or 1
|
||||
local order = AnimCoords.monDrawOrder(st)
|
||||
for k, v in ipairs(order) do
|
||||
if v == id then return (k - 1) * 100 + 95 end
|
||||
end
|
||||
return 95
|
||||
end
|
||||
|
||||
function AnimCoords.particleBand(k, st)
|
||||
local n = #AnimCoords.monDrawOrder(st)
|
||||
if k <= 0 then return 0, 99 end
|
||||
if k >= n then return k * 100 + 1, 999 end
|
||||
return k * 100 + 1, k * 100 + 99
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim.c:630
|
||||
function AnimCoords.layerZ(sub, monbg, bgPrio, st)
|
||||
sub = tonumber(sub) or 0
|
||||
local order = AnimCoords.monDrawOrder(st)
|
||||
local layer = 0
|
||||
for k = #order, 1, -1 do
|
||||
local id = order[k]
|
||||
local front
|
||||
if monbg and rawget(monbg, id) then
|
||||
front = ((bgPrio and bgPrio[AnimCoords.BG_PRIORITY_RANK[id]]) or 2) >= 2
|
||||
else
|
||||
front = sub < AnimCoords.SUBPRIORITY[id]
|
||||
end
|
||||
if front then
|
||||
layer = k
|
||||
break
|
||||
end
|
||||
end
|
||||
local rank = math.max(0, math.min(98, 98 - sub))
|
||||
if layer == 0 then return rank end
|
||||
return layer * 100 + 1 + rank
|
||||
end
|
||||
|
||||
function AnimCoords.zFor(pri, sub, vm, st)
|
||||
pri = tonumber(pri) or 2
|
||||
sub = tonumber(sub) or 0
|
||||
if pri <= 1 then return 900 + (255 - sub) % 99 end
|
||||
if pri >= 3 then return math.max(1, math.min(98, 98 - sub)) end
|
||||
return AnimCoords.layerZ(sub, vm and vm._monbg, vm and vm._bgPrio, st)
|
||||
end
|
||||
|
||||
local SIDE_KEY_MT = {
|
||||
__index = function(t, k)
|
||||
if k == "player" or k == "enemy" then return rawget(t, AnimCoords.idOf(k)) end
|
||||
return nil
|
||||
end,
|
||||
__newindex = function(t, k, v)
|
||||
if k == "player" or k == "enemy" then k = AnimCoords.idOf(k) end
|
||||
rawset(t, k, v)
|
||||
end,
|
||||
}
|
||||
|
||||
function AnimCoords.idTable(init)
|
||||
local t = setmetatable({}, SIDE_KEY_MT)
|
||||
if init then
|
||||
for k, v in pairs(init) do t[k] = v end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
return AnimCoords
|
||||
@@ -1,4 +1,5 @@
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local AnimCtx = {}
|
||||
|
||||
@@ -7,8 +8,8 @@ local function battle_state()
|
||||
return Battle and Battle._st
|
||||
end
|
||||
|
||||
local function battler_id(side)
|
||||
return (side == "enemy") and 1 or 0
|
||||
local function battler_id(key)
|
||||
return AnimCoords.fixedId(key) or ((key == "enemy") and 1 or 0)
|
||||
end
|
||||
|
||||
local function terrain_id(st)
|
||||
@@ -19,10 +20,11 @@ local function terrain_id(st)
|
||||
end
|
||||
|
||||
function AnimCtx.behindSubstitute(lowered)
|
||||
local out = {}
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
local p = Anim._present[side]
|
||||
out[side] = ((p and p.substitute) or (lowered and lowered[side])) and true or false
|
||||
local out = AnimCoords.idTable()
|
||||
for id = 0, 3 do
|
||||
local p = rawget(Anim._present, id)
|
||||
local low = lowered and (lowered[id] or (id < 2 and lowered[AnimCoords.sideOf(id)]))
|
||||
out[id] = ((p and p.substitute) or low) and true or false
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -33,14 +35,16 @@ function AnimCtx.build(attacker, target, opts)
|
||||
local st = battle_state()
|
||||
attacker = attacker or "player"
|
||||
target = target or attacker
|
||||
local a = st and st[attacker]
|
||||
local atkId = battler_id(attacker)
|
||||
local a = AnimCoords.battler(st, atkId)
|
||||
local mon = a and a.mon or {}
|
||||
local ap = Anim._present[attacker]
|
||||
local ap = rawget(Anim._present, atkId)
|
||||
local ctx = {
|
||||
behindSubstitute = AnimCtx.behindSubstitute(opts.lowered),
|
||||
battlerAttacker = battler_id(attacker),
|
||||
battlerAttacker = atkId,
|
||||
battlerTarget = battler_id(target),
|
||||
effectBattler = battler_id(opts.effectBattler or target),
|
||||
isDouble = AnimCoords.isDouble(st),
|
||||
animArg = tonumber(opts.animArg) or 0,
|
||||
movePower = 0,
|
||||
moveDmg = tonumber(opts.moveDmg) or 0,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
local bit = require("bit")
|
||||
local Trig = require("src.core.game3.trig")
|
||||
local AnimSprites = require("src.core.game3.battle.anim_sprites")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local band, bor, bxor, rshift, arshift, lshift = bit.band, bit.bor, bit.bxor, bit.rshift, bit.arshift, bit.lshift
|
||||
local floor = math.floor
|
||||
@@ -27,7 +28,7 @@ P.SOUND_PAN_ATTACKER = -64
|
||||
P.SOUND_PAN_TARGET = 63
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:31
|
||||
P.COORDS = { player = { x = 72, y = 80 }, enemy = { x = 176, y = 40 } }
|
||||
P.COORDS = setmetatable({}, { __index = function(_, k) return AnimCoords.coords(nil, k) end })
|
||||
|
||||
function P.s16(v)
|
||||
v = band(floor(tonumber(v) or 0), 0xFFFF)
|
||||
@@ -110,11 +111,13 @@ end
|
||||
|
||||
function P.atk(vm)
|
||||
vm = vm or P.vm()
|
||||
if vm and vm.allyPair and vm:allyPair() then return vm:attackerId() end
|
||||
return vm and vm.attackerSide and vm:attackerSide() or "player"
|
||||
end
|
||||
|
||||
function P.tgt(vm)
|
||||
vm = vm or P.vm()
|
||||
if vm and vm.allyPair and vm:allyPair() then return vm:targetId() end
|
||||
return vm and vm.targetSide and vm:targetSide() or "enemy"
|
||||
end
|
||||
|
||||
@@ -129,9 +132,35 @@ end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:821
|
||||
function P.isOpponent(side)
|
||||
if type(side) == "number" then return AnimCoords.sideOf(side) ~= "player" end
|
||||
return side ~= "player"
|
||||
end
|
||||
|
||||
function P.atkId(vm)
|
||||
vm = vm or P.vm()
|
||||
return vm and vm.attackerId and vm:attackerId() or 0
|
||||
end
|
||||
|
||||
function P.tgtId(vm)
|
||||
vm = vm or P.vm()
|
||||
return vm and vm.targetId and vm:targetId() or 1
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim.c:617
|
||||
function P.spriteVisible(id)
|
||||
if not AnimCoords.spritePresent(nil, id) then return false end
|
||||
local p = P.anim().present(id)
|
||||
return p ~= nil
|
||||
end
|
||||
|
||||
function P.visibleIds(except1, except2)
|
||||
local out = {}
|
||||
for _, id in ipairs(AnimCoords.ids()) do
|
||||
if id ~= except1 and id ~= except2 and (id < 2 or P.spriteVisible(id)) then out[#out + 1] = id end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function P.species(vm, side)
|
||||
vm = vm or P.vm()
|
||||
if not vm then return nil end
|
||||
@@ -219,9 +248,18 @@ function P.attr(vm, side, attr)
|
||||
return 0
|
||||
end
|
||||
|
||||
function P.bySide(fn)
|
||||
return AnimCoords.sideArg(fn, 2)
|
||||
end
|
||||
P.coord = P.bySide(P.coord)
|
||||
P.coord2 = P.coord
|
||||
P.yDelta = P.bySide(P.yDelta)
|
||||
P.yWithElevation = P.bySide(P.yWithElevation)
|
||||
P.attr = P.bySide(P.attr)
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1908
|
||||
function P.subpriorityOf(side)
|
||||
return side == "player" and 30 or 40
|
||||
return AnimCoords.subpriority(side)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1924
|
||||
@@ -266,6 +304,7 @@ end
|
||||
function P.zFor(pri, sub)
|
||||
pri = tonumber(pri) or 2
|
||||
sub = tonumber(sub) or 0
|
||||
if AnimCoords.isDouble() then return AnimCoords.zFor(pri, sub, P.anim()._vm) end
|
||||
local key = (3 - math.max(0, math.min(3, pri))) * 100 + (99 - math.max(0, math.min(99, sub)))
|
||||
local off = floor(key * 97 / 400)
|
||||
local fp = in_front_of(pri, sub, "player")
|
||||
@@ -282,7 +321,7 @@ end
|
||||
-- pokefirered/src/battle_anim.c:349
|
||||
local function op_subpriority(vm, op)
|
||||
local raw = tonumber(op.subpriority) or 0
|
||||
local side = (op.animBattler == "target") and P.tgt(vm) or P.atk(vm)
|
||||
local side = (op.animBattler == "target") and P.tgtId(vm) or P.atkId(vm)
|
||||
local sub
|
||||
if raw >= 64 then sub = P.subpriorityOf(side) + (raw - 64) else sub = P.subpriorityOf(side) - raw end
|
||||
if sub < 3 then sub = 3 end
|
||||
@@ -328,8 +367,8 @@ end
|
||||
P.Pal = {}
|
||||
local Pal = P.Pal
|
||||
|
||||
Pal.faded = {}
|
||||
Pal.unfaded = {}
|
||||
Pal.faded = AnimCoords.idTable()
|
||||
Pal.unfaded = AnimCoords.idTable()
|
||||
Pal.remap = {}
|
||||
Pal.backup = {}
|
||||
Pal.tagsTouched = {}
|
||||
@@ -403,8 +442,8 @@ function Pal.fromBackup(slot, key)
|
||||
end
|
||||
|
||||
function Pal.reset()
|
||||
Pal.faded = {}
|
||||
Pal.unfaded = {}
|
||||
Pal.faded = AnimCoords.idTable()
|
||||
Pal.unfaded = AnimCoords.idTable()
|
||||
Pal.remap = {}
|
||||
Pal.backup = {}
|
||||
Pal.dirty = true
|
||||
@@ -422,9 +461,9 @@ function Pal.flush()
|
||||
if not Pal.dirty then return end
|
||||
Pal.dirty = false
|
||||
local Anim = P.anim()
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
local p = Anim.present(side)
|
||||
local st = Pal.faded[side] or Pal.unfaded[side]
|
||||
for id = 0, 3 do
|
||||
local p = (id < 2) and Anim.present(id) or rawget(Anim._present, id)
|
||||
local st = rawget(Pal.faded, id) or rawget(Pal.unfaded, id)
|
||||
if p and (st or p._g1Blend) then
|
||||
local k, r, g, b = to_lerp(st)
|
||||
p.blendCoeff = k
|
||||
@@ -479,8 +518,16 @@ end
|
||||
function P.palettesMask(vm, bg, atk, tgt, atkPartner, tgtPartner, anim1, anim2)
|
||||
local keys = {}
|
||||
if bg then keys[#keys + 1] = "bg" end
|
||||
if atk then keys[#keys + 1] = P.atk(vm) end
|
||||
if tgt then keys[#keys + 1] = P.tgt(vm) end
|
||||
if atk then keys[#keys + 1] = P.atkId(vm) end
|
||||
if tgt then keys[#keys + 1] = P.tgtId(vm) end
|
||||
if atkPartner then
|
||||
local id = AnimCoords.partner(P.atkId(vm))
|
||||
if P.spriteVisible(id) then keys[#keys + 1] = id end
|
||||
end
|
||||
if tgtPartner then
|
||||
local id = AnimCoords.partner(P.tgtId(vm))
|
||||
if P.spriteVisible(id) then keys[#keys + 1] = id end
|
||||
end
|
||||
if anim1 then keys[#keys + 1] = "anim1" end
|
||||
if anim2 then keys[#keys + 1] = "anim2" end
|
||||
return keys
|
||||
|
||||
@@ -40,6 +40,7 @@ function K.battlerSide(vm, animBattler)
|
||||
animBattler = tonumber(animBattler) or 0
|
||||
if animBattler == 0 then return P.atk(vm) end
|
||||
if animBattler == 1 then return P.tgt(vm) end
|
||||
if (animBattler == 2 or animBattler == 3) and vm and vm.battlerId then return vm:battlerId(animBattler) end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -66,8 +67,8 @@ end
|
||||
function K.sideFromCtx(v)
|
||||
if v == "player" or v == "enemy" then return v end
|
||||
local n = tonumber(v)
|
||||
if n == nil then return nil end
|
||||
return (n % 2 == 0) and "player" or "enemy"
|
||||
if n == nil or n < 0 or n > 3 then return nil end
|
||||
return n
|
||||
end
|
||||
|
||||
function K.battleState()
|
||||
|
||||
@@ -217,8 +217,10 @@ end
|
||||
T.BlendBattleAnimPal = K.wrap(function(t, vm)
|
||||
local sel = t._A[0]
|
||||
local keys = P.unpackSelected(vm, sel)
|
||||
if band(sel, 0x80) ~= 0 then keys[#keys + 1] = "player" end
|
||||
if band(sel, 0x200) ~= 0 then keys[#keys + 1] = "enemy" end
|
||||
if band(sel, 0x80) ~= 0 then keys[#keys + 1] = 0 end
|
||||
if band(sel, 0x100) ~= 0 and P.spriteVisible(2) then keys[#keys + 1] = 2 end
|
||||
if band(sel, 0x200) ~= 0 then keys[#keys + 1] = 1 end
|
||||
if band(sel, 0x400) ~= 0 and P.spriteVisible(3) then keys[#keys + 1] = 3 end
|
||||
start_blend_anim_sprite_color(t, keys)
|
||||
end)
|
||||
|
||||
@@ -229,18 +231,20 @@ T.BlendBattleAnimPalExclude = K.wrap(function(t, vm)
|
||||
local ex1, ex2
|
||||
if cmd == 2 or cmd == 0 then
|
||||
if cmd == 2 then keys = {} end
|
||||
ex1 = P.atk(vm)
|
||||
ex1 = P.atkId(vm)
|
||||
elseif cmd == 3 or cmd == 1 then
|
||||
if cmd == 3 then keys = {} end
|
||||
ex1 = P.tgt(vm)
|
||||
ex1 = P.tgtId(vm)
|
||||
elseif cmd == 4 then
|
||||
ex1, ex2 = P.atk(vm), P.tgt(vm)
|
||||
elseif cmd == 6 or cmd == 7 then
|
||||
ex1, ex2 = P.atkId(vm), P.tgtId(vm)
|
||||
elseif cmd == 6 then
|
||||
keys = {}
|
||||
ex1 = bxor(P.atkId(vm), 2)
|
||||
elseif cmd == 7 then
|
||||
keys = {}
|
||||
ex1 = bxor(P.tgtId(vm), 2)
|
||||
end
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
if side ~= ex1 and side ~= ex2 then keys[#keys + 1] = side end
|
||||
end
|
||||
for _, id in ipairs(P.visibleIds(ex1, ex2)) do keys[#keys + 1] = id end
|
||||
start_blend_anim_sprite_color(t, keys)
|
||||
end)
|
||||
|
||||
@@ -413,6 +417,7 @@ end)
|
||||
-- pokefirered/src/battle_anim_effects_1.c:5289
|
||||
T.MusicNotesRainbowBlend = K.wrap(function(t, vm)
|
||||
local AnimPal = require("src.core.game3.battle.anim_pal")
|
||||
require("src.core.game3.battle.anim_port.g1_callbacks_b")
|
||||
for j = 0, 3 do
|
||||
local row = P.PARTICLES_COLOR_BLEND[j]
|
||||
local f = nil
|
||||
@@ -432,6 +437,7 @@ end)
|
||||
-- pokefirered/src/battle_anim_effects_1.c:5317
|
||||
T.MusicNotesClearRainbowBlend = K.wrap(function(t, vm)
|
||||
local AnimPal = require("src.core.game3.battle.anim_pal")
|
||||
require("src.core.game3.battle.anim_port.g1_callbacks_b")
|
||||
for j = 1, 3 do
|
||||
AnimPal.free(P.PARTICLES_COLOR_BLEND[j][1])
|
||||
end
|
||||
|
||||
@@ -81,7 +81,8 @@ T.ShakeMon2 = K.wrap(function(t, vm)
|
||||
if A[0] < 4 then
|
||||
side = K.battlerSide(vm, A[0])
|
||||
elseif A[0] ~= 8 then
|
||||
if A[0] == 4 then side = "player" elseif A[0] == 6 then side = "enemy" end
|
||||
if A[0] == 4 then side = 0 elseif A[0] == 5 then side = 2 elseif A[0] == 6 then side = 1 else side = 3 end
|
||||
if side >= 2 and not P.spriteVisible(side) then side = nil end
|
||||
else
|
||||
side = P.atk(vm)
|
||||
end
|
||||
@@ -972,8 +973,7 @@ local function flash_step(t)
|
||||
d[1] = 0
|
||||
d[2] = d[2] - 1
|
||||
P.Pal.blend("bg", d[2], 0x7FFF)
|
||||
P.Pal.blend("player", d[2], 0)
|
||||
P.Pal.blend("enemy", d[2], 0)
|
||||
for _, id in ipairs(P.visibleIds()) do P.Pal.blend(id, d[2], 0) end
|
||||
P.Pal.flush()
|
||||
if d[2] == 0 then d[0] = d[0] + 1 end
|
||||
end
|
||||
@@ -983,8 +983,7 @@ local function flash_step(t)
|
||||
end
|
||||
|
||||
T.Flash = K.wrap(function(t, vm)
|
||||
P.Pal.setFaded("player", { m = 0, r = 0, g = 0, b = 0 })
|
||||
P.Pal.setFaded("enemy", { m = 0, r = 0, g = 0, b = 0 })
|
||||
for _, id in ipairs(P.visibleIds()) do P.Pal.setFaded(id, { m = 0, r = 0, g = 0, b = 0 }) end
|
||||
P.Pal.setFaded("bg", { m = 0, r = 1, g = 1, b = 1 })
|
||||
P.Pal.flush()
|
||||
t.data[0] = 0
|
||||
@@ -1026,7 +1025,10 @@ end)
|
||||
T.BlendNonAttackerPalettes = K.wrap(function(t, vm)
|
||||
local A = t._A
|
||||
for j = 5, 1, -1 do A[j] = A[j - 1] end
|
||||
local keys = { P.other(P.atk(vm)) }
|
||||
local keys = {}
|
||||
for id = 0, 3 do
|
||||
if id ~= P.atkId(vm) and (id < 2 or P.spriteVisible(id)) then keys[#keys + 1] = id end
|
||||
end
|
||||
t._keys = keys
|
||||
local d = t.data
|
||||
d[2] = A[1]
|
||||
@@ -1052,8 +1054,10 @@ end)
|
||||
|
||||
-- pokefirered/src/battle_anim_utility_funcs.c:719
|
||||
T.SetAllNonAttackersInvisiblity = K.wrap(function(t, vm)
|
||||
local p = P.present(P.other(P.atk(vm)))
|
||||
if p then p.visible = (t._A[0] == 0) end
|
||||
for _, id in ipairs(P.visibleIds(P.atkId(vm))) do
|
||||
local p = P.present(id)
|
||||
if p then p.visible = (t._A[0] == 0) end
|
||||
end
|
||||
K.destroy(t)
|
||||
end)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local AnimSprites = require("src.core.game3.battle.anim_sprites")
|
||||
local AnimPal = require("src.core.game3.battle.anim_pal")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local P = {}
|
||||
|
||||
@@ -124,11 +125,13 @@ end
|
||||
|
||||
function P.atk()
|
||||
local vm = P.vm
|
||||
if vm and vm.allyPair and vm:allyPair() then return vm:attackerId() end
|
||||
return vm and vm:attackerSide() or "player"
|
||||
end
|
||||
|
||||
function P.tgt()
|
||||
local vm = P.vm
|
||||
if vm and vm.allyPair and vm:allyPair() then return vm:targetId() end
|
||||
return vm and vm:targetSide() or "enemy"
|
||||
end
|
||||
|
||||
@@ -185,7 +188,7 @@ function P.species(b)
|
||||
return tonumber(sp)
|
||||
end
|
||||
|
||||
local BASE = { player = { 72, 80 }, enemy = { 176, 40 } }
|
||||
local BASE = setmetatable({}, { __index = function(_, k) return AnimCoords.coords(nil, k) end })
|
||||
P.BASE = BASE
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:233
|
||||
@@ -257,10 +260,14 @@ function P.coordAttr(b, attr)
|
||||
return 0
|
||||
end
|
||||
|
||||
P.coord = AnimCoords.sideArg(P.coord, 1)
|
||||
P.defaultY = AnimCoords.sideArg(P.defaultY, 1)
|
||||
P.yWithElevation = AnimCoords.sideArg(P.yWithElevation, 1)
|
||||
P.coordAttr = AnimCoords.sideArg(P.coordAttr, 1)
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1908
|
||||
function P.subpriorityOf(b)
|
||||
if b == "player" then return 30 end
|
||||
return 40
|
||||
return AnimCoords.subpriority(b)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1924
|
||||
@@ -268,15 +275,14 @@ function P.bgPriority(b)
|
||||
local vm = P.vm
|
||||
local bp = vm and vm._bgPrio
|
||||
if bp then
|
||||
if b == "player" then return bp[2] or 2 end
|
||||
return bp[1] or 2
|
||||
return bp[AnimCoords.bgPriorityRank(b)] or 2
|
||||
end
|
||||
return 2
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1934
|
||||
function P.bgPriorityRank(b)
|
||||
if b == "player" then return 2 end
|
||||
return 1
|
||||
return AnimCoords.bgPriorityRank(b)
|
||||
end
|
||||
|
||||
function P.present(b)
|
||||
@@ -1517,7 +1523,11 @@ end
|
||||
function P.SetAverageBattlerPositions(b, respect)
|
||||
local xt, yt = 0, 1
|
||||
if respect then xt, yt = 2, 3 end
|
||||
return P.coord(b, xt), P.coord(b, yt)
|
||||
local id = AnimCoords.idOf(b) or 1
|
||||
local x, y = P.coord(id, xt), P.coord(id, yt)
|
||||
if not AnimCoords.isDouble() then return x, y end
|
||||
local partner = AnimCoords.partner(id)
|
||||
return P.div(x + P.coord(partner, xt), 2), P.div(y + P.coord(partner, yt), 2)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1482
|
||||
|
||||
@@ -407,7 +407,7 @@ local function setGreyscaleOrOriginal(p, restore)
|
||||
end
|
||||
end
|
||||
|
||||
local POSITION_SIDE = { [4] = "player", [6] = "enemy" }
|
||||
local POSITION_ID = { [4] = 0, [5] = 2, [6] = 1, [7] = 3 }
|
||||
|
||||
-- pokefirered/src/battle_anim_dark.c:869
|
||||
T.SetGrayscaleOrOriginalPal = P.task(function(t, vm)
|
||||
@@ -416,8 +416,9 @@ T.SetGrayscaleOrOriginalPal = P.task(function(t, vm)
|
||||
if a >= 0 and a <= 3 then
|
||||
p = P.monSprite(vm, a)
|
||||
elseif a >= 4 and a <= 7 then
|
||||
local side = POSITION_SIDE[a]
|
||||
if side and not P.monHidden(vm, side) then p = P.monPresent(side) end
|
||||
local id = POSITION_ID[a]
|
||||
local ok = id ~= nil and (id < 2 or require("src.core.game3.battle.anim_coords").spritePresent(nil, id))
|
||||
if ok and not P.monHidden(vm, id) then p = P.monPresent(id) end
|
||||
end
|
||||
if p then setGreyscaleOrOriginal(p, t.ga[1] ~= 0) end
|
||||
P.DestroyAnimVisualTask(t)
|
||||
|
||||
@@ -12,8 +12,7 @@ end
|
||||
|
||||
function H.bgPriority(vm, side)
|
||||
local bp = vm and vm._bgPrio
|
||||
if side == "player" then return (bp and bp[2]) or 2 end
|
||||
return (bp and bp[1]) or 2
|
||||
return (bp and bp[require("src.core.game3.battle.anim_coords").bgPriorityRank(side)]) or 2
|
||||
end
|
||||
|
||||
function H.monX(vm, side)
|
||||
|
||||
@@ -411,7 +411,7 @@ T.GrudgeFlames = P.task(function(t, vm)
|
||||
d[11] = P.div(P.coordAttr(vm, atk, P.ATTR_WIDTH), 2) + 8
|
||||
d[7] = 0
|
||||
local bp = vm._bgPrio
|
||||
if atk == "player" then d[5] = (bp and bp[2]) or 2 else d[5] = (bp and bp[1]) or 2 end
|
||||
d[5] = (bp and bp[require("src.core.game3.battle.anim_coords").bgPriorityRank(atk)]) or 2
|
||||
d[6] = P.subpriorityOf(atk) - 2
|
||||
d[3] = 0
|
||||
d[4] = 16
|
||||
@@ -715,25 +715,26 @@ T.DestinyBondWhiteShadow = P.task(function(t, vm)
|
||||
d[5], d[6], d[7], d[8] = 0, 0, 0, 0
|
||||
d[9] = 16
|
||||
d[10] = t.ga[0]
|
||||
local atk = P.atk(vm)
|
||||
local atk = P.atkId(vm)
|
||||
local baseX = P.coord(vm, atk, P.COORD_X_2)
|
||||
local baseY = P.coordAttr(vm, atk, P.ATTR_BOTTOM)
|
||||
t._sprites = {}
|
||||
local other = P.tgt(vm)
|
||||
if other ~= atk and not P.monHidden(vm, other) then
|
||||
local s = P.CreateSprite(vm, "gDestinyBondWhiteShadowSpriteTemplate", baseX, baseY, 55, destinyBondShadowStep)
|
||||
if s then
|
||||
local x = P.coord(vm, other, P.COORD_X_2)
|
||||
local y = P.coordAttr(vm, other, P.ATTR_BOTTOM)
|
||||
s.data[0] = P.s16(baseX * 16)
|
||||
s.data[1] = P.s16(baseY * 16)
|
||||
s.data[2] = P.s16(P.div((x - baseX) * 16, t.ga[1]))
|
||||
s.data[3] = P.s16(P.div((y - baseY) * 16, t.ga[1]))
|
||||
s.data[4] = t.ga[1]
|
||||
s.data[5] = x
|
||||
s.data[6] = y
|
||||
t._sprites[#t._sprites + 1] = s
|
||||
d[12] = d[12] + 1
|
||||
for other = 0, 3 do
|
||||
if other ~= atk and other ~= P.bxor(atk, 2) and not P.monHidden(vm, other) then
|
||||
local s = P.CreateSprite(vm, "gDestinyBondWhiteShadowSpriteTemplate", baseX, baseY, 55, destinyBondShadowStep)
|
||||
if s then
|
||||
local x = P.coord(vm, other, P.COORD_X_2)
|
||||
local y = P.coordAttr(vm, other, P.ATTR_BOTTOM)
|
||||
s.data[0] = P.s16(baseX * 16)
|
||||
s.data[1] = P.s16(baseY * 16)
|
||||
s.data[2] = P.s16(P.div((x - baseX) * 16, t.ga[1]))
|
||||
s.data[3] = P.s16(P.div((y - baseY) * 16, t.ga[1]))
|
||||
s.data[4] = t.ga[1]
|
||||
s.data[5] = x
|
||||
s.data[6] = y
|
||||
t._sprites[#t._sprites + 1] = s
|
||||
d[12] = d[12] + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
t.fn = dbwsTaskStep
|
||||
|
||||
@@ -2,6 +2,7 @@ local P = {}
|
||||
|
||||
local bit = require("bit")
|
||||
local AnimPal = require("src.core.game3.battle.anim_pal")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
P.band, P.bor, P.bxor, P.lshift, P.rshift = bit.band, bit.bor, bit.bxor, bit.lshift, bit.rshift
|
||||
|
||||
local SINE = {
|
||||
@@ -91,8 +92,14 @@ function P.vm() return Anim()._vm end
|
||||
function P.AnimSprites() return require("src.core.game3.battle.anim_sprites") end
|
||||
function P.AnimTasks() return package.loaded["src.core.game3.battle.anim_tasks"] or require("src.core.game3.battle.anim_tasks") end
|
||||
|
||||
function P.atk(vm) return vm:attackerSide() end
|
||||
function P.tgt(vm) return vm:targetSide() end
|
||||
function P.atk(vm)
|
||||
if vm.allyPair and vm:allyPair() then return vm:attackerId() end
|
||||
return vm:attackerSide()
|
||||
end
|
||||
function P.tgt(vm)
|
||||
if vm.allyPair and vm:allyPair() then return vm:targetId() end
|
||||
return vm:targetSide()
|
||||
end
|
||||
function P.side(vm, animBattler)
|
||||
animBattler = tonumber(animBattler) or 0
|
||||
if animBattler == 0 then return vm:attackerSide() end
|
||||
@@ -112,7 +119,7 @@ local function picCoords()
|
||||
return PicCoords or nil
|
||||
end
|
||||
|
||||
local BASE = { player = { 72, 80 }, enemy = { 176, 40 } }
|
||||
local BASE = setmetatable({}, { __index = function(_, k) return AnimCoords.coords(nil, k) end })
|
||||
|
||||
function P.species(vm, side)
|
||||
local sp = vm and vm.speciesForSide and vm:speciesForSide(side)
|
||||
@@ -160,11 +167,19 @@ function P.yWithElevation(vm, side)
|
||||
return y
|
||||
end
|
||||
|
||||
P.yDelta = AnimCoords.sideArg(P.yDelta, 2)
|
||||
P.elevation = AnimCoords.sideArg(P.elevation, 2)
|
||||
P.coord = AnimCoords.sideArg(P.coord, 2)
|
||||
P.yWithElevation = AnimCoords.sideArg(P.yWithElevation, 2)
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1908
|
||||
function P.subpriorityOf(side)
|
||||
return side == "player" and 30 or 40
|
||||
return AnimCoords.subpriority(side)
|
||||
end
|
||||
|
||||
function P.atkId(vm) return vm:attackerId() end
|
||||
function P.tgtId(vm) return vm:targetId() end
|
||||
|
||||
function P.monPresent(side)
|
||||
return Anim().present(side)
|
||||
end
|
||||
@@ -375,15 +390,7 @@ function P.zFor(pri, sub, vm)
|
||||
elseif pri >= 3 then
|
||||
return 2 + (100 - sub) * 0.01
|
||||
end
|
||||
local monbg = vm and vm._monbg or {}
|
||||
local bgPrio = vm and vm._bgPrio or {}
|
||||
local frontEnemy, frontPlayer
|
||||
if monbg.enemy then frontEnemy = (bgPrio[1] or 2) >= 2 else frontEnemy = sub < 40 end
|
||||
if monbg.player then frontPlayer = (bgPrio[2] or 2) >= 2 else frontPlayer = sub < 30 end
|
||||
local rank = math.max(0, math.min(98, 98 - sub))
|
||||
if frontPlayer then return 201 + rank end
|
||||
if frontEnemy then return 101 + rank end
|
||||
return rank
|
||||
return AnimCoords.layerZ(sub, vm and vm._monbg, vm and vm._bgPrio)
|
||||
end
|
||||
|
||||
-- pokefirered/src/sprite.c:905
|
||||
@@ -816,7 +823,7 @@ function P.CloneMon(vm, side)
|
||||
local cx, cy = P.monCenter(vm, side)
|
||||
local img = P.monImage(vm, side)
|
||||
local s = AnimSprites.acquire({
|
||||
x = cx, y = cy, z = (side == "player") and 195 or 95, hostId = side,
|
||||
x = cx, y = cy, z = AnimCoords.monBehindZ(side), hostId = side,
|
||||
w = 64, h = 64, image = img, callback = genericCallback,
|
||||
})
|
||||
if not s then return nil end
|
||||
@@ -1525,4 +1532,8 @@ function P.ensureColorOverlay(vm)
|
||||
return s
|
||||
end
|
||||
|
||||
P.coordAttr = AnimCoords.sideArg(P.coordAttr, 2)
|
||||
P.monCenter = AnimCoords.sideArg(P.monCenter, 2)
|
||||
P.monImage = AnimCoords.sideArg(P.monImage, 2)
|
||||
|
||||
return P
|
||||
|
||||
@@ -228,7 +228,7 @@ return function(C)
|
||||
s.ox = s.ox + P.Sin(s.data[5], s.data[6])
|
||||
s.oy = s.oy + P.Cos(s.data[5], -6)
|
||||
local side = s._fogSide
|
||||
local bgp = (s._vm and s._vm._bgPrio and s._vm._bgPrio[side == "enemy" and 1 or 2]) or 2
|
||||
local bgp = (s._vm and s._vm._bgPrio and s._vm._bgPrio[require("src.core.game3.battle.anim_coords").bgPriorityRank(side)]) or 2
|
||||
if P.u16(s.data[5] - 64) <= 0x7F then
|
||||
P.setPriority(s, bgp, s.subpriority)
|
||||
else
|
||||
@@ -355,7 +355,7 @@ return function(C)
|
||||
end
|
||||
s.x = P.coord(vm, P.atk(vm), P.X_2)
|
||||
s.y = P.coord(vm, P.atk(vm), P.Y_PIC_OFFSET)
|
||||
local bgp = (vm._bgPrio and vm._bgPrio[P.tgt(vm) == "enemy" and 1 or 2]) or 2
|
||||
local bgp = (vm._bgPrio and vm._bgPrio[require("src.core.game3.battle.anim_coords").bgPriorityRank(P.tgt(vm))]) or 2
|
||||
if a[7] ~= 0 then
|
||||
s.data[1] = s.x + a[1]
|
||||
s.data[2] = P.coord(vm, P.tgt(vm), P.X_2) + a[3]
|
||||
|
||||
@@ -2,6 +2,7 @@ local bit = require("bit")
|
||||
local Trig = require("src.core.game3.trig")
|
||||
local AnimSprites = require("src.core.game3.battle.anim_sprites")
|
||||
local AnimPal = require("src.core.game3.battle.anim_pal")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local band, bor, bxor, rshift, arshift, lshift = bit.band, bit.bor, bit.bxor, bit.rshift, bit.arshift, bit.lshift
|
||||
local floor = math.floor
|
||||
@@ -24,7 +25,7 @@ P.Y_PIC_OFFSET = 3
|
||||
P.Y_PIC_OFFSET_DEFAULT = 4
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:31
|
||||
P.COORDS = { player = { x = 72, y = 80 }, enemy = { x = 176, y = 40 } }
|
||||
P.COORDS = setmetatable({}, { __index = function(_, k) return AnimCoords.coords(nil, k) end })
|
||||
|
||||
function P.s16(v)
|
||||
v = band(floor(tonumber(v) or 0), 0xFFFF)
|
||||
@@ -87,14 +88,17 @@ function P.other(side)
|
||||
end
|
||||
|
||||
function P.atk(vm)
|
||||
if vm and vm.allyPair and vm:allyPair() then return vm:attackerId() end
|
||||
return vm and vm.attackerSide and vm:attackerSide() or "player"
|
||||
end
|
||||
|
||||
function P.tgt(vm)
|
||||
if vm and vm.allyPair and vm:allyPair() then return vm:targetId() end
|
||||
return vm and vm.targetSide and vm:targetSide() or "enemy"
|
||||
end
|
||||
|
||||
function P.sideId(side)
|
||||
if type(side) == "number" then return side % 2 end
|
||||
return side == "player" and 0 or 1
|
||||
end
|
||||
|
||||
@@ -102,6 +106,7 @@ function P.battlerSide(vm, animBattler)
|
||||
animBattler = tonumber(animBattler) or 0
|
||||
if animBattler == 0 then return P.atk(vm) end
|
||||
if animBattler == 1 then return P.tgt(vm) end
|
||||
if (animBattler == 2 or animBattler == 3) and vm and vm.battlerId then return vm:battlerId(animBattler) end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -147,19 +152,27 @@ function P.yWithElevation(vm, side)
|
||||
return y
|
||||
end
|
||||
|
||||
P.coord = AnimCoords.sideArg(P.coord, 2)
|
||||
P.yWithElevation = AnimCoords.sideArg(P.yWithElevation, 2)
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:286
|
||||
function P.substituteY(side)
|
||||
return P.COORDS[side].y + (side ~= "player" and 16 or 17)
|
||||
local id = AnimCoords.idOf(side) or 1
|
||||
return P.COORDS[id].y + (AnimCoords.sideOf(id) ~= "player" and 16 or 17)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:1908
|
||||
function P.subpriorityOf(side)
|
||||
return side == "player" and 30 or 40
|
||||
return AnimCoords.subpriority(side)
|
||||
end
|
||||
|
||||
function P.zFor(priority, subpriority)
|
||||
priority = tonumber(priority) or 2
|
||||
subpriority = tonumber(subpriority) or 0
|
||||
if AnimCoords.isDouble() then
|
||||
local Anim = package.loaded["src.core.game3.battle.anim"]
|
||||
return AnimCoords.zFor(priority, subpriority, Anim and Anim._vm)
|
||||
end
|
||||
if priority <= 1 then return 900 + (255 - subpriority) % 100 end
|
||||
local z = 500 - 10 * subpriority - 1
|
||||
if priority >= 3 then z = math.min(z, 99) end
|
||||
@@ -221,10 +234,14 @@ end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:2098
|
||||
function P.averagePositions(vm, side, respect)
|
||||
if respect then
|
||||
return P.coord(vm, side, P.X_2), P.coord(vm, side, P.Y_PIC_OFFSET)
|
||||
end
|
||||
return P.coord(vm, side, P.X), P.coord(vm, side, P.Y)
|
||||
local xt, yt = P.X, P.Y
|
||||
if respect then xt, yt = P.X_2, P.Y_PIC_OFFSET end
|
||||
local id = AnimCoords.idOf(side) or 1
|
||||
local x, y = P.coord(vm, id, xt), P.coord(vm, id, yt)
|
||||
if not AnimCoords.isDouble() then return x, y end
|
||||
local partner = AnimCoords.partner(id)
|
||||
local px, py = P.coord(vm, partner, xt), P.coord(vm, partner, yt)
|
||||
return P.cdiv(x + px, 2), P.cdiv(y + py, 2)
|
||||
end
|
||||
|
||||
function P.destroy(s)
|
||||
|
||||
@@ -114,13 +114,13 @@ return function(K)
|
||||
if d[0] == 0 then
|
||||
local x = P.coord(vm, side, P.X)
|
||||
local y = P.coord(vm, side, P.Y)
|
||||
local id = vm.attackerId and vm:attackerId() or P.sideId(side)
|
||||
local item = vm.ctx and (vm.ctx.ballItem and vm.ctx.ballItem[side] or vm.ctx.pokeball)
|
||||
if not item then
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local b = Battle and Battle._st and Battle._st[side]
|
||||
local b = require("src.core.game3.battle.anim_coords").battler(nil, id)
|
||||
item = b and b.mon and b.mon.pokeball
|
||||
end
|
||||
if BallOpen then BallOpen.start(side, x, y + 32 + 5, item, false) end
|
||||
if BallOpen then BallOpen.start(id, x, y + 32 + 5, item, false) end
|
||||
d[0] = d[0] + 1
|
||||
elseif d[0] == 1 then
|
||||
if not BallOpen or #BallOpen._tasks == 0 then D(t) end
|
||||
@@ -304,15 +304,10 @@ return function(K)
|
||||
D(t)
|
||||
end
|
||||
|
||||
local function side_of_battler(id)
|
||||
id = P.band(tonumber(id) or 0, 0xFF)
|
||||
return (P.band(id, 1) == 0) and "player" or "enemy"
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_special.c:2299
|
||||
TK.GetBattlersFromArg = function(t, vm)
|
||||
local m = P.u16(vm.animArg or 0)
|
||||
P.setBattlers(vm, side_of_battler(P.band(m, 0xFF)), side_of_battler(P.rshift(m, 8)))
|
||||
P.setBattlers(vm, P.band(m, 3), P.band(P.rshift(m, 8), 3))
|
||||
D(t)
|
||||
end
|
||||
|
||||
|
||||
@@ -482,8 +482,9 @@ return function(K)
|
||||
t.func = shakeTerrain
|
||||
elseif a[0] == 4 then
|
||||
t._mons = {}
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
local p = P.present(side)
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
for _, id in ipairs(AnimCoords.ids()) do
|
||||
local p = (id < 2 or AnimCoords.spritePresent(nil, id)) and P.present(id) or nil
|
||||
if p and p.visible ~= false then t._mons[#t._mons + 1] = p end
|
||||
end
|
||||
t.func = shakeBattlers
|
||||
|
||||
+167
-125
@@ -1,5 +1,7 @@
|
||||
local bit = require("bit")
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local AnimCtx = require("src.core.game3.battle.anim_ctx")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local AnimSeq = {}
|
||||
|
||||
@@ -31,12 +33,23 @@ local SUB_EXEMPT_GENERAL = {
|
||||
-- pokefirered/data/battle_scripts_1.s:3913
|
||||
local TARGET_ACTIVE_GENERAL = { ITEM_STEAL = true, ITEM_KNOCKOFF = true, SNATCH_MOVE = true }
|
||||
|
||||
local function battler_id(side)
|
||||
return (side == "enemy") and 1 or 0
|
||||
local function to_id(v)
|
||||
if type(v) == "number" then return v end
|
||||
return AnimCoords.fixedId(v)
|
||||
end
|
||||
|
||||
local function other(side)
|
||||
return (side == "player") and "enemy" or "player"
|
||||
local function side_of(id)
|
||||
return AnimCoords.sideOf(id)
|
||||
end
|
||||
|
||||
local function opposite(id)
|
||||
return bit.bxor(id or 0, 1)
|
||||
end
|
||||
|
||||
local function ev_id(ev, idKey, sideKey)
|
||||
local v = ev[idKey]
|
||||
if type(v) == "number" then return v end
|
||||
return to_id(ev[sideKey])
|
||||
end
|
||||
|
||||
local function battle_state()
|
||||
@@ -44,15 +57,15 @@ local function battle_state()
|
||||
return Battle and Battle._st
|
||||
end
|
||||
|
||||
local function battler_of(side)
|
||||
local st = battle_state()
|
||||
return st and st[side]
|
||||
local function battler_of(id)
|
||||
return AnimCoords.battler(battle_state(), to_id(id))
|
||||
end
|
||||
|
||||
local function species_of(side)
|
||||
local b = Anim.shownBattler(side, battler_of(side))
|
||||
local function species_of(id)
|
||||
id = to_id(id)
|
||||
local b = Anim.shownBattler(id, battler_of(id))
|
||||
if type(b) ~= "table" then return nil end
|
||||
local p = Anim._present[side]
|
||||
local p = rawget(Anim._present, id)
|
||||
if b.expTransform and p and not p.pendingTransform then
|
||||
return p.transformSpecies or b.expTransform.species
|
||||
end
|
||||
@@ -79,10 +92,10 @@ local function effectiveness_se(eff)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:1883
|
||||
local function play_effectiveness_se(eff, side)
|
||||
local function play_effectiveness_se(eff, battler)
|
||||
local id = effectiveness_se(eff)
|
||||
if not id then return end
|
||||
local pan = (side == "player") and -64 or 63
|
||||
local pan = (side_of(to_id(battler) or 1) == "player") and -64 or 63
|
||||
pcall(function()
|
||||
require("src.core.game3.audio").playSe(id, { pan = pan })
|
||||
end)
|
||||
@@ -92,17 +105,18 @@ local function flush_pending_eff()
|
||||
local p = AnimSeq._pendingEff
|
||||
AnimSeq._pendingEff = nil
|
||||
if p and p.effectiveness ~= nil then
|
||||
play_effectiveness_se(p.effectiveness, p.side)
|
||||
play_effectiveness_se(p.effectiveness, p.battler or p.side)
|
||||
end
|
||||
end
|
||||
|
||||
local function stand_in(side, slot)
|
||||
local function stand_in(id, slot)
|
||||
local st = battle_state()
|
||||
local side = side_of(id)
|
||||
local party = st and ((side == "player") and st.playerParty or st.foeParty)
|
||||
local mon = party and slot and party[slot]
|
||||
if not mon then return nil end
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local ok, b = pcall(State.makeBattler, mon, side, { partyIndex = slot })
|
||||
local ok, b = pcall(State.makeBattler, mon, side, { partyIndex = slot, id = id })
|
||||
return ok and b or nil
|
||||
end
|
||||
|
||||
@@ -131,8 +145,8 @@ end
|
||||
local function reconcile_sprites()
|
||||
local st = battle_state()
|
||||
if not st or Anim._headless then return end
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
local p = Anim._present[side]
|
||||
for _, id in ipairs(AnimCoords.ids(st)) do
|
||||
local p = rawget(Anim._present, id)
|
||||
if p then
|
||||
p.shown = nil
|
||||
p.blinkHidden = false
|
||||
@@ -140,8 +154,8 @@ local function reconcile_sprites()
|
||||
p.invisible = nil
|
||||
p.battlerInvisible = nil
|
||||
end
|
||||
local b = st[side]
|
||||
local alive = b and b.mon and (tonumber(b.mon.hp) or 0) > 0
|
||||
local b = AnimCoords.battler(st, id)
|
||||
local alive = b and b.mon and (tonumber(b.mon.hp) or 0) > 0 and not (st.absent and st.absent[id])
|
||||
if p and alive and not st.over then
|
||||
if b.semiInvulnerable and AnimSeq._scene then
|
||||
p.visible = false
|
||||
@@ -150,8 +164,8 @@ local function reconcile_sprites()
|
||||
p.alpha = 1
|
||||
end
|
||||
local subbed = (b.substituteHP or 0) > 0
|
||||
if subbed ~= (p.substitute == true) and not AnimSeq._subLowered[side] then
|
||||
Anim.setSubstitute(side, subbed)
|
||||
if subbed ~= (p.substitute == true) and not AnimSeq._subLowered[id] then
|
||||
Anim.setSubstitute(id, subbed)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -201,8 +215,9 @@ function AnimSeq.buildSteps(events, meta)
|
||||
end
|
||||
if lastMove and tostring(ev.text or ""):find("SUBSTITUTE took damage") then
|
||||
-- pokefirered/src/battle_script_commands.c:5300
|
||||
add("hitfx", { side = lastMove.target or other(lastMove.attacker or "player"),
|
||||
effectiveness = meta.effectiveness or 1 })
|
||||
local a = ev_id(lastMove, "attackerId", "attacker") or 0
|
||||
local t = ev_id(lastMove, "targetId", "target") or opposite(a)
|
||||
add("hitfx", { side = side_of(t), battler = t, effectiveness = meta.effectiveness or 1 })
|
||||
end
|
||||
add("msg", { text = ev.text, wait = ev.wait })
|
||||
lastMsg = ev.text
|
||||
@@ -214,10 +229,12 @@ function AnimSeq.buildSteps(events, meta)
|
||||
lastMove = ev
|
||||
add("move", {
|
||||
moveId = ev.moveId, attacker = ev.attacker, target = ev.target,
|
||||
attackerId = ev_id(ev, "attackerId", "attacker"), targetId = ev_id(ev, "targetId", "target"),
|
||||
turn = ev.turn or 0, calledBy = meta.calledBy, damage = ev.damage, power = ev.power,
|
||||
})
|
||||
elseif k == "anim" then
|
||||
local d = { anim = ev.anim, name = ev.name, attacker = ev.attacker, target = ev.target, arg = ev.arg }
|
||||
local d = { anim = ev.anim, name = ev.name, attacker = ev.attacker, target = ev.target, arg = ev.arg,
|
||||
attackerId = ev_id(ev, "attackerId", "attacker"), targetId = ev_id(ev, "targetId", "target") }
|
||||
if ev.anim == "special" and (ev.name == "SUBSTITUTE_TO_MON" or ev.name == "MON_TO_SUBSTITUTE") then
|
||||
if ev.name == "SUBSTITUTE_TO_MON" then
|
||||
for j = i + 1, n do
|
||||
@@ -229,34 +246,41 @@ function AnimSeq.buildSteps(events, meta)
|
||||
end
|
||||
add("anim", d)
|
||||
elseif k == "hit" then
|
||||
local b = ev_id(ev, "battler", "side")
|
||||
local eff = 1
|
||||
if prevKind == "move" or (lastMove and prevKind ~= "hit" and prevKind ~= "hp") then
|
||||
if ev.effectiveness ~= nil then
|
||||
eff = ev.effectiveness
|
||||
elseif prevKind == "move" or (lastMove and prevKind ~= "hit" and prevKind ~= "hp") then
|
||||
eff = meta.effectiveness or 1
|
||||
end
|
||||
add("hitfx", { side = ev.side, effectiveness = eff })
|
||||
add("hp", { side = ev.side, from = ev.from, to = ev.to, maxHp = ev.maxHp })
|
||||
add("hitfx", { side = ev.side, battler = b, effectiveness = eff })
|
||||
add("hp", { side = ev.side, battler = b, from = ev.from, to = ev.to, maxHp = ev.maxHp })
|
||||
elseif k == "hp" then
|
||||
local b = ev_id(ev, "battler", "side")
|
||||
if prevKind == "msg" and tostring(lastMsg or ""):find("hurt itself in its") then
|
||||
-- pokefirered/data/battle_scripts_1.s:3741
|
||||
add("hitfx", { side = ev.side, effectiveness = 1 })
|
||||
add("hitfx", { side = ev.side, battler = b, effectiveness = 1 })
|
||||
end
|
||||
add("hp", { side = ev.side, from = ev.from, to = ev.to, maxHp = ev.maxHp })
|
||||
add("hp", { side = ev.side, battler = b, from = ev.from, to = ev.to, maxHp = ev.maxHp })
|
||||
elseif k == "faint" then
|
||||
local b = ev_id(ev, "battler", "side")
|
||||
-- pokefirered/data/battle_scripts_1.s:2810
|
||||
add("faint_cry", { side = ev.side })
|
||||
add("faint_cry", { side = ev.side, battler = b })
|
||||
add("pause", { frames = 64 })
|
||||
add("faint", { side = ev.side })
|
||||
add("faint", { side = ev.side, battler = b })
|
||||
elseif k == "switch_out" then
|
||||
add("switch_out", { side = ev.side, reason = ev.reason })
|
||||
add("switch_out", { side = ev.side, battler = ev_id(ev, "battler", "side"), reason = ev.reason })
|
||||
elseif k == "switch" then
|
||||
add("switch_out", { side = ev.side, reason = ev.reason, slot = ev.from })
|
||||
local b = ev_id(ev, "battler", "side")
|
||||
add("switch_out", { side = ev.side, battler = b, reason = ev.reason, slot = ev.from })
|
||||
local hp = nil
|
||||
for j = i + 1, n do
|
||||
local e2 = events[j]
|
||||
if (e2.kind == "hp" or e2.kind == "hit") and e2.side == ev.side then hp = e2.from break end
|
||||
if e2.kind == "switch" and e2.side == ev.side then break end
|
||||
local b2 = ev_id(e2, "battler", "side")
|
||||
if (e2.kind == "hp" or e2.kind == "hit") and b2 == b then hp = e2.from break end
|
||||
if e2.kind == "switch" and b2 == b then break end
|
||||
end
|
||||
local d = { side = ev.side, slot = ev.to, hp = hp, reason = ev.reason }
|
||||
local d = { side = ev.side, battler = b, slot = ev.to, hp = hp, reason = ev.reason }
|
||||
if ev.reason == "baton_pass" and events[i + 1] and events[i + 1].kind == "msg" then
|
||||
-- pokefirered/data/battle_scripts_1.s:1690
|
||||
deferredIn = d
|
||||
@@ -285,7 +309,7 @@ local function legacy_steps(result)
|
||||
restStart = 2
|
||||
end
|
||||
local us = result.user and result.user.side or "player"
|
||||
local ts = result.target and result.target.side or other(us)
|
||||
local ts = result.target and result.target.side or ((us == "player") and "enemy" or "player")
|
||||
if result.hits and #result.hits > 0 then
|
||||
for hi, hit in ipairs(result.hits) do
|
||||
add("move", { moveId = result.moveId, attacker = us, target = ts, turn = 0 })
|
||||
@@ -320,12 +344,16 @@ local function start(steps, pushMsg)
|
||||
end
|
||||
local held = {}
|
||||
for _, step in ipairs(steps) do
|
||||
if step.kind == "move" and tonumber(step.data.moveId) == MOVE_TRANSFORM then
|
||||
local p = Anim.present(step.data.attacker or "player")
|
||||
local d = step.data or {}
|
||||
if step.kind == "move" and tonumber(d.moveId) == MOVE_TRANSFORM then
|
||||
local p = Anim.present(ev_id(d, "attackerId", "attacker") or 0)
|
||||
if p then p.pendingTransform = true end
|
||||
elseif step.kind == "switch_out" and step.data.slot and not held[step.data.side] then
|
||||
held[step.data.side] = true
|
||||
Anim.setShown(step.data.side, stand_in(step.data.side, step.data.slot))
|
||||
elseif step.kind == "switch_out" and d.slot then
|
||||
local b = ev_id(d, "battler", "side")
|
||||
if b ~= nil and not held[b] then
|
||||
held[b] = true
|
||||
Anim.setShown(b, stand_in(b, d.slot))
|
||||
end
|
||||
end
|
||||
end
|
||||
AnimSeq._steps = steps
|
||||
@@ -374,9 +402,40 @@ local function ctx_for(attacker, target, opts)
|
||||
return AnimCtx.build(attacker, target, opts)
|
||||
end
|
||||
|
||||
local function species_by_id(a, t)
|
||||
if not AnimCoords.isDouble(battle_state()) then return nil end
|
||||
local out = {}
|
||||
for id = 0, 3 do
|
||||
if id ~= a and id ~= t then out[id] = species_of(id) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function launch_opts(a, t, extra)
|
||||
local o = {
|
||||
attackerSide = side_of(a),
|
||||
targetSide = side_of(t),
|
||||
attackerId = a,
|
||||
targetId = t,
|
||||
isReversed = side_of(a) == "enemy",
|
||||
attackerSpecies = species_of(a),
|
||||
targetSpecies = species_of(t),
|
||||
speciesById = species_by_id(a, t),
|
||||
}
|
||||
for k, v in pairs(extra or {}) do o[k] = v end
|
||||
return o
|
||||
end
|
||||
|
||||
local function move_ids(d)
|
||||
local a = ev_id(d, "attackerId", "attacker") or 0
|
||||
local t = ev_id(d, "targetId", "target")
|
||||
if t == nil then t = opposite(a) end
|
||||
return a, t
|
||||
end
|
||||
|
||||
local function move_ctx(d)
|
||||
local attacker = d.attacker or "player"
|
||||
local ctx = ctx_for(attacker, d.target or other(attacker), { moveId = d.moveId, moveDmg = d.damage })
|
||||
local attacker, target = move_ids(d)
|
||||
local ctx = ctx_for(attacker, target, { moveId = d.moveId, moveDmg = d.damage })
|
||||
if tonumber(d.power) then ctx.movePower = tonumber(d.power) end
|
||||
local okM, Moves = pcall(require, "src.core.game3.battle.moves")
|
||||
local mv = okM and Moves.get and Moves.get(d.moveId)
|
||||
@@ -384,15 +443,15 @@ local function move_ctx(d)
|
||||
end
|
||||
|
||||
local function after_move(d, mv)
|
||||
local side = d.attacker or "player"
|
||||
local p = Anim.present(side)
|
||||
local id = move_ids(d)
|
||||
local p = Anim.present(id)
|
||||
if not p then return end
|
||||
if tonumber(d.moveId) == MOVE_TRANSFORM then p.pendingTransform = nil end
|
||||
if not AnimSeq._scene then return end
|
||||
if tonumber(d.moveId) == MOVE_SUBSTITUTE then
|
||||
local b = battler_of(side)
|
||||
local b = battler_of(id)
|
||||
if b and (b.substituteHP or 0) > 0 and not p.substitute then
|
||||
Anim.setSubstitute(side, true)
|
||||
Anim.setSubstitute(id, true)
|
||||
p.visible = true
|
||||
p.ox, p.oy = 0, 0
|
||||
end
|
||||
@@ -416,36 +475,31 @@ local function run_move(d)
|
||||
end
|
||||
local ctx, mv = move_ctx(d)
|
||||
wait_anim()
|
||||
local attacker = d.attacker or "player"
|
||||
local target = d.target or other(attacker)
|
||||
Anim.launchMove(d.moveId, {
|
||||
attackerSide = attacker,
|
||||
targetSide = target,
|
||||
isReversed = attacker == "enemy",
|
||||
attackerSpecies = species_of(attacker),
|
||||
targetSpecies = species_of(target),
|
||||
local attacker, target = move_ids(d)
|
||||
Anim.launchMove(d.moveId, launch_opts(attacker, target, {
|
||||
moveTurn = d.turn or 0,
|
||||
ctx = ctx,
|
||||
onEnd = function()
|
||||
after_move(d, mv)
|
||||
if AnimSeq._waiting then advance() end
|
||||
end,
|
||||
})
|
||||
}))
|
||||
launch_done()
|
||||
end
|
||||
|
||||
local function general_arg(d)
|
||||
if d.name == "LEECH_SEED_DRAIN" then
|
||||
-- pokefirered/src/battle_util.c:798
|
||||
local seeder = d.target or other(d.attacker or "player")
|
||||
return battler_id(seeder) + battler_id(d.attacker or "player") * 256
|
||||
local a, seeder = move_ids(d)
|
||||
return seeder + a * 256
|
||||
end
|
||||
return tonumber(d.arg) or 0
|
||||
end
|
||||
|
||||
local function run_general(d)
|
||||
local name = d.name
|
||||
local active = (TARGET_ACTIVE_GENERAL[name] and d.target) or d.attacker or "player"
|
||||
local a = move_ids(d)
|
||||
local active = (TARGET_ACTIVE_GENERAL[name] and ev_id(d, "targetId", "target")) or a
|
||||
local p = Anim.present(active)
|
||||
local castform = name == "CASTFORM_CHANGE"
|
||||
if castform then
|
||||
@@ -482,12 +536,7 @@ local function run_general(d)
|
||||
return
|
||||
end
|
||||
wait_anim()
|
||||
Anim.launchGeneral(name, {
|
||||
attackerSide = active,
|
||||
targetSide = active,
|
||||
isReversed = active == "enemy",
|
||||
attackerSpecies = species_of(active),
|
||||
targetSpecies = species_of(active),
|
||||
Anim.launchGeneral(name, launch_opts(active, active, {
|
||||
animArg = general_arg(d),
|
||||
ctx = ctx_for(active, active, { animArg = general_arg(d) }),
|
||||
onEnd = function()
|
||||
@@ -506,37 +555,33 @@ local function run_general(d)
|
||||
end
|
||||
if AnimSeq._waiting then advance() end
|
||||
end,
|
||||
})
|
||||
}))
|
||||
launch_done()
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:5494
|
||||
local function run_status(d)
|
||||
local side = d.attacker or d.target or "player"
|
||||
local b = battler_of(side)
|
||||
local id = ev_id(d, "attackerId", "attacker")
|
||||
if id == nil then id = ev_id(d, "targetId", "target") or 0 end
|
||||
local b = battler_of(id)
|
||||
if not AnimSeq._scene or (b and (b.semiInvulnerable or (b.substituteHP or 0) > 0)) then
|
||||
advance()
|
||||
return
|
||||
end
|
||||
wait_anim()
|
||||
Anim.launchStatus(d.name, {
|
||||
Anim.launchStatus(d.name, launch_opts(id, id, {
|
||||
force = true,
|
||||
attackerSide = side,
|
||||
targetSide = side,
|
||||
isReversed = side == "enemy",
|
||||
attackerSpecies = species_of(side),
|
||||
targetSpecies = species_of(side),
|
||||
ctx = ctx_for(side, side),
|
||||
ctx = ctx_for(id, id),
|
||||
onEnd = function()
|
||||
if AnimSeq._waiting then advance() end
|
||||
end,
|
||||
})
|
||||
}))
|
||||
launch_done()
|
||||
end
|
||||
|
||||
local function run_special(d)
|
||||
local side = d.attacker or "player"
|
||||
local p = Anim.present(side)
|
||||
local id = ev_id(d, "attackerId", "attacker") or 0
|
||||
local p = Anim.present(id)
|
||||
if d.name == "SUBSTITUTE_TO_MON" or d.name == "MON_TO_SUBSTITUTE" then
|
||||
local mid = tonumber(d.moveId)
|
||||
if not AnimSeq._scene and mid ~= MOVE_TRANSFORM and mid ~= MOVE_SUBSTITUTE then
|
||||
@@ -546,42 +591,38 @@ local function run_special(d)
|
||||
-- pokefirered/src/battle_controller_player.c:2338
|
||||
if d.name == "SUBSTITUTE_TO_MON" then
|
||||
if not (p and p.substitute) then advance() return end
|
||||
AnimSeq._subLowered[side] = true
|
||||
AnimSeq._subLowered[id] = true
|
||||
else
|
||||
if not AnimSeq._subLowered[side] then advance() return end
|
||||
AnimSeq._subLowered[side] = nil
|
||||
local b = battler_of(side)
|
||||
if not AnimSeq._subLowered[id] then advance() return end
|
||||
AnimSeq._subLowered[id] = nil
|
||||
local b = battler_of(id)
|
||||
if b and (b.substituteHP or 0) <= 0 then advance() return end
|
||||
end
|
||||
end
|
||||
wait_anim()
|
||||
Anim.launchSpecial(d.name, {
|
||||
attackerSide = side,
|
||||
targetSide = side,
|
||||
isReversed = side == "enemy",
|
||||
attackerSpecies = species_of(side),
|
||||
targetSpecies = species_of(side),
|
||||
ctx = ctx_for(side, side),
|
||||
Anim.launchSpecial(d.name, launch_opts(id, id, {
|
||||
ctx = ctx_for(id, id),
|
||||
onEnd = function()
|
||||
if d.name == "SUBSTITUTE_TO_MON" then
|
||||
Anim.setSubstitute(side, false)
|
||||
Anim.setSubstitute(id, false)
|
||||
elseif d.name == "MON_TO_SUBSTITUTE" then
|
||||
Anim.setSubstitute(side, true)
|
||||
Anim.setSubstitute(id, true)
|
||||
end
|
||||
local pp = Anim.present(side)
|
||||
local pp = Anim.present(id)
|
||||
if pp then pp.ox = 0 end
|
||||
if AnimSeq._waiting then advance() end
|
||||
end,
|
||||
})
|
||||
}))
|
||||
launch_done()
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_controller_player.c:2144
|
||||
local function run_switch_out(d)
|
||||
local side = d.side or "enemy"
|
||||
local p = Anim.present(side)
|
||||
local id = ev_id(d, "battler", "side") or 1
|
||||
local side = side_of(id)
|
||||
local p = Anim.present(id)
|
||||
local function hide()
|
||||
local pp = Anim.present(side)
|
||||
local pp = Anim.present(id)
|
||||
if pp then
|
||||
pp.visible = false
|
||||
pp.switchedOut = true
|
||||
@@ -589,37 +630,33 @@ local function run_switch_out(d)
|
||||
pp.sx, pp.sy = 1, 1
|
||||
end
|
||||
local s = Anim.stage()
|
||||
if s and s.healthbox and s.healthbox[side] then s.healthbox[side].visible = false end
|
||||
if s and s.healthbox and s.healthbox[id] then s.healthbox[id].visible = false end
|
||||
if AnimSeq._waiting then advance() end
|
||||
end
|
||||
if d.slot then Anim.setShown(side, stand_in(side, d.slot)) end
|
||||
if d.slot then Anim.setShown(id, stand_in(id, d.slot)) end
|
||||
if not p or p.visible == false then
|
||||
AnimSeq._waiting = true
|
||||
hide()
|
||||
return
|
||||
end
|
||||
local function out()
|
||||
Anim.launchSpecial((side == "player") and "SWITCH_OUT_PLAYER_MON" or "SWITCH_OUT_OPPONENT_MON", {
|
||||
attackerSide = side,
|
||||
targetSide = side,
|
||||
isReversed = side == "enemy",
|
||||
attackerSpecies = species_of(side),
|
||||
targetSpecies = species_of(side),
|
||||
ctx = ctx_for(side, side),
|
||||
Anim.launchSpecial((side == "player") and "SWITCH_OUT_PLAYER_MON" or "SWITCH_OUT_OPPONENT_MON", launch_opts(id, id, {
|
||||
ctx = ctx_for(id, id),
|
||||
onEnd = hide,
|
||||
})
|
||||
}))
|
||||
end
|
||||
wait_anim()
|
||||
if p.substitute then
|
||||
Anim.launchSpecial("SUBSTITUTE_TO_MON", {
|
||||
attackerSide = side, targetSide = side, isReversed = side == "enemy",
|
||||
ctx = ctx_for(side, side),
|
||||
local o = launch_opts(id, id, {
|
||||
ctx = ctx_for(id, id),
|
||||
onEnd = function()
|
||||
Anim.setSubstitute(side, false)
|
||||
Anim.setSubstitute(id, false)
|
||||
p.ox = 0
|
||||
out()
|
||||
end,
|
||||
})
|
||||
o.attackerSpecies, o.targetSpecies, o.speciesById = nil, nil, nil
|
||||
Anim.launchSpecial("SUBSTITUTE_TO_MON", o)
|
||||
else
|
||||
out()
|
||||
end
|
||||
@@ -627,16 +664,17 @@ local function run_switch_out(d)
|
||||
end
|
||||
|
||||
local function run_switch_in(d)
|
||||
local side = d.side or "enemy"
|
||||
local id = ev_id(d, "battler", "side") or 1
|
||||
local side = side_of(id)
|
||||
local st = battle_state()
|
||||
local p = Anim.present(side)
|
||||
Anim.setShown(side, nil)
|
||||
local p = Anim.present(id)
|
||||
Anim.setShown(id, nil)
|
||||
if p then
|
||||
p.switchedOut = nil
|
||||
-- pokefirered/src/battle_gfx_sfx_util.c:997
|
||||
p.castformForm, p.castformMon = nil, nil
|
||||
Anim.setSubstitute(side, false)
|
||||
local b = st and st[side]
|
||||
Anim.setSubstitute(id, false)
|
||||
local b = AnimCoords.battler(st, id)
|
||||
if b and b.mon then
|
||||
p.displayHp = tonumber(d.hp) or tonumber(b.mon.hp) or 0
|
||||
p.displayMaxHp = tonumber(b.mon.maxHp) or 1
|
||||
@@ -645,7 +683,8 @@ local function run_switch_in(d)
|
||||
end
|
||||
local SwitchSeq = require("src.core.game3.battle.switch_seq")
|
||||
AnimSeq._waitSwitch = true
|
||||
local started = SwitchSeq.beginEventSwitchIn(st, side, {
|
||||
local started = SwitchSeq.beginEventSwitchIn(st, (id < 2) and side or id, {
|
||||
battler = id,
|
||||
headless = Anim._headless,
|
||||
pushMsg = AnimSeq._pushMsg,
|
||||
onDone = function()
|
||||
@@ -671,8 +710,9 @@ local function run_step(step)
|
||||
return
|
||||
end
|
||||
if kind == "faint_cry" then
|
||||
local side = d.side or "enemy"
|
||||
local sp = species_of(side)
|
||||
local id = ev_id(d, "battler", "side") or 1
|
||||
local side = side_of(id)
|
||||
local sp = species_of(id)
|
||||
if sp and not Anim._headless then
|
||||
-- pokefirered/src/battle_controller_player.c:2696
|
||||
pcall(function()
|
||||
@@ -697,28 +737,30 @@ local function run_step(step)
|
||||
end
|
||||
if kind == "hitfx" then
|
||||
-- pokefirered/src/battle_controller_player.c:2658
|
||||
if d.effectiveness ~= nil then play_effectiveness_se(d.effectiveness, d.side) end
|
||||
local id = ev_id(d, "battler", "side")
|
||||
if d.effectiveness ~= nil then play_effectiveness_se(d.effectiveness, id) end
|
||||
wait_anim()
|
||||
Anim.blinkMon(d.side, { onComplete = function() if AnimSeq._waiting then advance() end end })
|
||||
Anim.blinkMon(id, { onComplete = function() if AnimSeq._waiting then advance() end end })
|
||||
launch_done()
|
||||
return
|
||||
end
|
||||
if kind == "hp" then
|
||||
wait_anim()
|
||||
Anim.tweenHp(d.side, d.from, d.to, d.maxHp, {
|
||||
Anim.tweenHp(ev_id(d, "battler", "side"), d.from, d.to, d.maxHp, {
|
||||
onComplete = function() if AnimSeq._waiting then advance() end end,
|
||||
})
|
||||
launch_done()
|
||||
return
|
||||
end
|
||||
if kind == "faint" then
|
||||
local p = Anim.present(d.side or "enemy")
|
||||
local id = ev_id(d, "battler", "side") or 1
|
||||
local p = Anim.present(id)
|
||||
if p then
|
||||
Anim.setSubstitute(d.side or "enemy", false)
|
||||
Anim.setSubstitute(id, false)
|
||||
p.blinkHidden = false
|
||||
end
|
||||
wait_anim()
|
||||
Anim.faintMon(d.side or "enemy", {
|
||||
Anim.faintMon(id, {
|
||||
onComplete = function() if AnimSeq._waiting then advance() end end,
|
||||
})
|
||||
launch_done()
|
||||
|
||||
@@ -136,9 +136,11 @@ end
|
||||
function AnimSprites.clearHost(hostId)
|
||||
if not hostId then return end
|
||||
AnimSprites.init()
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
local want = AnimCoords.idOf(hostId)
|
||||
for i = 1, AnimSprites.MAX do
|
||||
local s = AnimSprites._pool[i]
|
||||
if s.active and s.hostId == hostId then
|
||||
if s.active and s.hostId ~= nil and (s.hostId == hostId or (want ~= nil and AnimCoords.idOf(s.hostId) == want)) then
|
||||
clear_slot(s)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2865,8 +2865,11 @@ end
|
||||
AnimTasks.REGISTRY.GetTargetSide = AnimTasks.GetTargetSide
|
||||
AnimTasks.REGISTRY.AnimTask_GetTargetSide = AnimTasks.GetTargetSide
|
||||
|
||||
-- pokefirered/src/battle_anim_utility_funcs.c:712
|
||||
function AnimTasks.GetTargetIsAttackerPartner(t, vm)
|
||||
vm.args[7] = 0 -- In singles, target is never partner
|
||||
local atk = vm.attackerId and vm:attackerId() or 0
|
||||
local tgt = vm.targetId and vm:targetId() or 1
|
||||
vm.args[7] = (require("bit").bxor(atk, 2) == tgt) and 1 or 0
|
||||
destroy_task(t)
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ local bit = require("bit")
|
||||
local AnimSprites = require("src.core.game3.battle.anim_sprites")
|
||||
local AnimTasks = require("src.core.game3.battle.anim_tasks")
|
||||
local AnimPal = require("src.core.game3.battle.anim_pal")
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local band, rshift = bit.band, bit.rshift
|
||||
|
||||
@@ -140,6 +141,8 @@ function AnimVm.new()
|
||||
isReversed = false,
|
||||
_attackerSide = "player",
|
||||
_targetSide = "enemy",
|
||||
_atkId = 0,
|
||||
_tgtId = 1,
|
||||
pc = 1,
|
||||
script = nil,
|
||||
callStack = {},
|
||||
@@ -158,7 +161,7 @@ function AnimVm.new()
|
||||
_shader = nil,
|
||||
_cbMode = "run",
|
||||
_phase = "cb1",
|
||||
_monbg = {},
|
||||
_monbg = AnimCoords.idTable(),
|
||||
_bgPrio = { [1] = 2, [2] = 2 },
|
||||
_tagBlend = {},
|
||||
}
|
||||
@@ -174,10 +177,87 @@ function AnimVm:targetSide()
|
||||
return self._targetSide or (self._attackerSide == "player" and "enemy" or "player")
|
||||
end
|
||||
|
||||
function AnimVm:setBattlers(atkSide, tgtSide)
|
||||
if atkSide then self._attackerSide = atkSide end
|
||||
if tgtSide then self._targetSide = tgtSide end
|
||||
function AnimVm:attackerId()
|
||||
local id = self._atkId
|
||||
if id ~= nil and AnimCoords.sideOf(id) == self:attackerSide() then return id end
|
||||
return AnimCoords.fixedId(self:attackerSide()) or 0
|
||||
end
|
||||
|
||||
function AnimVm:targetId()
|
||||
local id = self._tgtId
|
||||
if id ~= nil and AnimCoords.sideOf(id) == self:targetSide() then return id end
|
||||
return AnimCoords.fixedId(self:targetSide()) or 1
|
||||
end
|
||||
|
||||
function AnimVm:allyPair()
|
||||
local a, t = self:attackerId(), self:targetId()
|
||||
return a ~= t and AnimCoords.sideOf(a) == AnimCoords.sideOf(t)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:860
|
||||
function AnimVm:isDouble()
|
||||
return AnimCoords.isDouble()
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:831
|
||||
function AnimVm:battlerAtPosition(position)
|
||||
local id = tonumber(position)
|
||||
if id == nil or id < 0 or id > 3 then return nil end
|
||||
if id >= 2 and not AnimCoords.isDouble() then return nil end
|
||||
return id
|
||||
end
|
||||
|
||||
local function key_to_id(key, cur)
|
||||
if type(key) == "number" then return AnimCoords.idOf(key) end
|
||||
if key == "player" or key == "enemy" then
|
||||
if cur ~= nil and AnimCoords.sideOf(cur) == key then return cur end
|
||||
return AnimCoords.idOf(key)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function AnimVm:applyBind()
|
||||
if self.active then AnimCoords.bind(self:attackerId(), self:targetId()) end
|
||||
end
|
||||
|
||||
function AnimVm:setBattlers(atk, tgt)
|
||||
local a = key_to_id(atk, self._atkId)
|
||||
local t = key_to_id(tgt, self._tgtId)
|
||||
if a ~= nil then
|
||||
self._atkId = a
|
||||
self._attackerSide = AnimCoords.sideOf(a)
|
||||
elseif atk then
|
||||
self._attackerSide = atk
|
||||
end
|
||||
if t ~= nil then
|
||||
self._tgtId = t
|
||||
self._targetSide = AnimCoords.sideOf(t)
|
||||
elseif tgt then
|
||||
self._targetSide = tgt
|
||||
end
|
||||
self.isReversed = (self._attackerSide == "enemy")
|
||||
self:applyBind()
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:333
|
||||
function AnimVm:battlerId(token)
|
||||
if token == nil then return self:targetId() end
|
||||
local n = tonumber(token)
|
||||
local s = type(token) == "string" and token:lower() or nil
|
||||
if n == 0 or s == "attacker" or s == "anim_attacker" then return self:attackerId() end
|
||||
if n == 1 or s == "target" or s == "anim_target" then return self:targetId() end
|
||||
local id
|
||||
if n == 2 or s == "atk_partner" or s == "anim_atk_partner" then
|
||||
id = AnimCoords.partner(self:attackerId())
|
||||
elseif n == 3 or s == "def_partner" or s == "anim_def_partner" then
|
||||
id = AnimCoords.partner(self:targetId())
|
||||
elseif s == "player" or s == "enemy" then
|
||||
return AnimCoords.idOf(s)
|
||||
else
|
||||
return self:targetId()
|
||||
end
|
||||
if AnimCoords.spritePresent(nil, id) then return id end
|
||||
return nil
|
||||
end
|
||||
|
||||
function AnimVm:resolveBattlerSide(token)
|
||||
@@ -185,6 +265,7 @@ function AnimVm:resolveBattlerSide(token)
|
||||
if type(token) == "number" then
|
||||
if token == 0 then return self:attackerSide()
|
||||
elseif token == 1 then return self:targetSide()
|
||||
elseif token == 2 or token == 3 then return self:battlerId(token)
|
||||
else return nil end
|
||||
end
|
||||
local s = tostring(token):lower()
|
||||
@@ -196,7 +277,7 @@ function AnimVm:resolveBattlerSide(token)
|
||||
end
|
||||
if s == "player" or s == "enemy" then return s end
|
||||
if s == "atk_partner" or s == "anim_atk_partner" or s == "def_partner" or s == "anim_def_partner" then
|
||||
return nil
|
||||
return self:battlerId(s)
|
||||
end
|
||||
return self:targetSide()
|
||||
end
|
||||
@@ -207,11 +288,25 @@ function AnimVm:x(v)
|
||||
return v
|
||||
end
|
||||
|
||||
local function live_species(id)
|
||||
local b = AnimCoords.battler(nil, id)
|
||||
if type(b) ~= "table" then return nil end
|
||||
if b.expTransform and b.expTransform.species then return b.expTransform.species end
|
||||
return b.species or (b.mon and (b.mon.species or b.mon.speciesId))
|
||||
end
|
||||
|
||||
function AnimVm:speciesForSide(side)
|
||||
if side == nil then return nil end
|
||||
local id = AnimCoords.idOf(side)
|
||||
if self._speciesBySide and self._speciesBySide[side] ~= nil then return self._speciesBySide[side] end
|
||||
if side == self:attackerSide() then return self._attackerSpecies end
|
||||
if side == self:targetSide() then return self._targetSpecies end
|
||||
if id ~= nil and id == self:attackerId() and self._attackerSpecies ~= nil then return self._attackerSpecies end
|
||||
if id ~= nil and id == self:targetId() and self._targetSpecies ~= nil then return self._targetSpecies end
|
||||
if id == nil then
|
||||
if side == self:attackerSide() then return self._attackerSpecies end
|
||||
if side == self:targetSide() then return self._targetSpecies end
|
||||
return nil
|
||||
end
|
||||
if id >= 2 then return live_species(id) end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -231,6 +326,8 @@ end
|
||||
|
||||
function AnimVm:battlerCenter(side)
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
if side == "attacker" then side = self:attackerId()
|
||||
elseif side == "target" then side = self:targetId() end
|
||||
return Anim.battlerCenter(side)
|
||||
end
|
||||
|
||||
@@ -291,8 +388,9 @@ function AnimVm:reset()
|
||||
self._attackerSpecies = nil
|
||||
self._targetSpecies = nil
|
||||
self._speciesBySide = nil
|
||||
self._monbg = {}
|
||||
self._monbg = AnimCoords.idTable()
|
||||
self._bgPrio = { [1] = 2, [2] = 2 }
|
||||
AnimCoords.bind(nil)
|
||||
self._tagBlend = {}
|
||||
self.bldAlpha = nil
|
||||
self.statusAnimActive = false
|
||||
@@ -320,7 +418,8 @@ local function finish(self)
|
||||
self._onEnd = nil
|
||||
AnimSprites.reset()
|
||||
AnimTasks.reset()
|
||||
self._monbg = {}
|
||||
self._monbg = AnimCoords.idTable()
|
||||
AnimCoords.bind(nil)
|
||||
if cb then pcall(cb) end
|
||||
end
|
||||
|
||||
@@ -342,22 +441,33 @@ local function begin(self, script, opts)
|
||||
self.script = script
|
||||
self.pc = 1
|
||||
self.isReversed = opts.isReversed and true or false
|
||||
self._attackerSide = opts.attackerSide or (self.isReversed and "enemy" or "player")
|
||||
self._targetSide = opts.targetSide or (self.isReversed and "player" or "enemy")
|
||||
if opts.attackerSide and opts.isReversed == nil then
|
||||
local atk, tgt = opts.attackerSide, opts.targetSide
|
||||
local atkId = tonumber(opts.attackerId) or AnimCoords.fixedId(atk)
|
||||
local tgtId = tonumber(opts.targetId) or AnimCoords.fixedId(tgt)
|
||||
if type(atk) ~= "string" then atk = atkId and AnimCoords.sideOf(atkId) or nil end
|
||||
if type(tgt) ~= "string" then tgt = tgtId and AnimCoords.sideOf(tgtId) or nil end
|
||||
self._attackerSide = atk or (self.isReversed and "enemy" or "player")
|
||||
self._targetSide = tgt or (self.isReversed and "player" or "enemy")
|
||||
if atk and opts.isReversed == nil then
|
||||
self.isReversed = (self._attackerSide == "enemy")
|
||||
end
|
||||
self._atkId = atkId or AnimCoords.fixedId(self._attackerSide)
|
||||
self._tgtId = tgtId or AnimCoords.fixedId(self._targetSide)
|
||||
AnimCoords.bind(self._atkId, self._tgtId)
|
||||
self._attackerSpecies = opts.attackerSpecies
|
||||
self._targetSpecies = opts.targetSpecies
|
||||
self._speciesBySide = {}
|
||||
self._speciesBySide = AnimCoords.idTable()
|
||||
if opts.speciesById then
|
||||
for k, v in pairs(opts.speciesById) do self._speciesBySide[k] = v end
|
||||
end
|
||||
if opts.speciesBySide then
|
||||
for k, v in pairs(opts.speciesBySide) do self._speciesBySide[k] = v end
|
||||
end
|
||||
if opts.attackerSpecies ~= nil and self._speciesBySide[self._attackerSide] == nil then
|
||||
self._speciesBySide[self._attackerSide] = opts.attackerSpecies
|
||||
if opts.attackerSpecies ~= nil and rawget(self._speciesBySide, self._atkId) == nil then
|
||||
rawset(self._speciesBySide, self._atkId, opts.attackerSpecies)
|
||||
end
|
||||
if opts.targetSpecies ~= nil and self._speciesBySide[self._targetSide] == nil then
|
||||
self._speciesBySide[self._targetSide] = opts.targetSpecies
|
||||
if opts.targetSpecies ~= nil and rawget(self._speciesBySide, self._tgtId) == nil then
|
||||
rawset(self._speciesBySide, self._tgtId, opts.targetSpecies)
|
||||
end
|
||||
self._onEnd = opts.onEnd
|
||||
self._turn = tonumber(opts.moveTurn or opts.turn) or 0
|
||||
@@ -487,13 +597,7 @@ local function effective_z(vm, s)
|
||||
local sub = tonumber(s.subpriority) or 0
|
||||
if pri <= 1 then return 900 + (255 - sub) % 99 end
|
||||
if pri >= 3 then return math.max(1, math.min(98, 98 - sub)) end
|
||||
local frontEnemy, frontPlayer
|
||||
if vm._monbg.enemy then frontEnemy = (vm._bgPrio[1] or 2) >= 2 else frontEnemy = sub < 40 end
|
||||
if vm._monbg.player then frontPlayer = (vm._bgPrio[2] or 2) >= 2 else frontPlayer = sub < 30 end
|
||||
local rank = math.max(0, math.min(98, 98 - sub))
|
||||
if frontPlayer then return 201 + rank end
|
||||
if frontEnemy then return 101 + rank end
|
||||
return rank
|
||||
return AnimCoords.layerZ(sub, vm._monbg, vm._bgPrio)
|
||||
end
|
||||
|
||||
local function draw_anim_bg(vm)
|
||||
@@ -770,9 +874,8 @@ local function pret_subpriority(vm, op)
|
||||
local raw = tonumber(op.subpriority) or 0
|
||||
local argVar = band(raw, 0x7F)
|
||||
if argVar >= 64 then argVar = argVar - 64 else argVar = -argVar end
|
||||
local side = (op.animBattler == "target") and vm:targetSide() or vm:attackerSide()
|
||||
local base = (side == "player") and 30 or 40
|
||||
local sub = base + argVar
|
||||
local id = (op.animBattler == "target") and vm:targetId() or vm:attackerId()
|
||||
local sub = AnimCoords.SUBPRIORITY[id] + argVar
|
||||
if sub < 3 then sub = 3 end
|
||||
return sub
|
||||
end
|
||||
@@ -878,36 +981,37 @@ local function run_createsprite(vm, op)
|
||||
)
|
||||
local isDynamicArg1 = (isSlash or cbName == "EndureEnergy")
|
||||
|
||||
local anchorSide = vm:resolveBattlerSide(op.animBattler or "attacker")
|
||||
local role = (op.animBattler == "target") and "target" or "attacker"
|
||||
local hFlip = false
|
||||
if isAttackerAlways then
|
||||
anchorSide = vm:resolveBattlerSide("attacker")
|
||||
role = "attacker"
|
||||
elseif isTravelDiagonally then
|
||||
local battlerArg = args[6]
|
||||
if battlerArg == 1 or battlerArg == "target" then
|
||||
anchorSide = vm:resolveBattlerSide("target")
|
||||
role = "target"
|
||||
else
|
||||
anchorSide = vm:resolveBattlerSide("attacker")
|
||||
role = "attacker"
|
||||
end
|
||||
elseif isTargetAlways then
|
||||
anchorSide = vm:resolveBattlerSide("target")
|
||||
role = "target"
|
||||
elseif isDynamicArg3 then
|
||||
local which = args[3]
|
||||
if which == 0 or which == "attacker" then
|
||||
anchorSide = vm:resolveBattlerSide("attacker")
|
||||
role = "attacker"
|
||||
elseif which == 1 or which == 2 or which == "target" or (which and which ~= 0) then
|
||||
anchorSide = vm:resolveBattlerSide("target")
|
||||
role = "target"
|
||||
end
|
||||
elseif isDynamicArg1 then
|
||||
if args[1] == 0 or args[1] == "attacker" then
|
||||
anchorSide = vm:resolveBattlerSide("attacker")
|
||||
role = "attacker"
|
||||
else
|
||||
anchorSide = vm:resolveBattlerSide("target")
|
||||
role = "target"
|
||||
end
|
||||
end
|
||||
anchorSide = anchorSide or vm:targetSide()
|
||||
local anchorId = (role == "attacker") and vm:attackerId() or vm:targetId()
|
||||
local anchorSide = (role == "attacker") and vm:attackerSide() or vm:targetSide()
|
||||
|
||||
local cx, cy = vm:battlerCenter(anchorSide)
|
||||
local cx, cy = vm:battlerCenter(anchorId)
|
||||
if isCutting and anchorSide == "player" then cy = cy + 8 end
|
||||
|
||||
local ox, oy, dir = 0, 0, 0
|
||||
@@ -948,7 +1052,7 @@ local function run_createsprite(vm, op)
|
||||
z = AnimSprites.Z.MID_FIELD,
|
||||
priority = 2,
|
||||
subpriority = subpri,
|
||||
hostId = anchorSide,
|
||||
hostId = anchorId,
|
||||
blendMode = "alpha",
|
||||
image = (not noGfx) and img or nil,
|
||||
w = bw,
|
||||
@@ -972,6 +1076,7 @@ local function run_createsprite(vm, op)
|
||||
spr._reversed = vm.isReversed
|
||||
spr._args = args
|
||||
spr._anchorSide = anchorSide
|
||||
spr._anchorId = anchorId
|
||||
spr._cbName = cbName
|
||||
if op.z or op.depth then
|
||||
spr._pz = nil
|
||||
@@ -983,8 +1088,8 @@ local function run_createsprite(vm, op)
|
||||
spr.data[0] = 0
|
||||
spr.data[1] = 0
|
||||
spr.data[2] = isCutting and dir or (cbName == "RoarNoiseLine" and dir or (tonumber(args[3]) or 0))
|
||||
local tx, ty = vm:battlerCenter(vm:targetSide())
|
||||
local ax, ay = vm:battlerCenter(vm:attackerSide())
|
||||
local tx, ty = vm:battlerCenter(vm:targetId())
|
||||
local ax, ay = vm:battlerCenter(vm:attackerId())
|
||||
spr._attackerX, spr._attackerY = ax, ay
|
||||
if isProjectile then
|
||||
spr._targetX = tx + vm:x(tonumber(args[3]) or 0)
|
||||
@@ -1141,10 +1246,13 @@ end
|
||||
local function task_clear_monbg(t, vm)
|
||||
t._n = (t._n or 0) + 1
|
||||
if t._n ~= 1 then
|
||||
if t._side then vm._monbg[t._side] = nil end
|
||||
local p = t._side and require("src.core.game3.battle.anim").present(t._side)
|
||||
if p and t._origZ then p.z = t._origZ end
|
||||
if p then p.monbg = false end
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
for _, e in ipairs(t._ids or {}) do
|
||||
rawset(vm._monbg, e.id, nil)
|
||||
local p = Anim.present(e.id)
|
||||
if p and e.origZ then p.z = e.origZ end
|
||||
if p then p.monbg = false end
|
||||
end
|
||||
AnimTasks.destroy(t)
|
||||
end
|
||||
end
|
||||
@@ -1152,8 +1260,16 @@ AnimTasks.REGISTRY._G4ClearMonBg = task_clear_monbg
|
||||
|
||||
local function battler_from_monbg_token(vm, token)
|
||||
local s = tostring(token or "target")
|
||||
if s == "attacker" or s == "atk_partner" then return vm:attackerSide() end
|
||||
return vm:targetSide()
|
||||
if s == "attacker" or s == "atk_partner" then return vm:attackerId() end
|
||||
return vm:targetId()
|
||||
end
|
||||
|
||||
local function monbg_ids(vm, token)
|
||||
local id = battler_from_monbg_token(vm, token)
|
||||
local out = { id }
|
||||
local partner = AnimCoords.partner(id)
|
||||
if AnimCoords.spritePresent(nil, partner) then out[2] = partner end
|
||||
return out
|
||||
end
|
||||
|
||||
local OPS = {}
|
||||
@@ -1350,37 +1466,42 @@ OPS.panse_adjustall = function(vm, op) return panse(vm, op, "adjustall") end
|
||||
-- pokefirered/src/battle_anim.c:531
|
||||
OPS.monbg = function(vm, op)
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local side = battler_from_monbg_token(vm, op.battler)
|
||||
local p = Anim.present(side)
|
||||
if p and p.visible ~= false then
|
||||
vm._monbg[side] = true
|
||||
vm._bgPrio[side == "enemy" and 1 or 2] = 2
|
||||
if p._g4OrigZ == nil then p._g4OrigZ = p.z end
|
||||
p.z = AnimVm.Z.BEHIND
|
||||
p.monbg = true
|
||||
for _, id in ipairs(monbg_ids(vm, op.battler)) do
|
||||
local p = Anim.present(id)
|
||||
if p and p.visible ~= false then
|
||||
rawset(vm._monbg, id, true)
|
||||
vm._bgPrio[AnimCoords.BG_PRIORITY_RANK[id]] = 2
|
||||
if p._g4OrigZ == nil then p._g4OrigZ = p.z end
|
||||
p.z = AnimVm.Z.BEHIND
|
||||
p.monbg = true
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
OPS.monbg_static = function(vm, op)
|
||||
local side = battler_from_monbg_token(vm, op.battler)
|
||||
vm._bgPrio[side == "enemy" and 1 or 2] = 2
|
||||
local id = battler_from_monbg_token(vm, op.battler)
|
||||
vm._bgPrio[AnimCoords.BG_PRIORITY_RANK[id]] = 2
|
||||
return true
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_anim.c:754
|
||||
OPS.clearmonbg = function(vm, op)
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local side = battler_from_monbg_token(vm, op.battler)
|
||||
local p = Anim.present(side)
|
||||
local ids = {}
|
||||
for _, id in ipairs(monbg_ids(vm, op.battler)) do
|
||||
local p = Anim.present(id)
|
||||
local e = { id = id }
|
||||
e.origZ = p and p._g4OrigZ or ((AnimCoords.sideOf(id) == "player") and AnimVm.Z.PLAYER or AnimVm.Z.ENEMY)
|
||||
if rawget(vm._monbg, id) then ids[#ids + 1] = e end
|
||||
if p then p._g4OrigZ = nil end
|
||||
end
|
||||
local t = AnimTasks.spawn("_G4ClearMonBg", 5, {}, vm)
|
||||
if t then
|
||||
t._g4kind = "aux"
|
||||
t._side = vm._monbg[side] and side or nil
|
||||
t._origZ = p and p._g4OrigZ or ((side == "player") and AnimVm.Z.PLAYER or AnimVm.Z.ENEMY)
|
||||
elseif vm._monbg[side] then
|
||||
vm._monbg[side] = nil
|
||||
t._ids = ids
|
||||
else
|
||||
for _, e in ipairs(ids) do rawset(vm._monbg, e.id, nil) end
|
||||
end
|
||||
if p then p._g4OrigZ = nil end
|
||||
return true
|
||||
end
|
||||
OPS.clearmonbg_static = OPS.nop
|
||||
@@ -1503,9 +1624,9 @@ end
|
||||
|
||||
-- pokefirered/src/battle_anim.c:1574
|
||||
OPS.splitbgprio = function(vm, op)
|
||||
local side = (op.battler == "attacker") and vm:attackerSide() or vm:targetSide()
|
||||
local id = (op.battler == "attacker") and vm:attackerId() or vm:targetId()
|
||||
if op.mode == "foes" and vm:attackerSide() == vm:targetSide() then return true end
|
||||
if op.mode == "all" or side == "player" then
|
||||
if op.mode == "all" or AnimCoords.BG_PRIORITY_RANK[id] == 2 then
|
||||
vm._bgPrio[1] = 1
|
||||
vm._bgPrio[2] = 2
|
||||
end
|
||||
@@ -1526,8 +1647,8 @@ end
|
||||
-- pokefirered/src/battle_anim.c:1631
|
||||
local function set_visible(vm, op, visible)
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local side = vm:resolveBattlerSide(op.battler or "attacker")
|
||||
local p = side and Anim.present(side)
|
||||
local id = vm:battlerId(op.battler or "attacker")
|
||||
local p = id and Anim.present(id)
|
||||
if p then p.visible = visible end
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
local BallOpen = {}
|
||||
|
||||
BallOpen.CACHE_SUB = "pokemon/battle/ball_open"
|
||||
@@ -36,7 +38,7 @@ function BallOpen.reset()
|
||||
BallOpen._sprites = {}
|
||||
BallOpen._particles = {}
|
||||
BallOpen._tasks = {}
|
||||
BallOpen._mon = {}
|
||||
BallOpen._mon = AnimCoords.idTable()
|
||||
BallOpen._fade = new_fade()
|
||||
end
|
||||
|
||||
@@ -398,6 +400,7 @@ end
|
||||
|
||||
-- pokefirered/src/pokeball.c:763
|
||||
function BallOpen.start(side, x, y, ballItem, unfadeLater)
|
||||
side = AnimCoords.fixedId(side) or side
|
||||
local ballId = BallOpen.ballIdForItem(ballItem)
|
||||
-- pokefirered/src/battle_anim_special.c:1427
|
||||
local tasks = BallOpen._tasks
|
||||
@@ -505,7 +508,7 @@ function BallOpen.fadeActive()
|
||||
end
|
||||
|
||||
function BallOpen.monBlend(side)
|
||||
local m = BallOpen._mon[side]
|
||||
local m = BallOpen._mon[AnimCoords.fixedId(side) or side]
|
||||
if not m or m.coeff <= 0 then return 0 end
|
||||
local d = BallOpen.data()
|
||||
local c = d and d.fadeColors and d.fadeColors[m.ballId + 1]
|
||||
|
||||
@@ -43,6 +43,7 @@ function CatchSeq.reset()
|
||||
CatchSeq._catchResult = nil
|
||||
CatchSeq._ball = nil
|
||||
CatchSeq._waitingBall = false
|
||||
CatchSeq._target = 1
|
||||
end
|
||||
|
||||
function CatchSeq.busy()
|
||||
@@ -86,6 +87,7 @@ function CatchSeq.begin(st, itemId, caught, shakes, opts)
|
||||
CatchSeq._headless = opts.headless and true or false
|
||||
CatchSeq._session = opts.session
|
||||
CatchSeq._result = caught and "catch" or "fail_catch"
|
||||
CatchSeq._target = tonumber(opts.target) or 1
|
||||
|
||||
local session = opts.session
|
||||
if not session then
|
||||
@@ -348,7 +350,7 @@ function CB.arcFlight(b)
|
||||
for i = 0, 7 do b.data[i] = 0 end
|
||||
b.cb = CB.tenFrameDelay
|
||||
-- pokefirered/src/battle_anim_special.c:857
|
||||
BallOpen.start("enemy", b.x, b.y, b.itemId, false)
|
||||
BallOpen.start(b.target or 1, b.x, b.y, b.itemId, false)
|
||||
play_se(SE.SE_BALL_OPEN)
|
||||
end
|
||||
|
||||
@@ -552,7 +554,7 @@ function CB.beginBreakOut(b)
|
||||
start_anim(b, 1)
|
||||
affine_start(b.aff, 0)
|
||||
b.cb = CB.runBreakOut
|
||||
BallOpen.start("enemy", b.x, b.y, b.itemId, true)
|
||||
BallOpen.start(b.target or 1, b.x, b.y, b.itemId, true)
|
||||
play_se(SE.SE_BALL_OPEN)
|
||||
b.mon.visible = true
|
||||
b.monAff = { paused = false }
|
||||
@@ -684,22 +686,24 @@ end
|
||||
-- pokefirered/src/battle_anim_special.c:734
|
||||
local function start_ball(d)
|
||||
local st = CatchSeq._st
|
||||
local enemy = st and st.enemy
|
||||
local target = tonumber(d.target) or CatchSeq._target or 1
|
||||
local enemy = Anim.Coords.battler(st, target)
|
||||
local sp = enemy and enemy.species
|
||||
if not sp and enemy and enemy.mon then
|
||||
sp = Pokemon.speciesOf and Pokemon.speciesOf(enemy.mon) or enemy.mon.species or enemy.mon.speciesId
|
||||
end
|
||||
local base = Anim.coords(st, target) or Anim.ENEMY_MON
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
local monY = Anim.ENEMY_MON.y
|
||||
local monY = base.y
|
||||
if Ui.battlerSpriteCenter then
|
||||
local _, cy = Ui.battlerSpriteCenter("enemy", sp, Anim.ENEMY_MON)
|
||||
local _, cy = Ui.battlerSpriteCenter("enemy", sp, { x = base.x, y = base.y })
|
||||
monY = cy
|
||||
end
|
||||
local stage = Anim.stage().ball
|
||||
stage.visible, stage.darken, stage.flash, stage.side = false, 0, 0, "enemy"
|
||||
local b = {
|
||||
x = 32, y = 80, x2 = 0, y2 = 0,
|
||||
data = { [0] = 34, Anim.ENEMY_MON.x, Anim.ENEMY_MON.y - 16, 0, 0, 0, 0, 0 },
|
||||
data = { [0] = 34, base.x, base.y - 16, 0, 0, 0, 0, 0 },
|
||||
anims = BALL_ANIMS, animNum = 0, animBeginning = true, animEnded = false, animPaused = false,
|
||||
frame = 0, hFlip = false, delay = 0, cmd = 1,
|
||||
aff = { paused = false },
|
||||
@@ -708,7 +712,8 @@ local function start_ball(d)
|
||||
subpx = 0,
|
||||
caseId = d.caseId or 0,
|
||||
itemId = d.itemId,
|
||||
mon = Anim.present("enemy"),
|
||||
mon = Anim.present(target),
|
||||
target = target,
|
||||
monY = monY,
|
||||
stage = stage,
|
||||
cb = CB.init,
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
-- Pure functions for catch calculations, ball multipliers, shake factor math,
|
||||
-- and caught Pokémon persistence into party or PC storage.
|
||||
--
|
||||
-- Emits standard mod event bus signals:
|
||||
-- - "battle.ball_thrown" { ball, target }
|
||||
-- - "catch.rate" { ball, rate, target }
|
||||
-- - "mon.caught" { mon, location, firstTime }
|
||||
-- Emits battle.ball_thrown and pokemon.caught; the roll is hooked as catch.rate.
|
||||
|
||||
local ItemsData = require("src.core.game3.items_data")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local Types = require("src.core.game3.battle.types")
|
||||
local Dex = require("src.core.game3.dex")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Catching = {}
|
||||
|
||||
@@ -32,13 +30,6 @@ Catching.BALL_BONUS = {
|
||||
[12] = 10, -- PREMIER_BALL
|
||||
}
|
||||
|
||||
local function emit(event, payload)
|
||||
local okR, Runtime = pcall(require, "src.mods.Runtime")
|
||||
if okR and Runtime and Runtime.emit then
|
||||
pcall(Runtime.emit, event, payload)
|
||||
end
|
||||
end
|
||||
|
||||
local function roll_rng(rng, lo, hi)
|
||||
lo = lo or 0
|
||||
hi = hi or 255
|
||||
@@ -56,6 +47,14 @@ local function roll_rng(rng, lo, hi)
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:9471
|
||||
function Catching.targetFor(st, attackerId)
|
||||
local id = (tonumber(attackerId) or 0)
|
||||
local t = (id % 2 == 0) and (id + 1) or (id - 1)
|
||||
if t == 1 or not st then return st and st.enemy end
|
||||
return st.battlers and st.battlers[t] or st.enemy
|
||||
end
|
||||
|
||||
function Catching.isBall(id)
|
||||
local num = ItemsData.toNumericId(id) or tonumber(id)
|
||||
return num and num >= 1 and num <= 12
|
||||
@@ -140,18 +139,13 @@ function Catching.catchOdds(itemId, foeBattler, st, session)
|
||||
return math.max(1, math.min(255, odds))
|
||||
end
|
||||
|
||||
--- Attempt to catch the foe.
|
||||
-- Returns: caught (bool), shakes (0..4).
|
||||
function Catching.tryCatch(itemId, foeBattler, st, session, rng)
|
||||
emit("battle.ball_thrown", { ball = itemId, target = foeBattler })
|
||||
local function vanilla_catch(itemId, foeBattler, st, session, rng)
|
||||
local num = ItemsData.toNumericId(itemId) or tonumber(itemId) or 4
|
||||
if num == 1 then
|
||||
emit("catch.rate", { ball = itemId, rate = 255, target = foeBattler })
|
||||
return true, 4
|
||||
end
|
||||
|
||||
local odds = Catching.catchOdds(itemId, foeBattler, st, session)
|
||||
emit("catch.rate", { ball = itemId, rate = odds, target = foeBattler })
|
||||
|
||||
if odds >= 255 then
|
||||
return true, 4
|
||||
@@ -174,6 +168,49 @@ function Catching.tryCatch(itemId, foeBattler, st, session, rng)
|
||||
return true, 4
|
||||
end
|
||||
|
||||
local function battle_state(st)
|
||||
if st then return st end
|
||||
local B = package.loaded["src.core.game3.battle.init"] or package.loaded["src.core.game3.battle"]
|
||||
return B and B.getState and B.getState() or nil
|
||||
end
|
||||
|
||||
--- Attempt to catch the foe.
|
||||
-- Returns: caught (bool), shakes (0..4).
|
||||
-- pokefirered/src/battle_script_commands.c:9463
|
||||
function Catching.tryCatch(itemId, foeBattler, st, session, rng)
|
||||
local caught, shakes
|
||||
if ModRuntime.wantsHook("catch.rate") then
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local mon = foeBattler and foeBattler.mon
|
||||
local species = foeBattler and (foeBattler.species or (mon and (mon.species or mon.speciesId)))
|
||||
local ballNum = ItemsData.toNumericId(itemId) or tonumber(itemId) or 4
|
||||
caught, shakes = ModRuntime.call("catch.rate", function(b, _, _, o)
|
||||
local id = (b ~= nil and G3.itemId(b)) or ballNum
|
||||
return vanilla_catch(id, o.target, st, o.session, o.rng)
|
||||
end, G3.itemName(ballNum) or "POKE_BALL", mon, G3.speciesView(species), {
|
||||
battle = battle_state(st), target = foeBattler, session = session, rng = rng,
|
||||
ballId = ballNum, species = G3.speciesName(species), speciesId = tonumber(species),
|
||||
rate = Catching.catchOdds(itemId, foeBattler, st, session),
|
||||
})
|
||||
caught = caught and true or false
|
||||
shakes = tonumber(shakes) or (caught and 4 or 0)
|
||||
else
|
||||
caught, shakes = vanilla_catch(itemId, foeBattler, st, session, rng)
|
||||
end
|
||||
if ModRuntime.wants("battle.ball_thrown") then
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local mon = foeBattler and foeBattler.mon
|
||||
local species = foeBattler and (foeBattler.species or (mon and (mon.species or mon.speciesId)))
|
||||
local ballNum = ItemsData.toNumericId(itemId) or tonumber(itemId) or 4
|
||||
ModRuntime.emit("battle.ball_thrown", {
|
||||
battle = battle_state(st), ball = G3.itemName(ballNum), ballId = ballNum,
|
||||
caught = caught, shakes = shakes, mon = mon, target = foeBattler,
|
||||
species = G3.speciesName(species), speciesId = tonumber(species),
|
||||
})
|
||||
end
|
||||
return caught, shakes
|
||||
end
|
||||
|
||||
local function clone_mon(mon)
|
||||
if type(mon) ~= "table" then return nil end
|
||||
local copy = {}
|
||||
@@ -240,7 +277,18 @@ function Catching.storeCaught(session, foeBattler, ballId)
|
||||
end
|
||||
end
|
||||
|
||||
emit("mon.caught", { mon = mon, location = location, firstTime = firstTimeCaught, box = boxId, slot = boxSlot })
|
||||
-- pokefirered/src/battle_script_commands.c:9617
|
||||
if ModRuntime.wants("pokemon.caught") then
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local R = package.loaded["src.core.game3.runtime"]
|
||||
ModRuntime.emit("pokemon.caught", {
|
||||
battle = battle_state(nil), mon = mon, species = G3.speciesName(species),
|
||||
speciesId = tonumber(species), isNew = firstTimeCaught,
|
||||
ball = G3.itemName(mon.pokeball), ballId = mon.pokeball,
|
||||
destination = location == "pc" and "box" or "party",
|
||||
box = boxId, slot = boxSlot, game = R and R._game or nil,
|
||||
})
|
||||
end
|
||||
|
||||
return {
|
||||
success = true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
-- Battle commands: FIGHT / BAG / POKéMON / RUN (+ move slots).
|
||||
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Commands = {}
|
||||
|
||||
Commands.MENU = { "FIGHT", "BAG", "POKEMON", "RUN" }
|
||||
@@ -8,9 +10,36 @@ function Commands.defaultMenuIndex()
|
||||
return 1 -- FIGHT
|
||||
end
|
||||
|
||||
local function battler_of(st, id)
|
||||
if id == nil or id == 0 then return st and st.player end
|
||||
if id == 1 then return st and st.enemy end
|
||||
return st and st.battlers and st.battlers[id]
|
||||
end
|
||||
Commands.battlerOf = battler_of
|
||||
|
||||
local function move_target_type(mv)
|
||||
local ok, Moves = pcall(require, "src.core.game3.battle.moves")
|
||||
if not ok or mv == nil then return nil end
|
||||
local m = Moves.get(mv)
|
||||
return tonumber(m and m.target) or 0
|
||||
end
|
||||
|
||||
local function tag(act, id, targetId)
|
||||
if not act then return act end
|
||||
act.battler = id or 0
|
||||
if targetId ~= nil then act.target = targetId end
|
||||
if act.kind == "move" then act.targetType = move_target_type(act.move) end
|
||||
return act
|
||||
end
|
||||
|
||||
--- Build a player action from menu selection.
|
||||
-- menuIndex 1..4; moveSlot 1..4 when FIGHT.
|
||||
function Commands.playerAction(st, menuIndex, moveSlot)
|
||||
function Commands.playerAction(st, menuIndex, moveSlot, battlerId, targetId)
|
||||
if battlerId ~= nil or targetId ~= nil then
|
||||
local b = battler_of(st, battlerId)
|
||||
local act = Commands.playerAction({ player = b }, menuIndex, moveSlot, nil, nil)
|
||||
return tag(act, battlerId or 0, targetId)
|
||||
end
|
||||
menuIndex = menuIndex or 1
|
||||
local kind = Commands.MENU[menuIndex] or "FIGHT"
|
||||
if kind == "FIGHT" then
|
||||
@@ -67,11 +96,21 @@ local function foe_of(st, b)
|
||||
return (b.side == "enemy") and st.player or st.enemy
|
||||
end
|
||||
|
||||
local function foes_of(st, b)
|
||||
if st and st.double then
|
||||
local State = require("src.core.game3.battle.state")
|
||||
return State.foes(st, b)
|
||||
end
|
||||
return { foe_of(st, b) }
|
||||
end
|
||||
|
||||
local function imprisoned(st, b, num)
|
||||
local foe = foe_of(st, b)
|
||||
if not (foe and foe.expImprison and foe.mon and foe.mon.moves) then return false end
|
||||
for i = 1, 4 do
|
||||
if move_num(foe.mon.moves[i]) == num and num ~= 0 then return true end
|
||||
for _, foe in ipairs(foes_of(st, b)) do
|
||||
if foe and foe.expImprison and foe.mon and foe.mon.moves then
|
||||
for i = 1, 4 do
|
||||
if move_num(foe.mon.moves[i]) == num and num ~= 0 then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
@@ -84,8 +123,8 @@ local function choiced(b)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_util.c:302
|
||||
function Commands.selectionError(st, slot)
|
||||
local b = st and st.player
|
||||
function Commands.selectionError(st, slot, battlerId)
|
||||
local b = battler_of(st, battlerId)
|
||||
local mon = b and b.mon
|
||||
if not mon or not slot then return nil end
|
||||
local mv = mon.moves and mon.moves[slot]
|
||||
@@ -123,24 +162,26 @@ function Commands.selectionError(st, slot)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_util.c:361
|
||||
function Commands.moveUsable(st, slot)
|
||||
local mon = st and st.player and st.player.mon
|
||||
function Commands.moveUsable(st, slot, battlerId)
|
||||
local b = battler_of(st, battlerId)
|
||||
local mon = b and b.mon
|
||||
local mv = mon and mon.moves and mon.moves[slot]
|
||||
if move_num(mv) == 0 then return false end
|
||||
return Commands.selectionError(st, slot) == nil
|
||||
return Commands.selectionError(st, slot, battlerId) == nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_main.c:3146
|
||||
function Commands.fightShortcut(st)
|
||||
local b = st and st.player
|
||||
function Commands.fightShortcut(st, battlerId)
|
||||
local b = battler_of(st, battlerId)
|
||||
if not b or not b.mon then return nil end
|
||||
local any = false
|
||||
for i = 1, 4 do
|
||||
if Commands.moveUsable(st, i) then any = true break end
|
||||
if Commands.moveUsable(st, i, battlerId) then any = true break end
|
||||
end
|
||||
if not any then
|
||||
return { kind = "move", move = "STRUGGLE", slot = nil, user = "player" },
|
||||
string.format("%s has no\nmoves left!", battler_name(b))
|
||||
local act = { kind = "move", move = "STRUGGLE", slot = nil, user = "player" }
|
||||
if battlerId ~= nil then tag(act, battlerId) end
|
||||
return act, string.format("%s has no\nmoves left!", battler_name(b))
|
||||
end
|
||||
if b.expEncoreMove and (tonumber(b.expEncoreTurns) or 0) > 0 then
|
||||
local slot = b.expEncoreSlot
|
||||
@@ -150,14 +191,16 @@ function Commands.fightShortcut(st)
|
||||
end
|
||||
end
|
||||
if slot then
|
||||
return { kind = "move", move = b.mon.moves[slot], slot = slot, user = "player" }
|
||||
local act = { kind = "move", move = b.mon.moves[slot], slot = slot, user = "player" }
|
||||
if battlerId ~= nil then tag(act, battlerId) end
|
||||
return act
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:5916
|
||||
function Commands.switchError(st, slot, forced)
|
||||
function Commands.switchError(st, slot, forced, battlerId)
|
||||
local party = st and st.playerParty
|
||||
local mon = party and party[slot]
|
||||
if not mon then return nil end
|
||||
@@ -165,35 +208,131 @@ function Commands.switchError(st, slot, forced)
|
||||
local name = Pokemon.displayMonName and Pokemon.displayMonName(mon) or "POKéMON"
|
||||
if (tonumber(mon.hp) or 0) <= 0 then return name .. " has no energy\nleft to battle!" end
|
||||
if st.player and st.player.partyIndex == slot then return name .. " is already\nin battle!" end
|
||||
if st.double then
|
||||
-- pokefirered/src/party_menu.c:5934
|
||||
local b2 = battler_of(st, 2)
|
||||
if b2 and b2.partyIndex == slot and not (st.absent and st.absent[2]) then
|
||||
return name .. " is already\nin battle!"
|
||||
end
|
||||
local pend = st.monToSwitchInto or {}
|
||||
local partner = (battlerId == 2) and 0 or 2
|
||||
if battlerId ~= nil and pend[partner] == slot then
|
||||
return name .. " has already been\nselected."
|
||||
end
|
||||
end
|
||||
if mon.isEgg then return "An EGG can't battle!" end
|
||||
if forced then return nil end
|
||||
local Engine = package.loaded["src.core.game3.battle.engine"]
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local ad = Battle and Battle._adapter
|
||||
if Engine and Engine.canSwitch and ad then
|
||||
local ok, why = Engine.canSwitch(st, ad, st.player)
|
||||
local ok, why = Engine.canSwitch(st, ad, battler_of(st, battlerId))
|
||||
if not ok then return why end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Commands.enemyAction(st)
|
||||
local ok, act = pcall(function()
|
||||
local Ai = require("src.core.game3.battle.ai")
|
||||
return Ai.chooseMove(st)
|
||||
end)
|
||||
if ok and act and act.kind == "move" then
|
||||
return act
|
||||
end
|
||||
local mon = st.enemy and st.enemy.mon
|
||||
Commands.aiHook = nil
|
||||
|
||||
function Commands.setAiHook(fn) Commands.aiHook = fn end
|
||||
|
||||
local function first_usable_action(b, id)
|
||||
local mon = b and b.mon
|
||||
for i = 1, 4 do
|
||||
local mv = mon and mon.moves and mon.moves[i]
|
||||
local p = mon and mon.pp and mon.pp[i]
|
||||
if mv and mv ~= 0 and mv ~= "" and (p == nil or tonumber(p) > 0) then
|
||||
return { kind = "move", move = mv, slot = i, user = "enemy" }
|
||||
return { kind = "move", move = mv, slot = i, user = "enemy", battler = id }
|
||||
end
|
||||
end
|
||||
return { kind = "move", move = "STRUGGLE", slot = nil, user = "enemy" }
|
||||
return { kind = "move", move = "STRUGGLE", slot = nil, user = "enemy", battler = id }
|
||||
end
|
||||
|
||||
local function vanilla_enemy_action(st, battlerId)
|
||||
local id = battlerId or 1
|
||||
if st and st.double then
|
||||
local b = battler_of(st, id)
|
||||
local act
|
||||
local hook = Commands.aiHook
|
||||
local ok, res = pcall(function()
|
||||
if hook then return hook(st, id) end
|
||||
local Ai = require("src.core.game3.battle.ai")
|
||||
if Ai.chooseAction then return Ai.chooseAction(st, id) end
|
||||
return Ai.chooseMove(st, { battler = id })
|
||||
end)
|
||||
if ok and res and res.kind and (res.battler == id or (res.battler == nil and id == 1)) then act = res end
|
||||
act = act or first_usable_action(b, id)
|
||||
return tag(act, id, act.target)
|
||||
end
|
||||
local ok, act = pcall(function()
|
||||
local Ai = require("src.core.game3.battle.ai")
|
||||
if Ai.chooseAction then return Ai.chooseAction(st, 1) end
|
||||
return Ai.chooseMove(st)
|
||||
end)
|
||||
if ok and act and act.kind == "move" then
|
||||
act.battler = (battlerId ~= nil) and 1 or nil
|
||||
return act
|
||||
end
|
||||
if ok and act and (act.kind == "switch" or act.kind == "item" or act.kind == "run" or act.kind == "watch") then
|
||||
act.battler = 1
|
||||
return act
|
||||
end
|
||||
local fb = first_usable_action(st.enemy, nil)
|
||||
fb.battler = nil
|
||||
if battlerId ~= nil then fb.battler = 1 end
|
||||
return fb
|
||||
end
|
||||
|
||||
local function normalize_enemy_action(st, res, battlerId)
|
||||
local id = battlerId or 1
|
||||
if type(res) == "string" or type(res) == "number" then res = { kind = "move", move = res } end
|
||||
if type(res) ~= "table" then return nil end
|
||||
local act = {}
|
||||
for k, v in pairs(res) do act[k] = v end
|
||||
act.kind = act.kind or "move"
|
||||
if act.kind == "move" then
|
||||
local ref = act.move or act.id
|
||||
local num = ref ~= nil and require("src.mods.Gen3Compat").moveId(ref) or nil
|
||||
if not num then return nil end
|
||||
local b = battler_of(st, id)
|
||||
local moves = b and b.mon and b.mon.moves or {}
|
||||
act.move, act.id = num, nil
|
||||
if not act.slot or tonumber(moves[act.slot]) ~= num then
|
||||
act.slot = nil
|
||||
for i = 1, 4 do
|
||||
if tonumber(moves[i]) == num then act.slot = i break end
|
||||
end
|
||||
end
|
||||
end
|
||||
act.user = act.user or "enemy"
|
||||
if st and st.double then return tag(act, id, act.target) end
|
||||
act.battler = (battlerId ~= nil) and 1 or nil
|
||||
return act
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_controller_opponent.c:1350
|
||||
function Commands.enemyAction(st, battlerId)
|
||||
if not ModRuntime.wantsHook("battle.enemy_action") then
|
||||
return vanilla_enemy_action(st, battlerId)
|
||||
end
|
||||
local vanilla
|
||||
local res = ModRuntime.call("battle.enemy_action", function(battle, bid)
|
||||
vanilla = vanilla_enemy_action(battle, bid)
|
||||
return vanilla
|
||||
end, st, battlerId)
|
||||
if res ~= nil and res == vanilla then return vanilla end
|
||||
return normalize_enemy_action(st, res, battlerId) or vanilla or vanilla_enemy_action(st, battlerId)
|
||||
end
|
||||
|
||||
function Commands.enemyActions(st)
|
||||
local out = {}
|
||||
for _, id in ipairs({ 1, 3 }) do
|
||||
local b = battler_of(st, id)
|
||||
if b and not (st.absent and st.absent[id]) and (id == 1 or st.double) then
|
||||
out[id] = Commands.enemyAction(st, id)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
--- Wild flee: pret-ish odds from speed (simplified).
|
||||
|
||||
@@ -4,6 +4,7 @@ local Rules = require("src.core.game3.battle.rules")
|
||||
local Types = require("src.core.game3.battle.types")
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
local EffectIds = require("src.core.game3.battle.effect_ids")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Damage = {}
|
||||
|
||||
@@ -272,7 +273,15 @@ function Damage.base(attacker, defender, move, opts)
|
||||
damage = math.floor(damage / helper)
|
||||
damage = math.floor(damage / 50)
|
||||
if status_of(attacker) == "BRN" and aAb ~= "GUTS" then damage = math.floor(damage / 2) end
|
||||
if opts.reflect and not crit then damage = math.floor(damage / 2) end
|
||||
if opts.reflect and not crit then
|
||||
if opts.doubleScreens then
|
||||
damage = 2 * math.floor(damage / 3)
|
||||
else
|
||||
damage = math.floor(damage / 2)
|
||||
end
|
||||
end
|
||||
-- pokefirered/src/pokemon.c:2552
|
||||
if opts.spread then damage = math.floor(damage / 2) end
|
||||
if damage == 0 then damage = 1 end
|
||||
end
|
||||
|
||||
@@ -289,7 +298,15 @@ function Damage.base(attacker, defender, move, opts)
|
||||
local helper = math.max(1, Damage.applyStage(spDefense, defStage))
|
||||
damage = math.floor(damage / helper)
|
||||
damage = math.floor(damage / 50)
|
||||
if opts.lightScreen and not crit then damage = math.floor(damage / 2) end
|
||||
if opts.lightScreen and not crit then
|
||||
if opts.doubleScreens then
|
||||
damage = 2 * math.floor(damage / 3)
|
||||
else
|
||||
damage = math.floor(damage / 2)
|
||||
end
|
||||
end
|
||||
-- pokefirered/src/pokemon.c:2604
|
||||
if opts.spread then damage = math.floor(damage / 2) end
|
||||
local weather = opts.weatherKind
|
||||
if weather == "RAIN" then
|
||||
if moveType == Types.ID.FIRE then damage = math.floor(damage / 2)
|
||||
@@ -393,8 +410,13 @@ function Damage.calc(attacker, defender, moveId, opts)
|
||||
end
|
||||
elseif effectByte == EffectIds.REVENGE and not opts.dmgMultiplier then
|
||||
-- pokefirered/src/battle_script_commands.c:8946
|
||||
if (attacker.damageTakenThisTurn or 0) > 0
|
||||
and (attacker.expHurtBy == nil or attacker.expHurtBy == defender.side) then
|
||||
local hurtOk
|
||||
if opts.adapter and opts.adapter._st and opts.adapter._st.double then
|
||||
hurtOk = attacker.expHurtById == nil or attacker.expHurtById == defender.id
|
||||
else
|
||||
hurtOk = attacker.expHurtBy == nil or attacker.expHurtBy == defender.side
|
||||
end
|
||||
if (attacker.damageTakenThisTurn or 0) > 0 and hurtOk then
|
||||
dmgMultiplier = dmgMultiplier * 2
|
||||
end
|
||||
elseif effectByte == EffectIds.SMELLINGSALT and not opts.dmgMultiplier then
|
||||
@@ -468,7 +490,19 @@ function Damage.calc(attacker, defender, moveId, opts)
|
||||
if opts.forceCrit ~= nil then
|
||||
crit = opts.forceCrit and true or false
|
||||
elseif defAb ~= "BATTLE_ARMOR" and defAb ~= "SHELL_ARMOR" and not opts.noCrit then
|
||||
crit = Rules.crit.roll(attacker, move, opts.highCrit, rng)
|
||||
-- pokefirered/src/battle_script_commands.c:1170
|
||||
if opts.adapter and ModRuntime.wantsHook("battle.crit") then
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local num = tonumber(move.numId) or G3.moveId(move.id)
|
||||
crit = ModRuntime.call("battle.crit", function(c)
|
||||
return Rules.crit.roll(c.attacker, move, c.highCrit, c.rng)
|
||||
end, { battle = opts.adapter._st, attacker = attacker, target = defender,
|
||||
moveId = G3.moveName(num) or move.id, moveNum = num, rng = rng,
|
||||
highCrit = opts.highCrit,
|
||||
stage = Rules.crit.stage(attacker, move, opts.highCrit) }) and true or false
|
||||
else
|
||||
crit = Rules.crit.roll(attacker, move, opts.highCrit, rng)
|
||||
end
|
||||
end
|
||||
local critMul = crit and Rules.crit.multiplier() or 1
|
||||
|
||||
@@ -479,6 +513,8 @@ function Damage.calc(attacker, defender, moveId, opts)
|
||||
adapter = opts.adapter,
|
||||
reflect = opts.reflect,
|
||||
lightScreen = opts.lightScreen,
|
||||
doubleScreens = opts.doubleScreens,
|
||||
spread = opts.spread,
|
||||
weatherKind = weatherKind,
|
||||
isSolarBeam = effectByte == EffectIds.SOLAR_BEAM,
|
||||
mudSport = opts.mudSport,
|
||||
@@ -489,6 +525,8 @@ function Damage.calc(attacker, defender, moveId, opts)
|
||||
if attacker.expCharged and tonumber(move.type) == Types.ID.ELECTRIC then
|
||||
dmg = dmg * 2
|
||||
end
|
||||
-- pokefirered/src/battle_script_commands.c:1219
|
||||
if attacker.expHelpingHand then dmg = math.floor(dmg * 15 / 10) end
|
||||
|
||||
local stab = 1
|
||||
local aT1, aT2 = attacker.type1, attacker.type2
|
||||
|
||||
@@ -113,13 +113,15 @@ function Healing.wish(ctx)
|
||||
local side = ctx.adapter:ownSide(ctx.user)
|
||||
if not side then return H.sayFail(ctx) end
|
||||
side.tokens = side.tokens or {}
|
||||
local double = ctx.adapter._st and ctx.adapter._st.double
|
||||
for _, tok in ipairs(side.tokens) do
|
||||
if tok.id == "EXP_WISH" then return H.sayFail(ctx) end
|
||||
if tok.id == "EXP_WISH" and (not double or tok.battlerId == ctx.user.id) then return H.sayFail(ctx) end
|
||||
end
|
||||
side.tokens[#side.tokens + 1] = {
|
||||
id = "EXP_WISH",
|
||||
turns = 2,
|
||||
wisher = name(ctx, ctx.user),
|
||||
battlerId = ctx.user.id,
|
||||
}
|
||||
H.attackAnim(ctx)
|
||||
end
|
||||
@@ -133,8 +135,16 @@ function Healing.healBell(ctx)
|
||||
local active = State.partyMon(user)
|
||||
local blocked = isBell and ad:abilityOf(user) == "SOUNDPROOF"
|
||||
if not blocked then ad:clearStatus(user) end
|
||||
local partner = ad._st and ad._st.double and ad:partnerOf(user) or nil
|
||||
local partnerBlocked = partner and isBell and ad:abilityOf(partner) == "SOUNDPROOF"
|
||||
-- pokefirered/src/battle_script_commands.c:8023
|
||||
if partner and not partnerBlocked then
|
||||
ad:clearStatus(partner)
|
||||
partner.expNightmare = nil
|
||||
end
|
||||
local partnerMon = partner and State.partyMon(partner)
|
||||
for _, mon in ipairs(ad:partyMons(user)) do
|
||||
if mon and mon ~= active and mon.status then
|
||||
if mon and mon ~= active and mon ~= partnerMon and mon.status then
|
||||
mon.status = nil
|
||||
mon.sleep = nil
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ local Types = require("src.core.game3.battle.types")
|
||||
local Rules = require("src.core.game3.battle.rules")
|
||||
local Secondary = require("src.core.game3.battle.effects.secondary")
|
||||
local HeldItems = require("src.core.game3.battle.held_items")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local E = EffectIds
|
||||
local Hit = {}
|
||||
@@ -18,6 +19,24 @@ local function move_is_sleep_talk(ref)
|
||||
return tonumber(m and m.effect) == E.SLEEP_TALK or tonumber(ref) == MOVE_SLEEP_TALK
|
||||
end
|
||||
|
||||
local function dealt_event(M, target, dealt, info, sub)
|
||||
if not ModRuntime.wants("battle.damage_dealt") then return end
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local view = G3.moveView(M.move)
|
||||
local eff = tonumber(info.effectiveness)
|
||||
local mult = eff and math.floor(eff * 10 + 0.5) or 10
|
||||
local physical = info.physical
|
||||
if physical == nil then physical = Types.isPhysical(M.moveType or M.move.type) end
|
||||
ModRuntime.emit("battle.damage_dealt", {
|
||||
battle = M.st, user = M.user, target = target, move = view,
|
||||
moveId = view and view.id, moveNum = M.mnum, damage = dealt,
|
||||
crit = info.critical and true or false, typeMult = mult, effectiveness = mult,
|
||||
side = target.side, userId = State.idOf(M.user), targetId = State.idOf(target),
|
||||
kind = physical and "physical" or "special", substitute = sub or nil,
|
||||
})
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:1744
|
||||
function Hit.dealDamage(M, dmg, info)
|
||||
local ad, user, target = M.adapter, M.user, M.target
|
||||
@@ -37,6 +56,7 @@ function Hit.dealDamage(M, dmg, info)
|
||||
if (M.firstDmg or 0) == 0 then M.firstDmg = dmg end
|
||||
M.hpDealt = dealt
|
||||
M.hitsLanded = (M.hitsLanded or 0) + 1
|
||||
dealt_event(M, target, dealt, info, true)
|
||||
return dealt
|
||||
end
|
||||
local before = ad:hp(target)
|
||||
@@ -45,6 +65,7 @@ function Hit.dealDamage(M, dmg, info)
|
||||
local dealt = before - after
|
||||
M.anim.hits[#M.anim.hits + 1] = {
|
||||
side = target.side or "enemy",
|
||||
battler = target.id,
|
||||
from = before,
|
||||
to = after,
|
||||
maxHp = ad:maxHp(target),
|
||||
@@ -53,12 +74,17 @@ function Hit.dealDamage(M, dmg, info)
|
||||
if (M.firstDmg or 0) == 0 then M.firstDmg = dealt end
|
||||
if dealt > 0 then M.targetDamaged = true end
|
||||
target.expHurtBy = user.side
|
||||
target.expHurtById = user.id
|
||||
target.expLastHitById = user.id
|
||||
local physical = info.physical
|
||||
if physical == nil then physical = Types.isPhysical(M.moveType or M.move.type) end
|
||||
-- pokefirered/src/battle_script_commands.c:1824
|
||||
if physical then
|
||||
target.lastPhysicalDamageTaken = dealt
|
||||
target.lastPhysicalById = user.id
|
||||
else
|
||||
target.lastSpecialDamageTaken = dealt
|
||||
target.lastSpecialById = user.id
|
||||
M.specialHit = true
|
||||
end
|
||||
if (target.bideTurns or 0) > 0 then
|
||||
@@ -67,6 +93,7 @@ function Hit.dealDamage(M, dmg, info)
|
||||
end
|
||||
M.hpDealt = dealt
|
||||
M.hitsLanded = (M.hitsLanded or 0) + 1
|
||||
dealt_event(M, target, dealt, info, false)
|
||||
return dealt
|
||||
end
|
||||
|
||||
@@ -120,10 +147,20 @@ local function field_sport(M, key)
|
||||
return M.st and M.st[key == "mudSport" and "expMudSport" or "expWaterSport"] and true or false
|
||||
end
|
||||
|
||||
local MOVE_TARGET_BOTH = 8
|
||||
|
||||
local function calc_opts(M, extra)
|
||||
local ad, target = M.adapter, M.target
|
||||
local defSide = ad:ownSide(target)
|
||||
local st = M.st
|
||||
local twoDef = false
|
||||
if st and st.double and target then
|
||||
local State = require("src.core.game3.battle.state")
|
||||
twoDef = State.countPresentOnSide(st, target.side) == 2
|
||||
end
|
||||
local o = {
|
||||
doubleScreens = twoDef,
|
||||
spread = twoDef and tonumber(M.move and M.move.target) == MOVE_TARGET_BOTH,
|
||||
rng = ad:rng(),
|
||||
weather = Rules.weather.effective(M.st, ad),
|
||||
adapter = ad,
|
||||
@@ -166,6 +203,18 @@ local function pre_checks(M)
|
||||
-- pokefirered/src/battle_script_commands.c:7568
|
||||
local taken = (eff == E.COUNTER) and user.lastPhysicalDamageTaken or user.lastSpecialDamageTaken
|
||||
local foe = ad:foeOf(user)
|
||||
if M.st and M.st.double then
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local src = (eff == E.COUNTER) and user.lastPhysicalById or user.lastSpecialById
|
||||
foe = src ~= nil and State.battler(M.st, src) or nil
|
||||
if foe and src % 2 == State.idOf(user) % 2 then foe = nil end
|
||||
if foe and ad:hp(foe) > 0 then
|
||||
local Engine = require("src.core.game3.battle.engine")
|
||||
local fm = Engine.followMeId(M.st, ad, user)
|
||||
local fb = fm ~= nil and State.battler(M.st, fm) or nil
|
||||
if fb and ad:hp(fb) > 0 then foe = fb end
|
||||
end
|
||||
end
|
||||
if not taken or taken <= 0 or not foe or ad:isFainted(foe)
|
||||
or (user.expHurtBy and user.expHurtBy == user.side) then
|
||||
fail(M)
|
||||
@@ -200,9 +249,10 @@ local function pre_checks(M)
|
||||
M.anim.missed = true
|
||||
return true
|
||||
end
|
||||
if eff == E.EXPLOSION then
|
||||
if eff == E.EXPLOSION and not M.explosionStarted then
|
||||
for _, b in ipairs(ad:activeBattlers()) do
|
||||
if ad:abilityOf(b) == "DAMP" then
|
||||
M.stopTargets = true
|
||||
M:attackString()
|
||||
M:ppReduce()
|
||||
M:say(ad:displayName(b) .. "'s DAMP\nprevents " .. M.uname .. "\nfrom using " .. M.moveName .. "!")
|
||||
@@ -376,10 +426,35 @@ local function fury_cutter_power(M)
|
||||
return power
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:1209
|
||||
local function damage_calc(M, target, calcOpts)
|
||||
if not ModRuntime.wantsHook("battle.damage") then
|
||||
return Damage.calc(M.user, target, M.move, calcOpts)
|
||||
end
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local view = G3.moveView(M.move)
|
||||
local vanillaInfo
|
||||
local dmg, info = ModRuntime.call("battle.damage", function(c)
|
||||
local d, i = Damage.calc(c.user, c.target, M.move, c.opts)
|
||||
vanillaInfo = i
|
||||
return d, i
|
||||
end, { battle = M.st, user = M.user, target = target, move = view,
|
||||
moveId = view and view.id, moveNum = M.mnum, opts = calcOpts,
|
||||
rng = calcOpts and calcOpts.rng })
|
||||
if type(info) ~= "table" then
|
||||
info = vanillaInfo or {
|
||||
move = M.move, effectiveness = 1, critical = false,
|
||||
moveType = tonumber(M.move.type), power = tonumber(M.move.power),
|
||||
typeFlags = { super = false, notVery = false, immune = false },
|
||||
}
|
||||
end
|
||||
return math.max(0, math.floor(tonumber(dmg) or 0)), info
|
||||
end
|
||||
|
||||
local function hit_once(M, opts)
|
||||
local ad, user, target = M.adapter, M.user, M.target
|
||||
opts = opts or {}
|
||||
local dmg, info = Damage.calc(user, target, M.move, calc_opts(M, opts.calc))
|
||||
local dmg, info = damage_calc(M, target, calc_opts(M, opts.calc))
|
||||
M.moveType = info.moveType or M.moveType
|
||||
if info.failed then
|
||||
M.failed = true
|
||||
@@ -426,17 +501,18 @@ function Hit.run(M)
|
||||
-- pokefirered/data/battle_scripts_1.s:376
|
||||
M:attackString()
|
||||
M:ppReduce()
|
||||
M.explosionStarted = true
|
||||
ad:setHp(user, 0)
|
||||
local dmg, info = Damage.calc(user, M.target, M.move, calc_opts(M))
|
||||
local dmg, info = damage_calc(M, M.target, calc_opts(M))
|
||||
if not M:accuracyCheck("normal", true) then
|
||||
M.anim.missed = true
|
||||
M.noEffect = true
|
||||
M:tryFaintUser()
|
||||
if not M.deferUserFaint then M:tryFaintUser() end
|
||||
return
|
||||
end
|
||||
if flags_immune(M, info) then
|
||||
M.noEffect = true
|
||||
M:tryFaintUser()
|
||||
if not M.deferUserFaint then M:tryFaintUser() end
|
||||
return
|
||||
end
|
||||
local target = M.target
|
||||
@@ -453,18 +529,19 @@ function Hit.run(M)
|
||||
if line then M:say(line) end
|
||||
end
|
||||
M:tryFaintTarget()
|
||||
M:tryFaintUser()
|
||||
if not M.deferUserFaint then M:tryFaintUser() end
|
||||
return
|
||||
end
|
||||
|
||||
M:attackString()
|
||||
M:ppReduce()
|
||||
|
||||
local magnitude
|
||||
if eff == E.MAGNITUDE then
|
||||
local magnitude = M.magnitude
|
||||
if eff == E.MAGNITUDE and not magnitude then
|
||||
local r = roll(ad, 0, 99)
|
||||
local _, info = Damage.calc(user, M.target, M.move, calc_opts(M, { magnitudeRoll = r, forceCrit = false, noRandom = true }))
|
||||
magnitude = { power = info.power, value = info.magnitude }
|
||||
M.magnitude = magnitude
|
||||
M:say(string.format("MAGNITUDE %d!", magnitude.value or 4))
|
||||
end
|
||||
|
||||
@@ -517,7 +594,10 @@ function Hit.run(M)
|
||||
local base = Damage.base(user, target, M.move, {
|
||||
adapter = ad, weatherKind = Rules.weather.effective(M.st, ad),
|
||||
reflect = calc_opts(M).reflect, lightScreen = calc_opts(M).lightScreen,
|
||||
doubleScreens = calc_opts(M).doubleScreens,
|
||||
}) * n
|
||||
-- pokefirered/src/battle_script_commands.c:6603
|
||||
if user.expHelpingHand then base = math.floor(base * 15 / 10) end
|
||||
local aT1, aT2 = user.type1, user.type2
|
||||
if aT1 == tonumber(M.move.type) or aT2 == tonumber(M.move.type) then base = math.floor(base * 15 / 10) end
|
||||
local dmg, flags = Types.typeCalc(M.move.type, target.type1, target.type2, base, target.expIdentified)
|
||||
@@ -659,6 +739,8 @@ function Hit.beatUp(M)
|
||||
local dmg = atk * (tonumber(M.move.power) or 10) * (math.floor(lvl * 2 / 5) + 2)
|
||||
dmg = math.floor(dmg / math.max(1, def))
|
||||
dmg = math.floor(dmg / 50) + 2
|
||||
-- pokefirered/src/battle_script_commands.c:8606
|
||||
if user.expHelpingHand then dmg = math.floor(dmg * 15 / 10) end
|
||||
local name = (mon.nickname and mon.nickname ~= "") and mon.nickname or Pokemon.name(sp)
|
||||
M:say(tostring(name) .. "'s attack!")
|
||||
local crit = Rules.crit.roll(user, M.move, nil, ad:rng())
|
||||
|
||||
@@ -9,6 +9,9 @@ local Setup = {}
|
||||
local function name(ctx, b) return ctx.adapter:displayName(b) end
|
||||
|
||||
local function moved_last(ctx)
|
||||
local st = ctx.adapter and ctx.adapter._st
|
||||
-- pokefirered/src/battle_script_commands.c:9148
|
||||
if st and st.double then return ctx.user and ctx.user.expTurnOrder == 4 end
|
||||
return ctx.user and ctx.user.expTurnOrder == 2
|
||||
end
|
||||
|
||||
@@ -91,6 +94,7 @@ function Setup.lockOn(ctx)
|
||||
if not H.accuracy(ctx, "normal") then return end
|
||||
ctx.target.expLockedOn = 2
|
||||
ctx.target.expLockedOnBy = ctx.user.side
|
||||
ctx.target.expLockedOnById = ctx.user.id
|
||||
H.attackAnim(ctx)
|
||||
ctx.adapter:say(name(ctx, ctx.user) .. " took aim\nat " .. name(ctx, ctx.target) .. "!")
|
||||
end
|
||||
@@ -115,15 +119,18 @@ end
|
||||
function Setup.imprison(ctx)
|
||||
local user = ctx.user
|
||||
if user.expImprison then return H.sayFail(ctx) end
|
||||
local foe = ctx.adapter:foeOf(user)
|
||||
local st = ctx.adapter._st
|
||||
local foes = (st and st.double) and ctx.adapter:foesOf(user) or { ctx.adapter:foeOf(user) }
|
||||
local shared = false
|
||||
local um = user.mon and user.mon.moves or {}
|
||||
local fm = foe and foe.mon and foe.mon.moves or {}
|
||||
for i = 1, 4 do
|
||||
local a = H.moveNum(um[i])
|
||||
if a and a ~= 0 then
|
||||
for j = 1, 4 do
|
||||
if H.moveNum(fm[j]) == a then shared = true end
|
||||
for _, foe in ipairs(foes) do
|
||||
local fm = foe and foe.mon and foe.mon.moves or {}
|
||||
for i = 1, 4 do
|
||||
local a = H.moveNum(um[i])
|
||||
if a and a ~= 0 then
|
||||
for j = 1, 4 do
|
||||
if H.moveNum(fm[j]) == a then shared = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -202,8 +209,11 @@ function Setup.futureSight(ctx)
|
||||
local side = ad:foeSide(ctx.user)
|
||||
if not side then return H.sayFail(ctx) end
|
||||
side.tokens = side.tokens or {}
|
||||
local double = ad._st and ad._st.double
|
||||
for _, tok in ipairs(side.tokens) do
|
||||
if tok.id == "EXP_FUTURE_SIGHT" then return H.sayFail(ctx) end
|
||||
if tok.id == "EXP_FUTURE_SIGHT" and (not double or tok.targetId == ctx.target.id) then
|
||||
return H.sayFail(ctx)
|
||||
end
|
||||
end
|
||||
local Damage = require("src.core.game3.battle.damage")
|
||||
local Rules = require("src.core.game3.battle.rules")
|
||||
@@ -213,7 +223,10 @@ function Setup.futureSight(ctx)
|
||||
weatherKind = Rules.weather.effective(ad._st, ad),
|
||||
reflect = defSide and (defSide.expReflectTurns or 0) > 0,
|
||||
lightScreen = defSide and (defSide.expLightScreenTurns or 0) > 0,
|
||||
doubleScreens = double and ad._st and require("src.core.game3.battle.state").countPresentOnSide(ad._st, ctx.target.side) == 2,
|
||||
})
|
||||
-- pokefirered/src/battle_script_commands.c:8558
|
||||
if ctx.user.expHelpingHand then dmg = math.floor(dmg * 15 / 10) end
|
||||
side.tokens[#side.tokens + 1] = {
|
||||
id = "EXP_FUTURE_SIGHT",
|
||||
turns = 3,
|
||||
@@ -221,6 +234,8 @@ function Setup.futureSight(ctx)
|
||||
moveId = ctx.moveId,
|
||||
moveName = ctx.opts and ctx.opts.moveName or "FUTURE SIGHT",
|
||||
attackerSide = ctx.user.side,
|
||||
attackerId = ctx.user.id,
|
||||
targetId = ctx.target.id,
|
||||
}
|
||||
H.attackAnim(ctx)
|
||||
local tok = side.tokens[#side.tokens]
|
||||
@@ -270,7 +285,24 @@ end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:8779
|
||||
function Setup.helpingHand(ctx)
|
||||
return H.sayFail(ctx)
|
||||
local ad, user = ctx.adapter, ctx.user
|
||||
local st = ad._st
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local pid = State.PARTNER(State.idOf(user))
|
||||
local partner = State.battler(st, pid)
|
||||
if not (st and st.double) or not State.isPresent(st, pid) or not partner
|
||||
or user.expHelpingHand or partner.expHelpingHand then
|
||||
return H.sayFail(ctx)
|
||||
end
|
||||
partner.expHelpingHand = true
|
||||
ctx.target = partner
|
||||
local M = H.move(ctx)
|
||||
if M then
|
||||
M.target = partner
|
||||
M.tname = ad:displayName(partner)
|
||||
end
|
||||
H.attackAnim(ctx)
|
||||
ad:say(name(ctx, user) .. " is ready to\nhelp " .. name(ctx, partner) .. "!")
|
||||
end
|
||||
|
||||
function Setup.splash(ctx)
|
||||
|
||||
@@ -39,7 +39,7 @@ function Special.roar(ctx)
|
||||
end
|
||||
if not H.accuracy(ctx, "lockon") then return end
|
||||
if not H.accuracy(ctx, "normal") then return end
|
||||
local candidates = engine().switchCandidates(st, target.side)
|
||||
local candidates = engine().switchCandidates(st, st.double and target.id or target.side)
|
||||
if not st.wild and #candidates < 1 then return H.sayFail(ctx) end
|
||||
local uLvl = tonumber(user.mon and user.mon.level) or 1
|
||||
local tLvl = tonumber(target.mon and target.mon.level) or 1
|
||||
@@ -57,7 +57,7 @@ function Special.roar(ctx)
|
||||
return
|
||||
end
|
||||
local slot = candidates[ad:roll(1, #candidates)]
|
||||
local nb = engine().performSwitch(st, ad, target.side, slot, { reason = "roar" })
|
||||
local nb = engine().performSwitch(st, ad, st.double and target.id or target.side, slot, { reason = "roar" })
|
||||
if nb then
|
||||
local M = H.move(ctx)
|
||||
if M then M.target = nb end
|
||||
@@ -94,6 +94,9 @@ function Special.conversion2(ctx)
|
||||
local lastType = user.expLastHitByType
|
||||
if not last or last == 0 or lastType == nil then return H.sayFail(ctx) end
|
||||
local foe = ad:foeOf(user)
|
||||
if ad._st.double and user.expLastHitById ~= nil then
|
||||
foe = require("src.core.game3.battle.state").battler(ad._st, user.expLastHitById) or foe
|
||||
end
|
||||
if engine().isTwoTurnMove(last) and foe and foe.twoTurnMove then return H.sayFail(ctx) end
|
||||
local valid = {}
|
||||
local t = Types.TABLE
|
||||
@@ -228,7 +231,7 @@ end
|
||||
function Special.batonPass(ctx)
|
||||
local ad, user = ctx.adapter, ctx.user
|
||||
local st = ad._st
|
||||
local candidates = engine().switchCandidates(st, user.side)
|
||||
local candidates = engine().switchCandidates(st, st.double and user.id or user.side)
|
||||
if #candidates == 0 then return H.sayFail(ctx) end
|
||||
H.attackAnim(ctx)
|
||||
local pick
|
||||
@@ -237,16 +240,16 @@ function Special.batonPass(ctx)
|
||||
if ok then pick = tonumber(v) end
|
||||
elseif user.side == "player" and st.interactiveChoices and coroutine.running() then
|
||||
-- pokefirered/src/battle_script_commands.c:4626
|
||||
pick = tonumber(coroutine.yield({ kind = "baton_pass", side = user.side, candidates = candidates }))
|
||||
pick = tonumber(coroutine.yield({ kind = "baton_pass", side = user.side, battler = user.id, candidates = candidates }))
|
||||
elseif user.side == "enemy" then
|
||||
-- pokefirered/src/battle_controller_opponent.c:1410
|
||||
pick = engine().mostSuitableMon(st, ad, "enemy")
|
||||
pick = engine().mostSuitableMon(st, ad, st.double and user.id or "enemy")
|
||||
end
|
||||
local slot = candidates[1]
|
||||
for _, c in ipairs(candidates) do
|
||||
if c == pick then slot = pick end
|
||||
end
|
||||
local nb = engine().performSwitch(st, ad, user.side, slot, { batonPass = true, reason = "baton_pass" })
|
||||
local nb = engine().performSwitch(st, ad, st.double and user.id or user.side, slot, { batonPass = true, reason = "baton_pass" })
|
||||
if nb then
|
||||
local M = H.move(ctx)
|
||||
if M then M.user = nb end
|
||||
@@ -267,6 +270,10 @@ function Special.teleport(ctx)
|
||||
local ab = ad:abilityOf(user)
|
||||
local foe = ad:foeOf(user)
|
||||
local fab = foe and ad:abilityOf(foe)
|
||||
if ad._st.double then
|
||||
local Abilities = require("src.core.game3.battle.abilities")
|
||||
foe, fab = Abilities.escapeBlocker(ad, user)
|
||||
end
|
||||
if not (ab == "RUN_AWAY" or item == 194) then
|
||||
if fab == "SHADOW_TAG" or (fab == "ARENA_TRAP" and not H.hasType(ctx, user, Types.ID.FLYING) and ab ~= "LEVITATE")
|
||||
or (fab == "MAGNET_PULL" and H.hasType(ctx, user, Types.ID.STEEL)) then
|
||||
@@ -284,7 +291,10 @@ end
|
||||
-- pokefirered/src/battle_script_commands.c:8702
|
||||
function Special.followMe(ctx)
|
||||
local side = ctx.adapter:ownSide(ctx.user)
|
||||
if side then side.expFollowMe = ctx.user end
|
||||
if side then
|
||||
side.expFollowMe = ctx.user
|
||||
side.expFollowMeId = ctx.user.id
|
||||
end
|
||||
H.attackAnim(ctx)
|
||||
ctx.adapter:say(name(ctx, ctx.user) .. " became the\ncenter of attention!")
|
||||
end
|
||||
|
||||
@@ -12,7 +12,8 @@ local function protect_like(ctx, onSuccess)
|
||||
local last = user.expLastResulting and H.moveNum(user.expLastResulting)
|
||||
if last ~= 182 and last ~= 197 and last ~= 203 then user.expProtectStreak = 0 end
|
||||
local streak = user.expProtectStreak or 0
|
||||
local ok = user.expTurnOrder ~= 2
|
||||
local st = ctx.adapter._st
|
||||
local ok = user.expTurnOrder ~= ((st and st.double) and 4 or 2)
|
||||
if ok and streak > 0 then
|
||||
local denom = 2 ^ math.min(streak, 3)
|
||||
ok = ctx.adapter:roll(0, denom - 1) == 0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,11 +100,14 @@ function ExpSeq.begin(awards, pushMsg, thenMsgs, opts)
|
||||
local gained = result.gained or entry.amount or 0
|
||||
local pi = entry.partyIndex or 1
|
||||
local isBench = (entry.battler == nil)
|
||||
local key = "player"
|
||||
if opts.double and entry.battler and entry.battler.id ~= nil then key = entry.battler.id end
|
||||
if gained > 0 then
|
||||
-- pokefirered/src/battle_message.c:53
|
||||
add("msg", { text = name .. " gained" .. (entry.boosted and " a boosted" or "") .. "\n" .. tostring(gained) .. " EXP. Points!" })
|
||||
for _, step in ipairs(result.steps or {}) do
|
||||
if not isBench then
|
||||
-- pokefirered/src/battle_controller_player.c:1034
|
||||
if not isBench and not opts.double then
|
||||
add("exp", {
|
||||
side = "player",
|
||||
level = step.level,
|
||||
@@ -115,7 +118,7 @@ function ExpSeq.begin(awards, pushMsg, thenMsgs, opts)
|
||||
if step.grewTo then
|
||||
ExpSeq._leveled[pi] = true
|
||||
add("level", {
|
||||
side = "player",
|
||||
side = key,
|
||||
isBench = isBench,
|
||||
mon = mon,
|
||||
level = step.grewTo,
|
||||
@@ -215,10 +218,14 @@ local function run_step(step)
|
||||
-- pokefirered/src/battle_controller_player.c:1143
|
||||
d._lvlAnim = true
|
||||
local side = d.side or "player"
|
||||
local bid = (type(side) == "number") and side or nil
|
||||
if bid then side = State.sideOf(bid) end
|
||||
ExpSeq._lvlAnimWait = true
|
||||
Anim.launchSpecial("LVL_UP", {
|
||||
attackerSide = side,
|
||||
targetSide = side,
|
||||
attackerId = bid,
|
||||
targetId = bid,
|
||||
onEnd = function() ExpSeq._lvlAnimWait = false end,
|
||||
})
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local SummaryData = require("src.core.game3.summary_data")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Experience = {}
|
||||
|
||||
@@ -164,6 +165,19 @@ function Experience.apply(mon, amount)
|
||||
curExp = nextThresh
|
||||
levels[#levels + 1] = curLevel
|
||||
apply_level_stats(mon, curLevel)
|
||||
-- pokefirered/src/battle_script_commands.c:3298
|
||||
if ModRuntime.wants("pokemon.level_up") then
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local learnable, learnableIds = {}, {}
|
||||
for _, mv in ipairs(Pokemon.movesLearnedAt(tonumber(mon.species or mon.speciesId), curLevel)) do
|
||||
learnable[#learnable + 1] = G3.moveName(mv)
|
||||
learnableIds[#learnableIds + 1] = mv
|
||||
end
|
||||
ModRuntime.emit("pokemon.level_up", {
|
||||
mon = mon, level = curLevel, prevLevel = curLevel - 1,
|
||||
learnable = learnable, learnableIds = learnableIds,
|
||||
})
|
||||
end
|
||||
local newStats = get_mon_stats(mon)
|
||||
steps[#steps].hp = tonumber(mon.hp)
|
||||
steps[#steps].maxHp = tonumber(mon.maxHp)
|
||||
@@ -236,9 +250,27 @@ function Experience.awardFoe(st, foeBattler, opts)
|
||||
local sentIn = {}
|
||||
if opts.partyIndices then
|
||||
for _, pi in ipairs(opts.partyIndices) do sentIn[pi] = true end
|
||||
elseif st.double and foeBattler.participants then
|
||||
-- pokefirered/src/battle_script_commands.c:3123
|
||||
for pi in pairs(foeBattler.participants) do sentIn[pi] = true end
|
||||
elseif st.player and st.player.mon and (tonumber(st.player.mon.hp) or 0) > 0 then
|
||||
sentIn[st.player.partyIndex or 1] = true
|
||||
end
|
||||
local b0 = st.player
|
||||
local b2 = st.double and st.battlers and st.battlers[2] or nil
|
||||
local absent = st.absent or {}
|
||||
local function on_field(pi)
|
||||
if b0 and b0.partyIndex == pi and not absent[0] then return b0 end
|
||||
if b2 and b2.partyIndex == pi and not absent[2] then return b2 end
|
||||
return nil
|
||||
end
|
||||
-- pokefirered/src/battle_script_commands.c:3248
|
||||
local function getter_id(pi)
|
||||
if not st.double then return 0 end
|
||||
if b2 and b2.partyIndex == pi and not absent[2] then return 2 end
|
||||
if not absent[0] then return 0 end
|
||||
return 2
|
||||
end
|
||||
local party = st.playerParty or {}
|
||||
local function alive(mon)
|
||||
return mon and (tonumber(mon.species or mon.speciesId) or 0) ~= 0 and (tonumber(mon.hp) or 0) > 0
|
||||
@@ -273,20 +305,53 @@ function Experience.awardFoe(st, foeBattler, opts)
|
||||
local share = mon and has_share(mon)
|
||||
if mon and (sentIn[pi] or share) and (tonumber(mon.level) or 1) < Experience.MAX_LEVEL and alive(mon) then
|
||||
local per = opts.getOpts and opts.getOpts(mon, pi) or Experience.recipientOpts(st, mon)
|
||||
local amount = sentIn[pi] and exp or 0
|
||||
if share then amount = amount + shareExp end
|
||||
if per.luckyEgg then amount = math.floor(amount * 150 / 100) end
|
||||
if isTrainer then amount = math.floor(amount * 150 / 100) end
|
||||
if per.traded then amount = math.floor(amount * 150 / 100) end
|
||||
local function vanilla_amount()
|
||||
local amount = sentIn[pi] and exp or 0
|
||||
if share then amount = amount + shareExp end
|
||||
if per.luckyEgg then amount = math.floor(amount * 150 / 100) end
|
||||
if isTrainer then amount = math.floor(amount * 150 / 100) end
|
||||
if per.traded then amount = math.floor(amount * 150 / 100) end
|
||||
return amount
|
||||
end
|
||||
local amount
|
||||
-- pokefirered/src/battle_script_commands.c:3230
|
||||
if ModRuntime.wantsHook("exp.gain") then
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
amount = ModRuntime.call("exp.gain", function() return vanilla_amount() end, {
|
||||
defeatedDef = G3.speciesView(foeSpecies), level = foeLevel, isTrainer = isTrainer,
|
||||
participants = viaSentIn, traded = per.traded, luckyEgg = per.luckyEgg,
|
||||
expShare = share and true or false, mon = mon, index = pi,
|
||||
battle = st, loser = foeBattler,
|
||||
})
|
||||
amount = math.max(0, math.floor(tonumber(amount) or 0))
|
||||
else
|
||||
amount = vanilla_amount()
|
||||
end
|
||||
local result = Experience.apply(mon, amount)
|
||||
if st.player and st.player.partyIndex == pi then
|
||||
local fieldB
|
||||
if st.double then
|
||||
fieldB = on_field(pi)
|
||||
if fieldB then
|
||||
fieldB.mon = mon
|
||||
fieldB.fainted = (tonumber(mon.hp) or 0) <= 0
|
||||
end
|
||||
elseif st.player and st.player.partyIndex == pi then
|
||||
st.player.mon = mon
|
||||
st.player.fainted = (tonumber(mon.hp) or 0) <= 0
|
||||
fieldB = st.player
|
||||
end
|
||||
-- pokefirered/src/battle_script_commands.c:3278
|
||||
if ModRuntime.wants("battle.exp_gained") then
|
||||
ModRuntime.emit("battle.exp_gained", {
|
||||
battle = st, mon = mon, gained = result.gained, levels = result.levels,
|
||||
index = pi, battler = fieldB, battlerId = getter_id(pi),
|
||||
})
|
||||
end
|
||||
out[#out + 1] = {
|
||||
mon = mon,
|
||||
partyIndex = pi,
|
||||
battler = (st.player and st.player.partyIndex == pi) and st.player or nil,
|
||||
battler = fieldB,
|
||||
expGetterBattlerId = getter_id(pi),
|
||||
amount = amount,
|
||||
boosted = per.traded and true or false,
|
||||
result = result,
|
||||
|
||||
@@ -16,6 +16,23 @@ local Healthbox = {}
|
||||
Healthbox.ENEMY_CENTER = { x = 44, y = 30 }
|
||||
Healthbox.PLAYER_CENTER = { x = 158, y = 88 }
|
||||
|
||||
-- pokefirered/src/battle_interface.c:726
|
||||
Healthbox.CENTERS = {
|
||||
[false] = { [0] = Healthbox.PLAYER_CENTER, [1] = Healthbox.ENEMY_CENTER },
|
||||
[true] = {
|
||||
[0] = { x = 159, y = 75 },
|
||||
[1] = { x = 44, y = 19 },
|
||||
[2] = { x = 171, y = 100 },
|
||||
[3] = { x = 32, y = 44 },
|
||||
},
|
||||
}
|
||||
|
||||
function Healthbox.center(st, id)
|
||||
id = tonumber(id) or 0
|
||||
local t = Healthbox.CENTERS[(st and st.double) and true or false]
|
||||
return t[id] or t[id % 2]
|
||||
end
|
||||
|
||||
local function c5(v)
|
||||
return math.floor(v * 255 / 31 + 0.5) / 255
|
||||
end
|
||||
@@ -39,9 +56,9 @@ local HB_FEMALE = {
|
||||
shadow = { 156 / 255, 65 / 255, 57 / 255, 1 },
|
||||
}
|
||||
|
||||
-- Inner cream right edge of assembled sheets (exclusive text end X).
|
||||
local PLAYER_TEXT_RIGHT = 96
|
||||
local ENEMY_TEXT_RIGHT = 89
|
||||
-- pokefirered/src/battle_interface.c:773
|
||||
local PLAYER_LVL_X = 72
|
||||
local ENEMY_LVL_X = 64
|
||||
|
||||
-- pret AddTextPrinterAndCreateWindowOnHealthbox(..., y=3) for nick / level.
|
||||
local TEXT_Y = 3
|
||||
@@ -168,12 +185,11 @@ local function draw_name_gender(name, gender, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
--- Pret UpdateLvlInHealthbox: {LV_2}+digits, right-aligned in the level slot.
|
||||
local function draw_level(lv, boxX, y, textRight)
|
||||
local digits = tostring(lv or 1)
|
||||
-- pokefirered/src/battle_interface.c:759
|
||||
local function draw_level(lv, boxX, y, winX)
|
||||
local digits = tostring(math.max(0, math.min(999, math.floor(tonumber(lv) or 1))))
|
||||
local lvW = FrlgFont.advance(FrlgFont.CHAR_LV_2, { small = true })
|
||||
local digW = FrlgFont.measure(digits, { small = true })
|
||||
local x = boxX + textRight - (lvW + digW)
|
||||
local x = boxX + winX + 5 * (3 - #digits)
|
||||
FrlgFont.drawGlyph(FrlgFont.CHAR_LV_2, x, y, small_opts(HB_TEXT))
|
||||
FrlgFont.draw(digits, x + lvW, y, small_opts(HB_TEXT))
|
||||
end
|
||||
@@ -229,16 +245,140 @@ local function set_level_up_shader(blend)
|
||||
return true
|
||||
end
|
||||
|
||||
function Healthbox.draw(side, battler)
|
||||
local function live_st()
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
return Battle and Battle._st
|
||||
end
|
||||
|
||||
local function anim_key(id)
|
||||
if id == 0 then return "player" elseif id == 1 then return "enemy" end
|
||||
return id
|
||||
end
|
||||
|
||||
local function stage_entry(tbl, id)
|
||||
if type(tbl) ~= "table" then return nil end
|
||||
local e = tbl[id]
|
||||
if e == nil then e = tbl[anim_key(id)] end
|
||||
return e
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_interface.c:992
|
||||
function Healthbox.hpTextShown(st, id)
|
||||
return (st and st.double and st._hpNumbersNoBars and st._hpNumbersNoBars[id]) and true or false
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_interface.c:992
|
||||
function Healthbox.swapHpBarsWithHpText(st)
|
||||
if not (st and st.double) then return end
|
||||
st._hpNumbersNoBars = st._hpNumbersNoBars or {}
|
||||
for _, id in ipairs({ 0, 2 }) do
|
||||
if st.battlers and st.battlers[id] then
|
||||
st._hpNumbersNoBars[id] = not st._hpNumbersNoBars[id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local BAR_FG = { c5(7), c5(7), c5(7), 1 }
|
||||
local BAR_SHADOW = { c5(26), c5(25), c5(23), 1 }
|
||||
local BOTTOM_RIGHT_CORNER_HP_AS_TEXT = 116
|
||||
|
||||
-- pokefirered/src/battle_interface.c:864
|
||||
local function draw_hp_text_doubles(bx, by, cur, maxHp)
|
||||
local opts = { small = true, colors = { fg = BAR_FG, shadow = BAR_SHADOW } }
|
||||
local left = string.format("%3d/", math.max(0, math.min(999, math.floor(cur or 0))))
|
||||
local right = string.format("%3d", math.max(0, math.min(999, math.floor(maxHp or 0))))
|
||||
if BattleChrome.hasHpBoldDigits() then
|
||||
for i = 1, 4 do
|
||||
local ch = left:sub(i, i)
|
||||
if ch ~= " " then BattleChrome.drawHpBoldChar(ch, bx + 8 * i, by) end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local ch = right:sub(i, i)
|
||||
if ch ~= " " then BattleChrome.drawHpBoldChar(ch, bx + 32 + 8 * i, by) end
|
||||
end
|
||||
return
|
||||
end
|
||||
-- pokefirered/src/text.c:1268
|
||||
local ty = by - 3
|
||||
for i = 1, 4 do
|
||||
local ch = left:sub(i, i)
|
||||
if ch ~= " " then FrlgFont.draw(ch, bx + 8 * i, ty, opts) end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local ch = right:sub(i, i)
|
||||
if ch ~= " " then FrlgFont.draw(ch, bx + 32 + 8 * i, ty, opts) end
|
||||
end
|
||||
end
|
||||
|
||||
local function draw_doubles(id, battler, st, opts)
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local stage = Anim.stage and Anim.stage()
|
||||
local hb = stage and stage_entry(stage.healthbox, id)
|
||||
if hb and hb.visible == false then return end
|
||||
local ox = ((hb and hb.ox) or 0) + ((opts and opts.ox) or 0)
|
||||
local oy = ((hb and hb.oy) or 0) + ((opts and opts.oy) or 0)
|
||||
local isPlayer = (id % 2) == 0
|
||||
local c = Healthbox.center(st, id)
|
||||
local cx, cy = c.x + ox, c.y + oy
|
||||
local tlX, tlY = cx - 32, cy - 16
|
||||
|
||||
local lvl = isPlayer and love and love.graphics and set_level_up_shader(hb and hb.levelUpBlend)
|
||||
BattleChrome.drawDoublesBox(isPlayer, tlX, tlY)
|
||||
if lvl then love.graphics.setShader() end
|
||||
erase_placeholder_ink(tlX, tlY, isPlayer and PLAYER_PLACEHOLDER_INK or ENEMY_PLACEHOLDER_INK)
|
||||
|
||||
local SummaryChrome = require("src.ui.game3.summary_chrome")
|
||||
local SummaryData = require("src.core.game3.summary_data")
|
||||
local stObj = battler.status or (battler.mon and (battler.mon.status or battler.mon.status1))
|
||||
local ailment = SummaryData.statusAilment({ status = stObj, hp = battler.mon and battler.mon.hp })
|
||||
local statused = ailment >= 1 and ailment <= 6
|
||||
local hpText = isPlayer and Healthbox.hpTextShown(st, id)
|
||||
|
||||
local barCx = cx + (isPlayer and 16 or 8)
|
||||
local bx, by = hp_bar_top_left(barCx, cy)
|
||||
local hp, maxHp = hp_values(id, battler)
|
||||
if hpText then
|
||||
draw_hp_text_doubles(bx, by, hp, maxHp)
|
||||
-- pokefirered/src/battle_interface.c:925
|
||||
BattleChrome.drawElementTile(BOTTOM_RIGHT_CORNER_HP_AS_TEXT, tlX + 96, tlY + 16, true)
|
||||
else
|
||||
BattleChrome.drawHpBar(bx, by, hp, maxHp, statused)
|
||||
end
|
||||
|
||||
local name = State.displayName(battler)
|
||||
local lv = battler.mon and battler.mon.level or 1
|
||||
local p = Anim.present and Anim.present(id)
|
||||
if p and p.displayLevel then lv = p.displayLevel end
|
||||
local ty = tlY + TEXT_Y
|
||||
local gender = healthbox_gender(battler.mon)
|
||||
-- pokefirered/src/battle_interface.c:1531
|
||||
draw_name_gender(name, gender, tlX + (isPlayer and 16 or 8), ty)
|
||||
draw_level(lv, tlX, ty, isPlayer and PLAYER_LVL_X or ENEMY_LVL_X)
|
||||
if statused then
|
||||
-- pokefirered/src/battle_interface.c:1608
|
||||
SummaryChrome.drawStatusIcon(tlX + (isPlayer and 10 or 2), tlY + 16, ailment)
|
||||
end
|
||||
end
|
||||
|
||||
function Healthbox.draw(side, battler, opts)
|
||||
if not battler then return end
|
||||
if type(side) == "number" then
|
||||
local st = (opts and opts.st) or live_st()
|
||||
if st and st.double then
|
||||
return draw_doubles(side, battler, st, opts)
|
||||
end
|
||||
side = (side % 2 == 0) and "player" or "enemy"
|
||||
end
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local stage = Anim.stage and Anim.stage()
|
||||
local hb = stage and stage.healthbox and stage.healthbox[side]
|
||||
if hb and hb.visible == false then return end
|
||||
local ox = (hb and hb.ox) or 0
|
||||
local ox = ((hb and hb.ox) or 0) + ((opts and opts.ox) or 0)
|
||||
local oy = (opts and opts.oy) or 0
|
||||
|
||||
local isPlayer = side == "player"
|
||||
local c = isPlayer and Healthbox.PLAYER_CENTER or Healthbox.ENEMY_CENTER
|
||||
local c0 = isPlayer and Healthbox.PLAYER_CENTER or Healthbox.ENEMY_CENTER
|
||||
local c = { x = c0.x, y = c0.y + oy }
|
||||
local tlX, tlY
|
||||
if isPlayer then
|
||||
tlX, tlY = player_top_left(c.x + ox, c.y)
|
||||
@@ -255,7 +395,16 @@ function Healthbox.draw(side, battler)
|
||||
|
||||
local barCx, barCy = hp_bar_center(side, c.x + ox, c.y)
|
||||
local bx, by = hp_bar_top_left(barCx, barCy)
|
||||
BattleChrome.drawHpBar(bx, by, hp_values(side, battler))
|
||||
local statusBorder = false
|
||||
if not isPlayer then
|
||||
local SummaryData = require("src.core.game3.summary_data")
|
||||
local st1 = battler.status or (battler.mon and (battler.mon.status or battler.mon.status1))
|
||||
local a = SummaryData.statusAilment({ status = st1, hp = battler.mon and battler.mon.hp })
|
||||
-- pokefirered/src/battle_interface.c:1668
|
||||
statusBorder = a >= 1 and a <= 6
|
||||
end
|
||||
local hpNow, hpMax = hp_values(side, battler)
|
||||
BattleChrome.drawHpBar(bx, by, hpNow, hpMax, statusBorder)
|
||||
|
||||
local name = State.displayName(battler)
|
||||
local lv = battler.mon and battler.mon.level or 1
|
||||
@@ -268,7 +417,7 @@ function Healthbox.draw(side, battler)
|
||||
end
|
||||
|
||||
local ty = tlY + TEXT_Y
|
||||
local textRight = isPlayer and PLAYER_TEXT_RIGHT or ENEMY_TEXT_RIGHT
|
||||
local lvlX = isPlayer and PLAYER_LVL_X or ENEMY_LVL_X
|
||||
local gender = healthbox_gender(battler.mon)
|
||||
-- pokefirered/src/battle_interface.c:1506
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
@@ -292,9 +441,10 @@ function Healthbox.draw(side, battler)
|
||||
|
||||
if isPlayer then
|
||||
draw_name_gender(name, gender, tlX + 16, ty)
|
||||
draw_level(lv, tlX, ty, textRight)
|
||||
draw_level(lv, tlX, ty, lvlX)
|
||||
if ailment >= 1 and ailment <= 6 then
|
||||
SummaryChrome.drawStatusIcon(tlX + 16, tlY + 19, ailment)
|
||||
-- pokefirered/src/battle_interface.c:1608
|
||||
SummaryChrome.drawStatusIcon(tlX + 10, tlY + 24, ailment)
|
||||
end
|
||||
local mon = battler.mon
|
||||
if mon then
|
||||
@@ -315,9 +465,10 @@ function Healthbox.draw(side, battler)
|
||||
BattleChrome.drawExpBar(tlX + 32, tlY + 32, expRatio)
|
||||
else
|
||||
draw_name_gender(name, gender, tlX + 8, ty)
|
||||
draw_level(lv, tlX, ty, textRight)
|
||||
draw_level(lv, tlX, ty, lvlX)
|
||||
if ailment >= 1 and ailment <= 6 then
|
||||
SummaryChrome.drawStatusIcon(tlX - 2, tlY + 17, ailment)
|
||||
-- pokefirered/src/battle_interface.c:1614
|
||||
SummaryChrome.drawStatusIcon(tlX + 2, tlY + 16, ailment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -360,7 +360,9 @@ end
|
||||
-- pokefirered/src/battle_util.c:2851
|
||||
function HeldItems.moveEnd(ad)
|
||||
local any = false
|
||||
for _, b in ipairs({ ad._st.player, ad._st.enemy }) do
|
||||
local list = { ad._st.player, ad._st.enemy }
|
||||
if ad._st.double then list = ad:activeBattlers() end
|
||||
for _, b in ipairs(list) do
|
||||
if b and not ad:isFainted(b) then
|
||||
local he, _, item = HeldItems.of(b)
|
||||
local did = false
|
||||
|
||||
+932
-101
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ function IntroSeq.reset()
|
||||
IntroSeq._pendingSlideIn = nil
|
||||
IntroSeq._pushMsg = nil
|
||||
IntroSeq._opts = nil
|
||||
IntroSeq._cryQueue = nil
|
||||
end
|
||||
|
||||
function IntroSeq.busy()
|
||||
@@ -51,6 +52,54 @@ local function stage()
|
||||
return Anim.stage()
|
||||
end
|
||||
|
||||
local function present_of(key)
|
||||
if type(key) ~= "number" then return Anim.present(key) end
|
||||
return Anim.present(key) or (key < 2 and Anim.present(State.sideOf(key))) or nil
|
||||
end
|
||||
|
||||
local function healthbox_of(s, key)
|
||||
local hb = s and s.healthbox
|
||||
if not hb then return nil end
|
||||
if type(key) ~= "number" then return hb[key] end
|
||||
return hb[key] or (key < 2 and hb[State.sideOf(key)]) or nil
|
||||
end
|
||||
|
||||
local function center_of(st, key)
|
||||
if type(key) == "number" and Anim.coords then
|
||||
local a, b = Anim.coords(st, key)
|
||||
if type(a) == "table" then return a.x or a[1], a.y or a[2] end
|
||||
if a then return a, b end
|
||||
end
|
||||
local side = (type(key) == "number") and State.sideOf(key) or key
|
||||
local base = (side == "player") and Anim.PLAYER_MON or Anim.ENEMY_MON
|
||||
return base.x, base.y
|
||||
end
|
||||
|
||||
local function battler_of(st, key)
|
||||
if not st then return nil end
|
||||
if type(key) == "number" then return State.battler(st, key) end
|
||||
return st[key]
|
||||
end
|
||||
|
||||
local function ball_for(s, key)
|
||||
if type(key) ~= "number" then return s.ball end
|
||||
s.balls = s.balls or {}
|
||||
local b = s.balls[key]
|
||||
if not b then
|
||||
b = { visible = false, x = 0, y = 0, frame = 0, rot = 0, battler = key, side = State.sideOf(key) }
|
||||
s.balls[key] = b
|
||||
end
|
||||
return b
|
||||
end
|
||||
|
||||
local function present_ids(st, ids)
|
||||
local out = {}
|
||||
for _, id in ipairs(ids or {}) do
|
||||
if st and not State.isAbsent(st, id) and State.battler(st, id) then out[#out + 1] = id end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function ball_status(mon)
|
||||
if not mon then return "empty" end
|
||||
local hp = tonumber(mon.hp) or 0
|
||||
@@ -210,6 +259,33 @@ local function build_trainer(st, opts)
|
||||
playerBalls = playerBalls,
|
||||
frames = 20,
|
||||
})
|
||||
if st.double then
|
||||
local foeIds = present_ids(st, { 1, 3 })
|
||||
local plIds = present_ids(st, { 0, 2 })
|
||||
local sentOut = strings.sentOut
|
||||
if #foeIds == 2 then
|
||||
-- pokefirered/src/battle_message.c:392
|
||||
local who = sentOut:match("^(.-) sent\n") or ""
|
||||
sentOut = who .. " sent\nout " .. State.displayName(State.battler(st, 1)) .. " and "
|
||||
.. State.displayName(State.battler(st, 3)) .. "!"
|
||||
end
|
||||
local goText = "Go! " .. pname .. "!"
|
||||
if #plIds == 2 then
|
||||
-- pokefirered/src/battle_message.c:400
|
||||
goText = "Go! " .. State.displayName(State.battler(st, 0)) .. " and\n"
|
||||
.. State.displayName(State.battler(st, 2)) .. "!"
|
||||
end
|
||||
add("msg", { text = strings.wants })
|
||||
add("msg", { text = sentOut })
|
||||
add("opponent_sendout", { toX = 280, frames = 35, ids = foeIds })
|
||||
add("cry", { side = "enemy", release = true, ids = foeIds })
|
||||
add("healthbox", { side = "enemy", frames = 23, from = -115, ids = foeIds })
|
||||
add("msg", { text = goText, linger = true })
|
||||
add("player_throw", { ids = plIds })
|
||||
add("healthbox", { side = "player", frames = 23, from = 115, ids = plIds })
|
||||
add("wait", { frames = 3 })
|
||||
return steps
|
||||
end
|
||||
add("msg", { text = strings.wants })
|
||||
add("msg", { text = strings.sentOut })
|
||||
add("opponent_sendout", { toX = 280, frames = 35 })
|
||||
@@ -251,6 +327,15 @@ function IntroSeq.begin(st, opts)
|
||||
Anim.present("enemy").darken = 0
|
||||
Anim.present("player").scale = 1
|
||||
Anim.present("enemy").scale = 1
|
||||
if st.double then
|
||||
s.balls = {}
|
||||
for id = 0, 3 do
|
||||
local p = present_of(id)
|
||||
if p then p.visible, p.ox, p.darken, p.scale = false, 0, 0, 1 end
|
||||
local hb = healthbox_of(s, id)
|
||||
if hb then hb.visible = false end
|
||||
end
|
||||
end
|
||||
|
||||
-- Park terrain and sliding sprites off-screen immediately so the first
|
||||
-- rendered frame (and fade-in) starts with them in initial slide positions.
|
||||
@@ -528,17 +613,25 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "opponent_sendout" then
|
||||
local cx, cy = Anim.ENEMY_MON.x, Anim.ENEMY_MON.y
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local st = Battle and Battle._st
|
||||
local keys = d.ids or { "enemy" }
|
||||
local mons = {}
|
||||
for n, key in ipairs(keys) do
|
||||
local cx, cy = center_of(st, key)
|
||||
local ball = ball_for(s, key)
|
||||
ball.visible = true
|
||||
ball.frame = 0
|
||||
ball.rot = 0
|
||||
ball.side = "enemy"
|
||||
ball.x = cx
|
||||
ball.y = cy + 24
|
||||
mons[n] = { key = key, ball = ball }
|
||||
end
|
||||
local tr = s.trainer.enemy
|
||||
local exitFrom = tr.ox or 0
|
||||
local exitTo = (d.toX or 280) - 176
|
||||
s.partyBar.enemy.visible = false
|
||||
s.ball.visible = true
|
||||
s.ball.frame = 0
|
||||
s.ball.rot = 0
|
||||
s.ball.side = "enemy"
|
||||
s.ball.x = cx
|
||||
s.ball.y = cy + 24
|
||||
wait_busy()
|
||||
-- pret OpponentHandleIntroTrainerBallThrow: starts linear slide-out (35 frames)
|
||||
-- AND StartSendOutAnim (16f delay + 12f emergence).
|
||||
@@ -551,48 +644,64 @@ local function run_step(step)
|
||||
if f >= totalFrames then
|
||||
tr.visible = false
|
||||
end
|
||||
-- Ball opens after 16 frames delay (SpriteCB_OpponentMonSendOut)
|
||||
if f == 16 then
|
||||
s.ball.frame = 1
|
||||
if not openedSe then
|
||||
openedSe = true
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_OPEN, { pan = 63 }) end)
|
||||
for _, m in ipairs(mons) do
|
||||
local ball = m.ball
|
||||
-- Ball opens after 16 frames delay (SpriteCB_OpponentMonSendOut)
|
||||
if f == 16 then
|
||||
ball.frame = 1
|
||||
if not openedSe then
|
||||
openedSe = true
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_OPEN, { pan = 63 }) end)
|
||||
end
|
||||
local p = present_of(m.key)
|
||||
if p then
|
||||
p.visible = true
|
||||
p.ox = 0
|
||||
p.oy = 16
|
||||
p.scale = 0.16
|
||||
p.darken = 0
|
||||
end
|
||||
Anim.ballOpen(m.key, ball.x, ball.y)
|
||||
end
|
||||
-- Emergence over 12 frames (frames 16..28) matching pret BATTLER_AFFINE_EMERGE
|
||||
if f > 16 and f <= 28 then
|
||||
local eu = (f - 16) / 12
|
||||
local p = present_of(m.key)
|
||||
if p then
|
||||
p.oy = 16 * (1 - eu)
|
||||
p.scale = 0.16 + 0.84 * eu
|
||||
end
|
||||
ball.frame = (eu < 0.5) and 1 or 2
|
||||
end
|
||||
if f > 28 then
|
||||
local p = present_of(m.key)
|
||||
if p then
|
||||
p.oy = 0
|
||||
p.scale = 1
|
||||
end
|
||||
ball.visible = false
|
||||
end
|
||||
local p = Anim.present("enemy")
|
||||
p.visible = true
|
||||
p.ox = 0
|
||||
p.oy = 16
|
||||
p.scale = 0.16
|
||||
p.darken = 0
|
||||
Anim.ballOpen("enemy", s.ball.x, s.ball.y)
|
||||
end
|
||||
-- Emergence over 12 frames (frames 16..28) matching pret BATTLER_AFFINE_EMERGE
|
||||
if f > 16 and f <= 28 then
|
||||
local eu = (f - 16) / 12
|
||||
local p = Anim.present("enemy")
|
||||
p.oy = 16 * (1 - eu)
|
||||
p.scale = 0.16 + 0.84 * eu
|
||||
s.ball.frame = (eu < 0.5) and 1 or 2
|
||||
end
|
||||
if f > 28 then
|
||||
local p = Anim.present("enemy")
|
||||
p.oy = 0
|
||||
p.scale = 1
|
||||
s.ball.visible = false
|
||||
end
|
||||
end, function()
|
||||
tr.visible = false
|
||||
tr.ox = exitTo
|
||||
s.ball.visible = false
|
||||
local p = Anim.present("enemy")
|
||||
p.oy = 0
|
||||
p.scale = 1
|
||||
for _, m in ipairs(mons) do
|
||||
m.ball.visible = false
|
||||
local p = present_of(m.key)
|
||||
if p then
|
||||
p.oy = 0
|
||||
p.scale = 1
|
||||
end
|
||||
end
|
||||
advance()
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if kind == "player_throw" then
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local st = Battle and Battle._st
|
||||
local keys = d.ids or { "player" }
|
||||
local tr = s.trainer.player
|
||||
if not tr.visible then
|
||||
tr.visible = true
|
||||
@@ -605,7 +714,11 @@ local function run_step(step)
|
||||
local pose = { { 1, 20 }, { 2, 6 }, { 3, 6 }, { 4, 24 }, { 0, 1 } }
|
||||
local poseFrame, poseLeft, poseI = 0, 0, 0
|
||||
local exitTo = -120
|
||||
local pcx, pcy = Anim.PLAYER_MON.x, Anim.PLAYER_MON.y
|
||||
local mons = {}
|
||||
for n, key in ipairs(keys) do
|
||||
local pcx, pcy = center_of(st, key)
|
||||
mons[n] = { key = key, ball = ball_for(s, key), tx = pcx, ty = pcy + 24 }
|
||||
end
|
||||
local threwSe, openedSe = false, false
|
||||
wait_busy()
|
||||
Anim.tweenStage(57, function(u, t)
|
||||
@@ -626,63 +739,84 @@ local function run_step(step)
|
||||
tr.visible = false
|
||||
tr.ox = exitTo
|
||||
end
|
||||
-- pret Task_StartSendOutAnim (31f delay) + Task_DoPokeballSendOutAnim (1f delay) -> spawn at frame 32
|
||||
if f == 32 then
|
||||
s.ball.visible = true
|
||||
s.ball.frame = 0
|
||||
s.ball.rot = 0
|
||||
s.ball.side = "player"
|
||||
s.ball.x = 48
|
||||
s.ball.y = 70
|
||||
s.ball._sx, s.ball._sy = 48, 70
|
||||
s.ball._tx, s.ball._ty = pcx, pcy + 24
|
||||
if not threwSe then
|
||||
threwSe = true
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_THROW, { pan = -64 }) end)
|
||||
for _, m in ipairs(mons) do
|
||||
local ball = m.ball
|
||||
-- pret Task_StartSendOutAnim (31f delay) + Task_DoPokeballSendOutAnim (1f delay) -> spawn at frame 32
|
||||
if f == 32 then
|
||||
ball.visible = true
|
||||
ball.frame = 0
|
||||
ball.rot = 0
|
||||
ball.side = "player"
|
||||
ball.x = 48
|
||||
ball.y = 70
|
||||
ball._sx, ball._sy = 48, 70
|
||||
ball._tx, ball._ty = m.tx, m.ty
|
||||
if not threwSe then
|
||||
threwSe = true
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_THROW, { pan = -64 }) end)
|
||||
end
|
||||
end
|
||||
-- pret SpriteCB_PlayerMonSendOut_1 / 2: 25 frames arc flight with affine rotation
|
||||
if f > 32 and f <= 57 and ball.visible then
|
||||
local bu = (f - 32) / 25
|
||||
local sx, sy = ball._sx, ball._sy
|
||||
local tx, ty = ball._tx, ball._ty
|
||||
ball.x = sx + (tx - sx) * bu
|
||||
ball.y = sy + (ty - sy) * bu + (-30 * 4 * bu * (1 - bu))
|
||||
-- pret sAffineAnim_BallRotate_4: 25 units per frame (approx 0.613 rad/frame)
|
||||
ball.rot = (f - 32) * ((25 / 256) * math.pi * 2)
|
||||
end
|
||||
end
|
||||
-- pret SpriteCB_PlayerMonSendOut_1 / 2: 25 frames arc flight with affine rotation
|
||||
if f > 32 and f <= 57 and s.ball.visible then
|
||||
local bu = (f - 32) / 25
|
||||
local sx, sy = s.ball._sx, s.ball._sy
|
||||
local tx, ty = s.ball._tx, s.ball._ty
|
||||
s.ball.x = sx + (tx - sx) * bu
|
||||
s.ball.y = sy + (ty - sy) * bu + (-30 * 4 * bu * (1 - bu))
|
||||
-- pret sAffineAnim_BallRotate_4: 25 units per frame (approx 0.613 rad/frame)
|
||||
s.ball.rot = (f - 32) * ((25 / 256) * math.pi * 2)
|
||||
end
|
||||
end, function()
|
||||
tr.visible = false
|
||||
tr.ox = exitTo
|
||||
s.ball.frame = 1
|
||||
s.ball.rot = 0
|
||||
if not openedSe then
|
||||
openedSe = true
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_OPEN, { pan = -64 }) end)
|
||||
end
|
||||
Anim.ballOpen("player", s.ball.x, s.ball.y)
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local st = Battle and Battle._st
|
||||
local species = st and st.player and (st.player.species or (st.player.mon and (st.player.mon.species or st.player.mon.speciesId)))
|
||||
if species then
|
||||
-- pokefirered/src/pokeball.c:782
|
||||
pcall(function() Audio.playCry(species, release_cry_mode(st.player.mon), -25) end)
|
||||
for _, m in ipairs(mons) do
|
||||
m.ball.frame = 1
|
||||
m.ball.rot = 0
|
||||
Anim.ballOpen(m.key, m.ball.x, m.ball.y)
|
||||
local p = present_of(m.key)
|
||||
if p then
|
||||
p.visible = true
|
||||
p.ox = 0
|
||||
p.oy = 16
|
||||
p.scale = 0.16
|
||||
end
|
||||
end
|
||||
if not d.ids then
|
||||
local b = st and st.player
|
||||
local species = b and (b.species or (b.mon and (b.mon.species or b.mon.speciesId)))
|
||||
if species then
|
||||
-- pokefirered/src/pokeball.c:782
|
||||
pcall(function() Audio.playCry(species, release_cry_mode(b.mon), -25) end)
|
||||
end
|
||||
end
|
||||
local p = Anim.present("player")
|
||||
p.visible = true
|
||||
p.ox = 0
|
||||
p.oy = 16
|
||||
p.scale = 0.16
|
||||
-- pret BATTLER_AFFINE_EMERGE: 12 frames scaling 40/256 to 256/256
|
||||
Anim.tweenStage(12, function(uu)
|
||||
p.oy = 16 * (1 - uu)
|
||||
p.scale = 0.16 + 0.84 * uu
|
||||
s.ball.frame = (uu < 0.5) and 1 or 2
|
||||
for _, m in ipairs(mons) do
|
||||
local p = present_of(m.key)
|
||||
if p then
|
||||
p.oy = 16 * (1 - uu)
|
||||
p.scale = 0.16 + 0.84 * uu
|
||||
end
|
||||
m.ball.frame = (uu < 0.5) and 1 or 2
|
||||
end
|
||||
end, function()
|
||||
p.oy = 0
|
||||
p.scale = 1
|
||||
s.ball.visible = false
|
||||
s.ball.rot = 0
|
||||
for _, m in ipairs(mons) do
|
||||
local p = present_of(m.key)
|
||||
if p then
|
||||
p.oy = 0
|
||||
p.scale = 1
|
||||
end
|
||||
m.ball.visible = false
|
||||
m.ball.rot = 0
|
||||
end
|
||||
if d.ids and #d.ids > 0 then
|
||||
IntroSeq._cryQueue = { side = "player", ids = d.ids }
|
||||
end
|
||||
advance()
|
||||
end)
|
||||
end)
|
||||
@@ -723,6 +857,12 @@ local function run_step(step)
|
||||
return
|
||||
end
|
||||
|
||||
if kind == "cry" and d.ids then
|
||||
IntroSeq._cryQueue = { side = d.side or "enemy", ids = d.ids }
|
||||
advance()
|
||||
return
|
||||
end
|
||||
|
||||
if kind == "cry" then
|
||||
local side = d.side or "enemy"
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
@@ -740,6 +880,27 @@ local function run_step(step)
|
||||
return
|
||||
end
|
||||
|
||||
if kind == "healthbox" and d.ids then
|
||||
local from = d.from or ((d.side == "player") and 115 or -115)
|
||||
local boxes = {}
|
||||
for _, id in ipairs(d.ids) do
|
||||
local hb = healthbox_of(s, id)
|
||||
if hb then
|
||||
hb.visible = true
|
||||
hb.ox = from
|
||||
boxes[#boxes + 1] = hb
|
||||
end
|
||||
end
|
||||
wait_busy()
|
||||
Anim.tweenStage(d.frames or 23, function(u)
|
||||
for _, hb in ipairs(boxes) do hb.ox = from * (1 - u) end
|
||||
end, function()
|
||||
for _, hb in ipairs(boxes) do hb.ox = 0 end
|
||||
advance()
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if kind == "healthbox" then
|
||||
local side = d.side or "enemy"
|
||||
local hb = s.healthbox[side]
|
||||
@@ -767,9 +928,34 @@ local function run_step(step)
|
||||
advance()
|
||||
end
|
||||
|
||||
-- pokefirered/src/pokeball.c:680
|
||||
local function run_cry_queue()
|
||||
local q = IntroSeq._cryQueue
|
||||
if not q then return true end
|
||||
if q.waiting and Audio.isCryFinished and not Audio.isCryFinished() then return false end
|
||||
q.i = (q.i or 0) + 1
|
||||
local id = q.ids[q.i]
|
||||
if id == nil then
|
||||
IntroSeq._cryQueue = nil
|
||||
return true
|
||||
end
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local b = Battle and Battle._st and State.battler(Battle._st, id)
|
||||
local species = b and (b.species or (b.mon and (b.mon.species or b.mon.speciesId)))
|
||||
if species then
|
||||
local weak = release_cry_mode(b.mon) ~= 0
|
||||
local mode
|
||||
if #q.ids > 1 and q.i == 1 then mode = weak and 12 or 1 else mode = weak and 11 or 0 end
|
||||
pcall(function() Audio.playCry(species, mode, (State.sideOf(id) == "player") and -25 or 25) end)
|
||||
end
|
||||
q.waiting = true
|
||||
return false
|
||||
end
|
||||
|
||||
function IntroSeq.update()
|
||||
if not IntroSeq._steps then return true end
|
||||
if IntroSeq._waitingGen then return false end
|
||||
if IntroSeq._cryQueue and not run_cry_queue() then return false end
|
||||
|
||||
if IntroSeq._pendingSlideIn and Anim.introSlideDone() then
|
||||
local fn = IntroSeq._pendingSlideIn
|
||||
@@ -817,7 +1003,7 @@ function IntroSeq.update()
|
||||
while IntroSeq._steps and IntroSeq._i <= #IntroSeq._steps do
|
||||
run_step(IntroSeq._steps[IntroSeq._i])
|
||||
if IntroSeq._waiting or IntroSeq._waitingFade or IntroSeq._waitingCry
|
||||
or IntroSeq._waitingMsg or IntroSeq._pendingSlideIn then
|
||||
or IntroSeq._waitingMsg or IntroSeq._pendingSlideIn or IntroSeq._cryQueue then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -91,8 +91,13 @@ function BattleItems.storeCaught(session, foeBattler, ballId)
|
||||
return res.location
|
||||
end
|
||||
|
||||
local function sync_player_battler(st)
|
||||
local b = st.player
|
||||
local function user_battler(st, battlerId)
|
||||
if battlerId == nil or battlerId == 0 then return st.player end
|
||||
return st.battlers and st.battlers[battlerId] or st.player
|
||||
end
|
||||
|
||||
local function sync_player_battler(st, battlerId)
|
||||
local b = user_battler(st, battlerId)
|
||||
if not b or not b.mon then return end
|
||||
b.fainted = (tonumber(b.mon.hp) or 0) <= 0
|
||||
b.status = b.mon.status
|
||||
@@ -103,7 +108,7 @@ end
|
||||
-- msgs: string list
|
||||
-- endsTurn: bool (enemy may still move unless endsBattle)
|
||||
-- endsBattle: bool
|
||||
function BattleItems.use(st, adapter, bag, session, itemId, partySlot)
|
||||
function BattleItems.use(st, adapter, bag, session, itemId, partySlot, battlerId)
|
||||
local msgs = {}
|
||||
local function say(t)
|
||||
msgs[#msgs + 1] = t
|
||||
@@ -142,11 +147,12 @@ function BattleItems.use(st, adapter, bag, session, itemId, partySlot)
|
||||
Bag.remove(bag, itemId, 1)
|
||||
say(tostring(session and session.name or "RED") .. " used\nthe " .. name .. "!")
|
||||
local rng = adapter and adapter.rng and adapter:rng() or math.random
|
||||
local caught, shakes = BattleItems.tryCatch(itemId, st.enemy, st, rng, session)
|
||||
local foe = Catching.targetFor(st, battlerId)
|
||||
local caught, shakes = BattleItems.tryCatch(itemId, foe, st, rng, session)
|
||||
if caught then
|
||||
local res = Catching.storeCaught(session, st.enemy, itemId)
|
||||
local ename = (st.enemy and st.enemy.mon and (st.enemy.mon.nickname or st.enemy.mon.name))
|
||||
or Pokemon.name(st.enemy and st.enemy.species) or "POKéMON"
|
||||
local res = Catching.storeCaught(session, foe, itemId)
|
||||
local ename = (foe and foe.mon and (foe.mon.nickname or foe.mon.name))
|
||||
or Pokemon.name(foe and foe.species) or "POKéMON"
|
||||
say("Gotcha!\n" .. ename .. " was caught!")
|
||||
if res and res.firstTimeCaught then
|
||||
say(ename .. "'s data was\nadded to the POKéDEX.")
|
||||
@@ -171,7 +177,7 @@ function BattleItems.use(st, adapter, bag, session, itemId, partySlot)
|
||||
-- X items
|
||||
if num and X_STAT[num] then
|
||||
local stat = X_STAT[num]
|
||||
local battler = st.player
|
||||
local battler = user_battler(st, battlerId)
|
||||
if not battler or not battler.stages then
|
||||
return "error", msgs, false, false
|
||||
end
|
||||
@@ -235,6 +241,8 @@ function BattleItems.use(st, adapter, bag, session, itemId, partySlot)
|
||||
if st.player and st.player.partyIndex == partySlot then
|
||||
sync_player_battler(st)
|
||||
end
|
||||
local b2 = st.double and st.battlers and st.battlers[2]
|
||||
if b2 and b2.partyIndex == partySlot then sync_player_battler(st, 2) end
|
||||
return "heal", msgs, true, false
|
||||
end
|
||||
|
||||
@@ -242,4 +250,124 @@ function BattleItems.use(st, adapter, bag, session, itemId, partySlot)
|
||||
return "error", msgs, false, false
|
||||
end
|
||||
|
||||
local ENEMY_CURE_TEXT = {
|
||||
[0] = "\nsnapped it out of confusion!",
|
||||
[1] = "\ncured paralysis!",
|
||||
[2] = "\ndefrosted it!",
|
||||
[3] = "\nhealed its burn!",
|
||||
[4] = "\ncured poison!",
|
||||
[5] = "\nwoke it from its sleep!",
|
||||
}
|
||||
|
||||
local ENEMY_STAT_NAME = { [1] = "ATTACK", [2] = "DEFENSE", [3] = "SPEED", [4] = "SP. ATK", [5] = "SP. DEF", [6] = "accuracy" }
|
||||
|
||||
-- pokefirered/src/pokemon.c:4001
|
||||
local function enemy_item_effects(st, ad, b, e)
|
||||
local AiItems = require("src.core.game3.battle.ai_items")
|
||||
local band = AiItems.band
|
||||
if band(e[1], 0x80) ~= 0 and b.expInfatuated then b.expInfatuated, b.expInfatuatedWith = nil, nil end
|
||||
if band(e[1], 0x30) ~= 0 and not b.expFocusEnergy then
|
||||
b.expFocusEnergy, b.focusEnergy = true, true
|
||||
end
|
||||
local function raise(key, n)
|
||||
local cur = b.stages and b.stages[key] or 0
|
||||
if n > 0 and cur < 6 then ad:changeStages(b, { [key] = n }) end
|
||||
end
|
||||
raise("attack", band(e[1], 0x0F))
|
||||
raise("defense", math.floor(band(e[2], 0xF0) / 16))
|
||||
raise("speed", band(e[2], 0x0F))
|
||||
raise("accuracy", math.floor(band(e[3], 0xF0) / 16))
|
||||
raise("spAtk", band(e[3], 0x0F))
|
||||
local side = ad:ownSide(b)
|
||||
if band(e[4], 0x80) ~= 0 and side and (tonumber(side.expMistTurns) or 0) == 0 then
|
||||
side.expMistTurns = 5
|
||||
end
|
||||
local s = AiItems.statusName(b)
|
||||
local cure = (band(e[4], 0x20) ~= 0 and s == "SLP") or (band(e[4], 0x10) ~= 0 and (s == "PSN" or s == "TOX"))
|
||||
or (band(e[4], 0x08) ~= 0 and s == "BRN") or (band(e[4], 0x04) ~= 0 and s == "FRZ")
|
||||
or (band(e[4], 0x02) ~= 0 and s == "PAR")
|
||||
if cure then
|
||||
if s == "SLP" then b.expNightmare = nil end
|
||||
ad:clearStatus(b)
|
||||
end
|
||||
if band(e[4], 0x01) ~= 0 and (tonumber(b.confusionTurns) or 0) > 0 then b.confusionTurns = nil end
|
||||
if band(e[5], 0x04) ~= 0 then
|
||||
local hp, maxHp = ad:hp(b), ad:maxHp(b)
|
||||
local revive = band(e[5], 0x40) ~= 0
|
||||
if (revive and hp == 0) or (not revive and hp ~= 0) then
|
||||
local data = e.hp or 0
|
||||
if data == AiItems.HEAL_HP_FULL then
|
||||
data = maxHp - hp
|
||||
elseif data == AiItems.HEAL_HP_HALF then
|
||||
data = math.floor(maxHp / 2)
|
||||
if data == 0 then data = 1 end
|
||||
elseif data == AiItems.HEAL_HP_LVL_UP then
|
||||
data = 0
|
||||
end
|
||||
if maxHp ~= hp then ad:heal(b, data) end
|
||||
end
|
||||
end
|
||||
if b.mon then b.status = b.mon.status end
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_main.c:4150
|
||||
function BattleItems.enemyUse(st, adapter, act)
|
||||
local AiItems = require("src.core.game3.battle.ai_items")
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local id = act and (act.battler or 1) or 1
|
||||
local b = State.battler(st, id)
|
||||
if not b or not b.mon or not act.item then return false end
|
||||
local item = act.item
|
||||
local e = AiItems.effect(item)
|
||||
local kind = act.aiItemType or (e and AiItems.itemType(item, e))
|
||||
local flags = tonumber(act.aiItemFlags) or 0
|
||||
b.expFuryCutter, b.destinyBond, b.expDestinyBond, b.expGrudge = 0, nil, nil, nil
|
||||
local iname = ItemsData.displayName(item)
|
||||
local bname = adapter:displayName(b)
|
||||
local trainer = (st.trainerClassName and st.trainerClassName ~= "")
|
||||
and (st.trainerClassName .. " " .. (st.trainerName or "")) or (st.trainerName or "TRAINER")
|
||||
-- pokefirered/data/battle_scripts_2.s:134
|
||||
adapter:pushEvent({ kind = "item_use", battler = id, side = b.side, item = item, se = "SE_USE_ITEM" })
|
||||
adapter:say(trainer .. "\nused " .. iname .. "!")
|
||||
if e then enemy_item_effects(st, adapter, b, e) end
|
||||
local T = AiItems.TYPE
|
||||
if kind == T.FULL_RESTORE or kind == T.HEAL_HP then
|
||||
adapter:say(bname .. "'s " .. iname .. "\nrestored health!")
|
||||
adapter:pushEvent({ kind = "status", battler = id, side = b.side })
|
||||
elseif kind == T.CURE_CONDITION then
|
||||
local chooser = 0
|
||||
if flags % 2 == 1 then
|
||||
if AiItems.band(flags, 0x3E) ~= 0 then chooser = 5 end
|
||||
else
|
||||
local f = flags
|
||||
while f > 0 and f % 2 == 0 do
|
||||
f = math.floor(f / 2)
|
||||
chooser = chooser + 1
|
||||
end
|
||||
end
|
||||
adapter:say(bname .. "'s " .. iname .. (ENEMY_CURE_TEXT[chooser] or ENEMY_CURE_TEXT[0]))
|
||||
adapter:pushEvent({ kind = "status", battler = id, side = b.side })
|
||||
elseif kind == T.X_STAT then
|
||||
if AiItems.band(flags, 0x80) ~= 0 then
|
||||
adapter:say(bname .. " used\n" .. iname .. " to hustle!")
|
||||
else
|
||||
local stat, f = 1, flags
|
||||
while f > 0 and f % 2 == 0 do
|
||||
f = math.floor(f / 2)
|
||||
stat = stat + 1
|
||||
end
|
||||
adapter:say("Using " .. iname .. ", the " .. (ENEMY_STAT_NAME[stat] or "ATTACK")
|
||||
.. "\nof " .. bname .. " rose!")
|
||||
end
|
||||
elseif kind == T.GUARD_SPECS then
|
||||
-- pokefirered/src/battle_main.c:4216
|
||||
if st.double then
|
||||
adapter:say(bname .. " is getting\npumped!")
|
||||
else
|
||||
adapter:say(((b.side == "player") and "Ally" or "Foe") .. " became\nshrouded in MIST!")
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return BattleItems
|
||||
|
||||
@@ -174,12 +174,39 @@ function Moves.loadRomPack(cache)
|
||||
if not pack then pack = load_lua(root) end
|
||||
if pack and pack.moves then
|
||||
Moves._rom = pack.moves
|
||||
Moves._runReloadHooks()
|
||||
return true
|
||||
end
|
||||
Moves._rom = nil
|
||||
return false
|
||||
end
|
||||
|
||||
Moves._reloadHooks = {}
|
||||
|
||||
function Moves.onReload(fn, key)
|
||||
if type(fn) ~= "function" then return function() end end
|
||||
local hooks = Moves._reloadHooks
|
||||
for i = #hooks, 1, -1 do
|
||||
local h = hooks[i]
|
||||
if h.fn == fn or (key ~= nil and h.key == key) then
|
||||
table.remove(hooks, i)
|
||||
end
|
||||
end
|
||||
local entry = { fn = fn, key = key }
|
||||
hooks[#hooks + 1] = entry
|
||||
return function()
|
||||
for i = #hooks, 1, -1 do
|
||||
if hooks[i] == entry then table.remove(hooks, i) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Moves._runReloadHooks()
|
||||
local snapshot = {}
|
||||
for i, h in ipairs(Moves._reloadHooks) do snapshot[i] = h end
|
||||
for _, h in ipairs(snapshot) do pcall(h.fn, Moves) end
|
||||
end
|
||||
|
||||
function Moves.romReady()
|
||||
if not Moves._romLoaded then Moves.loadRomPack(nil) end
|
||||
return Moves._rom ~= nil
|
||||
|
||||
@@ -62,4 +62,23 @@ function PartyView.firstAliveIndex(party)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_setup.c:542
|
||||
function PartyView.doubleTransitionLevels(playerParty, foeParty)
|
||||
local pSum, need = 0, 2
|
||||
for _, mon in ipairs(playerParty or {}) do
|
||||
local sp = tonumber(mon.species or mon.speciesId) or 0
|
||||
if sp ~= 0 and sp ~= 412 and not mon.isEgg and (tonumber(mon.hp) or 0) ~= 0 then
|
||||
pSum = (pSum + (tonumber(mon.level or mon.lvl) or 0)) % 256
|
||||
need = need - 1
|
||||
if need == 0 then break end
|
||||
end
|
||||
end
|
||||
-- pokefirered/src/battle_setup.c:561
|
||||
local eSum = 0
|
||||
for i = 1, math.min(2, #(foeParty or {})) do
|
||||
eSum = (eSum + (tonumber(foeParty[i].level or foeParty[i].lvl) or 0)) % 256
|
||||
end
|
||||
return pSum, eSum
|
||||
end
|
||||
|
||||
return PartyView
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Auto-derived from pret gMonFront/BackPicCoords + gEnemyMonElevation (FRLG).
|
||||
-- y_offset per internal SPECIES id. Do not edit by hand.
|
||||
return {
|
||||
local PicCoords = {
|
||||
front = {
|
||||
[0] = 0,
|
||||
[1] = 16,
|
||||
@@ -894,3 +894,18 @@ return {
|
||||
[411] = 12,
|
||||
},
|
||||
}
|
||||
|
||||
local AnimCoords = require("src.core.game3.battle.anim_coords")
|
||||
|
||||
-- pokefirered/src/battle_anim_mons.c:31
|
||||
PicCoords.BATTLER_COORDS = { [false] = AnimCoords.SINGLES, [true] = AnimCoords.DOUBLES }
|
||||
PicCoords.SUBPRIORITY = AnimCoords.SUBPRIORITY
|
||||
PicCoords.BG_PRIORITY_RANK = AnimCoords.BG_PRIORITY_RANK
|
||||
PicCoords.DRAW_ORDER = AnimCoords.DRAW_ORDER_DOUBLES
|
||||
|
||||
function PicCoords.battlerCoords(double, id)
|
||||
local t = PicCoords.BATTLER_COORDS[double and true or false]
|
||||
return t[tonumber(id) or 0] or t[0]
|
||||
end
|
||||
|
||||
return PicCoords
|
||||
|
||||
@@ -18,6 +18,17 @@ local function side_battler(ad, sideKey)
|
||||
return sideKey == "player" and st.player or st.enemy
|
||||
end
|
||||
|
||||
local function slot_battler(ad, id)
|
||||
local State = require("src.core.game3.battle.state")
|
||||
if id == nil or not State.isPresent(ad._st, id) then return nil end
|
||||
return State.battler(ad._st, id)
|
||||
end
|
||||
|
||||
local function token_battler(ad, key, tok, field)
|
||||
if ad._st.double and tok[field] ~= nil then return slot_battler(ad, tok[field]) end
|
||||
return side_battler(ad, key)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_util.c:505
|
||||
local function side_timer(field, label)
|
||||
return function(ctx)
|
||||
@@ -102,7 +113,7 @@ function Handlers.tickWeather(ad)
|
||||
ad:applyHpLoss(b, dmg)
|
||||
if ad:isFainted(b) then
|
||||
b._faintAnnounced = true
|
||||
ad:pushEvent({ kind = "faint", side = b.side })
|
||||
ad:pushEvent({ kind = "faint", side = b.side, battler = b.id })
|
||||
ad:say(name(ad, b) .. " fainted!")
|
||||
ad:emitFaint(b)
|
||||
end
|
||||
@@ -123,6 +134,7 @@ function Handlers.registerAll()
|
||||
-- pokefirered/src/battle_util.c:603
|
||||
Residuals.register("wish", function(ctx)
|
||||
local ad = ctx.adapter
|
||||
if ad._st.double then return Handlers.wishDoubles(ad) end
|
||||
for _, key in ipairs({ "player", "enemy" }) do
|
||||
local side = key == "player" and ad._st.playerSide or ad._st.enemySide
|
||||
if side and side.tokens then
|
||||
@@ -156,6 +168,43 @@ function Handlers.registerAll()
|
||||
end
|
||||
end)
|
||||
|
||||
-- pokefirered/src/battle_util.c:603
|
||||
function Handlers.wishDoubles(ad)
|
||||
local st = ad._st
|
||||
for _, b0 in ipairs(Residuals.sortedBattlers(ad)) do
|
||||
local id = b0.id
|
||||
local side = (id % 2 == 0) and st.playerSide or st.enemySide
|
||||
if side and side.tokens then
|
||||
local keep = {}
|
||||
for _, tok in ipairs(side.tokens) do
|
||||
if tok.id == "EXP_WISH" and (tok.battlerId == nil or tok.battlerId == id) then
|
||||
tok.turns = (tok.turns or 1) - 1
|
||||
if tok.turns <= 0 then
|
||||
local b = slot_battler(ad, id)
|
||||
if b and ad:hp(b) > 0 then
|
||||
ad:playAnim("general", "WISH_HEAL", b, b)
|
||||
ad:say(tostring(tok.wisher or name(ad, b)) .. "'s WISH\ncame true!")
|
||||
if ad:hp(b) >= ad:maxHp(b) then
|
||||
ad:say(name(ad, b) .. "'s\nHP is full!")
|
||||
else
|
||||
local heal = math.floor(ad:maxHp(b) / 2)
|
||||
if heal == 0 then heal = 1 end
|
||||
ad:heal(b, heal)
|
||||
ad:say(name(ad, b) .. " regained\nhealth!")
|
||||
end
|
||||
end
|
||||
else
|
||||
keep[#keep + 1] = tok
|
||||
end
|
||||
else
|
||||
keep[#keep + 1] = tok
|
||||
end
|
||||
end
|
||||
side.tokens = keep
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Residuals.register("weather_continue", function(ctx)
|
||||
Handlers.tickWeather(ctx.adapter)
|
||||
end)
|
||||
@@ -197,7 +246,11 @@ function Handlers.registerAll()
|
||||
local ad, b = ctx.adapter, ctx.target
|
||||
if not b or not b.expSeeded then return end
|
||||
local src = b.expSeedSource
|
||||
if src and src.side then src = side_battler(ad, src.side) end
|
||||
if src and ad._st.double then
|
||||
src = slot_battler(ad, src.id)
|
||||
elseif src and src.side then
|
||||
src = side_battler(ad, src.side)
|
||||
end
|
||||
if not src or ad:isFainted(src) or ad:isFainted(b) then return end
|
||||
local dmg = math.floor(ad:maxHp(b) / 8)
|
||||
if dmg == 0 then dmg = 1 end
|
||||
@@ -401,12 +454,13 @@ function Handlers.registerAll()
|
||||
-- pokefirered/src/battle_util.c:1081
|
||||
Residuals.register("future_sight", function(ctx)
|
||||
local ad = ctx.adapter
|
||||
if ad._st.double then return Handlers.futureSightDoubles(ad) end
|
||||
for _, key in ipairs({ "player", "enemy" }) do
|
||||
local side = key == "player" and ad._st.playerSide or ad._st.enemySide
|
||||
local target = side_battler(ad, key)
|
||||
if side and side.tokens then
|
||||
local keep = {}
|
||||
for _, tok in ipairs(side.tokens) do
|
||||
local target = token_battler(ad, key, tok, "targetId")
|
||||
if tok.id == "EXP_FUTURE_SIGHT" then
|
||||
tok.turns = (tok.turns or 1) - 1
|
||||
if tok.turns <= 0 then
|
||||
@@ -444,10 +498,40 @@ function Handlers.registerAll()
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_util.c:1081
|
||||
function Handlers.futureSightDoubles(ad)
|
||||
local st = ad._st
|
||||
for id = 0, 3 do
|
||||
local side = (id % 2 == 0) and st.playerSide or st.enemySide
|
||||
if side and side.tokens then
|
||||
local keep = {}
|
||||
for _, tok in ipairs(side.tokens) do
|
||||
local tid = tok.targetId or ((id % 2 == 0) and 0 or 1)
|
||||
if tok.id == "EXP_FUTURE_SIGHT" and tid == id then
|
||||
tok.turns = (tok.turns or 1) - 1
|
||||
if tok.turns <= 0 then
|
||||
local target = slot_battler(ad, id)
|
||||
if target and ad:hp(target) > 0 then Handlers.futureSightHit(ad, tok, target) end
|
||||
else
|
||||
keep[#keep + 1] = tok
|
||||
end
|
||||
else
|
||||
keep[#keep + 1] = tok
|
||||
end
|
||||
end
|
||||
side.tokens = keep
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/data/battle_scripts_1.s:3461
|
||||
function Handlers.futureSightHit(ad, tok, target)
|
||||
local Engine = require("src.core.game3.battle.engine")
|
||||
local attacker = side_battler(ad, tok.attackerSide or (target.side == "player" and "enemy" or "player"))
|
||||
if ad._st.double and tok.attackerId ~= nil then
|
||||
local State = require("src.core.game3.battle.state")
|
||||
attacker = State.battler(ad._st, tok.attackerId) or attacker
|
||||
end
|
||||
ad:say(name(ad, target) .. " took the\n" .. tostring(tok.moveName or "FUTURE SIGHT") .. " attack!")
|
||||
local anim = { moveId = tok.moveId, user = attacker, target = target, hits = {}, heals = {}, faints = {} }
|
||||
local M = Engine.newContext(attacker, target, tok.moveId or 248, nil, ad, ad._st, {}, anim, { futureSight = true })
|
||||
|
||||
@@ -28,6 +28,27 @@ end
|
||||
|
||||
-- pokefirered/src/battle_util.c:494
|
||||
local function sortedBattlers(adapter)
|
||||
local st = adapter and adapter._st
|
||||
if st and st.double then
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local ids = st._endTurnOrder
|
||||
if not ids then
|
||||
local pri
|
||||
if st.turnActions then
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
pri = {}
|
||||
for _, act in ipairs(st.turnActions) do
|
||||
if act.battler ~= nil and act.kind == "move" then pri[act.battler] = Moves.priority(act.move) end
|
||||
end
|
||||
end
|
||||
ids = State.speedOrder(st, adapter, { priority = pri })
|
||||
end
|
||||
local out = {}
|
||||
for _, id in ipairs(ids) do
|
||||
if State.isPresent(st, id) then out[#out + 1] = State.battler(st, id) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
local list = adapter:activeBattlers() or {}
|
||||
local a, b = list[1], list[2]
|
||||
if a and b then
|
||||
@@ -48,7 +69,7 @@ local function runStepAndRecord(adapter, battler, phase, fn, events)
|
||||
local hpBefore = {}
|
||||
for _, b in ipairs(active) do
|
||||
if b and b.side then
|
||||
hpBefore[b.side] = adapter:hp(b)
|
||||
hpBefore[b] = adapter:hp(b)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,21 +89,22 @@ local function runStepAndRecord(adapter, battler, phase, fn, events)
|
||||
local faints = {}
|
||||
for _, b in ipairs(active) do
|
||||
if b and b.side then
|
||||
local before = hpBefore[b.side] or 0
|
||||
local before = hpBefore[b] or 0
|
||||
local after = adapter:hp(b)
|
||||
if before ~= after then
|
||||
hpChanges[#hpChanges + 1] = {
|
||||
side = b.side,
|
||||
battler = b.id,
|
||||
from = before,
|
||||
to = after,
|
||||
maxHp = adapter:maxHp(b),
|
||||
}
|
||||
end
|
||||
if adapter:isFainted(b) and before > 0 then
|
||||
faints[#faints + 1] = { side = b.side }
|
||||
faints[#faints + 1] = { side = b.side, battler = b.id }
|
||||
if not b._faintAnnounced then
|
||||
b._faintAnnounced = true
|
||||
if adapter.pushEvent then adapter:pushEvent({ kind = "faint", side = b.side }) end
|
||||
if adapter.pushEvent then adapter:pushEvent({ kind = "faint", side = b.side, battler = b.id }) end
|
||||
adapter:say(adapter:displayName(b) .. " fainted!")
|
||||
end
|
||||
adapter:emitFaint(b)
|
||||
@@ -134,23 +156,36 @@ function Residuals.collectEvents(adapter)
|
||||
b.expEnduring = nil
|
||||
end
|
||||
|
||||
local st = adapter._st
|
||||
if st and st.double then
|
||||
st._endTurnOrder = nil
|
||||
-- pokefirered/src/battle_util.c:484
|
||||
local order = {}
|
||||
for _, b in ipairs(sortedBattlers(adapter)) do order[#order + 1] = b.id end
|
||||
st._endTurnOrder = order
|
||||
st.turnOrder = order
|
||||
end
|
||||
|
||||
for _, phase in ipairs(Rules.FIELD_PHASES_ORDER) do
|
||||
if adapter:isBattleDecided() then return events end
|
||||
if adapter:isBattleDecided() then break end
|
||||
run_phase(adapter, phase, nil, events)
|
||||
end
|
||||
|
||||
for _, battler in ipairs(sortedBattlers(adapter)) do
|
||||
for _, phase in ipairs(Rules.BATTLER_PHASES_ORDER) do
|
||||
if adapter:isBattleDecided() or adapter:isFainted(battler) then break end
|
||||
if run_phase(adapter, phase, battler, events) then break end
|
||||
if not adapter:isBattleDecided() then
|
||||
for _, battler in ipairs(sortedBattlers(adapter)) do
|
||||
for _, phase in ipairs(Rules.BATTLER_PHASES_ORDER) do
|
||||
if adapter:isBattleDecided() or adapter:isFainted(battler) then break end
|
||||
if run_phase(adapter, phase, battler, events) then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, phase in ipairs(Rules.POST_PHASES_ORDER) do
|
||||
if adapter:isBattleDecided() then return events end
|
||||
if adapter:isBattleDecided() then break end
|
||||
run_phase(adapter, phase, nil, events)
|
||||
end
|
||||
|
||||
if st then st._endTurnOrder = nil end
|
||||
return events
|
||||
end
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ end
|
||||
|
||||
function State.makeBattler(mon, side, opts)
|
||||
opts = opts or {}
|
||||
local id = tonumber(opts.id) or ((side == "enemy") and 1 or 0)
|
||||
mon = Damage.ensureStats(mon, mon and mon.level)
|
||||
local species = species_id(mon)
|
||||
local t1, t2 = types_for(species)
|
||||
@@ -39,7 +40,9 @@ function State.makeBattler(mon, side, opts)
|
||||
end
|
||||
return {
|
||||
mon = mon,
|
||||
id = id,
|
||||
side = side, -- "player" | "enemy"
|
||||
flank = (id < 2) and "left" or "right",
|
||||
partyIndex = opts.partyIndex or 1,
|
||||
species = species,
|
||||
type1 = t1,
|
||||
@@ -57,6 +60,174 @@ function State.makeBattler(mon, side, opts)
|
||||
}
|
||||
end
|
||||
|
||||
function State.PARTNER(id) return (id + 2) % 4 end
|
||||
function State.OPPOSITE(id) return (id % 2 == 0) and (id + 1) or (id - 1) end
|
||||
function State.sideOf(id) return (id % 2 == 0) and "player" or "enemy" end
|
||||
function State.flankOf(id) return (id < 2) and "left" or "right" end
|
||||
function State.positionsOnSide(side) return (side == "player") and { 0, 2 } or { 1, 3 } end
|
||||
|
||||
function State.idOf(b)
|
||||
if type(b) == "number" then return b end
|
||||
if type(b) == "string" then return (b == "enemy") and 1 or 0 end
|
||||
if type(b) ~= "table" then return nil end
|
||||
if b.id then return b.id end
|
||||
if b.side == "enemy" then return 1 end
|
||||
if b.side == "player" then return 0 end
|
||||
return nil
|
||||
end
|
||||
|
||||
function State.battler(st, id)
|
||||
if not st or id == nil then return nil end
|
||||
if st.battlers then return st.battlers[id] end
|
||||
if id == 0 then return st.player end
|
||||
if id == 1 then return st.enemy end
|
||||
return nil
|
||||
end
|
||||
|
||||
function State.occupant(st, b)
|
||||
if b == nil or not st then return b end
|
||||
if type(b) == "string" then return st[b] end
|
||||
local id = State.idOf(b)
|
||||
if id == nil then return b end
|
||||
return State.battler(st, id) or b
|
||||
end
|
||||
|
||||
function State.isAbsent(st, id)
|
||||
return st and st.absent and st.absent[id] and true or false
|
||||
end
|
||||
|
||||
function State.isPresent(st, id)
|
||||
return State.battler(st, id) ~= nil and not State.isAbsent(st, id)
|
||||
end
|
||||
|
||||
function State.isAlive(st, id)
|
||||
return State.isPresent(st, id) and not State.isFainted(State.battler(st, id))
|
||||
end
|
||||
|
||||
function State.partner(st, b)
|
||||
local id = State.idOf(b)
|
||||
if id == nil or not (st and st.double) then return nil end
|
||||
local p = State.PARTNER(id)
|
||||
if not State.isPresent(st, p) then return nil end
|
||||
return State.battler(st, p)
|
||||
end
|
||||
|
||||
function State.opposite(st, b)
|
||||
local id = State.idOf(b)
|
||||
if id == nil then return nil end
|
||||
return State.battler(st, State.OPPOSITE(id))
|
||||
end
|
||||
|
||||
function State.presentIds(st)
|
||||
local out = {}
|
||||
for id = 0, 3 do
|
||||
if State.isPresent(st, id) then out[#out + 1] = id end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function State.present(st)
|
||||
local out = {}
|
||||
for id = 0, 3 do
|
||||
if State.isPresent(st, id) then out[#out + 1] = State.battler(st, id) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function State.foes(st, b)
|
||||
local id = State.idOf(b)
|
||||
local out = {}
|
||||
if id == nil then return out end
|
||||
for i = 0, 3 do
|
||||
if i % 2 ~= id % 2 and State.isPresent(st, i) then out[#out + 1] = State.battler(st, i) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function State.allies(st, b)
|
||||
local id = State.idOf(b)
|
||||
local out = {}
|
||||
if id == nil then return out end
|
||||
for i = 0, 3 do
|
||||
if i % 2 == id % 2 and State.isPresent(st, i) then out[#out + 1] = State.battler(st, i) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- pokefirered/src/pokemon.c:2651
|
||||
function State.countPresentOnSide(st, side)
|
||||
local n = 0
|
||||
for _, id in ipairs(State.positionsOnSide(side)) do
|
||||
if State.isPresent(st, id) then n = n + 1 end
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_main.c:3400
|
||||
function State.speedOrder(st, adapter, opts)
|
||||
opts = opts or {}
|
||||
local Engine = package.loaded["src.core.game3.battle.engine"]
|
||||
local ids = State.presentIds(st)
|
||||
local spe = {}
|
||||
for _, id in ipairs(ids) do
|
||||
local b = State.battler(st, id)
|
||||
if Engine and Engine.speedOf then
|
||||
spe[id] = Engine.speedOf(b, st, adapter)
|
||||
else
|
||||
spe[id] = tonumber(b.mon and (b.mon.speed or b.mon.spe)) or 50
|
||||
end
|
||||
end
|
||||
local function coin()
|
||||
if adapter and adapter.roll then return adapter:roll(0, 1) end
|
||||
return math.random(0, 1)
|
||||
end
|
||||
for i = 1, #ids - 1 do
|
||||
for j = i + 1, #ids do
|
||||
local a, b = ids[i], ids[j]
|
||||
local pa = opts.priority and opts.priority[a] or 0
|
||||
local pb = opts.priority and opts.priority[b] or 0
|
||||
local swap
|
||||
if pa ~= pb then
|
||||
swap = pa < pb
|
||||
elseif spe[a] == spe[b] then
|
||||
swap = coin() == 1
|
||||
else
|
||||
swap = spe[a] < spe[b]
|
||||
end
|
||||
if swap then ids[i], ids[j] = b, a end
|
||||
end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
local function battler_slots(st)
|
||||
return setmetatable({}, {
|
||||
__index = function(_, k)
|
||||
if k == 0 then return st.player end
|
||||
if k == 1 then return st.enemy end
|
||||
return nil
|
||||
end,
|
||||
__newindex = function(t, k, v)
|
||||
if k == 0 then st.player = v
|
||||
elseif k == 1 then st.enemy = v
|
||||
else rawset(t, k, v) end
|
||||
end,
|
||||
})
|
||||
end
|
||||
State.newSlots = battler_slots
|
||||
|
||||
local function first_usable(party, exclude)
|
||||
for i = 1, #(party or {}) do
|
||||
local m = party[i]
|
||||
if i ~= exclude and m and not m.isEgg and (tonumber(m.hp) or 0) > 0
|
||||
and (tonumber(m.species or m.speciesId) or 0) ~= 0 then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
State.firstUsable = first_usable
|
||||
|
||||
function State.new(opts)
|
||||
opts = opts or {}
|
||||
local playerParty = opts.playerParty or {}
|
||||
@@ -81,14 +252,79 @@ function State.new(opts)
|
||||
fleeAttempts = 0,
|
||||
log = {},
|
||||
}
|
||||
st.double = opts.double and true or false
|
||||
st.battlersCount = st.double and 4 or 2
|
||||
st.battlers = battler_slots(st)
|
||||
st.absent = {}
|
||||
st.chosen = {}
|
||||
st.turnOrder = {}
|
||||
st.monToSwitchInto = {}
|
||||
st.moveTarget = {}
|
||||
local pMon = playerParty[pi]
|
||||
st.player = State.makeBattler(pMon, "player", { partyIndex = pi })
|
||||
st.player = State.makeBattler(pMon, "player", { partyIndex = pi, id = 0 })
|
||||
local eMon = foeMon or st.foeParty[1]
|
||||
st.enemy = State.makeBattler(eMon, "enemy", { partyIndex = 1 })
|
||||
State.trackParticipant(st, st.enemy, pi)
|
||||
st.enemy = State.makeBattler(eMon, "enemy", { partyIndex = 1, id = 1 })
|
||||
if not st.double then
|
||||
State.trackParticipant(st, st.enemy, pi)
|
||||
return st
|
||||
end
|
||||
-- pokefirered/src/battle_controllers.c:290
|
||||
local p2 = opts.partnerIndex or first_usable(playerParty, pi)
|
||||
if p2 and playerParty[p2] then
|
||||
st.battlers[2] = State.makeBattler(playerParty[p2], "player", { partyIndex = p2, id = 2 })
|
||||
else
|
||||
st.absent[2] = true
|
||||
end
|
||||
local e2 = opts.foePartnerIndex or first_usable(st.foeParty, st.enemy.partyIndex)
|
||||
if e2 and st.foeParty[e2] then
|
||||
st.battlers[3] = State.makeBattler(st.foeParty[e2], "enemy", { partyIndex = e2, id = 3 })
|
||||
else
|
||||
st.absent[3] = true
|
||||
end
|
||||
State.resetSentPokes(st)
|
||||
return st
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_util.c:239
|
||||
function State.resetSentPokes(st)
|
||||
local sent = {}
|
||||
for _, id in ipairs({ 0, 2 }) do
|
||||
local b = State.battler(st, id)
|
||||
if b and b.partyIndex then sent[#sent + 1] = b.partyIndex end
|
||||
end
|
||||
for _, id in ipairs({ 1, 3 }) do
|
||||
local foe = State.battler(st, id)
|
||||
if foe then
|
||||
foe.participants = {}
|
||||
for _, pi in ipairs(sent) do foe.participants[pi] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_util.c:254
|
||||
function State.opponentSwitchInResetSentPokes(st, foeBattler)
|
||||
if not foeBattler then return end
|
||||
foeBattler.participants = {}
|
||||
for _, id in ipairs({ 0, 2 }) do
|
||||
local b = State.battler(st, id)
|
||||
if b and not State.isAbsent(st, id) and b.partyIndex then
|
||||
foeBattler.participants[b.partyIndex] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_util.c:273
|
||||
function State.updateSentPokes(st, battler)
|
||||
if not battler then return end
|
||||
if battler.side == "enemy" then
|
||||
return State.opponentSwitchInResetSentPokes(st, battler)
|
||||
end
|
||||
for _, id in ipairs({ 1, 3 }) do
|
||||
local foe = State.battler(st, id)
|
||||
if foe then State.trackParticipant(st, foe, battler.partyIndex) end
|
||||
end
|
||||
end
|
||||
|
||||
function State.displayName(battler)
|
||||
if not battler then return "POKéMON" end
|
||||
local mon = battler.mon
|
||||
|
||||
@@ -57,6 +57,44 @@ local function stage()
|
||||
return Anim.stage()
|
||||
end
|
||||
|
||||
local function step_battler(st, d)
|
||||
if d.id ~= nil then return st and State.battler(st, d.id) end
|
||||
return st and st[d.side or "player"]
|
||||
end
|
||||
|
||||
local function step_side(d)
|
||||
if d.id ~= nil then return State.sideOf(d.id) end
|
||||
return d.side or "player"
|
||||
end
|
||||
|
||||
local function step_present(d)
|
||||
if d.id == nil then return Anim.present(d.side or "player") end
|
||||
return Anim.present(d.id) or (d.id < 2 and Anim.present(State.sideOf(d.id))) or nil
|
||||
end
|
||||
|
||||
local function step_healthbox(s, d)
|
||||
local hb = s and s.healthbox
|
||||
if not hb then return nil end
|
||||
if d.id == nil then return hb[d.side or "player"] end
|
||||
return hb[d.id] or (d.id < 2 and hb[State.sideOf(d.id)]) or nil
|
||||
end
|
||||
|
||||
local function step_center(st, d)
|
||||
if d.id ~= nil and Anim.coords then
|
||||
local a, b = Anim.coords(st, d.id)
|
||||
if type(a) == "table" then return a.x or a[1], a.y or a[2] end
|
||||
if a then return a, b end
|
||||
end
|
||||
local base = (step_side(d) == "player") and Anim.PLAYER_MON or Anim.ENEMY_MON
|
||||
return base.x, base.y
|
||||
end
|
||||
|
||||
local function trainer_label(st)
|
||||
return (st and st.trainerClassName and st.trainerClassName ~= "")
|
||||
and (st.trainerClassName .. " " .. (st.trainerName or ""))
|
||||
or (st and st.trainerName or "TRAINER")
|
||||
end
|
||||
|
||||
local function withdraw_text(battler)
|
||||
local name = battler and State.displayName(battler) or "POKéMON"
|
||||
local hp = tonumber(battler and battler.mon and battler.mon.hp) or 0
|
||||
@@ -149,7 +187,7 @@ local function engine_entry_events(st, sides)
|
||||
if not (Engine and Engine.switchInEffects and battle_adapter()) then return nil end
|
||||
return capture_events(function(ad)
|
||||
for _, side in ipairs(sides) do
|
||||
local b = st and st[side]
|
||||
local b = st and ((type(side) == "number") and State.battler(st, side) or st[side])
|
||||
if b and b.mon and (tonumber(b.mon.hp) or 0) > 0 then
|
||||
Engine.switchInEffects(st, ad, b, { spikes = true })
|
||||
end
|
||||
@@ -273,6 +311,72 @@ function SwitchSeq.beginSendOut(st, side, newSlot, opts)
|
||||
return true
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_message.c:1633
|
||||
function SwitchSeq.returnText(st, id)
|
||||
local old = State.battler(st, id)
|
||||
if State.sideOf(id) == "player" then
|
||||
return State.displayName(old) .. ", come back!"
|
||||
end
|
||||
return trainer_label(st) .. "\nwithdrew " .. State.displayName(old) .. "!"
|
||||
end
|
||||
|
||||
-- pokefirered/data/battle_scripts_1.s:3046
|
||||
function SwitchSeq.beginDoubleSwitch(st, id, newSlot, opts)
|
||||
opts = opts or {}
|
||||
SwitchSeq.reset()
|
||||
SwitchSeq._st = st
|
||||
SwitchSeq._headless = opts.headless and true or false
|
||||
SwitchSeq._pushMsg = opts.pushMsg
|
||||
SwitchSeq._onDone = opts.onDone
|
||||
local side = State.sideOf(id)
|
||||
local withdrawMsg
|
||||
if opts.withdraw and not opts.noWithdrawMsg then
|
||||
withdrawMsg = SwitchSeq.returnText(st, id)
|
||||
end
|
||||
local reason = opts.reason or (opts.withdraw and "switch" or "replace")
|
||||
|
||||
if SwitchSeq._headless then
|
||||
if withdrawMsg and SwitchSeq._pushMsg then SwitchSeq._pushMsg(withdrawMsg) end
|
||||
local Engine = package.loaded["src.core.game3.battle.engine"]
|
||||
local party = (side == "player") and st.playerParty or st.foeParty
|
||||
if Engine and Engine.performSwitch and battle_adapter() and party and party[newSlot] then
|
||||
capture_events(function(ad)
|
||||
Engine.performSwitch(st, ad, id, newSlot, { batonPass = opts.batonPass, reason = reason })
|
||||
end)
|
||||
end
|
||||
Anim.syncDisplayFromState(st)
|
||||
local nb = State.battler(st, id)
|
||||
if SwitchSeq._pushMsg then
|
||||
if side == "player" then
|
||||
SwitchSeq._pushMsg("Go! " .. State.displayName(nb) .. "!")
|
||||
else
|
||||
SwitchSeq._pushMsg(trainer_label(st) .. " sent\nout " .. State.displayName(nb) .. "!")
|
||||
end
|
||||
end
|
||||
headless_entry(st, { id })
|
||||
finish()
|
||||
return false
|
||||
end
|
||||
|
||||
local steps = {}
|
||||
if withdrawMsg then
|
||||
steps[#steps + 1] = { kind = "msg", data = { text = withdrawMsg } }
|
||||
end
|
||||
if opts.withdraw then
|
||||
steps[#steps + 1] = { kind = "withdraw", data = { id = id } }
|
||||
end
|
||||
steps[#steps + 1] = { kind = "swap_data", data = { id = id, newSlot = newSlot, batonPass = opts.batonPass, reason = reason } }
|
||||
steps[#steps + 1] = { kind = "msg_sendout", data = { id = id } }
|
||||
steps[#steps + 1] = { kind = (side == "player") and "sendout_player" or "sendout_enemy", data = { id = id, slot = newSlot } }
|
||||
steps[#steps + 1] = { kind = "shiny_check", data = { id = id } }
|
||||
steps[#steps + 1] = { kind = "cry", data = { id = id } }
|
||||
steps[#steps + 1] = { kind = "healthbox", data = { id = id } }
|
||||
steps[#steps + 1] = { kind = "entry_triggers", data = { id = id } }
|
||||
SwitchSeq._steps = steps
|
||||
SwitchSeq._i = 1
|
||||
return true
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_controller_player.c:2105
|
||||
function SwitchSeq.beginEventSwitchIn(st, side, opts)
|
||||
opts = opts or {}
|
||||
@@ -285,11 +389,15 @@ function SwitchSeq.beginEventSwitchIn(st, side, opts)
|
||||
finish()
|
||||
return false
|
||||
end
|
||||
local id = tonumber(opts.battler) or ((type(side) == "number") and side or nil)
|
||||
if id ~= nil and not (st and st.double) and id < 2 then id = nil end
|
||||
if type(side) == "number" then side = State.sideOf(side) end
|
||||
local d = (id ~= nil) and { id = id } or { side = side }
|
||||
SwitchSeq._steps = {
|
||||
{ kind = (side == "player") and "sendout_player" or "sendout_enemy", data = { side = side } },
|
||||
{ kind = "shiny_check", data = { side = side } },
|
||||
{ kind = "cry", data = { side = side } },
|
||||
{ kind = "healthbox", data = { side = side } },
|
||||
{ kind = (side == "player") and "sendout_player" or "sendout_enemy", data = d },
|
||||
{ kind = "shiny_check", data = d },
|
||||
{ kind = "cry", data = d },
|
||||
{ kind = "healthbox", data = d },
|
||||
}
|
||||
SwitchSeq._i = 1
|
||||
return true
|
||||
@@ -397,15 +505,12 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "msg_sendout" then
|
||||
local side = d.side or "player"
|
||||
local side = step_side(d)
|
||||
local text = ""
|
||||
if side == "player" then
|
||||
text = "Go! " .. State.displayName(st and st.player) .. "!"
|
||||
text = "Go! " .. State.displayName(step_battler(st, d)) .. "!"
|
||||
else
|
||||
local tname = (st and st.trainerClassName and st.trainerClassName ~= "")
|
||||
and (st.trainerClassName .. " " .. (st.trainerName or ""))
|
||||
or (st and st.trainerName or "TRAINER")
|
||||
text = tname .. " sent\nout " .. State.displayName(st and st.enemy) .. "!"
|
||||
text = trainer_label(st) .. " sent\nout " .. State.displayName(step_battler(st, d)) .. "!"
|
||||
end
|
||||
if side == "player" and not SwitchSeq._headless then
|
||||
-- pokefirered/src/battle_message.c:399
|
||||
@@ -419,9 +524,8 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "withdraw" then
|
||||
local side = d.side or "player"
|
||||
local p = Anim.present(side)
|
||||
local hb = s.healthbox[side]
|
||||
local p = step_present(d)
|
||||
local hb = step_healthbox(s, d)
|
||||
if hb then hb.visible = false end
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_OPEN) end)
|
||||
wait_busy()
|
||||
@@ -440,16 +544,17 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "swap_data" then
|
||||
local side = d.side or "player"
|
||||
local side = step_side(d)
|
||||
local newSlot = d.newSlot or 1
|
||||
local Engine = package.loaded["src.core.game3.battle.engine"]
|
||||
local party = st and ((side == "player") and st.playerParty or st.foeParty)
|
||||
local pres = Anim.present(side)
|
||||
local pres = step_present(d)
|
||||
-- pokefirered/src/battle_gfx_sfx_util.c:997
|
||||
if pres then pres.castformForm, pres.castformMon = nil, nil end
|
||||
if Engine and Engine.performSwitch and battle_adapter() and st and st[side] and party and party[newSlot] then
|
||||
if Engine and Engine.performSwitch and battle_adapter() and step_battler(st, d) and party and party[newSlot] then
|
||||
capture_events(function(ad)
|
||||
Engine.performSwitch(st, ad, side, newSlot, { batonPass = d.batonPass, reason = "switch" })
|
||||
Engine.performSwitch(st, ad, d.id ~= nil and d.id or side, newSlot,
|
||||
{ batonPass = d.batonPass, reason = d.reason or "switch" })
|
||||
end)
|
||||
Anim.syncDisplayFromState(st)
|
||||
advance()
|
||||
@@ -477,7 +582,7 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "sendout_player" then
|
||||
local pcx, pcy = Anim.PLAYER_MON.x, Anim.PLAYER_MON.y
|
||||
local pcx, pcy = step_center(st, d.id ~= nil and d or { side = "player" })
|
||||
s.ball.visible = true
|
||||
s.ball.frame = 0
|
||||
s.ball.rot = 0
|
||||
@@ -497,8 +602,8 @@ local function run_step(step)
|
||||
s.ball.frame = 1
|
||||
s.ball.rot = 0
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_OPEN, { pan = -64 }) end)
|
||||
Anim.ballOpen("player", s.ball.x, s.ball.y)
|
||||
local p = Anim.present("player")
|
||||
Anim.ballOpen(d.id ~= nil and d.id or "player", s.ball.x, s.ball.y)
|
||||
local p = step_present(d.id ~= nil and d or { side = "player" }) or {}
|
||||
p.visible = true
|
||||
p.ox = 0
|
||||
p.oy = 16
|
||||
@@ -520,7 +625,7 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "sendout_enemy" then
|
||||
local cx, cy = Anim.ENEMY_MON.x, Anim.ENEMY_MON.y
|
||||
local cx, cy = step_center(st, d.id ~= nil and d or { side = "enemy" })
|
||||
s.ball.visible = true
|
||||
s.ball.frame = 0
|
||||
s.ball.side = "enemy"
|
||||
@@ -530,8 +635,8 @@ local function run_step(step)
|
||||
Anim.tweenStage(16, function() end, function()
|
||||
s.ball.frame = 1
|
||||
pcall(function() Audio.playSe(SE.SE_BALL_OPEN, { pan = 63 }) end)
|
||||
Anim.ballOpen("enemy", s.ball.x, s.ball.y)
|
||||
local p = Anim.present("enemy")
|
||||
Anim.ballOpen(d.id ~= nil and d.id or "enemy", s.ball.x, s.ball.y)
|
||||
local p = step_present(d.id ~= nil and d or { side = "enemy" }) or {}
|
||||
p.visible = true
|
||||
p.ox = 0
|
||||
p.oy = 16
|
||||
@@ -552,8 +657,7 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "shiny_check" then
|
||||
local side = d.side or "player"
|
||||
local b = st and st[side]
|
||||
local b = step_battler(st, d)
|
||||
local mon = b and b.mon
|
||||
local isShiny = mon and SummaryData.isShiny(mon)
|
||||
if isShiny then
|
||||
@@ -569,8 +673,8 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "cry" then
|
||||
local side = d.side or "player"
|
||||
local b = st and st[side]
|
||||
local side = step_side(d)
|
||||
local b = step_battler(st, d)
|
||||
local sp = b and (b.species or (b.mon and (b.mon.species or b.mon.speciesId)))
|
||||
if sp then
|
||||
-- pokefirered/src/pokeball.c:782
|
||||
@@ -583,8 +687,8 @@ local function run_step(step)
|
||||
end
|
||||
|
||||
if kind == "healthbox" then
|
||||
local side = d.side or "player"
|
||||
local hb = s.healthbox[side]
|
||||
local side = step_side(d)
|
||||
local hb = step_healthbox(s, d) or {}
|
||||
local from = (side == "player") and 115 or -115
|
||||
hb.visible = true
|
||||
hb.ox = from
|
||||
@@ -600,6 +704,7 @@ local function run_step(step)
|
||||
|
||||
if kind == "entry_triggers" then
|
||||
local sides = d.sides or { d.side or "player" }
|
||||
if d.id ~= nil then sides = { d.id } end
|
||||
if #sides > 1 then
|
||||
table.sort(sides, function(a, bSide)
|
||||
local spA = st and st[a] and (st[a].speed or (st[a].mon and st[a].mon.speed)) or 0
|
||||
@@ -635,6 +740,14 @@ local function run_step(step)
|
||||
if p then p.visible = false end
|
||||
local hb = s.healthbox and s.healthbox.enemy
|
||||
if hb then hb.visible = false end
|
||||
if st and st.double then
|
||||
for _, id in ipairs({ 1, 3 }) do
|
||||
local pp = step_present({ id = id })
|
||||
if pp then pp.visible = false end
|
||||
local hh = step_healthbox(s, { id = id })
|
||||
if hh then hh.visible = false end
|
||||
end
|
||||
end
|
||||
local Trainers = require("src.core.game3.scripting.trainers")
|
||||
local info = st and st.trainerId and Trainers.info(st.trainerId)
|
||||
local picId = (st and st.trainerPicId) or (info and info.pic) or 0
|
||||
|
||||
+801
-29
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
local Party = require("src.core.game3.party")
|
||||
local PartyView = require("src.core.game3.battle.party_view")
|
||||
local Downgrade = require("src.core.game3.battle_downgrade")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local BattleBridge = {}
|
||||
|
||||
@@ -118,6 +119,41 @@ function BattleBridge.installWhiteoutIntercept(mod, game)
|
||||
end
|
||||
end
|
||||
|
||||
local LEAD_FIELDS = { "species", "level", "rawIv", "iv", "ivs", "evs", "heldItem",
|
||||
"moves", "personality" }
|
||||
|
||||
-- pokefirered/src/battle_main.c:1539
|
||||
local function hooked_trainer_party(foe, trainerId)
|
||||
if type(foe) ~= "table" or type(foe.party) ~= "table" or #foe.party == 0 then return foe end
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local named = G3.partyNames(foe.party)
|
||||
local out = ModRuntime.call("trainer.party", function(_, _, party)
|
||||
return party
|
||||
end, foe.trainerClass, trainerId, named)
|
||||
if type(out) ~= "table" or #out == 0 then return foe end
|
||||
local copy = {}
|
||||
for k, v in pairs(foe) do copy[k] = v end
|
||||
copy.party = G3.partyNums(out)
|
||||
local lead = copy.party[1]
|
||||
for _, key in ipairs(LEAD_FIELDS) do copy[key] = lead[key] end
|
||||
return copy
|
||||
end
|
||||
|
||||
local function battle_payload(Battle, opts, foe, isDouble)
|
||||
local st = Battle.getState and Battle.getState()
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local enemy = st and st.enemy and st.enemy.mon
|
||||
local sp = enemy and tonumber(enemy.species or enemy.speciesId)
|
||||
local tid = opts.trainerId or (foe and foe.trainerId)
|
||||
return {
|
||||
battle = st, kind = opts.wild and "wild" or "trainer",
|
||||
trainerId = (not opts.wild) and tid or nil,
|
||||
trainerClass = (not opts.wild) and foe and foe.trainerClass or nil,
|
||||
species = G3.speciesName(sp), speciesId = sp,
|
||||
level = enemy and enemy.level, double = isDouble and true or false,
|
||||
}
|
||||
end
|
||||
|
||||
local function writeback(session, battleParty, remap, result, save, opts)
|
||||
opts = opts or {}
|
||||
if not session then return end
|
||||
@@ -178,6 +214,10 @@ function BattleBridge.start(mod, game, foe, opts)
|
||||
|
||||
local battleParty, remap = PartyView.fromSession(session.party, session.move_overlay)
|
||||
if #battleParty == 0 then return nil, "empty party" end
|
||||
local isDouble = (not opts.wild) and (opts.double or (foe and foe.doubleBattle)) and true or false
|
||||
if isDouble and Party.monsStateToDoubles(session.party) ~= Party.PLAYER_HAS_TWO_USABLE_MONS then
|
||||
return nil, "need two mons"
|
||||
end
|
||||
|
||||
BattleBridge._remap = remap
|
||||
BattleBridge._battleParty = battleParty
|
||||
@@ -185,8 +225,19 @@ function BattleBridge.start(mod, game, foe, opts)
|
||||
local save = game and game.save
|
||||
local done = opts.done
|
||||
|
||||
if not opts.wild and ModRuntime.wantsHook("trainer.party") then
|
||||
foe = hooked_trainer_party(foe, opts.trainerId or (foe and foe.trainerId))
|
||||
end
|
||||
|
||||
local function finish(result)
|
||||
writeback(session, battleParty, remap, result, save, opts)
|
||||
-- pokefirered/src/battle_main.c:3861
|
||||
if ModRuntime.wants("battle.ended") then
|
||||
local B = package.loaded["src.core.game3.battle"]
|
||||
ModRuntime.emit("battle.ended", {
|
||||
battle = B and B.getState and B.getState() or nil, result = result or "win",
|
||||
})
|
||||
end
|
||||
BattleBridge._remap = nil
|
||||
BattleBridge._battleParty = nil
|
||||
BattleBridge._finish = nil
|
||||
@@ -216,6 +267,7 @@ function BattleBridge.start(mod, game, foe, opts)
|
||||
|
||||
local startOpts = {
|
||||
wild = opts.wild,
|
||||
double = isDouble,
|
||||
playerParty = battleParty,
|
||||
foe = foe,
|
||||
headless = opts.headless,
|
||||
@@ -231,6 +283,12 @@ function BattleBridge.start(mod, game, foe, opts)
|
||||
onDone = function(result)
|
||||
finish(result)
|
||||
end,
|
||||
onStarted = function()
|
||||
-- pokefirered/src/battle_main.c:612
|
||||
if ModRuntime.wants("battle.started") then
|
||||
ModRuntime.emit("battle.started", battle_payload(Battle, opts, foe, isDouble))
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local function resolve_battle_song(o, so)
|
||||
@@ -298,6 +356,9 @@ function BattleBridge.start(mod, game, foe, opts)
|
||||
local leadMon = battleParty and battleParty[1]
|
||||
local playerLv = leadMon and (leadMon.level or leadMon.lvl) or 5
|
||||
local foeLv = (foe and foe.level) or (foe and foe.party and foe.party[1] and (foe.party[1].level or foe.party[1].lvl)) or 3
|
||||
if isDouble then
|
||||
playerLv, foeLv = PartyView.doubleTransitionLevels(battleParty, foe and foe.party)
|
||||
end
|
||||
|
||||
local pickOpts = {
|
||||
wild = opts.wild,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
-- RNG: pret wild_encounter.c — Random() for gate/slot/level, WildEncounterRandom for rate.
|
||||
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Encounters = {}
|
||||
|
||||
@@ -218,7 +219,7 @@ function Encounters.rollWater(mapId, enterFromOther)
|
||||
return roll_area(mapId, "water", WATER_WEIGHTS, enterFromOther, 15)
|
||||
end
|
||||
|
||||
function Encounters.onStep(mapId, terrain, opts)
|
||||
local function vanilla_step(mapId, terrain, opts)
|
||||
Encounters.ensureLoaded()
|
||||
opts = opts or {}
|
||||
local enterFromOther = opts.enterFromOther
|
||||
@@ -231,6 +232,54 @@ function Encounters.onStep(mapId, terrain, opts)
|
||||
return Encounters.rollLand(mapId, nil, enterFromOther)
|
||||
end
|
||||
|
||||
local function mod_encounter(enc)
|
||||
if type(enc) ~= "table" then return enc end
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local id = tonumber(enc.species)
|
||||
return {
|
||||
species = (id and Pokemon.keyName(id)) or enc.species,
|
||||
speciesId = id or Pokemon.speciesFromName(enc.species),
|
||||
level = enc.level,
|
||||
item = enc.item,
|
||||
}
|
||||
end
|
||||
|
||||
local function engine_encounter(enc)
|
||||
if type(enc) ~= "table" then return nil end
|
||||
local id = tonumber(enc.species)
|
||||
if not id and enc.species ~= nil then
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
id = Pokemon.speciesFromName(enc.species)
|
||||
end
|
||||
id = id or tonumber(enc.speciesId)
|
||||
if not id then return nil end
|
||||
return { species = id, level = tonumber(enc.level) or 5, item = enc.item }
|
||||
end
|
||||
|
||||
local function same_encounter(enc) return enc end
|
||||
|
||||
function Encounters.onStep(mapId, terrain, opts)
|
||||
local wantsRoll = ModRuntime.wantsHook("encounter.roll")
|
||||
local wantsSpecies = ModRuntime.wantsHook("encounter.species")
|
||||
if not (wantsRoll or wantsSpecies) then
|
||||
return vanilla_step(mapId, terrain, opts)
|
||||
end
|
||||
Encounters.ensureLoaded()
|
||||
local ctx = { mapId = mapId, terrain = terrain, rng = Rng.Random, opts = opts }
|
||||
local enc
|
||||
if wantsRoll then
|
||||
enc = ModRuntime.call("encounter.roll", function()
|
||||
return mod_encounter(vanilla_step(mapId, terrain, opts))
|
||||
end, table_for(mapId), ctx)
|
||||
else
|
||||
enc = mod_encounter(vanilla_step(mapId, terrain, opts))
|
||||
end
|
||||
if enc and wantsSpecies then
|
||||
enc = ModRuntime.call("encounter.species", same_encounter, enc, ctx)
|
||||
end
|
||||
return engine_encounter(enc)
|
||||
end
|
||||
|
||||
function Encounters.noteGrass(onGrass)
|
||||
Encounters._prevGrass = onGrass and true or false
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-- MVP: ROM EVO_LEVEL (method 4) only. Stones/trade/friendship later.
|
||||
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Evolution = {}
|
||||
|
||||
@@ -31,31 +32,66 @@ local function is_national_unlocked(session)
|
||||
return PokedexData.isNationalUnlocked(session)
|
||||
end
|
||||
|
||||
local function level_row(mon, evo, session)
|
||||
local level = tonumber(mon.level) or 1
|
||||
local method = tonumber(evo.method or evo[1]) or 0
|
||||
local param = tonumber(evo.param or evo[2]) or 0
|
||||
local target = tonumber(evo.target or evo[3]) or 0
|
||||
if method == Evolution.EVO_LEVEL and target > 0 and level >= param then
|
||||
-- National Dex gating: prevent evolving into non-Kanto species (target > 151) if locked
|
||||
if target > 151 and not is_national_unlocked(session) then
|
||||
return "stop"
|
||||
end
|
||||
return "match", target, param
|
||||
elseif (method == Evolution.EVO_FRIENDSHIP or method == Evolution.EVO_FRIENDSHIP_DAY or method == Evolution.EVO_FRIENDSHIP_NIGHT) and target > 0 then
|
||||
local friendship = tonumber(mon.friendship) or 220
|
||||
if friendship >= 220 then
|
||||
if target > 151 and not is_national_unlocked(session) then
|
||||
return "stop"
|
||||
end
|
||||
return "match", target, 0
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function evo_view(evo)
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local okS, Schemas = pcall(require, "src.mods.Schemas")
|
||||
local methods = okS and Schemas.gen3View and Schemas.gen3View.EVOLUTIONS or {}
|
||||
local method = tonumber(evo.method or evo[1]) or 0
|
||||
local param = tonumber(evo.param or evo[2]) or 0
|
||||
local target = tonumber(evo.target or evo[3]) or 0
|
||||
return {
|
||||
method = methods[method] or method, methodId = method, param = param,
|
||||
level = param, species = G3.speciesName(target), speciesId = target,
|
||||
}
|
||||
end
|
||||
|
||||
--- Target species for level-up evolution, or nil.
|
||||
-- pokefirered/src/pokemon.c:5025
|
||||
function Evolution.levelTarget(mon, session)
|
||||
if not mon then return nil end
|
||||
if held_is_everstone(mon) then return nil end
|
||||
local species = Pokemon.speciesOf(mon) or tonumber(mon.species or mon.speciesId)
|
||||
local level = tonumber(mon.level) or 1
|
||||
if not species then return nil end
|
||||
local hooked = ModRuntime.wantsHook("evolution.check")
|
||||
local R = hooked and package.loaded["src.core.game3.runtime"] or nil
|
||||
for _, evo in ipairs(Pokemon.evolutions(species)) do
|
||||
local method = tonumber(evo.method or evo[1]) or 0
|
||||
local param = tonumber(evo.param or evo[2]) or 0
|
||||
local target = tonumber(evo.target or evo[3]) or 0
|
||||
if method == Evolution.EVO_LEVEL and target > 0 and level >= param then
|
||||
-- National Dex gating: prevent evolving into non-Kanto species (target > 151) if locked
|
||||
if target > 151 and not is_national_unlocked(session) then
|
||||
return nil
|
||||
end
|
||||
return target, param
|
||||
elseif (method == Evolution.EVO_FRIENDSHIP or method == Evolution.EVO_FRIENDSHIP_DAY or method == Evolution.EVO_FRIENDSHIP_NIGHT) and target > 0 then
|
||||
local friendship = tonumber(mon.friendship) or 220
|
||||
if friendship >= 220 then
|
||||
if target > 151 and not is_national_unlocked(session) then
|
||||
return nil
|
||||
end
|
||||
return target, 0
|
||||
local kind, target, param = level_row(mon, evo, session)
|
||||
if hooked then
|
||||
local view = evo_view(evo)
|
||||
local ok = ModRuntime.call("evolution.check", function()
|
||||
return kind == "match"
|
||||
end, R and R._game or nil, mon, view, { kind = "levelup", session = session })
|
||||
if ok then
|
||||
if kind == "match" then return target, param end
|
||||
if view.speciesId > 0 then return view.speciesId, view.param end
|
||||
end
|
||||
if kind == "stop" then return nil end
|
||||
else
|
||||
if kind == "stop" then return nil end
|
||||
if kind == "match" then return target, param end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
@@ -103,7 +139,7 @@ function Evolution.renameMon(mon, preSpecies, postSpecies)
|
||||
end
|
||||
|
||||
--- Apply species change + stats. Point of no return.
|
||||
function Evolution.apply(mon, newSpecies, session, bag)
|
||||
function Evolution.apply(mon, newSpecies, session, bag, via)
|
||||
newSpecies = tonumber(newSpecies)
|
||||
if not mon or not newSpecies then return false end
|
||||
local preSpecies = Pokemon.speciesOf(mon) or tonumber(mon.species or mon.speciesId) or 1
|
||||
@@ -185,6 +221,17 @@ function Evolution.apply(mon, newSpecies, session, bag)
|
||||
end
|
||||
end
|
||||
|
||||
if ModRuntime.wants("pokemon.evolved") then
|
||||
ModRuntime.emit("pokemon.evolved", {
|
||||
mon = mon,
|
||||
fromSpecies = Pokemon.keyName(preSpecies) or preSpecies,
|
||||
toSpecies = Pokemon.keyName(newSpecies) or newSpecies,
|
||||
fromSpeciesId = preSpecies,
|
||||
toSpeciesId = newSpecies,
|
||||
via = via or "level",
|
||||
})
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
-- Game3 field loop coordinator (scripts, player input, heal/respawn).
|
||||
|
||||
local Player = require("src.core.game3.player")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Field = {}
|
||||
|
||||
@@ -55,6 +56,9 @@ function Field.update(_dt)
|
||||
if not Field.running then return end
|
||||
local game = Field._game
|
||||
|
||||
local Compat = package.loaded["src.mods.Gen3Compat"]
|
||||
if Compat and Compat.worldTick then Compat.worldTick(_dt) end
|
||||
|
||||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||||
if Space and Space.vm then
|
||||
local ad = Space.vm.adapters
|
||||
@@ -233,9 +237,30 @@ end
|
||||
|
||||
--- A-button field interact: NPC talk → bgEvent → metatile interaction → Surf.
|
||||
-- Returns true if a script (or handled action) started.
|
||||
local function interacted(fx, fy, kind, target)
|
||||
if not ModRuntime.wants("world.interacted") then return end
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
local session = Field._session
|
||||
ModRuntime.emit("world.interacted", {
|
||||
mapId = (session and session.map) or (Map and Map.current),
|
||||
x = fx, y = fy, kind = kind, target = target,
|
||||
})
|
||||
end
|
||||
|
||||
local inInteract = false
|
||||
|
||||
function Field.interact(game)
|
||||
game = game or Field._game
|
||||
if not Field.running then return false end
|
||||
local Compat = package.loaded["src.mods.Gen3Compat"]
|
||||
local replaced = not inInteract and Compat and Compat.interactWrapper and Compat.interactWrapper()
|
||||
if replaced then
|
||||
inInteract = true
|
||||
local ok, res = pcall(replaced, Compat.resolve("src.world.OverworldController"))
|
||||
inInteract = false
|
||||
if not ok then error(res, 0) end
|
||||
return res
|
||||
end
|
||||
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
if Runtime and Runtime.uiBusy and Runtime.uiBusy() then return false end
|
||||
@@ -320,9 +345,22 @@ function Field.interact(game)
|
||||
end
|
||||
elseif eo.def.scriptKey then
|
||||
local lid = eo.localId or eo.def.localId or eo.def.index or 0
|
||||
Objects.freeze(lid)
|
||||
Objects.facePlayer(lid, game)
|
||||
Space.startScript(eo.def.scriptKey, lid, facingDir)
|
||||
local talkTo = Compat and Compat.talkToWrapper and Compat.talkToWrapper()
|
||||
if talkTo and talkTo(Compat.resolve("src.world.OverworldController"), eo) then
|
||||
interacted(ox, oy, "npc", eo)
|
||||
return true
|
||||
end
|
||||
local function talk()
|
||||
Objects.freeze(lid)
|
||||
Objects.facePlayer(lid, game)
|
||||
Space.startScript(eo.def.scriptKey, lid, facingDir)
|
||||
end
|
||||
if ModRuntime.wantsHook("world.talk") then
|
||||
ModRuntime.call("world.talk", talk, game, eo)
|
||||
else
|
||||
talk()
|
||||
end
|
||||
interacted(ox, oy, "npc", eo)
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -333,14 +371,20 @@ function Field.interact(game)
|
||||
if elevation==0 then elevation=P.elevation or 0 end
|
||||
local sign = bg_event_at(game, fx, fy, elevation, facingDir)
|
||||
if sign and sign.scriptKey then
|
||||
if Space.startScript(sign.scriptKey, nil, facingDir) then return true end
|
||||
if Space.startScript(sign.scriptKey, nil, facingDir) then
|
||||
interacted(fx, fy, "sign", sign)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- 3) Original metatile interactions follow objects and map-specific scripts.
|
||||
local behavior=Collision.behavior(fx,fy)
|
||||
local key=require("src.core.game3.scripting.interaction_scripts").scriptFor(behavior,P.facing)
|
||||
if behavior==nil then key=CollisionStd.scriptFor(Collision.cell(fx,fy)) end
|
||||
if key and Space.startScript(key,nil,facingDir) then return true end
|
||||
if key and Space.startScript(key,nil,facingDir) then
|
||||
interacted(fx, fy, "script", key)
|
||||
return true
|
||||
end
|
||||
|
||||
-- 4) Water / Surf interact on facing water tile
|
||||
if not P.surfing and Collision.isWater and Collision.isWater(fx, fy) then
|
||||
@@ -489,7 +533,7 @@ function Field.executeFieldMove(payload)
|
||||
if payload.se then Audio.playSe(payload.se) end
|
||||
FieldEffects.startWarpSpin(act, function()
|
||||
Field.locked = false
|
||||
Field.respawnAtHeal()
|
||||
Field.respawnAtHeal({ fieldMove = true })
|
||||
end)
|
||||
if payload.text then
|
||||
Message.show(payload.text, function() Message.close() end)
|
||||
@@ -512,11 +556,17 @@ function Field.executeFieldMove(payload)
|
||||
end
|
||||
|
||||
--- White-out / heal respawn via game3 map loader (H7).
|
||||
function Field.respawnAtHeal()
|
||||
function Field.respawnAtHeal(opts)
|
||||
local session = Field._session
|
||||
if not session then return end
|
||||
local HealLocations = require("src.core.game3.heal_locations")
|
||||
HealLocations.normalizeSession(session)
|
||||
if not (opts and opts.fieldMove) and ModRuntime.wants("world.blacked_out") then
|
||||
ModRuntime.emit("world.blacked_out", {
|
||||
save = session,
|
||||
healTarget = { map = session.healMap, x = session.healX, y = session.healY },
|
||||
})
|
||||
end
|
||||
local Party = require("src.core.game3.party")
|
||||
Party.healAll(session.party)
|
||||
local Map = require("src.core.game3.map")
|
||||
@@ -563,6 +613,9 @@ function Field.setMetatile(x, y, metatile, isImpassable)
|
||||
}
|
||||
local session = Field._session
|
||||
local mapId = session and session.map
|
||||
if ModRuntime.wants("world.block_replaced") then
|
||||
ModRuntime.emit("world.block_replaced", { mapId = mapId, bx = x, by = y, block = metatile })
|
||||
end
|
||||
local game = Field._game
|
||||
local data = game and game.data and game.data.maps
|
||||
local mapDef = mapId and data and data[mapId]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
local ItemsData = require("src.core.game3.items_data")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local ItemUse = {}
|
||||
|
||||
@@ -343,6 +344,21 @@ function ItemUse.needsPartyTarget(id)
|
||||
return false
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:5018
|
||||
function ItemUse.levelUpEvent(mon, level)
|
||||
if not ModRuntime.wants("pokemon.level_up") then return end
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
local learnable, learnableIds = {}, {}
|
||||
for _, mv in ipairs(Pokemon.movesLearnedAt(tonumber(mon.species or mon.speciesId), level)) do
|
||||
learnable[#learnable + 1] = G3.moveName(mv)
|
||||
learnableIds[#learnableIds + 1] = mv
|
||||
end
|
||||
ModRuntime.emit("pokemon.level_up", {
|
||||
mon = mon, level = level, prevLevel = level - 1,
|
||||
learnable = learnable, learnableIds = learnableIds, via = "item",
|
||||
})
|
||||
end
|
||||
|
||||
function ItemUse.useRareCandy(session, mon)
|
||||
if not mon then return false, "none", "There's no POKéMON!" end
|
||||
local lvl = tonumber(mon.level) or 1
|
||||
@@ -356,6 +372,7 @@ function ItemUse.useRareCandy(session, mon)
|
||||
Pokemon.applyStats(mon)
|
||||
local newMax = tonumber(mon.maxHp) or tonumber(mon.maxhp) or oldMax
|
||||
mon.hp = math.min(newMax, oldHp + math.max(0, newMax - oldMax))
|
||||
ItemUse.levelUpEvent(mon, mon.level)
|
||||
local t = string.format("%s grew to\nLv. %d!", Pokemon.displayMonName(mon), mon.level)
|
||||
return true, "level", t
|
||||
end
|
||||
@@ -377,10 +394,11 @@ function ItemUse.useEvolutionStone(session, mon, itemId, bag)
|
||||
session = session,
|
||||
bag = bag,
|
||||
savedSong = Audio._mapSong,
|
||||
via = "item",
|
||||
})
|
||||
return true, "evo", "Evolving..."
|
||||
else
|
||||
Evolution.apply(mon, target, session, bag)
|
||||
Evolution.apply(mon, target, session, bag, "item")
|
||||
local t = string.format("%s evolved into\n%s!", oldName, newName)
|
||||
return true, "evo", t
|
||||
end
|
||||
@@ -522,7 +540,15 @@ local function useField(session, bag, id, partySlot)
|
||||
end
|
||||
|
||||
function ItemUse.useField(session,bag,id,partySlot)
|
||||
local ok,kind,text=useField(session,bag,id,partySlot)
|
||||
local ok,kind,text
|
||||
if ModRuntime.wantsHook("item.use") then
|
||||
local Runtime=package.loaded["src.core.game3.runtime"]
|
||||
ok,kind,text=ModRuntime.call("item.use",function(_,_,hid,hslot)
|
||||
return useField(session,bag,hid,hslot)
|
||||
end,Runtime and Runtime._game,nil,id,partySlot,bag)
|
||||
else
|
||||
ok,kind,text=useField(session,bag,id,partySlot)
|
||||
end
|
||||
if ok and kind~="tm" and kind~="tm_case" and kind~="berry_pouch" then
|
||||
local Items=require("src.core.game3.items")
|
||||
local Pokemon=require("src.core.game3.pokemon")
|
||||
|
||||
@@ -238,6 +238,10 @@ local function load_pack()
|
||||
return ItemsData._byId
|
||||
end
|
||||
|
||||
function ItemsData.ensureLoaded()
|
||||
return load_pack()
|
||||
end
|
||||
|
||||
function ItemsData.install(_cache)
|
||||
ItemsData._pack = nil
|
||||
ItemsData._byId = nil
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
-- MAPSETUP.WARP races ON_FRAME and wipes applymovement tracks (Bill intro).
|
||||
|
||||
local MapIds = require("src.core.game3.map_ids")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
local Map = {}
|
||||
|
||||
Map.current = nil
|
||||
Map._announced = nil
|
||||
Map.neighbors = {}
|
||||
Map._loadedLayouts = {}
|
||||
Map._def = nil
|
||||
@@ -263,9 +265,14 @@ function Map.load(mod, game, mapId, opts)
|
||||
return nil, "not a game3 map"
|
||||
end
|
||||
local Ghosts = require("src.core.game3.ghosts")
|
||||
local fromMapId = Map._announced
|
||||
if Map.current and Map.current ~= mapId then
|
||||
Ghosts.capture(Map.current)
|
||||
end
|
||||
if fromMapId and fromMapId ~= mapId and ModRuntime.wants("map.exited") then
|
||||
ModRuntime.emit("map.exited", { mapId = fromMapId, toMapId = mapId })
|
||||
end
|
||||
Map._announced = mapId
|
||||
Map.current = mapId
|
||||
Map._loadedLayouts = { [mapId] = true }
|
||||
|
||||
@@ -399,6 +406,17 @@ function Map.load(mod, game, mapId, opts)
|
||||
if not opts.seamless then
|
||||
require("src.core.game3.audio").setSavedSong(nil)
|
||||
end
|
||||
if fromMapId == mapId then
|
||||
if opts.reason and ModRuntime.wants("map.reloaded") then
|
||||
ModRuntime.emit("map.reloaded", { mapId = mapId, reason = opts.reason or "reload" })
|
||||
end
|
||||
elseif ModRuntime.wants("map.entered") then
|
||||
ModRuntime.emit("map.entered", {
|
||||
mapId = mapId, map = def, fromMapId = fromMapId,
|
||||
via = opts.via or (opts.seamless and "connection")
|
||||
or (opts.heal and "respawn") or (fromMapId and "warp" or "boot"),
|
||||
})
|
||||
end
|
||||
if Space and Space.runEnterScripts then
|
||||
Space.runEnterScripts(mod or Runtime._mod, mapId, game, world)
|
||||
elseif Space and Space.onMapEnter then
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
local Movement = require("src.core.game3.scripting.movement")
|
||||
local Opcodes = require("src.core.game3.scripting.opcodes")
|
||||
local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Objects = {}
|
||||
|
||||
@@ -302,12 +303,16 @@ function Objects.loadMap(game, mapId, mapDef)
|
||||
end
|
||||
end
|
||||
end
|
||||
local announce = ModRuntime.wants("world.npc_spawned")
|
||||
for _, def in ipairs(Objects._defs) do
|
||||
local eo = newEventObject(def)
|
||||
if eo.localId > 0 then
|
||||
applyPerm(eo, mapId)
|
||||
Objects._byId[eo.localId] = eo
|
||||
Objects._order[#Objects._order + 1] = eo.localId
|
||||
if announce then
|
||||
ModRuntime.emit("world.npc_spawned", { mapId = mapId, npcId = eo.localId, runtime = eo })
|
||||
end
|
||||
end
|
||||
end
|
||||
if not Objects._logged then
|
||||
|
||||
@@ -127,9 +127,31 @@ function Party.size(sessionParty)
|
||||
return #sessionParty
|
||||
end
|
||||
|
||||
Party.PLAYER_HAS_TWO_USABLE_MONS = 0
|
||||
Party.PLAYER_HAS_ONE_MON = 1
|
||||
Party.PLAYER_HAS_ONE_USABLE_MON = 2
|
||||
|
||||
-- pokefirered/src/pokemon.c:3769
|
||||
function Party.monsStateToDoubles(sessionParty)
|
||||
if type(sessionParty) ~= "table" then return Party.PLAYER_HAS_ONE_MON end
|
||||
local count = 0
|
||||
for _, mon in ipairs(sessionParty) do
|
||||
if (tonumber(mon.species or mon.speciesId) or 0) ~= 0 then count = count + 1 end
|
||||
end
|
||||
if count <= 1 then return Party.PLAYER_HAS_ONE_MON end
|
||||
local usable = 0
|
||||
for _, mon in ipairs(sessionParty) do
|
||||
local sp = tonumber(mon.species or mon.speciesId) or 0
|
||||
if (tonumber(mon.hp) or 0) ~= 0 and sp ~= 0 and sp ~= 412 and not mon.isEgg and not mon.egg then
|
||||
usable = usable + 1
|
||||
end
|
||||
end
|
||||
return (usable > 1) and Party.PLAYER_HAS_TWO_USABLE_MONS or Party.PLAYER_HAS_ONE_USABLE_MON
|
||||
end
|
||||
|
||||
--- Append a Gen3-shaped opaque mon for script givemon (starter / gifts).
|
||||
-- Returns true if added to party (slot available).
|
||||
function Party.giveMon(session, species, level)
|
||||
function Party.giveMon(session, species, level, nickname)
|
||||
if type(session) ~= "table" then return false end
|
||||
session.party = session.party or {}
|
||||
if #session.party >= 6 then return false end
|
||||
@@ -170,8 +192,9 @@ function Party.giveMon(session, species, level)
|
||||
species = species,
|
||||
speciesId = species,
|
||||
name = name,
|
||||
nickname = "",
|
||||
nickname = type(nickname) == "string" and nickname or "",
|
||||
level = level,
|
||||
metLevel = level,
|
||||
growthRate = growthRate,
|
||||
exp = SummaryData.expForLevel(growthRate, level),
|
||||
status = nil,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
-- Collision via game3.collision (owned COLL_* grid from extract bake).
|
||||
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Player = {}
|
||||
|
||||
@@ -361,6 +362,12 @@ function Player.tryMove(dir, game, run)
|
||||
obj.x = destBx
|
||||
obj.y = destBy
|
||||
if obj.def then obj.def.x = destBx; obj.def.y = destBy end
|
||||
if ModRuntime.wants("world.boulder_moved") then
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
ModRuntime.emit("world.boulder_moved", {
|
||||
mapId = Map and Map.current, npcId = obj.localId, x = destBx, y = destBy,
|
||||
})
|
||||
end
|
||||
beginStep(tx, ty, false, false)
|
||||
return "step"
|
||||
end
|
||||
@@ -466,6 +473,16 @@ local function finishStep(game)
|
||||
session.x, session.y, session.facing = Player.cellX, Player.cellY, Player.facing
|
||||
end
|
||||
|
||||
if ModRuntime.wants("world.stepped") then
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
ModRuntime.emit("world.stepped", {
|
||||
mapId = (session and session.map) or (Map and Map.current),
|
||||
x = Player.cellX, y = Player.cellY,
|
||||
tile = Collision.behavior and Collision.behavior(Player.cellX, Player.cellY),
|
||||
facing = Player.facing,
|
||||
})
|
||||
end
|
||||
|
||||
local onDoneCb = Player._onStepDone
|
||||
Player._onStepDone = nil
|
||||
if onDoneCb then
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
local Extract = require("src.import.gba.extract_island1")
|
||||
local PokemonExtract = require("src.import.gba.pokemon_extract")
|
||||
local Versions = require("src.import.gba.versions")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Pokemon = {}
|
||||
|
||||
@@ -154,6 +155,36 @@ function Pokemon.install(cache)
|
||||
else
|
||||
log("species pack missing — re-import FireRed ROM")
|
||||
end
|
||||
Pokemon._runReloadHooks()
|
||||
end
|
||||
|
||||
Pokemon._reloadHooks = {}
|
||||
|
||||
function Pokemon.onReload(fn, key)
|
||||
if type(fn) ~= "function" then return function() end end
|
||||
local hooks = Pokemon._reloadHooks
|
||||
for i = #hooks, 1, -1 do
|
||||
local h = hooks[i]
|
||||
if h.fn == fn or (key ~= nil and h.key == key) then
|
||||
table.remove(hooks, i)
|
||||
end
|
||||
end
|
||||
local entry = { fn = fn, key = key }
|
||||
hooks[#hooks + 1] = entry
|
||||
return function()
|
||||
for i = #hooks, 1, -1 do
|
||||
if hooks[i] == entry then table.remove(hooks, i) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Pokemon._runReloadHooks()
|
||||
local snapshot = {}
|
||||
for i, h in ipairs(Pokemon._reloadHooks) do snapshot[i] = h end
|
||||
for _, h in ipairs(snapshot) do
|
||||
local ok, err = pcall(h.fn, Pokemon)
|
||||
if not ok then log("onReload callback failed: " .. tostring(err)) end
|
||||
end
|
||||
end
|
||||
|
||||
function Pokemon.invalidate()
|
||||
@@ -196,6 +227,19 @@ function Pokemon.name(species)
|
||||
return string.format("POKéMON %03d", species)
|
||||
end
|
||||
|
||||
function Pokemon.keyName(species)
|
||||
species = tonumber(species)
|
||||
if not species or species < 1 then return nil end
|
||||
if not Pokemon._names then Pokemon.install(Pokemon._cache) end
|
||||
local n = Pokemon._names and Pokemon._names[species]
|
||||
if type(n) ~= "string" or n == "" or n == "??????????" then return nil end
|
||||
n = n:upper()
|
||||
n = n:gsub("♀", "_F"):gsub("♂", "_M")
|
||||
n = n:gsub("[%.']", "")
|
||||
n = n:gsub("[%s%-]+", "_")
|
||||
return n
|
||||
end
|
||||
|
||||
function Pokemon.speciesFromName(name)
|
||||
if name == nil then return nil end
|
||||
if not Pokemon._byName then Pokemon.install(Pokemon._cache) end
|
||||
@@ -649,6 +693,13 @@ function Pokemon.teachMove(mon, moveId)
|
||||
local max = move_max_pp(moveId)
|
||||
mon.pp[i] = max
|
||||
mon.maxPp[i] = max
|
||||
-- pokefirered/src/pokemon.c:2208
|
||||
if ModRuntime.wants("pokemon.move_learned") then
|
||||
ModRuntime.emit("pokemon.move_learned", {
|
||||
mon = mon, moveId = require("src.mods.Gen3Compat").moveName(moveId),
|
||||
moveNum = moveId, slot = i,
|
||||
})
|
||||
end
|
||||
return true, i
|
||||
end
|
||||
end
|
||||
@@ -669,6 +720,14 @@ function Pokemon.replaceMove(mon, slot, newMoveId)
|
||||
local max = move_max_pp(newMoveId)
|
||||
mon.pp[slot] = max
|
||||
mon.maxPp[slot] = max
|
||||
-- pokefirered/src/pokemon.c:2248
|
||||
if ModRuntime.wants("pokemon.move_learned") then
|
||||
local G3 = require("src.mods.Gen3Compat")
|
||||
ModRuntime.emit("pokemon.move_learned", {
|
||||
mon = mon, moveId = G3.moveName(newMoveId), moveNum = newMoveId, slot = slot,
|
||||
forgotten = G3.moveName(old), forgottenNum = tonumber(old),
|
||||
})
|
||||
end
|
||||
return old
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
local MapIds = require("src.core.game3.map_ids")
|
||||
local Options = require("src.core.game3.options")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Schema = {}
|
||||
|
||||
@@ -74,6 +75,11 @@ function Schema.newGame(opts)
|
||||
if type(opts.engineOptions) == "table" then
|
||||
Options.bind(session, opts.engineOptions)
|
||||
end
|
||||
session.modData = {}
|
||||
if ModRuntime.wantsHook("save.new_game") then
|
||||
local hooked = ModRuntime.call("save.new_game", function(s) return s end, session)
|
||||
if type(hooked) == "table" then session = hooked end
|
||||
end
|
||||
return session
|
||||
end
|
||||
|
||||
@@ -114,6 +120,8 @@ function Schema.toSaveTable(session)
|
||||
trainerId = session.trainerId,
|
||||
rng = session.rng,
|
||||
questLog = require("src.core.game3.quest_log").export(session),
|
||||
modData = session.modData,
|
||||
meta = session.meta,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -155,6 +163,8 @@ function Schema.fromSaveTable(save)
|
||||
trainerId = save.trainerId,
|
||||
rng = save.rng,
|
||||
questLog = require("src.core.game3.quest_log").restore(save.questLog),
|
||||
modData = type(save.modData) == "table" and save.modData or {},
|
||||
meta = save.meta,
|
||||
}
|
||||
Schema.ensureMonBalls(session)
|
||||
if type(save.options) == "table" then
|
||||
|
||||
@@ -469,12 +469,12 @@ function Adapters.host(mod, game, world)
|
||||
local MonPic = require("src.ui.game3.mon_pic")
|
||||
MonPic.hide()
|
||||
end,
|
||||
giveMon = function(species, level)
|
||||
giveMon = function(species, level, _, _, _, nickname)
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
if not session then return false end
|
||||
local Party = require("src.core.game3.party")
|
||||
return Party.giveMon(session, species, level)
|
||||
return Party.giveMon(session, species, level, nickname)
|
||||
end,
|
||||
freezeLocal = function(localId, snap)
|
||||
local G3 = useGame3Objects()
|
||||
@@ -1214,6 +1214,7 @@ function Adapters.host(mod, game, world)
|
||||
earlyRival = battleOpts.earlyRival,
|
||||
rivalFlags = battleOpts.rivalFlags,
|
||||
noWhiteout = battleOpts.noWhiteout,
|
||||
double = battleOpts.double,
|
||||
done = function(result)
|
||||
if done then done(result or "win") end
|
||||
tick_vm()
|
||||
|
||||
@@ -86,4 +86,22 @@ function Ctx.clearTemps(store)
|
||||
end
|
||||
end
|
||||
|
||||
function Ctx.modCtx(vm)
|
||||
local ok, Gen3Compat = pcall(require, "src.mods.Gen3Compat")
|
||||
if ok and type(Gen3Compat) == "table" and type(Gen3Compat.scriptCtx) == "function" then
|
||||
local okC, c = pcall(Gen3Compat.scriptCtx, vm)
|
||||
if okC and type(c) == "table" then return c end
|
||||
end
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
return {
|
||||
game = Runtime and Runtime._game,
|
||||
save = session,
|
||||
session = session,
|
||||
overworld = { map = { id = Map and Map.current } },
|
||||
runner = vm,
|
||||
}
|
||||
end
|
||||
|
||||
return Ctx
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
local Ctx = require("src.core.game3.scripting.ctx")
|
||||
local FlagsTable = require("src.core.game3.scripting.flags_table")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Flags = {}
|
||||
|
||||
@@ -247,6 +248,8 @@ end
|
||||
function Flags.setFlag(store, ctx, id, on)
|
||||
id = tonumber(id) or (type(id) == "string" and Flags.IDS[id]) or 0
|
||||
if not store or not store.flags then return end
|
||||
local announce = ModRuntime.wants("flag.changed")
|
||||
and Flags.getFlag(store, ctx, id) ~= (on and true or false)
|
||||
local strId = tostring(id)
|
||||
if on then
|
||||
store.flags[id] = true
|
||||
@@ -261,6 +264,9 @@ function Flags.setFlag(store, ctx, id, on)
|
||||
if name then store.flags[name] = nil end
|
||||
end
|
||||
end
|
||||
if announce then
|
||||
ModRuntime.emit("flag.changed", { name = Flags.NAMES[id] or id, id = id, value = on and true or false })
|
||||
end
|
||||
end
|
||||
|
||||
function Flags.getVar(store, ctx, id)
|
||||
|
||||
@@ -6,6 +6,7 @@ local Flags = require("src.core.game3.scripting.flags")
|
||||
local TextIR = require("src.core.game3.scripting.text_ir")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
local Movement = require("src.core.game3.scripting.movement")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Ops = {}
|
||||
|
||||
@@ -101,7 +102,7 @@ local function show_message(vm, ptr, stay)
|
||||
return false
|
||||
end
|
||||
|
||||
function Ops.dispatch(vm, row)
|
||||
local function dispatch(vm, row)
|
||||
local op = row.op
|
||||
local ctx = vm.ctx
|
||||
local store = vm.store
|
||||
@@ -394,15 +395,39 @@ function Ops.dispatch(vm, row)
|
||||
local species = var_get(store, ctx, row[1] or row.species)
|
||||
local level = var_get(store, ctx, row[2] or row.level)
|
||||
if level < 1 then level = 5 end
|
||||
local nickname
|
||||
if ModRuntime.wants("pokemon.before_give") then
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local gift = {
|
||||
ctx = Ctx.modCtx(vm),
|
||||
species = Pokemon.keyName(species) or species,
|
||||
speciesId = species,
|
||||
level = level,
|
||||
}
|
||||
ModRuntime.emit("pokemon.before_give", gift)
|
||||
local id = type(gift.species) == "number" and gift.species
|
||||
or Pokemon.speciesFromName(gift.species)
|
||||
if tonumber(id) and tonumber(id) >= 1 and tonumber(id) ~= species then
|
||||
species = tonumber(id)
|
||||
local src = tonumber(row[1] or row.species) or 0
|
||||
if src >= 0x4000 then Flags.setVar(store, ctx, src, species) end
|
||||
end
|
||||
if tonumber(gift.level) then
|
||||
level = math.max(1, math.min(100, math.floor(tonumber(gift.level))))
|
||||
end
|
||||
if type(gift.nickname) == "string" and gift.nickname ~= "" then
|
||||
nickname = gift.nickname
|
||||
end
|
||||
end
|
||||
local ok = false
|
||||
if a.giveMon then
|
||||
ok = a.giveMon(species, level, row[3], row[4], row[5])
|
||||
ok = a.giveMon(species, level, row[3], row[4], row[5], nickname)
|
||||
else
|
||||
local Party = require("src.core.game3.party")
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
if session then
|
||||
ok = Party.giveMon(session, species, level)
|
||||
ok = Party.giveMon(session, species, level, nickname)
|
||||
end
|
||||
end
|
||||
Flags.setVar(store, ctx, Ctx.VAR_RESULT, ok and 0 or 2) -- 0=party, 2=fail
|
||||
@@ -812,6 +837,37 @@ function Ops.dispatch(vm, row)
|
||||
return false
|
||||
end
|
||||
|
||||
local isDouble = battleType == 4 or battleType == 6 or battleType == 7 or battleType == 8
|
||||
if op == "trainerbattle" and isDouble and a.startTrainerBattle then
|
||||
local Party = require("src.core.game3.party")
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
-- pokefirered/data/scripts/trainer_battle.inc:30
|
||||
if Party.monsStateToDoubles(session and session.party) ~= Party.PLAYER_HAS_TWO_USABLE_MONS then
|
||||
local dialogs = Trainers.dialogs(trainerId) or {}
|
||||
local cantText = (row.notEnoughText and resolve_text(vm, row.notEnoughText)) or dialogs.notEnough
|
||||
if cantText and cantText ~= "" and a.openMessageAsync then
|
||||
ctx.mode = "native"
|
||||
ctx.status = "waiting"
|
||||
local shown = false
|
||||
ctx.nativePoll = function()
|
||||
if not shown then return false end
|
||||
ctx.status = "halted"
|
||||
return false
|
||||
end
|
||||
a.openMessageAsync(cantText, function() shown = true end)
|
||||
if shown then
|
||||
ctx.mode = "bytecode"
|
||||
ctx.status = "halted"
|
||||
ctx.nativePoll = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
ctx.status = "halted"
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if a.startTrainerBattle then
|
||||
local foe = Trainers.foeFromId(trainerId)
|
||||
if not foe then
|
||||
@@ -866,7 +922,7 @@ function Ops.dispatch(vm, row)
|
||||
if eventScript and (battleType == 1 or battleType == 2
|
||||
or battleType == 6 or battleType == 8) then
|
||||
pendingGoto = eventScript
|
||||
elseif battleType == 0 then
|
||||
elseif battleType == 0 or battleType == 4 then
|
||||
-- Single standard trainer: script ends after encounter
|
||||
shouldHalt = true
|
||||
end
|
||||
@@ -879,6 +935,7 @@ function Ops.dispatch(vm, row)
|
||||
noWhiteout = earlyRival and (rivalFlags % 2 == 1),
|
||||
defeatText = defeatText,
|
||||
victoryText = victoryText,
|
||||
double = (foe.doubleBattle == true) or nil,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -1096,10 +1153,49 @@ function Ops.dispatch(vm, row)
|
||||
or op == "trywondercardscript" or op == "erasebox" then
|
||||
return false
|
||||
else
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local game = Runtime and Runtime._game
|
||||
local commands = game and game.data and game.data.commands
|
||||
local record = type(commands) == "table" and commands[op]
|
||||
local fn = type(record) == "table" and record.fn or record
|
||||
if type(fn) == "function" then
|
||||
local okCall, res = pcall(fn, Ctx.modCtx(vm), unpack(row))
|
||||
if not okCall then
|
||||
if a.log then a.log("[game3] command " .. tostring(op) .. " failed: " .. tostring(res)) end
|
||||
return false
|
||||
end
|
||||
if type(res) == "string" and vm.scripts and vm.scripts[res] then
|
||||
jump(vm, res)
|
||||
elseif res == "end" then
|
||||
vm:halt()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
-- Unknown / Tier C: skip
|
||||
if a.log then a.log("[game3] skip op " .. tostring(op)) end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function commandVanilla(vm)
|
||||
return function(_, name, hrow)
|
||||
if type(hrow) ~= "table" then hrow = {} end
|
||||
if name ~= nil and name ~= hrow.op then
|
||||
local copy = {}
|
||||
for k, v in pairs(hrow) do copy[k] = v end
|
||||
copy.op = name
|
||||
hrow = copy
|
||||
end
|
||||
return dispatch(vm, hrow)
|
||||
end
|
||||
end
|
||||
|
||||
function Ops.dispatch(vm, row)
|
||||
if not ModRuntime.wantsHook("script.command") then
|
||||
return dispatch(vm, row)
|
||||
end
|
||||
return ModRuntime.call("script.command", commandVanilla(vm), Ctx.modCtx(vm), row.op, row)
|
||||
end
|
||||
|
||||
return Ops
|
||||
|
||||
@@ -133,6 +133,7 @@ local function persist_sidecar(mod, game)
|
||||
if session then
|
||||
session.flags = snap.flags
|
||||
session.vars = snap.vars
|
||||
return
|
||||
end
|
||||
game = resolve_game(mod, game)
|
||||
if not (game and game.save) then return end
|
||||
@@ -237,7 +238,7 @@ end
|
||||
|
||||
function Space.deactivate(mod)
|
||||
if Space.vm then
|
||||
Space.vm:halt()
|
||||
Space.vm:halt(Space.vm:isRunning())
|
||||
end
|
||||
persist_sidecar(mod or Space._mod)
|
||||
if Space.store then
|
||||
|
||||
@@ -80,6 +80,10 @@ local function decompose_ai_flags(flags)
|
||||
}
|
||||
end
|
||||
|
||||
function Trainers.pack()
|
||||
return load_pack() or nil
|
||||
end
|
||||
|
||||
--- Get full trainer definition record by trainerId.
|
||||
function Trainers.get(trainerId)
|
||||
trainerId = tonumber(trainerId)
|
||||
@@ -146,6 +150,42 @@ function Trainers.get(trainerId)
|
||||
return nil
|
||||
end
|
||||
|
||||
local GBA_CHAR = {
|
||||
[" "] = 0x00, ["é"] = 0x1B, ["&"] = 0x2D, ["+"] = 0x2E, ["!"] = 0xAB, ["?"] = 0xAC,
|
||||
["."] = 0xAD, ["-"] = 0xAE, ["…"] = 0xB0, ["“"] = 0xB1, ["”"] = 0xB2, ["‘"] = 0xB3,
|
||||
["’"] = 0xB4, ["'"] = 0xB4, ["♂"] = 0xB5, ["♀"] = 0xB6, [","] = 0xB8, ["/"] = 0xBA,
|
||||
}
|
||||
|
||||
local function gba_char_sum(text)
|
||||
local sum = 0
|
||||
for ch in tostring(text or ""):gmatch("[%z\1-\127\194-\244][\128-\191]*") do
|
||||
local b = ch:byte()
|
||||
local v = GBA_CHAR[ch]
|
||||
if not v then
|
||||
if #ch == 1 and b >= 48 and b <= 57 then v = 0xA1 + (b - 48)
|
||||
elseif #ch == 1 and b >= 65 and b <= 90 then v = 0xBB + (b - 65)
|
||||
elseif #ch == 1 and b >= 97 and b <= 122 then v = 0xD5 + (b - 97)
|
||||
else v = 0 end
|
||||
end
|
||||
sum = sum + v
|
||||
end
|
||||
return sum
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_main.c:1555
|
||||
local function double_personalities(t)
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local nameHash = 0
|
||||
local out = {}
|
||||
for i, m in ipairs(t.party or {}) do
|
||||
nameHash = (nameHash + gba_char_sum(t.name)) % 0x100000000
|
||||
nameHash = (nameHash + gba_char_sum(Pokemon.name(tonumber(m.species) or 0))) % 0x100000000
|
||||
out[i] = (0x80 + (nameHash * 256) % 0x100000000) % 0x100000000
|
||||
end
|
||||
return out
|
||||
end
|
||||
Trainers._doublePersonalities = double_personalities
|
||||
|
||||
--- Resolve a foe battler struct + full party for battle runtime.
|
||||
-- Guarantees:
|
||||
-- 1. Uniform Flat IV scaling: actualIv = (rawIv * 31) / 255 across all 6 stats
|
||||
@@ -161,7 +201,8 @@ function Trainers.foeFromId(trainerId)
|
||||
end
|
||||
|
||||
local foeParty = {}
|
||||
for _, m in ipairs(t.party) do
|
||||
local pers = t.doubleBattle and double_personalities(t) or {}
|
||||
for pi, m in ipairs(t.party) do
|
||||
local rawIv = tonumber(m.rawIv) or tonumber(m.iv) or 0
|
||||
local iv = tonumber(m.iv) or math.floor((rawIv * 31) / 255)
|
||||
local mon = {
|
||||
@@ -175,6 +216,7 @@ function Trainers.foeFromId(trainerId)
|
||||
heldItem = tonumber(m.heldItem) or nil,
|
||||
moves = m.moves,
|
||||
trainerId = trainerId,
|
||||
personality = pers[pi],
|
||||
}
|
||||
foeParty[#foeParty + 1] = mon
|
||||
end
|
||||
@@ -189,6 +231,7 @@ function Trainers.foeFromId(trainerId)
|
||||
evs = lead.evs,
|
||||
heldItem = lead.heldItem,
|
||||
moves = lead.moves,
|
||||
personality = lead.personality,
|
||||
trainerId = trainerId,
|
||||
aiFlags = t.aiFlags,
|
||||
ai = t.ai,
|
||||
|
||||
@@ -4,6 +4,7 @@ local Ctx = require("src.core.game3.scripting.ctx")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Ops = require("src.core.game3.scripting.ops_a")
|
||||
local Adapters = require("src.core.game3.scripting.adapters")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Vm = {}
|
||||
Vm.__index = Vm
|
||||
@@ -49,7 +50,16 @@ function Vm:setPc(listKey, index)
|
||||
self.ctx.pc = { listKey = listKey, index = index or 1 }
|
||||
end
|
||||
|
||||
function Vm:halt()
|
||||
function Vm:_scriptEnded(completed)
|
||||
if not self._scriptKey then return end
|
||||
local key = self._scriptKey
|
||||
self._scriptKey = nil
|
||||
if ModRuntime.wants("script.ended") then
|
||||
ModRuntime.emit("script.ended", { ctx = Ctx.modCtx(self), completed = completed and true or false, key = key })
|
||||
end
|
||||
end
|
||||
|
||||
function Vm:halt(aborted)
|
||||
local a = self.adapters
|
||||
local ctx = self.ctx
|
||||
if a and a.unfreezeLocal then
|
||||
@@ -58,6 +68,7 @@ function Vm:halt()
|
||||
end
|
||||
end
|
||||
Ctx.haltCleanup(self.ctx)
|
||||
self:_scriptEnded(not aborted)
|
||||
end
|
||||
|
||||
function Vm:isRunning()
|
||||
@@ -80,6 +91,7 @@ function Vm:start(scriptKey, facing)
|
||||
end
|
||||
return false
|
||||
end
|
||||
if self._scriptKey then self:_scriptEnded(false) end
|
||||
self.ctx.mode = "bytecode"
|
||||
self.ctx.status = "running"
|
||||
self.ctx.stack = {}
|
||||
@@ -103,6 +115,10 @@ function Vm:start(scriptKey, facing)
|
||||
self.ctx.specialVars[Ctx.VAR_FACING] = keptFacing
|
||||
end
|
||||
self:setPc(scriptKey, 1)
|
||||
self._scriptKey = scriptKey
|
||||
if ModRuntime.wants("script.started") then
|
||||
ModRuntime.emit("script.started", { ctx = Ctx.modCtx(self), key = scriptKey })
|
||||
end
|
||||
self:resume()
|
||||
return true
|
||||
end
|
||||
@@ -114,7 +130,7 @@ function Vm:resume()
|
||||
guard = guard + 1
|
||||
if guard > 10000 then
|
||||
self.adapters.log("[game3] runaway script")
|
||||
self:halt()
|
||||
self:halt(true)
|
||||
return
|
||||
end
|
||||
if ctx.mode == "native" then
|
||||
@@ -134,7 +150,7 @@ function Vm:resume()
|
||||
local list = self.scripts[pc.listKey]
|
||||
if not list then
|
||||
self.adapters.log("[game3] bad list " .. tostring(pc.listKey))
|
||||
self:halt()
|
||||
self:halt(true)
|
||||
return
|
||||
end
|
||||
local row = list[pc.index]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
-- 6. Repel Counter: Decrements steps -> "Repel's effect wore off..." on expiration.
|
||||
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local StepEvents = {}
|
||||
|
||||
@@ -80,6 +81,12 @@ local function trigger_white_out(session, game)
|
||||
local healX = (session and session.healX) or 6
|
||||
local healY = (session and session.healY) or 6
|
||||
local healFacing = (session and session.healFacing) or "down"
|
||||
if ModRuntime.wants("world.blacked_out") then
|
||||
ModRuntime.emit("world.blacked_out", {
|
||||
save = session,
|
||||
healTarget = { map = healMap, x = healX, y = healY },
|
||||
})
|
||||
end
|
||||
|
||||
-- Heal all party Pokémon
|
||||
if session and session.party then
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Ctx = require("src.core.game3.scripting.ctx")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local TrainerSight = {}
|
||||
|
||||
@@ -82,6 +83,7 @@ function TrainerSight.getTrainerId(eo)
|
||||
local tid = tonumber(row.trainer or row[1])
|
||||
if tid then
|
||||
eo.trainerId = tid
|
||||
eo.trainerBattleType = tonumber(row.type) or 0
|
||||
return tid
|
||||
end
|
||||
end
|
||||
@@ -110,6 +112,32 @@ function TrainerSight.isDefeated(eo, store, ctx)
|
||||
return false
|
||||
end
|
||||
|
||||
function TrainerSight.battleType(eo)
|
||||
if not eo then return nil end
|
||||
if eo.trainerBattleType == nil then
|
||||
local Sp = Space()
|
||||
local scriptKey = eo.scriptKey or (eo.def and eo.def.scriptKey)
|
||||
local list = scriptKey and Sp and Sp.bundle and Sp.bundle.scripts and Sp.bundle.scripts[scriptKey]
|
||||
eo.trainerBattleType = false
|
||||
for _, row in ipairs(type(list) == "table" and list or {}) do
|
||||
if row.op == "trainerbattle" or row.op == "dotrainerbattle" then
|
||||
eo.trainerBattleType = tonumber(row.type) or 0
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return eo.trainerBattleType or nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/trainer_see.c:114
|
||||
function TrainerSight.blockedByDoubles(eo)
|
||||
if TrainerSight.battleType(eo) ~= 4 then return false end
|
||||
local Party = require("src.core.game3.party")
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
return Party.monsStateToDoubles(session and session.party) ~= Party.PLAYER_HAS_TWO_USABLE_MONS
|
||||
end
|
||||
|
||||
--- Test if a metatile behavior byte represents a one-way ledge hop.
|
||||
local function is_ledge_tile(game, cx, cy)
|
||||
local Coll = Collision()
|
||||
@@ -222,6 +250,15 @@ function TrainerSight.engage(game, eo, dist)
|
||||
-- 3. Play encounter music immediately when trainer spots player (pret PlayTrainerEncounterMusic / EventScript_DoTrainerBattleFromApproach)
|
||||
local tid = TrainerSight.getTrainerId(eo)
|
||||
local okT, Trainers = pcall(require, "src.core.game3.scripting.trainers")
|
||||
-- pokefirered/src/trainer_see.c:105
|
||||
if ModRuntime.wants("world.trainer_engaged") then
|
||||
local info = okT and Trainers and tid and Trainers.info and Trainers.info(tid) or nil
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
ModRuntime.emit("world.trainer_engaged", {
|
||||
npc = eo, trainerClass = info and info.class, partyIndex = tid, trainerId = tid,
|
||||
mapId = Map and Map.current, sight = { distance = dist, facing = eo.facing },
|
||||
})
|
||||
end
|
||||
local musicId = okT and Trainers and Trainers.getEncounterMusic and Trainers.getEncounterMusic(tid)
|
||||
if not musicId then
|
||||
musicId = 285 -- MUS_ENCOUNTER_BOY fallback
|
||||
@@ -308,7 +345,7 @@ function TrainerSight.check(game, specificTrainer)
|
||||
local sight = tonumber(eo.sight or (eo.def and (eo.def.sight or eo.def.trainerRange))) or 0
|
||||
if sight > 0 and not TrainerSight.isDefeated(eo, store, ctx) then
|
||||
local spotted, dist = TrainerSight.checkLineOfSight(eo, P, game)
|
||||
if spotted then
|
||||
if spotted and not TrainerSight.blockedByDoubles(eo) then
|
||||
TrainerSight.engage(game, eo, dist)
|
||||
return true
|
||||
end
|
||||
@@ -325,7 +362,7 @@ function TrainerSight.check(game, specificTrainer)
|
||||
local sight = tonumber(eo.sight or (eo.def and (eo.def.sight or eo.def.trainerRange))) or 0
|
||||
if sight > 0 and not TrainerSight.isDefeated(eo, store, ctx) then
|
||||
local spotted, dist = TrainerSight.checkLineOfSight(eo, P, game)
|
||||
if spotted then
|
||||
if spotted and not TrainerSight.blockedByDoubles(eo) then
|
||||
-- Immediately engage and break iterator to suppress any other simultaneous spots
|
||||
TrainerSight.engage(game, eo, dist)
|
||||
return true
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
-- Warp / door / fade sequencing helpers for game3 field.
|
||||
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Warp = {}
|
||||
|
||||
Warp._pending = nil
|
||||
@@ -9,9 +11,30 @@ function Warp.isBusy()
|
||||
return Warp._busy == true
|
||||
end
|
||||
|
||||
local function sameDestination(map, x, y) return map, x, y end
|
||||
|
||||
local function announce(game, destMap, destX, destY, kind)
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
local fromMap = Map and Map.current
|
||||
local warp = { kind = kind, map = destMap, x = destX, y = destY }
|
||||
if ModRuntime.wantsHook("warp.destination") then
|
||||
local m, nx, ny = ModRuntime.call("warp.destination", sameDestination, destMap, destX, destY,
|
||||
{ warp = warp, lastMap = fromMap, data = game and game.data })
|
||||
if m then
|
||||
destMap, destX, destY = m, tonumber(nx) or destX, tonumber(ny) or destY
|
||||
end
|
||||
end
|
||||
if ModRuntime.wants("player.warped") then
|
||||
ModRuntime.emit("player.warped", { fromMap = fromMap, toMap = destMap,
|
||||
x = destX, y = destY, warp = warp })
|
||||
end
|
||||
return destMap, destX, destY
|
||||
end
|
||||
|
||||
--- Complete door entrance sequence (walking UP into a building)
|
||||
function Warp.startDoorEntrance(mod, game, destMap, destX, destY, doorX, doorY)
|
||||
if Warp._busy then return false end
|
||||
destMap, destX, destY = announce(game, destMap, destX, destY, "door")
|
||||
Warp._busy = true
|
||||
|
||||
local Field = package.loaded["src.core.game3.field"] or require("src.core.game3.field")
|
||||
@@ -65,6 +88,7 @@ end
|
||||
--- Complete door exit sequence (walking DOWN off exit mat out to town)
|
||||
function Warp.startDoorExit(mod, game, destMap, destX, destY, exitX, exitY)
|
||||
if Warp._busy then return false end
|
||||
destMap, destX, destY = announce(game, destMap, destX, destY, "exit_door")
|
||||
Warp._busy = true
|
||||
|
||||
local Field = package.loaded["src.core.game3.field"] or require("src.core.game3.field")
|
||||
@@ -123,6 +147,7 @@ end
|
||||
|
||||
function Warp.startEscalator(mod, game, destMap, destX, destY, dir, approachDir, escX, escY)
|
||||
if Warp._busy then return false end
|
||||
destMap, destX, destY = announce(game, destMap, destX, destY, "escalator")
|
||||
Warp._busy = true
|
||||
Warp._isEscalatorActive = true
|
||||
|
||||
@@ -270,6 +295,7 @@ end
|
||||
-- pokefirered/src/field_fadetransition.c:794
|
||||
function Warp.startStairWarp(mod, game, destMap, destX, destY, behavior)
|
||||
if Warp._busy then return false end
|
||||
destMap, destX, destY = announce(game, destMap, destX, destY, "stairs")
|
||||
Warp._busy = true
|
||||
|
||||
local Field = package.loaded["src.core.game3.field"] or require("src.core.game3.field")
|
||||
@@ -376,6 +402,7 @@ end
|
||||
--- Complete teleport spin sequence (Silph Co, Sabrina's Gym warp pads)
|
||||
function Warp.startTeleport(mod, game, destMap, destX, destY)
|
||||
if Warp._busy then return false end
|
||||
destMap, destX, destY = announce(game, destMap, destX, destY, "teleport")
|
||||
Warp._busy = true
|
||||
|
||||
local Field = package.loaded["src.core.game3.field"] or require("src.core.game3.field")
|
||||
@@ -420,6 +447,7 @@ end
|
||||
--- Complete fall hole sequence (Mt. Moon, Seafoam drop holes)
|
||||
function Warp.startFall(mod, game, destMap, destX, destY)
|
||||
if Warp._busy then return false end
|
||||
destMap, destX, destY = announce(game, destMap, destX, destY, "fall")
|
||||
Warp._busy = true
|
||||
|
||||
local Field = package.loaded["src.core.game3.field"] or require("src.core.game3.field")
|
||||
@@ -480,6 +508,7 @@ function Warp.request(mod, game, mapId, x, y, facing, opts)
|
||||
if opts.fall then
|
||||
return Warp.startFall(mod, game, mapId, x, y)
|
||||
end
|
||||
mapId, x, y = announce(game, mapId, x, y, "warp")
|
||||
|
||||
Warp._pending = {
|
||||
mapId = mapId,
|
||||
|
||||
@@ -20,7 +20,7 @@ CacheContract.VERSION_FORMAT = {
|
||||
-- engine/overworld/map_sprites.asm:181
|
||||
yellow = "rom-cache-v11-yellow1:",
|
||||
-- v8: M4A tracks retain reachable patterns and explicit entry offsets.
|
||||
firered = "rom-cache-v10-firered:",
|
||||
firered = "rom-cache-v12-firered:",
|
||||
}
|
||||
CacheContract.MARKER_PATH = "rom-cache.complete"
|
||||
|
||||
@@ -193,6 +193,9 @@ CacheContract.VERSION_REQUIRED_FILES_OVERRIDE = {
|
||||
"data/generated/gba/pokemon/battle_moves.lua",
|
||||
"data/generated/gba/pokemon/battle/manifest.lua",
|
||||
"data/generated/gba/pokemon/battle/healthbox_player.rgba",
|
||||
"data/generated/gba/pokemon/battle/healthbox_doubles_player.rgba",
|
||||
"data/generated/gba/pokemon/battle/healthbox_doubles_opponent.rgba",
|
||||
"data/generated/gba/pokemon/battle/hp_bold_digits.rgba",
|
||||
"data/generated/gba/pokemon/battle/terrain_building.rgba",
|
||||
"data/generated/gba/pokemon/battle/ball_open/manifest.lua",
|
||||
"data/generated/gba/pokemon/battle/ball_open/particles.rgba",
|
||||
@@ -208,6 +211,7 @@ CacheContract.VERSION_REQUIRED_FILES_OVERRIDE = {
|
||||
"data/generated/gba/doors/pallet.rgba",
|
||||
"data/generated/gba/native/manifest.lua",
|
||||
"data/generated/gba/pokemon/summary/manifest.lua",
|
||||
"data/generated/gba/pokemon/summary/menu_info.rgba",
|
||||
"data/generated/gba/pokemon/storage/manifest.lua",
|
||||
"data/generated/gba/pokedex/manifest.lua",
|
||||
"data/generated/gba/chrome/manifest.lua",
|
||||
|
||||
@@ -7,7 +7,7 @@ local Lz77 = require("src.import.gba.lz77")
|
||||
|
||||
local BattleChromeExtract = {}
|
||||
|
||||
BattleChromeExtract.FORMAT_VERSION = 4
|
||||
BattleChromeExtract.FORMAT_VERSION = 5
|
||||
BattleChromeExtract.CACHE_SUB = "pokemon/battle"
|
||||
|
||||
local function default_cache_root()
|
||||
@@ -140,6 +140,82 @@ local function bake_enemy_healthbox(gfx, pal)
|
||||
return indices_to_rgba(indices, pal, w, h), w, h
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_interface.c:568
|
||||
local function bake_doubles_healthbox(gfx, pal)
|
||||
return bake_enemy_healthbox(gfx, pal)
|
||||
end
|
||||
|
||||
BattleChromeExtract.DOUBLES_FILES = {
|
||||
player = "healthbox_doubles_player.rgba",
|
||||
opponent = "healthbox_doubles_opponent.rgba",
|
||||
}
|
||||
|
||||
function BattleChromeExtract.bakeDoubles(get, cfg)
|
||||
cfg = cfg or Versions.BATTLE_UI
|
||||
if not (cfg.healthbox_doubles_player and cfg.healthbox_doubles_opponent) then return nil end
|
||||
local raw = {}
|
||||
for i = 0, 31 do raw[i + 1] = get(cfg.healthbox_pal + i) end
|
||||
local hbPal = load_pal(raw, 16)
|
||||
local playerRgba = bake_doubles_healthbox(Lz77.decompress(get, cfg.healthbox_doubles_player), hbPal)
|
||||
local opponentRgba = bake_doubles_healthbox(Lz77.decompress(get, cfg.healthbox_doubles_opponent), hbPal)
|
||||
return playerRgba, opponentRgba
|
||||
end
|
||||
|
||||
BattleChromeExtract.HP_BOLD_FILE = "hp_bold_digits.rgba"
|
||||
BattleChromeExtract.HP_BOLD_CHARS = "0123456789/"
|
||||
BattleChromeExtract.HP_BOLD_W = 88
|
||||
BattleChromeExtract.HP_BOLD_H = 8
|
||||
|
||||
-- pokefirered/src/text_printer.c:187
|
||||
local function decode_bold_half_rows(get, base, dest, destX, stride)
|
||||
local map = { [0] = 0, 1, 3, 0 }
|
||||
for row = 0, 7 do
|
||||
local lo = get(base + row * 2) or 0
|
||||
local hi = get(base + row * 2 + 1) or 0
|
||||
for half = 0, 1 do
|
||||
local b = half == 0 and hi or lo
|
||||
for k = 0, 3 do
|
||||
local v = math.floor(b / 4 ^ (3 - k)) % 4
|
||||
dest[row * stride + destX + half * 4 + k + 1] = map[v]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/text.c:1688
|
||||
function BattleChromeExtract.bakeHpBoldDigits(get, cfg)
|
||||
cfg = cfg or Versions.BATTLE_UI
|
||||
if not cfg.font_bold_glyphs then return nil end
|
||||
local raw = {}
|
||||
for i = 0, 31 do raw[i + 1] = get(cfg.healthbar_pal + i) end
|
||||
local barPal = load_pal(raw, 16)
|
||||
local w, h = BattleChromeExtract.HP_BOLD_W, BattleChromeExtract.HP_BOLD_H
|
||||
local indices = {}
|
||||
for i = 1, w * h do indices[i] = 0 end
|
||||
local codes = { 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xBA }
|
||||
for n, id in ipairs(codes) do
|
||||
local glyph = cfg.font_bold_glyphs + 2 * (0x100 * math.floor(id / 16) + 8 * (id % 16))
|
||||
-- pokefirered/src/battle_interface.c:900
|
||||
decode_bold_half_rows(get, glyph + 2 * 0x80, indices, (n - 1) * 8, w)
|
||||
end
|
||||
return indices_to_rgba(indices, barPal, w, h)
|
||||
end
|
||||
|
||||
function BattleChromeExtract.runDoubles(rom, cache, opts)
|
||||
opts = opts or {}
|
||||
local root = (opts.cacheRoot or default_cache_root()) .. "/" .. BattleChromeExtract.CACHE_SUB
|
||||
local get = function(i) return rom:get(i) end
|
||||
local playerRgba, opponentRgba = BattleChromeExtract.bakeDoubles(get, opts.cfg)
|
||||
if not playerRgba then return false end
|
||||
cache:write(root .. "/" .. BattleChromeExtract.DOUBLES_FILES.player, playerRgba)
|
||||
cache:write(root .. "/" .. BattleChromeExtract.DOUBLES_FILES.opponent, opponentRgba)
|
||||
local boldRgba = BattleChromeExtract.bakeHpBoldDigits(get, opts.cfg)
|
||||
if boldRgba then
|
||||
cache:write(root .. "/" .. BattleChromeExtract.HP_BOLD_FILE, boldRgba)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function bake_sheet_rgba(gfx, pal, w, h)
|
||||
gfx = bytes_to_array(gfx)
|
||||
local tilesW, tilesH = math.floor(w / 8), math.floor(h / 8)
|
||||
@@ -322,6 +398,7 @@ function BattleChromeExtract.run(rom, cache, opts)
|
||||
local enemyRgba = bake_enemy_healthbox(enemyGfx, hbPal)
|
||||
cache:write(root .. "/healthbox_player.rgba", playerRgba)
|
||||
cache:write(root .. "/healthbox_enemy.rgba", enemyRgba)
|
||||
BattleChromeExtract.runDoubles(rom, cache, { cacheRoot = cacheRoot })
|
||||
|
||||
local elGfx = read_raw(rom, cfg.healthbox_elements, 320 * 24 / 2)
|
||||
-- HP bar sprite uses TAG_HEALTHBAR_PAL; EXP is blitted into the healthbox
|
||||
@@ -378,6 +455,8 @@ function BattleChromeExtract.run(rom, cache, opts)
|
||||
-- Player TL uses stale 64x32 centerToCorner (−32,−16) even though shape is 64x64
|
||||
playerBox = { w = 128, h = 64, x = 158, y = 88 },
|
||||
enemyBox = { w = 128, h = 32, x = 44, y = 30 },
|
||||
doublesPlayerBox = { w = 128, h = 32, file = "healthbox_doubles_player.rgba" },
|
||||
doublesOpponentBox = { w = 128, h = 32, file = "healthbox_doubles_opponent.rgba" },
|
||||
-- Sprite centers before pic y_offset; final Y = base + y_offset [+8 player]
|
||||
playerSprite = { x = 72, y = 80 },
|
||||
enemySprite = { x = 176, y = 40 },
|
||||
|
||||
@@ -472,6 +472,16 @@ function SummaryChromeExtract.run(rom, cache, opts)
|
||||
cache:write(root .. "/pokerus.rgba", pkrsRgba)
|
||||
end
|
||||
|
||||
-- pokefirered/src/list_menu.c:738
|
||||
if rom and Versions.MENU_INFO_GFX and Versions.MENU_INFO_PAL then
|
||||
local miGfx = read_bytes(Versions.MENU_INFO_GFX, 128 * 128 / 2)
|
||||
local miPal = read_bytes(Versions.MENU_INFO_PAL, 64)
|
||||
local caught = bake_sheet_rgba(miGfx, miPal, 16, 16, 0)
|
||||
local types = bake_sheet_rgba(miGfx, miPal, 16, 16, 1)
|
||||
local split = 16 * 128 * 4
|
||||
cache:write(root .. "/menu_info.rgba", caught:sub(1, split) .. types:sub(split + 1))
|
||||
end
|
||||
|
||||
local menuInfoBin = read_bin({
|
||||
"src/import/gba/chrome/menus/menu_info.png",
|
||||
"data/generated/gba/pokemon/summary/menu_info.png",
|
||||
|
||||
@@ -1178,8 +1178,11 @@ Versions.BATTLE_UI = {
|
||||
healthbox_elements = 0xD11BC4, -- uncompressed 320×24 4bpp
|
||||
healthbox_player = 0xD1F340, -- gHealthboxSinglesPlayerGfx LZ → 4096
|
||||
healthbox_enemy = 0xD1F604, -- gHealthboxSinglesOpponentGfx LZ → 2048
|
||||
healthbox_doubles_player = 0xD1F794, -- gHealthboxDoublesPlayerGfx LZ → 2048
|
||||
healthbox_doubles_opponent = 0xD1F928, -- gHealthboxDoublesOpponentGfx LZ → 2048
|
||||
healthbox_pal = 0xD11B84, -- uncompressed 32 (gBattleInterface_Healthbox_Pal)
|
||||
healthbar_pal = 0xD11BA4, -- uncompressed 32 (gBattleInterface_Healthbar_Pal)
|
||||
font_bold_glyphs = 0x22FC48, -- uncompressed 8192 2bpp (sFontBoldJapaneseGlyphs, src/text.c:384)
|
||||
terrain_grass = {
|
||||
pal = 0x248400, -- LZ → 96 (3 banks)
|
||||
tiles = 0x24844C, -- LZ → 3136
|
||||
@@ -1273,6 +1276,8 @@ Versions.SUMMARY_POKERUS_GFX = 0x463B20 -- sPokerus_Gfx (64 bytes)
|
||||
Versions.SUMMARY_POKERUS_PAL = 0x463B00 -- sPokerus_Pal (32 bytes)
|
||||
Versions.SUMMARY_HP_BAR_YELLOW_PAL = 0x463AAC -- sHpBar_Yellow_Pal (32 bytes)
|
||||
Versions.SUMMARY_HP_BAR_RED_PAL = 0x463ACC -- sHpBar_Red_Pal (32 bytes)
|
||||
Versions.MENU_INFO_GFX = 0xE95DDC -- gMenuInfoElements_Gfx (4bpp 128x128, 8192 bytes)
|
||||
Versions.MENU_INFO_PAL = 0xE95D9C -- gMenuInfoElements1_Pal + gMenuInfoElements2_Pal (64 bytes)
|
||||
|
||||
-- Bag / item-menu chrome + item icons (LZ; FireRed USA 1.0).
|
||||
Versions.BAG_BG_GFX = 0xE830CC -- gBagBg_Gfx
|
||||
|
||||
@@ -118,7 +118,10 @@ local GEN2_REGISTRANTS = {
|
||||
local GEN2_ONLY_ORDER = { "item_effects", "phone_contacts", "decorations",
|
||||
"apricorns", "radio_channels" }
|
||||
|
||||
local GEN3_REGISTRANTS = {}
|
||||
|
||||
local function registrantsFor(generation)
|
||||
if generation == 3 then return GEN3_REGISTRANTS end
|
||||
if generation ~= 2 then return REGISTRANTS end
|
||||
local out, taken = {}, {}
|
||||
for _, entry in ipairs(REGISTRANTS) do
|
||||
|
||||
@@ -48,6 +48,44 @@ local GEN2_ROOTS = {
|
||||
gen2Constants = "constants", gen2Landmarks = "landmarks",
|
||||
}
|
||||
|
||||
local GEN3_ROOTS = {
|
||||
maps = "maps",
|
||||
gen3Pokemon = {
|
||||
names = "gba/pokemon/names", types = "gba/pokemon/types",
|
||||
stats = "gba/pokemon/stats", abilities = "gba/pokemon/abilities",
|
||||
abilityNames = "gba/pokemon/ability_names", meta = "gba/pokemon/meta",
|
||||
learnsets = "gba/pokemon/learnsets", evolutions = "gba/pokemon/evolutions",
|
||||
dex = "gba/pokemon/dex", moveNames = "gba/pokemon/move_names",
|
||||
},
|
||||
gen3Moves = { rom = "gba/pokemon/battle_moves", names = "gba/pokemon/move_names" },
|
||||
gen3Items = "gba/items/pack",
|
||||
gen3Encounters = "gba/encounters",
|
||||
gen3Text = "gba/scripts/text",
|
||||
gen3Scripts = "gba/scripts/scripts",
|
||||
}
|
||||
|
||||
local ROOTS = { [1] = GEN1_ROOTS, [2] = GEN2_ROOTS, [3] = GEN3_ROOTS }
|
||||
|
||||
local function rootsFor(generation)
|
||||
return ROOTS[generation] or GEN1_ROOTS
|
||||
end
|
||||
|
||||
local function extraModules(generation)
|
||||
local names, seen = {}, {}
|
||||
if generation ~= 3 then return names end
|
||||
for _, entry in pairs(GEN3_ROOTS) do
|
||||
local parts = type(entry) == "table" and entry or { entry }
|
||||
for _, name in pairs(parts) do
|
||||
if name:find("/", 1, true) and not seen[name] then
|
||||
seen[name] = true
|
||||
names[#names + 1] = name
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(names)
|
||||
return names
|
||||
end
|
||||
|
||||
local function resolvePath(root, suffix)
|
||||
local node = root
|
||||
for key in suffix:gmatch("[^.]+") do
|
||||
@@ -141,6 +179,10 @@ function DatasetViews:_preflight(version, inspected)
|
||||
local path = inspected.prefix .. "data/generated/" .. name .. ".lua"
|
||||
if self.fs.getInfo(path, "file") then modules[#modules + 1] = name end
|
||||
end
|
||||
for _, name in ipairs(extraModules(GameVersion.generation(version))) do
|
||||
local path = inspected.prefix .. "data/generated/" .. name .. ".lua"
|
||||
if self.fs.getInfo(path, "file") then modules[#modules + 1] = name end
|
||||
end
|
||||
for _, name in ipairs(modules) do
|
||||
local path = inspected.prefix .. "data/generated/" .. name .. ".lua"
|
||||
local info = self.fs.getInfo(path, "file")
|
||||
@@ -200,15 +242,14 @@ function DatasetViews:_ready(view)
|
||||
return not view.invalid
|
||||
end
|
||||
|
||||
function DatasetViews:_module(view, root)
|
||||
local moduleName = view.modules[root]
|
||||
if not moduleName then return nil end
|
||||
function DatasetViews:_decode(view, moduleName, soft)
|
||||
local cached = view.moduleCache[moduleName]
|
||||
if cached then return cached.value end
|
||||
local path = view.plan.paths[moduleName]
|
||||
if not path then return nil end
|
||||
local source = self.fs.read(path)
|
||||
if type(source) ~= "string" then
|
||||
if soft then return nil end
|
||||
return self:_reject(view, moduleName, source,
|
||||
moduleName .. ": unreadable generated module")
|
||||
end
|
||||
@@ -219,6 +260,11 @@ function DatasetViews:_module(view, root)
|
||||
end
|
||||
local value, err = self.decoder(source, DECODE_LIMITS)
|
||||
if type(value) ~= "table" then
|
||||
if soft then
|
||||
Logger.warn("dataset %s: %s skipped: %s", view.version, moduleName,
|
||||
tostring(err or "non-table root"))
|
||||
return nil
|
||||
end
|
||||
return self:_reject(view, moduleName, source,
|
||||
moduleName .. ": " .. tostring(err or "non-table root"))
|
||||
end
|
||||
@@ -226,6 +272,26 @@ function DatasetViews:_module(view, root)
|
||||
return value
|
||||
end
|
||||
|
||||
function DatasetViews:_module(view, root)
|
||||
local moduleName = view.modules[root]
|
||||
if not moduleName then return nil end
|
||||
local soft = view.generation == 3
|
||||
if type(moduleName) ~= "table" then
|
||||
return self:_decode(view, moduleName, soft and moduleName:find("/", 1, true) ~= nil)
|
||||
end
|
||||
local composite, any = {}, false
|
||||
for key, name in pairs(moduleName) do
|
||||
local value = self:_decode(view, name, true)
|
||||
if view.invalid then return nil end
|
||||
if value ~= nil then
|
||||
composite[key] = value
|
||||
any = true
|
||||
end
|
||||
end
|
||||
if not any then return nil end
|
||||
return composite
|
||||
end
|
||||
|
||||
function DatasetViews:_data(view)
|
||||
if view.data then return view.data end
|
||||
local data = {}
|
||||
@@ -236,6 +302,7 @@ function DatasetViews:_data(view)
|
||||
return value
|
||||
end,
|
||||
})
|
||||
if view.generation == 3 then Schemas.bindGen3(data) end
|
||||
DatasetHydration.apply(data, view.version, self.engineRequire)
|
||||
if view.invalid then error(view.invalid.detail, 0) end
|
||||
view.data = data
|
||||
@@ -284,6 +351,7 @@ function DatasetViews:_registry(view, name)
|
||||
or Schemas.targetFor(registry.name, registry.spec, view.generation)
|
||||
local root = target and target:match("^[^%.]+")
|
||||
local moduleName = root and view.modules[root]
|
||||
if type(moduleName) ~= "string" then moduleName = nil end
|
||||
if root == "gen2HeldItems" then moduleName = "items" end
|
||||
local cached = moduleName and view.moduleCache[moduleName]
|
||||
service:_reject(view, moduleName, cached and cached.source,
|
||||
@@ -391,7 +459,7 @@ function DatasetViews:open(version)
|
||||
generation = GameVersion.generation(version),
|
||||
prefix = inspected.prefix,
|
||||
plan = plan,
|
||||
modules = GameVersion.generation(version) == 2 and GEN2_ROOTS or GEN1_ROOTS,
|
||||
modules = rootsFor(GameVersion.generation(version)),
|
||||
moduleCache = {},
|
||||
}
|
||||
self.datasets[version] = internal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+46
-19
@@ -21,6 +21,7 @@ local Schemas = require("src.mods.Schemas")
|
||||
local Semver = require("src.mods.Semver")
|
||||
local Events = require("src.mods.Events")
|
||||
local Gen2Compat = require("src.mods.Gen2Compat")
|
||||
local Gen3Compat = require("src.mods.Gen3Compat")
|
||||
local Hooks = require("src.mods.Hooks")
|
||||
local LegacyCompat = require("src.mods.LegacyCompat")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
@@ -121,17 +122,34 @@ local GEN1_ONLY_MODULES = {
|
||||
["src.ui.OptionsMenu"] = true,
|
||||
}
|
||||
|
||||
local function crossGenerationDenial(name, generation)
|
||||
if type(name) ~= "string" or generation ~= 1 then return nil end
|
||||
if not (name:find("^src%.[%w_]+%.gen2%.") or name == "src.core.Game2") then
|
||||
return nil
|
||||
local function moduleGeneration(name)
|
||||
if name:find("^src%.[%w_]+%.gen2%.") or name == "src.core.Game2" then
|
||||
return 2
|
||||
end
|
||||
return ("%s is a Gen 2 engine module and this is a Gen 1 game; the structs "
|
||||
if name:find("^src%.[%w_]+%.game3%.") or name == "src.core.Game3" then
|
||||
return 3
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function crossGenerationDenial(name, generation)
|
||||
if type(name) ~= "string" then return nil end
|
||||
local owner = moduleGeneration(name)
|
||||
if owner == nil or owner == generation then return nil end
|
||||
if owner == 2 and generation ~= 1 and generation ~= 3 then return nil end
|
||||
if owner == 3 and generation ~= 1 and generation ~= 2 then return nil end
|
||||
return ("%s is a Gen %d engine module and this is a Gen %d game; the structs "
|
||||
.. "it reads and writes are not this game's, so anything it stores lands "
|
||||
.. "on the save in the wrong shape. Take the game from mod.game and the "
|
||||
.. "world from mod.world, which resolve per generation"):format(name)
|
||||
.. "world from mod.world, which resolve per generation")
|
||||
:format(name, owner, generation)
|
||||
end
|
||||
|
||||
local COMPAT = {
|
||||
[2] = { module = Gen2Compat, file = "src/mods/Gen2Compat.lua", tag = "gen2" },
|
||||
[3] = { module = Gen3Compat, file = "src/mods/Gen3Compat.lua", tag = "gen3" },
|
||||
}
|
||||
|
||||
-- the src.* modules the mod surface points authors at: another mod's
|
||||
-- exports carry a version string that wants range-checking before use, and
|
||||
-- ChipAsm is the authoring path for chip music and sfx
|
||||
@@ -176,15 +194,17 @@ local function scanRequire(name)
|
||||
-- A Gen 1-only module on a Gold boot is not a permissions question, it is a
|
||||
-- dead patch: reported once, attributed, and onto the boot error feed the
|
||||
-- manager shows the player rather than a dev-only log line.
|
||||
local compat = COMPAT[devShim.generation]
|
||||
if devShim.generation ~= 1 and GEN1_ONLY_MODULES[name]
|
||||
and not Gen2Compat.serves(name) then
|
||||
local key = modId .. "|gen2|" .. name
|
||||
and not (compat and compat.module.serves(name)) then
|
||||
local key = modId .. "|" .. (compat and compat.tag or "gen?") .. "|" .. name
|
||||
if not devShim.warned[key] then
|
||||
devShim.warned[key] = true
|
||||
local message = ("%s: requires %s, which a Gen 2 game never runs and "
|
||||
.. "src/mods/Gen2Compat.lua has no adapter for; take the game from "
|
||||
local message = ("%s: requires %s, which a Gen %s game never runs and "
|
||||
.. "%s has no adapter for; take the game from "
|
||||
.. "the game.ready payload and mod.world")
|
||||
:format(modId, name)
|
||||
:format(modId, name, tostring(devShim.generation),
|
||||
compat and compat.file or "no compat layer")
|
||||
local errors = devShim.errors
|
||||
if errors then errors[#errors + 1] = message end
|
||||
Logger.error("%s", message)
|
||||
@@ -243,18 +263,19 @@ function Loader:_installDevShim()
|
||||
if denial then error(("[%s] %s"):format(id or "mod", denial), 0) end
|
||||
end
|
||||
if devShim.dev or devShim.generation ~= 1 then scanRequire(name) end
|
||||
-- The Gen 1 name a mod asked for, answered by the Gen 2 arm behind it.
|
||||
-- The Gen 1 name a mod asked for, answered by this generation's compat arm.
|
||||
-- Engine code keeps the real module: src/render/PaletteFX.lua:776
|
||||
-- requires src.core.Game on both generations and means it.
|
||||
if devShim.generation == 2 and Gen2Compat.serves(name)
|
||||
local compat = COMPAT[devShim.generation]
|
||||
if compat and compat.module.serves(name)
|
||||
and (owner or callerIsMod(3)) then
|
||||
local adapter = Gen2Compat.resolve(name, Runtime.currentMod)
|
||||
local adapter = compat.module.resolve(name, Runtime.currentMod)
|
||||
if adapter then
|
||||
local key = "adapter|" .. name
|
||||
local key = "adapter|" .. compat.tag .. "|" .. name
|
||||
if not devShim.warned[key] then
|
||||
devShim.warned[key] = true
|
||||
Logger.info("gen2 facade: %s -> %s", name,
|
||||
tostring(Gen2Compat.ADAPTERS[name]))
|
||||
Logger.info("%s facade: %s -> %s", compat.tag, name,
|
||||
tostring(compat.module.ADAPTERS[name]))
|
||||
end
|
||||
return adapter
|
||||
end
|
||||
@@ -1266,6 +1287,7 @@ function Loader:_api(mod)
|
||||
-- entry chunk can decide whether to register developer-only diagnostics
|
||||
-- without receiving the process environment or the loader itself.
|
||||
developer = loader.dev == true,
|
||||
generation = loader.generation,
|
||||
-- a deep copy: what a mod does to its own view never reaches the loader
|
||||
manifest = Merge.deepCopy(mod.manifest),
|
||||
datasets = {
|
||||
@@ -1641,7 +1663,8 @@ function Loader:_api(mod)
|
||||
local game = loader:_game()
|
||||
if key == "battle" then
|
||||
if battle then return battle end
|
||||
local module = game and engineRequire(loader.generation == 2
|
||||
local module = game and engineRequire(loader.generation == 3
|
||||
and "src.battle.game3.BattleAPI" or loader.generation == 2
|
||||
and "src.battle.gen2.BattleAPI" or "src.battle.BattleAPI")
|
||||
if not module then return nil end
|
||||
battle = module.new(game)
|
||||
@@ -1652,7 +1675,8 @@ function Loader:_api(mod)
|
||||
-- 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)
|
||||
local module = game and engineRequire(loader.generation == 2
|
||||
local module = game and engineRequire(loader.generation == 3
|
||||
and "src.world.game3.WorldAPI" or loader.generation == 2
|
||||
and "src.world.gen2.WorldAPI" or "src.world.WorldAPI")
|
||||
if not module then return nil end
|
||||
world = module.new(game, modId)
|
||||
@@ -1770,6 +1794,7 @@ end
|
||||
function Loader:_validateScripts()
|
||||
local registry = self.content.map_scripts
|
||||
if not registry or next(registry.ops) == nil then return end
|
||||
if registry.spec.semantics ~= "compose" then return end
|
||||
local MapScripts = engineRequire("src.script.MapScripts")
|
||||
if not MapScripts then return end
|
||||
local commands = self.content.commands
|
||||
@@ -1850,6 +1875,7 @@ function Loader:load(data, opts)
|
||||
self.arenaCartId = mode == "cartOnly" and opts.cartId or nil
|
||||
self.arenaSealBroken = opts.sealBroken == true
|
||||
self.baseData = data
|
||||
if self.generation == 3 then Schemas.bindGen3(data) end
|
||||
-- every registry folds against the pristine view of its Data target;
|
||||
-- resolution is lazy so optional namespaces may appear later
|
||||
for name, registry in pairs(self.content) do
|
||||
@@ -1939,6 +1965,7 @@ function Loader:load(data, opts)
|
||||
-- every touch: a mod captures the facade at file scope, before Game2 has a
|
||||
-- save or a world (src/mods/Gen2Compat.lua).
|
||||
Gen2Compat.bind(function() return self:_game() end)
|
||||
Gen3Compat.bind(function() return self:_game() end)
|
||||
-- Any boot with mods on it needs the gate, because require("io") is how a
|
||||
-- mod would walk out of Sandbox.envFor. Dev mode adds the permissions
|
||||
-- tripwire on top, and a Gold boot the Gen 1-only require report -- the
|
||||
|
||||
+966
-18
File diff suppressed because it is too large
Load Diff
@@ -236,11 +236,16 @@ function BagMenu.handleInput(input)
|
||||
local BattleItems = require("src.core.game3.battle.items")
|
||||
if BattleItems.needsPartySelect(row.id) then
|
||||
local PartyMenu = require("src.ui.game3.party_menu")
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local st = Battle and Battle._st
|
||||
-- pokefirered/src/party_menu.c:5878
|
||||
PartyMenu.show(party, BagMenu._session and BagMenu._session.moveOverlay, {
|
||||
session = BagMenu._session,
|
||||
bag = BagMenu._bag,
|
||||
item = row.id,
|
||||
mode = "use",
|
||||
battleOrder = st and st.playerParty and PartyMenu.battleOrder(st) or nil,
|
||||
layout = (st and st.double) and "double" or nil,
|
||||
onClose = function()
|
||||
BagMenu.mode = "list"
|
||||
clamp_cursor()
|
||||
|
||||
@@ -10,6 +10,8 @@ BattleChrome._manifest = nil
|
||||
BattleChrome._textbox = nil
|
||||
BattleChrome._playerBox = nil
|
||||
BattleChrome._enemyBox = nil
|
||||
BattleChrome._doublesPlayerBox = nil
|
||||
BattleChrome._doublesOpponentBox = nil
|
||||
BattleChrome._elements = nil
|
||||
BattleChrome._partyBar = nil
|
||||
BattleChrome._terrains = {}
|
||||
@@ -111,6 +113,12 @@ function BattleChrome.install(cache)
|
||||
BattleChrome._textbox = nil
|
||||
BattleChrome._playerBox = nil
|
||||
BattleChrome._enemyBox = nil
|
||||
BattleChrome._doublesPlayerBox = nil
|
||||
BattleChrome._doublesOpponentBox = nil
|
||||
BattleChrome._doublesTried = false
|
||||
BattleChrome._hpBold = nil
|
||||
BattleChrome._hpBoldQuads = {}
|
||||
BattleChrome._hpBoldTried = false
|
||||
BattleChrome._elements = nil
|
||||
BattleChrome._elementsExp = nil
|
||||
BattleChrome._partyBar = nil
|
||||
@@ -166,6 +174,98 @@ function BattleChrome.install(cache)
|
||||
end
|
||||
end
|
||||
|
||||
local function rom_bytes()
|
||||
local okP, Pokemon = pcall(require, "src.core.game3.pokemon")
|
||||
if okP and Pokemon and type(Pokemon._romBytes) == "string" then return Pokemon._romBytes end
|
||||
for _, p in ipairs({ "1636 - Pokemon Fire Red (U)(Squirrels).gba", "firered.gba", "Pokemon FireRed.gba" }) do
|
||||
local f = io.open(p, "rb")
|
||||
if f then
|
||||
local d = f:read("*a")
|
||||
f:close()
|
||||
if d and #d >= 0x1000000 then return d end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function load_doubles_boxes()
|
||||
if BattleChrome._doublesTried then return end
|
||||
BattleChrome._doublesTried = true
|
||||
local root = battle_root()
|
||||
local files = BattleChromeExtract.DOUBLES_FILES or {}
|
||||
local pRgba = read_bytes(root .. "/" .. (files.player or "healthbox_doubles_player.rgba"))
|
||||
local oRgba = read_bytes(root .. "/" .. (files.opponent or "healthbox_doubles_opponent.rgba"))
|
||||
if not (pRgba and oRgba) then
|
||||
local rom = rom_bytes()
|
||||
if rom then
|
||||
local ok, p2, o2 = pcall(BattleChromeExtract.bakeDoubles, function(i) return rom:byte(i + 1) or 0 end)
|
||||
if ok and p2 and o2 then pRgba, oRgba = p2, o2 end
|
||||
end
|
||||
end
|
||||
BattleChrome._doublesPlayerBox = rgba_to_image(pRgba, 128, 32)
|
||||
BattleChrome._doublesOpponentBox = rgba_to_image(oRgba, 128, 32)
|
||||
if not (BattleChrome._doublesPlayerBox and BattleChrome._doublesOpponentBox) then
|
||||
print("[game3/battle_chrome] doubles healthboxes missing; re-import the ROM to extract them")
|
||||
end
|
||||
end
|
||||
|
||||
local function load_hp_bold()
|
||||
if BattleChrome._hpBoldTried then return end
|
||||
BattleChrome._hpBoldTried = true
|
||||
local w, h = BattleChromeExtract.HP_BOLD_W or 88, BattleChromeExtract.HP_BOLD_H or 8
|
||||
local rgba = read_bytes(battle_root() .. "/" .. (BattleChromeExtract.HP_BOLD_FILE or "hp_bold_digits.rgba"))
|
||||
if not rgba then
|
||||
local rom = rom_bytes()
|
||||
if rom then
|
||||
local ok, r2 = pcall(BattleChromeExtract.bakeHpBoldDigits, function(i) return rom:byte(i + 1) or 0 end)
|
||||
if ok and r2 then rgba = r2 end
|
||||
end
|
||||
end
|
||||
BattleChrome._hpBold = rgba_to_image(rgba, w, h)
|
||||
BattleChrome._hpBoldQuads = {}
|
||||
if not BattleChrome._hpBold then
|
||||
print("[game3/battle_chrome] bold HP digits missing; re-import the ROM to extract them")
|
||||
end
|
||||
end
|
||||
|
||||
function BattleChrome.hasHpBoldDigits()
|
||||
load_hp_bold()
|
||||
return BattleChrome._hpBold ~= nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_interface.c:889
|
||||
function BattleChrome.drawHpBoldChar(ch, x, y)
|
||||
load_hp_bold()
|
||||
local img = BattleChrome._hpBold
|
||||
if not img then return false end
|
||||
local chars = BattleChromeExtract.HP_BOLD_CHARS or "0123456789/"
|
||||
local n = chars:find(ch, 1, true)
|
||||
if not n then return false end
|
||||
local q = BattleChrome._hpBoldQuads[n]
|
||||
if not q then
|
||||
q = love.graphics.newQuad((n - 1) * 8, 0, 8, 8, img:getDimensions())
|
||||
BattleChrome._hpBoldQuads[n] = q
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(img, q, x, y)
|
||||
return true
|
||||
end
|
||||
|
||||
function BattleChrome.hasDoublesBoxes()
|
||||
load_doubles_boxes()
|
||||
return BattleChrome._doublesPlayerBox ~= nil and BattleChrome._doublesOpponentBox ~= nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_gfx_sfx_util.c:39
|
||||
function BattleChrome.drawDoublesBox(isPlayer, x, y)
|
||||
load_doubles_boxes()
|
||||
local img = isPlayer and BattleChrome._doublesPlayerBox or BattleChrome._doublesOpponentBox
|
||||
img = img or BattleChrome._enemyBox
|
||||
if not img then return end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(img, x, y)
|
||||
end
|
||||
|
||||
function BattleChrome.ready()
|
||||
if BattleChrome._playerBox and next(BattleChrome._terrains) then return true end
|
||||
return BattleChromeExtract.ready(BattleChrome._cache, cache_root())
|
||||
@@ -320,6 +420,7 @@ end
|
||||
|
||||
-- Element tile bases (pret B_INTERFACE_GFX_*)
|
||||
local HP_TEXT_TILE = 1
|
||||
local HP_BAR_LEFT_BORDER = 65
|
||||
local HP_BAR_BASE = { green = 3, yellow = 47, red = 56 }
|
||||
local HP_BAR_TILES = 6
|
||||
local HP_BAR_PIXELS = 48
|
||||
@@ -391,16 +492,22 @@ local function filled_pixels_for_bar(ratio, numTiles)
|
||||
end
|
||||
|
||||
--- Draw pret HP bar: HP label tiles + 6 fill tiles (48px). Top-left of 64×8 strip.
|
||||
function BattleChrome.drawHpBar(x, y, hp, maxHp)
|
||||
function BattleChrome.drawHpBar(x, y, hp, maxHp, statusBorder)
|
||||
if not BattleChrome._elements then return end
|
||||
local color = BattleChrome.hpColor(hp, maxHp)
|
||||
local base = HP_BAR_BASE[color] or HP_BAR_BASE.green
|
||||
local pix = split_bar_pixels(BattleChrome.scaledHpFraction(hp, maxHp, HP_BAR_PIXELS), HP_BAR_TILES)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for i = 0, 1 do
|
||||
local q = elements_tile_quad(HP_TEXT_TILE + i)
|
||||
if q then love.graphics.draw(BattleChrome._elements, q, x + i * 8, y) end
|
||||
if statusBorder then
|
||||
-- pokefirered/src/battle_interface.c:1672
|
||||
local q = elements_tile_quad(HP_BAR_LEFT_BORDER)
|
||||
if q then love.graphics.draw(BattleChrome._elements, q, x + 8, y) end
|
||||
else
|
||||
for i = 0, 1 do
|
||||
local q = elements_tile_quad(HP_TEXT_TILE + i)
|
||||
if q then love.graphics.draw(BattleChrome._elements, q, x + i * 8, y) end
|
||||
end
|
||||
end
|
||||
for i = 0, HP_BAR_TILES - 1 do
|
||||
local q = elements_tile_quad(base + (pix[i + 1] or 0))
|
||||
@@ -408,6 +515,15 @@ function BattleChrome.drawHpBar(x, y, hp, maxHp)
|
||||
end
|
||||
end
|
||||
|
||||
function BattleChrome.drawElementTile(ti, x, y, healthboxPal)
|
||||
local sheet = healthboxPal and (BattleChrome._elementsExp or BattleChrome._elements) or BattleChrome._elements
|
||||
if not sheet then return end
|
||||
local q = elements_tile_quad(ti, sheet)
|
||||
if not q then return end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet, q, x, y)
|
||||
end
|
||||
|
||||
function BattleChrome.drawHpFill(x, y, hp, maxHp)
|
||||
BattleChrome.drawHpBar(x - 16, y, hp, maxHp)
|
||||
end
|
||||
|
||||
@@ -92,6 +92,7 @@ function EvolutionScene.start(mon, postSpecies, opts)
|
||||
EvolutionScene._canStop = opts.canStop ~= false
|
||||
EvolutionScene._session = opts.session
|
||||
EvolutionScene._bag = opts.bag
|
||||
EvolutionScene._via = opts.via
|
||||
EvolutionScene._onDone = opts.onDone
|
||||
EvolutionScene._isBattle = opts.isBattle and true or false
|
||||
EvolutionScene._headless = opts.headless and true or false
|
||||
@@ -489,7 +490,7 @@ function EvolutionScene.update(dt)
|
||||
EvolutionScene._postScale = 1.0
|
||||
|
||||
-- POINT OF NO RETURN: Mutate species, stats, nickname, dex, Shedinja now
|
||||
Evolution.apply(EvolutionScene._mon, EvolutionScene._postSpecies, EvolutionScene._session, EvolutionScene._bag)
|
||||
Evolution.apply(EvolutionScene._mon, EvolutionScene._postSpecies, EvolutionScene._session, EvolutionScene._bag, EvolutionScene._via)
|
||||
|
||||
pcall(function() Audio.playSe(SE.SE_EXP or 1) end)
|
||||
spawn_flash_spray()
|
||||
|
||||
@@ -10,6 +10,7 @@ local Chrome = require("src.ui.game3.chrome")
|
||||
local Window = require("src.ui.game3.window")
|
||||
local Naming = require("src.ui.game3.naming")
|
||||
local BallOpen = require("src.core.game3.battle.ball_open")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local Scene = {}
|
||||
Scene.__index = Scene
|
||||
@@ -380,9 +381,25 @@ function Scene:printerActive()
|
||||
end
|
||||
|
||||
-- pokefirered/src/oak_speech.c:1124
|
||||
function Scene:_answered(label, value, saveKey)
|
||||
if not ModRuntime.wants("intro.oak_speech.answered") then return end
|
||||
ModRuntime.emit("intro.oak_speech.answered", {
|
||||
speech = self, step = { id = label }, index = self._oakStep or 0,
|
||||
label = label, value = value, saveKey = saveKey,
|
||||
})
|
||||
end
|
||||
|
||||
function Scene:oakPrint(key, speed)
|
||||
local text = OAK_TEXT[key] or key
|
||||
text = text:gsub("{PLAYER}", self.playerName):gsub("{RIVAL}", self.rivalName)
|
||||
if self.section == "oak" then
|
||||
self._oakStep = (self._oakStep or 0) + 1
|
||||
if ModRuntime.wants("intro.oak_speech.step") then
|
||||
ModRuntime.emit("intro.oak_speech.step", {
|
||||
speech = self, step = { id = key, text = text }, index = self._oakStep,
|
||||
})
|
||||
end
|
||||
end
|
||||
self.win.dialog = true
|
||||
self.printer = newPrinter(text, speed == nil and self.textSpeed or speed, true)
|
||||
end
|
||||
@@ -890,6 +907,10 @@ function Scene.Task_OakSpeech_Init(self, t)
|
||||
return
|
||||
end
|
||||
self.section = "oak"
|
||||
self._oakStep = 0
|
||||
if ModRuntime.wants("intro.oak_speech.started") then
|
||||
ModRuntime.emit("intro.oak_speech.started", { speech = self, steps = {} })
|
||||
end
|
||||
self.bg1 = { image = self.assets.oakSpeechBg }
|
||||
d.nidoran = createSprite(self, {
|
||||
dims = Oam.SQUARE_64, priority = 1, image = self.assets.nidoranFront,
|
||||
@@ -1062,6 +1083,7 @@ function Scene.Task_OakSpeech_HandleGenderInput(self, t)
|
||||
else
|
||||
return
|
||||
end
|
||||
self:_answered("gender", self.gender, "gender")
|
||||
t.func = Scene.Task_OakSpeech_ClearGenderWindows
|
||||
end
|
||||
|
||||
@@ -1147,8 +1169,10 @@ function Scene:getDefaultName(choice)
|
||||
local list = self.gender == MALE and MALE_NAMES or FEMALE_NAMES
|
||||
local r = require("src.core.game3.rng").Random()
|
||||
self.playerName = list[(r % #list) + 1]
|
||||
self:_answered("name", self.playerName, "name")
|
||||
else
|
||||
self.rivalName = RIVAL_NAMES[choice + 1]
|
||||
self:_answered("rivalName", self.rivalName, "rivalName")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1197,6 +1221,11 @@ function Scene:enterNaming(rival)
|
||||
onDone = function(name)
|
||||
if name and name ~= "" then
|
||||
if rival then scene.rivalName = name else scene.playerName = name end
|
||||
if rival then
|
||||
scene:_answered("rivalName", name, "rivalName")
|
||||
else
|
||||
scene:_answered("name", name, "name")
|
||||
end
|
||||
end
|
||||
scene.naming.stage = "fade_out"
|
||||
scene.naming.pal:beginFade(Pal.ALL, 0, 0, 16, Pal.BLACK)
|
||||
@@ -1467,11 +1496,15 @@ end
|
||||
function Scene.Task_OakSpeech_FreeResources(self, t)
|
||||
-- pokefirered/src/oak_speech.c:1777
|
||||
destroyTask(t)
|
||||
local answers = { gender = self.gender, name = self.playerName, rivalName = self.rivalName }
|
||||
if ModRuntime.wants("intro.oak_speech.finished") then
|
||||
ModRuntime.emit("intro.oak_speech.finished", { speech = self, answers = answers })
|
||||
end
|
||||
self.result = {
|
||||
action = "new_game",
|
||||
gender = self.gender,
|
||||
name = self.playerName,
|
||||
rivalName = self.rivalName,
|
||||
gender = tonumber(answers.gender) or self.gender,
|
||||
name = type(answers.name) == "string" and answers.name ~= "" and answers.name or self.playerName,
|
||||
rivalName = type(answers.rivalName) == "string" and answers.rivalName ~= "" and answers.rivalName or self.rivalName,
|
||||
start = MapIds.NEW_GAME_START,
|
||||
}
|
||||
end
|
||||
|
||||
+253
-104
@@ -122,6 +122,123 @@ local INFO_RIGHT = {
|
||||
hp = { 102, 12 }, hpMax = { 117, 12 }, hpBar = { 88, 10 },
|
||||
}
|
||||
|
||||
-- pokefirered/src/data/party_menu.h:192
|
||||
local SLOT_WIN_DOUBLE = {
|
||||
{ left = 1, top = 1, w = 10, h = 7, kind = "main" },
|
||||
{ left = 1, top = 8, w = 10, h = 7, kind = "main" },
|
||||
{ left = 12, top = 1, w = 18, h = 3, kind = "wide" },
|
||||
{ left = 12, top = 5, w = 18, h = 3, kind = "wide" },
|
||||
{ left = 12, top = 9, w = 18, h = 3, kind = "wide" },
|
||||
{ left = 12, top = 13, w = 18, h = 3, kind = "wide" },
|
||||
}
|
||||
|
||||
-- pokefirered/src/data/party_menu.h:81
|
||||
local SLOT_SPRITES_DOUBLE = {
|
||||
{ 16, 24, 20, 34, 56, 36, 16, 18 },
|
||||
{ 16, 80, 20, 90, 56, 92, 16, 74 },
|
||||
{ 104, 18, 108, 28, 144, 27, 102, 25 },
|
||||
{ 104, 50, 108, 60, 144, 59, 102, 57 },
|
||||
{ 104, 82, 108, 92, 144, 91, 102, 89 },
|
||||
{ 104, 114, 108, 124, 144, 123, 102, 121 },
|
||||
}
|
||||
|
||||
local function is_double()
|
||||
return PartyMenu._layout == "double"
|
||||
end
|
||||
|
||||
local function slot_win(i)
|
||||
return (is_double() and SLOT_WIN_DOUBLE or SLOT_WIN)[i]
|
||||
end
|
||||
|
||||
local function slot_sprites(i)
|
||||
return (is_double() and SLOT_SPRITES_DOUBLE or SLOT_SPRITES)[i]
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:735
|
||||
local function slot_info(i)
|
||||
if i == 1 or (i == 2 and is_double()) then return INFO_LEFT end
|
||||
return INFO_RIGHT
|
||||
end
|
||||
|
||||
local function slot_filled(i)
|
||||
local mon = PartyMenu._party and PartyMenu._party[i]
|
||||
return mon ~= nil and (tonumber(mon.species or mon.speciesId) or 1) ~= 0
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:1499
|
||||
local function double_next_slot(slot, dir)
|
||||
while true do
|
||||
slot = slot + dir
|
||||
if slot < 1 or slot > 6 then return nil end
|
||||
if slot_filled(slot) then return slot end
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:1402
|
||||
local function nav_double(cur, dir)
|
||||
local last = PartyMenu._lastSelectedSlot
|
||||
if dir == "up" then
|
||||
if cur == 1 then return 7 end
|
||||
local from = cur
|
||||
if cur == 7 then from = 7 end
|
||||
return double_next_slot(from, -1) or cur
|
||||
elseif dir == "down" then
|
||||
if cur == 7 then return 1 end
|
||||
return double_next_slot(cur, 1) or 7
|
||||
elseif dir == "right" then
|
||||
if cur == 1 then
|
||||
if last == 4 then
|
||||
if slot_filled(4) then return 4 end
|
||||
elseif slot_filled(3) then
|
||||
return 3
|
||||
end
|
||||
elseif cur == 2 then
|
||||
if last == 6 then
|
||||
if slot_filled(6) then return 6 end
|
||||
elseif slot_filled(5) then
|
||||
return 5
|
||||
end
|
||||
end
|
||||
return cur
|
||||
elseif dir == "left" then
|
||||
if cur == 3 or cur == 4 then
|
||||
PartyMenu._lastSelectedSlot = cur
|
||||
return 1
|
||||
elseif cur == 5 or cur == 6 then
|
||||
PartyMenu._lastSelectedSlot = cur
|
||||
return 2
|
||||
end
|
||||
end
|
||||
return cur
|
||||
end
|
||||
|
||||
local function battle_nav_double(input)
|
||||
local oldCur = PartyMenu.cursor
|
||||
for _, dir in ipairs({ "up", "down", "left", "right" }) do
|
||||
if input:wasPressed(dir) then
|
||||
PartyMenu.cursor = nav_double(PartyMenu.cursor, dir)
|
||||
break
|
||||
end
|
||||
end
|
||||
if PartyMenu.cursor ~= oldCur then se(5) end
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:5905
|
||||
local function open_battle_actions_double(prevMode)
|
||||
local mon = PartyMenu._party and PartyMenu._party[PartyMenu.cursor]
|
||||
se(5)
|
||||
if not slot_filled(2) or (mon and mon.isEgg) then
|
||||
PartyMenu.ACTIONS = { "SUMMARY", "CANCEL" }
|
||||
elseif prevMode == "battle_faint" then
|
||||
PartyMenu.ACTIONS = { "SEND OUT", "SUMMARY", "CANCEL" }
|
||||
else
|
||||
PartyMenu.ACTIONS = { "SHIFT", "SUMMARY", "CANCEL" }
|
||||
end
|
||||
PartyMenu._previousMode = prevMode
|
||||
PartyMenu.mode = "action"
|
||||
PartyMenu.actionCursor = 1
|
||||
end
|
||||
|
||||
local function party_print(text, px, py, maxW)
|
||||
FrlgFont.draw(tostring(text or ""), px, py, {
|
||||
maxWidth = maxW or 56,
|
||||
@@ -191,7 +308,7 @@ local function get_hp_bar_level(hp, maxHp, isEgg)
|
||||
end
|
||||
|
||||
local function idle_mon_offset(slotIndex)
|
||||
local spr = SLOT_SPRITES[slotIndex]
|
||||
local spr = slot_sprites(slotIndex)
|
||||
if spr and spr[1] == 16 then
|
||||
return 0, -4
|
||||
end
|
||||
@@ -262,7 +379,7 @@ local function ensure_slot_sprites(i, mon, selected)
|
||||
slot = {}
|
||||
PartyMenu._oam[i] = slot
|
||||
end
|
||||
local spr = SLOT_SPRITES[i]
|
||||
local spr = slot_sprites(i)
|
||||
if not spr or not mon then
|
||||
destroy_id(slot.mon); slot.mon = nil
|
||||
destroy_id(slot.ball); slot.ball = nil
|
||||
@@ -384,12 +501,100 @@ local function sync_all_oam()
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:6005
|
||||
function PartyMenu.battleOrder(st)
|
||||
local party = st and st.playerParty or {}
|
||||
local n = 0
|
||||
for i = 1, 6 do if party[i] then n = i end end
|
||||
local order = st._partyOrder
|
||||
local valid = type(order) == "table" and #order == n
|
||||
if valid then
|
||||
local seen = {}
|
||||
for i = 1, n do
|
||||
local v = order[i]
|
||||
if type(v) ~= "number" or v < 1 or v > n or seen[v] then valid = false break end
|
||||
seen[v] = true
|
||||
end
|
||||
end
|
||||
local b0 = (st.battlers and st.battlers[0]) or st.player
|
||||
local b2 = st.double and st.battlers and st.battlers[2] or nil
|
||||
if not valid then
|
||||
order = {}
|
||||
local used = {}
|
||||
for _, b in ipairs({ b0, b2 }) do
|
||||
local pi = b and tonumber(b.partyIndex)
|
||||
if pi and pi >= 1 and pi <= n and not used[pi] then
|
||||
order[#order + 1] = pi
|
||||
used[pi] = true
|
||||
end
|
||||
end
|
||||
for i = 1, n do
|
||||
if not used[i] then order[#order + 1] = i end
|
||||
end
|
||||
st._partyOrder = order
|
||||
end
|
||||
-- pokefirered/src/party_menu.c:5972
|
||||
for pos, b in ipairs({ b0, b2 }) do
|
||||
local want = b and tonumber(b.partyIndex)
|
||||
if want and order[pos] ~= want then
|
||||
for j = 1, n do
|
||||
if order[j] == want then
|
||||
order[pos], order[j] = order[j], order[pos]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return order
|
||||
end
|
||||
|
||||
-- pokefirered/src/party_menu.c:6199
|
||||
local function apply_battle_order(party, overlay, opts)
|
||||
local order = opts.battleOrder
|
||||
if order == nil then
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
local st = Battle and Battle._st
|
||||
if not (st and st.playerParty and st.playerParty == party) then return party, overlay, opts end
|
||||
order = PartyMenu.battleOrder(st)
|
||||
end
|
||||
local view = {}
|
||||
for i, pi in ipairs(order) do view[i] = party[pi] end
|
||||
local viewOverlay = overlay
|
||||
if type(overlay) == "table" then
|
||||
viewOverlay = {}
|
||||
for i, pi in ipairs(order) do viewOverlay[i] = overlay[pi] end
|
||||
end
|
||||
local o = {}
|
||||
for k, v in pairs(opts) do o[k] = v end
|
||||
local active = opts.activeSlot
|
||||
for i, pi in ipairs(order) do
|
||||
if pi == active then o.activeSlot = i break end
|
||||
end
|
||||
local onSelect, validate = opts.onSelect, opts.validate
|
||||
if onSelect then
|
||||
o.onSelect = function(d, mon) return onSelect(d and order[d], mon) end
|
||||
end
|
||||
if validate then
|
||||
o.validate = function(d) return validate(d and order[d]) end
|
||||
end
|
||||
PartyMenu._order = order
|
||||
return view, viewOverlay, o
|
||||
end
|
||||
|
||||
function PartyMenu.show(sessionParty, moveOverlay, opts)
|
||||
if type(moveOverlay) == "table" and opts == nil and (moveOverlay.mode or moveOverlay.session or moveOverlay.battle or moveOverlay.onSelect or moveOverlay.activeSlot) then
|
||||
opts = moveOverlay
|
||||
moveOverlay = nil
|
||||
end
|
||||
opts = opts or {}
|
||||
PartyMenu._order = nil
|
||||
if opts.mode == "battle_switch" or opts.mode == "battle_faint" or (opts.mode == "use" and opts.battleOrder) then
|
||||
local party0 = sessionParty or (opts.session and opts.session.party)
|
||||
local ov0 = moveOverlay or (opts.session and (opts.session.move_overlay or opts.session.moveOverlay))
|
||||
if party0 then
|
||||
sessionParty, moveOverlay, opts = apply_battle_order(party0, ov0, opts)
|
||||
end
|
||||
end
|
||||
destroy_party_oam()
|
||||
PartyMenu.open = true
|
||||
PartyMenu._party = sessionParty or (opts.session and opts.session.party)
|
||||
@@ -398,8 +603,9 @@ function PartyMenu.show(sessionParty, moveOverlay, opts)
|
||||
PartyMenu._bag = opts.bag or (opts.session and (opts.session.bag or opts.session.inventory))
|
||||
PartyMenu._item = opts.item
|
||||
PartyMenu._activeSlot = opts.activeSlot or 1
|
||||
PartyMenu._layout = (opts.layout == "double") and "double" or "single"
|
||||
PartyMenu._battle = opts.battle or (opts.mode == "battle_switch" or opts.mode == "battle_faint")
|
||||
PartyMenu.cursor = (opts.mode == "battle_switch" and PartyMenu._activeSlot == 1 and #(PartyMenu._party or {}) > 1) and 2 or 1
|
||||
PartyMenu.cursor = 1
|
||||
PartyMenu.mode = opts.mode or "list"
|
||||
PartyMenu._previousMode = PartyMenu.mode
|
||||
PartyMenu.summaryPage = 1
|
||||
@@ -743,7 +949,7 @@ function PartyMenu.handleInput(input)
|
||||
se(5)
|
||||
elseif input:wasPressed("a") then
|
||||
local act = actions[PartyMenu.actionCursor]
|
||||
if act == "SHIFT" or (PartyMenu._previousMode == "battle_switch" and act == "SWITCH") then
|
||||
if act == "SHIFT" or act == "SEND OUT" or (PartyMenu._previousMode == "battle_switch" and act == "SWITCH") then
|
||||
se(5)
|
||||
local cb = PartyMenu._onSelect
|
||||
local chosen = PartyMenu.cursor
|
||||
@@ -883,100 +1089,34 @@ function PartyMenu.handleInput(input)
|
||||
return
|
||||
end
|
||||
|
||||
-- Battle switch mode
|
||||
if PartyMenu.mode == "battle_switch" then
|
||||
local oldCur = PartyMenu.cursor
|
||||
if input:wasPressed("up") then
|
||||
PartyMenu.cursor = nav_up(PartyMenu.cursor, n)
|
||||
elseif input:wasPressed("down") then
|
||||
PartyMenu.cursor = nav_down(PartyMenu.cursor, n)
|
||||
elseif input:wasPressed("left") then
|
||||
PartyMenu.cursor, PartyMenu._lastSelectedSlot = nav_left(PartyMenu.cursor, n, PartyMenu._lastSelectedSlot)
|
||||
elseif input:wasPressed("right") then
|
||||
PartyMenu.cursor = nav_right(PartyMenu.cursor, n, PartyMenu._lastSelectedSlot)
|
||||
end
|
||||
if PartyMenu.cursor ~= oldCur then
|
||||
se(5)
|
||||
end
|
||||
if input:wasPressed("a") then
|
||||
if PartyMenu.cursor == 7 then
|
||||
se(9)
|
||||
PartyMenu.close()
|
||||
else
|
||||
local mon = PartyMenu._party and PartyMenu._party[PartyMenu.cursor]
|
||||
local activeSlot = PartyMenu._activeSlot or 1
|
||||
local hp = tonumber(mon and mon.hp) or 0
|
||||
local name = mon and Pokemon.displayName(mon) or "POKéMON"
|
||||
if PartyMenu.cursor == activeSlot then
|
||||
se(9)
|
||||
PartyMenu.showMessage(name .. " is already in\nbattle!", function()
|
||||
PartyMenu.mode = "battle_switch"
|
||||
end)
|
||||
elseif hp <= 0 then
|
||||
se(9)
|
||||
PartyMenu.showMessage("There's no will to\nfight!", function()
|
||||
PartyMenu.mode = "battle_switch"
|
||||
end)
|
||||
elseif mon and mon.isEgg then
|
||||
se(9)
|
||||
PartyMenu.showMessage("An EGG can't battle!", function()
|
||||
PartyMenu.mode = "battle_switch"
|
||||
end)
|
||||
else
|
||||
se(5)
|
||||
PartyMenu.ACTIONS = { "SHIFT", "SUMMARY", "CANCEL" }
|
||||
PartyMenu._previousMode = "battle_switch"
|
||||
PartyMenu.mode = "action"
|
||||
PartyMenu.actionCursor = 1
|
||||
end
|
||||
if PartyMenu.mode == "battle_switch" or PartyMenu.mode == "battle_faint" then
|
||||
local sendOut = PartyMenu.mode == "battle_faint"
|
||||
if is_double() then
|
||||
battle_nav_double(input)
|
||||
else
|
||||
local oldCur = PartyMenu.cursor
|
||||
if input:wasPressed("up") then
|
||||
PartyMenu.cursor = nav_up(PartyMenu.cursor, n)
|
||||
elseif input:wasPressed("down") then
|
||||
PartyMenu.cursor = nav_down(PartyMenu.cursor, n)
|
||||
elseif input:wasPressed("left") then
|
||||
PartyMenu.cursor, PartyMenu._lastSelectedSlot = nav_left(PartyMenu.cursor, n, PartyMenu._lastSelectedSlot)
|
||||
elseif input:wasPressed("right") then
|
||||
PartyMenu.cursor = nav_right(PartyMenu.cursor, n, PartyMenu._lastSelectedSlot)
|
||||
end
|
||||
elseif input:wasPressed("b") or input:wasPressed("start") then
|
||||
se(9)
|
||||
PartyMenu.close()
|
||||
if PartyMenu.cursor ~= oldCur then se(5) end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Battle faint forced replacement mode
|
||||
if PartyMenu.mode == "battle_faint" then
|
||||
if input:wasPressed("up") then
|
||||
PartyMenu.cursor = ((PartyMenu.cursor - 2) % n) + 1
|
||||
se(5)
|
||||
elseif input:wasPressed("down") then
|
||||
PartyMenu.cursor = (PartyMenu.cursor % n) + 1
|
||||
se(5)
|
||||
elseif input:wasPressed("a") then
|
||||
local mon = PartyMenu._party and PartyMenu._party[PartyMenu.cursor]
|
||||
local activeSlot = PartyMenu._activeSlot
|
||||
local hp = tonumber(mon and mon.hp) or 0
|
||||
local name = mon and Pokemon.displayName(mon) or "POKéMON"
|
||||
if activeSlot and PartyMenu.cursor == activeSlot and hp <= 0 then
|
||||
se(9)
|
||||
PartyMenu.showMessage(name .. " has no will\nto fight!", function()
|
||||
PartyMenu.mode = "battle_faint"
|
||||
end)
|
||||
elseif hp <= 0 then
|
||||
se(9)
|
||||
PartyMenu.showMessage("There's no will to\nfight!", function()
|
||||
PartyMenu.mode = "battle_faint"
|
||||
end)
|
||||
elseif mon and mon.isEgg then
|
||||
se(9)
|
||||
PartyMenu.showMessage("An EGG can't battle!", function()
|
||||
PartyMenu.mode = "battle_faint"
|
||||
end)
|
||||
local cancel = input:wasPressed("b") or (input:wasPressed("a") and PartyMenu.cursor == 7)
|
||||
if cancel then
|
||||
-- pokefirered/src/party_menu.c:1229
|
||||
if sendOut then
|
||||
se(26)
|
||||
else
|
||||
se(5)
|
||||
PartyMenu.ACTIONS = { "SHIFT", "SUMMARY", "CANCEL" }
|
||||
PartyMenu._previousMode = "battle_faint"
|
||||
PartyMenu.mode = "action"
|
||||
PartyMenu.actionCursor = 1
|
||||
PartyMenu.close()
|
||||
end
|
||||
elseif input:wasPressed("b") or input:wasPressed("start") then
|
||||
se(9)
|
||||
PartyMenu.showMessage("Choose a POKéMON.", function()
|
||||
PartyMenu.mode = "battle_faint"
|
||||
end)
|
||||
elseif input:wasPressed("a") then
|
||||
open_battle_actions_double(PartyMenu.mode)
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -984,7 +1124,10 @@ function PartyMenu.handleInput(input)
|
||||
-- Selection mode for item USE
|
||||
if PartyMenu.mode == "use" then
|
||||
local oldCur = PartyMenu.cursor
|
||||
if input:wasPressed("up") then
|
||||
if is_double() then
|
||||
battle_nav_double(input)
|
||||
oldCur = PartyMenu.cursor
|
||||
elseif input:wasPressed("up") then
|
||||
PartyMenu.cursor = nav_up(PartyMenu.cursor, n)
|
||||
elseif input:wasPressed("down") then
|
||||
PartyMenu.cursor = nav_down(PartyMenu.cursor, n)
|
||||
@@ -1124,6 +1267,7 @@ function PartyMenu.handleInput(input)
|
||||
local newMax = newStats.maxHp
|
||||
local newHp = math.min(newMax, oldHp + math.max(0, newMax - oldMax))
|
||||
mon.hp = newHp
|
||||
ItemUse.levelUpEvent(mon, mon.level)
|
||||
|
||||
pcall(function() require("src.core.game3.audio").playFanfare(257) end) -- MUS_LEVEL_UP (257)
|
||||
|
||||
@@ -1242,7 +1386,8 @@ function PartyMenu.handleInput(input)
|
||||
-- Case 4: General Medicine / Potions / Status
|
||||
local startHp = tonumber(mon and mon.hp) or 0
|
||||
local maxHp = tonumber(mon and (mon.maxHp or mon.maxhp)) or 1
|
||||
local ok, reason, msgText = ItemUse.useField(PartyMenu._session, PartyMenu._bag, PartyMenu._item, PartyMenu.cursor)
|
||||
local realSlot = (PartyMenu._order and PartyMenu._order[PartyMenu.cursor]) or PartyMenu.cursor
|
||||
local ok, reason, msgText = ItemUse.useField(PartyMenu._session, PartyMenu._bag, PartyMenu._item, realSlot)
|
||||
local endHp = tonumber(mon and mon.hp) or startHp
|
||||
if ok then
|
||||
se(2)
|
||||
@@ -1433,11 +1578,11 @@ end
|
||||
|
||||
-- BG + text only; OAM sprites flushed by Display.present.
|
||||
local function draw_filled_slot(i, mon, selected)
|
||||
local win = SLOT_WIN[i]
|
||||
local win = slot_win(i)
|
||||
if not win then return end
|
||||
local T = Display.TILE or 8
|
||||
local baseX, baseY = win.left * T, win.top * T
|
||||
local info = (i == 1) and INFO_LEFT or INFO_RIGHT
|
||||
local info = slot_info(i)
|
||||
|
||||
PartyChrome.drawSlot(win.kind, win.left, win.top, selected)
|
||||
|
||||
@@ -1471,23 +1616,27 @@ function PartyMenu.draw()
|
||||
if not PartyMenu.open then return end
|
||||
local party = PartyMenu._party or {}
|
||||
|
||||
PartyChrome.drawBg()
|
||||
sync_all_oam()
|
||||
|
||||
if PartyMenu.mode == "summary" then
|
||||
SummaryMenu.draw()
|
||||
if PartyMenu.mode == "summary" or SummaryMenu.isOpen() then
|
||||
destroy_party_oam()
|
||||
if PartyMenu.mode == "summary" then
|
||||
PartyChrome.drawBg()
|
||||
SummaryMenu.draw()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
PartyChrome.drawBg()
|
||||
sync_all_oam()
|
||||
|
||||
destroy_id(PartyMenu._summaryIcon)
|
||||
PartyMenu._summaryIcon = nil
|
||||
|
||||
for i = 1, 6 do
|
||||
local mon = party[i]
|
||||
local win = SLOT_WIN[i]
|
||||
local win = slot_win(i)
|
||||
if mon then
|
||||
draw_filled_slot(i, mon, i == PartyMenu.cursor or PartyMenu.switchFrom == i)
|
||||
elseif i > 1 and win then
|
||||
elseif i > 1 and win and win.kind ~= "main" then
|
||||
PartyChrome.drawSlot("empty", win.left, win.top, false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ local Window = require("src.ui.game3.window")
|
||||
local Chrome = require("src.ui.game3.chrome")
|
||||
local FrlgFont = require("src.ui.game3.frlg_font")
|
||||
local Strings = require("src.core.Strings")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
|
||||
local StartMenu = {}
|
||||
|
||||
@@ -58,6 +59,11 @@ function StartMenu.show(opts)
|
||||
StartMenu._game = opts.game
|
||||
StartMenu._onClose = opts.onClose
|
||||
StartMenu.ENTRIES = build_entries(opts.session)
|
||||
if ModRuntime.wantsHook("ui.start_menu.items") then
|
||||
local hooked = ModRuntime.call("ui.start_menu.items", function(_, items) return items end,
|
||||
opts.game, StartMenu.ENTRIES)
|
||||
if type(hooked) == "table" then StartMenu.ENTRIES = hooked end
|
||||
end
|
||||
local pos = tonumber(StartMenu.cursor) or 1
|
||||
if pos < 1 or pos > #StartMenu.ENTRIES then pos = 1 end -- pokefirered/src/menu.c:276
|
||||
StartMenu.cursor = pos -- pokefirered/src/start_menu.c:329
|
||||
@@ -119,7 +125,10 @@ function StartMenu.confirm()
|
||||
local e = StartMenu.ENTRIES[StartMenu.cursor]
|
||||
if not e then return end
|
||||
local session = StartMenu._session
|
||||
if e.id == "exit" then
|
||||
if type(e.onSelect) == "function" then
|
||||
local ok, err = pcall(e.onSelect, StartMenu._game, session)
|
||||
if not ok then print("[game3/start_menu] onSelect failed: " .. tostring(err)) end
|
||||
elseif e.id == "exit" then
|
||||
StartMenu._confirmExit = true
|
||||
StartMenu._confirmCursor = 2 -- Default to NO
|
||||
elseif e.id == "bag" then
|
||||
|
||||
@@ -231,7 +231,11 @@ end
|
||||
|
||||
function SummaryChrome.menuInfoImage()
|
||||
if SummaryChrome._menuInfo then return SummaryChrome._menuInfo end
|
||||
local img = load_png(summary_root() .. "/menu_info.png")
|
||||
local raw = read_bytes(summary_root() .. "/menu_info.rgba")
|
||||
local img = raw and rgba_to_image(raw, 128, 128)
|
||||
if not img then
|
||||
img = load_png(summary_root() .. "/menu_info.png")
|
||||
end
|
||||
if not img then
|
||||
img = load_png("src/import/gba/chrome/menus/menu_info.png")
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user