bing bang bong

This commit is contained in:
bryanthaboi
2026-09-18 15:42:32 -04:00
parent 96d23ab93b
commit bdfac727aa
116 changed files with 13941 additions and 1814 deletions
+1 -1
View File
@@ -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

+42 -4
View File
@@ -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"
+2
View File
@@ -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."
}
+3 -1
View File
@@ -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 },
}
+290
View File
@@ -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
View File
@@ -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
-530
View File
@@ -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** |
-60
View File
@@ -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`.
+62 -19
View File
@@ -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)
+48 -5
View File
@@ -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
View File
@@ -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
+29 -6
View File
@@ -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
+231
View File
@@ -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
+377
View File
@@ -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
View File
@@ -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
+230
View File
@@ -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
+13 -9
View File
@@ -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,
+59 -12
View File
@@ -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()
+15 -9
View File
@@ -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
+12 -8
View File
@@ -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)
+18 -8
View File
@@ -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
+4 -3
View File
@@ -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)
+1 -2
View File
@@ -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)
+18 -17
View File
@@ -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
+25 -14
View File
@@ -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
+2 -2
View File
@@ -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]
+24 -7
View File
@@ -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
View File
@@ -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()
+3 -1
View File
@@ -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
+4 -1
View File
@@ -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
+189 -68
View File
@@ -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
+5 -2
View File
@@ -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]
+12 -7
View File
@@ -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,
+66 -18
View File
@@ -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,
+168 -29
View File
@@ -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).
+43 -5
View File
@@ -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
+12 -2
View File
@@ -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
+90 -8
View File
@@ -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())
+41 -9
View File
@@ -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)
+17 -7
View File
@@ -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
+2 -1
View File
@@ -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
+9 -2
View File
@@ -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
+72 -7
View File
@@ -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,
+168 -17
View File
@@ -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
+3 -1
View File
@@ -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
File diff suppressed because it is too large Load Diff
+270 -84
View File
@@ -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
+136 -8
View File
@@ -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
+27
View File
@@ -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
+19
View File
@@ -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
+16 -1
View File
@@ -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
+87 -3
View File
@@ -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 })
+45 -10
View File
@@ -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
+239 -3
View File
@@ -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
+143 -30
View File
@@ -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
File diff suppressed because it is too large Load Diff
+61
View File
@@ -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,
+50 -1
View File
@@ -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
+65 -18
View File
@@ -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
+60 -7
View File
@@ -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]
+28 -2
View File
@@ -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")
+4
View File
@@ -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
+18
View File
@@ -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
View File
@@ -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
+25 -2
View File
@@ -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,
+17
View File
@@ -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
+59
View File
@@ -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
+10
View File
@@ -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
+3 -2
View File
@@ -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()
+18
View File
@@ -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
+6
View File
@@ -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)
+100 -4
View File
@@ -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
+2 -1
View File
@@ -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
+44 -1
View File
@@ -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,
+19 -3
View File
@@ -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]
+7
View File
@@ -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
+39 -2
View File
@@ -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
+29
View File
@@ -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,
+5 -1
View File
@@ -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",
+80 -1
View File
@@ -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 },
+10
View File
@@ -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",
+5
View File
@@ -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
+3
View File
@@ -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
+72 -4
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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()
+120 -4
View File
@@ -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
+2 -1
View File
@@ -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()
+36 -3
View File
@@ -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
View File
@@ -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
+10 -1
View File
@@ -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
+5 -1
View File
@@ -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