mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
aed3bbced7
- Battle Animation Pipeline:
* Eliminate 90-frame audio polling stall in anim_vm end/waitsound opcodes,
allowing attack animations to transition instantly into target hit flicker
and HP bar drain.
* Correct visual task counting in AnimVm:visualCount to match pret gAnimVisualTaskCount.
* Signal bg task completion on restorebg (vm.args[7] = -1).
* Fix termination conditions on shake tasks (ShakeMon, ShakeMon2, ShakeBattleTerrain),
WaterSport droplet arcs, and StartSinAnimTimer.
* Handle RGB555 color args in start_blend_anim_sprite_color and route P.destroyTask
to AnimTasks.destroy.
- RNG System & Seeding Parity:
* Replace math.random in Party.giveMon with Rng.Random32() for personality values
and Rng.Random() for IV bitfield extraction (HP/Atk/Def/Spe/SpA/SpD) matching GBA format.
* Seed RNG on Title Screen A/Start press via Rng.seedNewGame() (matching SeedRngAndSetTrainerId)
and perturb RNG state on title menu inputs.
* Route NPC overworld idle/wander timers and wild encounter slot rolls through Rng.compat.
- Pokédex Habitat & Area Maps:
* Fix encounter tables extraction and map indexing to resolve Pokemon habitat
locations across Kanto/Sevii maps rather than showing Area Unknown.
* Render steady semi-transparent red overlays for Pokédex area map locations.
- Multichoice Menus & UI:
* Implement multichoice table extraction and data population for Viridian blackboard
status ailments and special dialog interactions.
* Improve choice box borders, pagination, and Town Map wall viewing.
- Overworld Events & Flags:
* Fix Route 1 entrance girl NPC spawning and script triggers after starter selection.
* Ensure consistent save reload state in Oak's Lab.
2549 lines
80 KiB
Lua
2549 lines
80 KiB
Lua
-- Game3 owned battle engine entry.
|
|
-- Anim VM + hit sequencer pace presentation; effects/residuals owned.
|
|
|
|
local State = require("src.core.game3.battle.state")
|
|
local Adapter = require("src.core.game3.battle.adapter")
|
|
local Engine = require("src.core.game3.battle.engine")
|
|
local Ui = require("src.core.game3.battle.ui")
|
|
local Damage = require("src.core.game3.battle.damage")
|
|
local Commands = require("src.core.game3.battle.commands")
|
|
local Moves = require("src.core.game3.battle.moves")
|
|
local Anim = require("src.core.game3.battle.anim")
|
|
local AnimSeq = require("src.core.game3.battle.anim_seq")
|
|
local ExpSeq = require("src.core.game3.battle.exp_seq")
|
|
local EvoSeq = require("src.core.game3.battle.evo_seq")
|
|
local IntroSeq = require("src.core.game3.battle.intro_seq")
|
|
local CatchSeq = require("src.core.game3.battle.catch_seq")
|
|
local Experience = require("src.core.game3.battle.experience")
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local Evolution = require("src.core.game3.evolution")
|
|
local LearnMove = require("src.core.game3.battle.learn_move")
|
|
local Task = require("src.core.game3.task")
|
|
local Trainers = require("src.core.game3.scripting.trainers")
|
|
local SwitchSeq = require("src.core.game3.battle.switch_seq")
|
|
local ModRuntime = require("src.mods.Runtime")
|
|
|
|
local Battle = {}
|
|
|
|
Battle._active = false
|
|
Battle._st = nil
|
|
Battle._adapter = nil
|
|
Battle._phase = nil
|
|
Battle._actions = nil
|
|
Battle._actionI = 1
|
|
Battle._onDone = nil
|
|
Battle._pendingEnd = nil
|
|
Battle._auto = false
|
|
Battle._metaAct = nil
|
|
Battle._headless = false
|
|
Battle._fade = true
|
|
Battle._lowHpSong = false
|
|
Battle._residualEvents = nil
|
|
Battle._residualIndex = 1
|
|
Battle._residualStepState = nil
|
|
|
|
local D = {}
|
|
|
|
-- pokefirered/src/battle_interface.c:2168
|
|
local function hp_bar_red(hp, maxHp)
|
|
local ok, BattleChrome = pcall(require, "src.ui.game3.battle_chrome")
|
|
if not ok or not BattleChrome or not BattleChrome.hpBarLevel then return false end
|
|
return BattleChrome.hpBarLevel(hp, maxHp) == "red"
|
|
end
|
|
|
|
local function stop_low_hp_song()
|
|
if not Battle._lowHpSong then return end
|
|
Battle._lowHpSong = false
|
|
pcall(function()
|
|
local Audio = require("src.core.game3.audio")
|
|
local SE = require("src.core.game3.se_ids")
|
|
Audio.stopSe(SE.SE_LOW_HEALTH)
|
|
end)
|
|
end
|
|
|
|
--- pret HandleLowHpMusicChange / HandleBattleLowHpMusicChange
|
|
local function update_low_hp_music()
|
|
local st = Battle._st
|
|
local mon = st and st.player and st.player.mon
|
|
if not mon then
|
|
stop_low_hp_song()
|
|
return
|
|
end
|
|
if Anim.hpTweening and Anim.hpTweening() then return end
|
|
local pres = Anim.present("player")
|
|
local hp = math.floor(tonumber(pres and pres.displayHp) or tonumber(mon.hp) or 0)
|
|
local maxHp = math.floor(tonumber(pres and pres.displayMaxHp) or tonumber(mon.maxHp) or 0)
|
|
local red = hp_bar_red(hp, maxHp)
|
|
if red and not Battle._lowHpSong then
|
|
Battle._lowHpSong = true
|
|
pcall(function()
|
|
local Audio = require("src.core.game3.audio")
|
|
local SE = require("src.core.game3.se_ids")
|
|
Audio.playSe(SE.SE_LOW_HEALTH, { loop = true })
|
|
end)
|
|
elseif not red then
|
|
stop_low_hp_song()
|
|
end
|
|
end
|
|
|
|
local function party_menu_input(PartyMenu, input)
|
|
local SummaryMenu = package.loaded["src.ui.game3.summary_menu"]
|
|
if SummaryMenu and SummaryMenu.isOpen and SummaryMenu.isOpen() then
|
|
if SummaryMenu.update then SummaryMenu.update(1 / 60) end
|
|
SummaryMenu.handleInput(input)
|
|
return
|
|
end
|
|
PartyMenu.handleInput(input)
|
|
end
|
|
|
|
-- pokefirered/src/battle_script_commands.c:1108
|
|
local function seq_push(text, wait)
|
|
local t = tostring(text or "")
|
|
local used = t:find(" used\n", 1, true) or t:find(" used ", 1, true)
|
|
Ui.pushTimed(t, tonumber(wait) or (used and 0 or 64))
|
|
end
|
|
|
|
local function push_msgs(list)
|
|
for _, t in ipairs(list or {}) do
|
|
Ui.push(t)
|
|
end
|
|
end
|
|
|
|
local function foe_mon_from(foe)
|
|
if type(foe) ~= "table" then
|
|
return Damage.ensureStats({
|
|
species = 16, level = 3,
|
|
moves = { 33, 45 }, pp = { 35, 40 },
|
|
}, 3)
|
|
end
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local Rng = require("src.core.game3.rng")
|
|
local species = foe.species or foe.id or 16
|
|
local personality = foe.personality
|
|
if personality == nil then
|
|
-- pret GenerateWildMon → CreateMonWithNature uses Random stream;
|
|
-- Random32 matches Unown path; nature comes from personality % 25.
|
|
personality = Rng.Random32()
|
|
end
|
|
local gender = foe.gender
|
|
if gender ~= "M" and gender ~= "F" and gender ~= "U" then
|
|
gender = Pokemon.gender and Pokemon.gender(species, personality) or "U"
|
|
end
|
|
local ivs = foe.ivs
|
|
if ivs == nil then
|
|
local iv1 = Rng.Random()
|
|
local iv2 = Rng.Random()
|
|
ivs = {
|
|
hp = iv1 % 32,
|
|
atk = math.floor(iv1 / 32) % 32,
|
|
def = math.floor(iv1 / 1024) % 32,
|
|
spe = iv2 % 32,
|
|
spa = math.floor(iv2 / 32) % 32,
|
|
spd = math.floor(iv2 / 1024) % 32,
|
|
}
|
|
end
|
|
local item = foe.item
|
|
if item == nil then
|
|
local meta = Pokemon.speciesMeta and Pokemon.speciesMeta(species)
|
|
if meta then
|
|
local common = tonumber(meta.itemCommon) or 0
|
|
local rare = tonumber(meta.itemRare) or 0
|
|
if common ~= 0 or rare ~= 0 then
|
|
local r = Rng.Random() % 100
|
|
if common ~= 0 and rare ~= 0 then
|
|
if r < 50 then item = common
|
|
elseif r < 55 then item = rare end
|
|
elseif common ~= 0 then
|
|
if r < 50 then item = common end
|
|
elseif rare ~= 0 then
|
|
if r < 5 then item = rare end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local mon = {
|
|
species = species,
|
|
level = foe.level or 5,
|
|
hp = foe.hp,
|
|
maxHp = foe.maxHp,
|
|
moves = foe.moves or { 33 },
|
|
pp = foe.pp or { 35, 40, 0, 0 },
|
|
status = foe.status,
|
|
attack = foe.attack or foe.atk,
|
|
defense = foe.defense or foe.def,
|
|
spAtk = foe.spAtk or foe.spa,
|
|
spDef = foe.spDef or foe.spd,
|
|
speed = foe.speed or foe.spe,
|
|
item = item,
|
|
gender = gender,
|
|
ivs = ivs,
|
|
evs = foe.evs,
|
|
personality = personality,
|
|
nature = foe.nature or (Pokemon.natureId and Pokemon.natureId(personality)) or 0,
|
|
ability = foe.ability or foe.abilityId,
|
|
dvs = foe.dvs,
|
|
}
|
|
local needMoves = not mon.moves or #mon.moves == 0
|
|
if needMoves then
|
|
if Pokemon.movesAtLevel then
|
|
local moves, pp, maxPp = Pokemon.movesAtLevel(mon.species, mon.level)
|
|
if moves and #moves > 0 then
|
|
mon.moves = moves
|
|
mon.pp = pp
|
|
mon.maxPp = maxPp
|
|
end
|
|
end
|
|
end
|
|
return Damage.ensureStats(mon, mon.level)
|
|
end
|
|
|
|
local ITEM_SILPH_SCOPE = 359
|
|
|
|
-- pokefirered/src/battle_setup.c:220
|
|
local function ghost_battle(opts, st)
|
|
if opts.ghost ~= nil then return opts.ghost and true or false end
|
|
if type(opts.foe) == "table" and opts.foe.ghost ~= nil then return opts.foe.ghost and true or false end
|
|
if not st.wild then return false end
|
|
local Map = package.loaded["src.core.game3.map"]
|
|
local mapId = opts.mapId or (Map and Map.current)
|
|
if type(mapId) ~= "string" then return false end
|
|
local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog")
|
|
local tower = false
|
|
for f = 1, 7 do
|
|
local id = okC and MapCatalog.pretToEngine("PokemonTower_" .. f .. "F") or ("FR_POKEMON_TOWER_" .. f .. "F")
|
|
if mapId == id then tower = true break end
|
|
end
|
|
if not tower then return false end
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = opts.session or (Runtime and Runtime.getSession and Runtime.getSession())
|
|
local okB, Bag = pcall(require, "src.core.game3.bag")
|
|
if okB and session and session.bag and Bag.has and Bag.has(session.bag, ITEM_SILPH_SCOPE, 1) then return false end
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/battle_util.c:1709
|
|
local function overworld_weather()
|
|
local W = package.loaded["src.core.game3.weather"]
|
|
local id = W and tonumber(W.get and W.get() or W.current) or 0
|
|
if id == 3 or id == 5 or id == 13 then return "RAIN" end
|
|
if id == 8 then return "SAND" end
|
|
if id == 12 then return "SUN" end
|
|
return nil
|
|
end
|
|
|
|
local function action_view(act)
|
|
if type(act) ~= "table" then return nil end
|
|
local G3 = require("src.mods.Gen3Compat")
|
|
local num = act.kind == "move" and act.move ~= nil and Engine.moveNum(act.move) or nil
|
|
local name = num and G3.moveName(num) or nil
|
|
return {
|
|
kind = act.kind, id = name, move = name, moveNum = num, slot = act.slot,
|
|
index = act.kind == "switch" and act.slot or nil,
|
|
item = act.itemId and G3.itemName(act.itemId) or nil, itemId = act.itemId,
|
|
battler = act.battler, target = act.target,
|
|
}
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3532
|
|
local function open_turn(st, playerAct, enemyAct, chosen)
|
|
st._modTurnOpen = true
|
|
if not ModRuntime.wants("battle.turn_started") then return end
|
|
local actions
|
|
if chosen then
|
|
actions = {}
|
|
for id = 0, 3 do actions[id] = action_view(chosen[id]) end
|
|
end
|
|
ModRuntime.emit("battle.turn_started", {
|
|
battle = st, turn = st.turn, playerAction = action_view(playerAct),
|
|
enemyAction = action_view(enemyAct), actions = actions,
|
|
})
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:2953
|
|
local function close_turn(st)
|
|
if not (st and st._modTurnOpen) then return end
|
|
st._modTurnOpen = nil
|
|
if ModRuntime.wants("battle.turn_ended") then
|
|
ModRuntime.emit("battle.turn_ended", { battle = st, turn = st.turn })
|
|
end
|
|
end
|
|
|
|
function Battle.isActive()
|
|
return Battle._active == true
|
|
end
|
|
|
|
function Battle.getResult()
|
|
return Battle._st and Battle._st.result
|
|
end
|
|
|
|
function Battle.getState()
|
|
return Battle._st
|
|
end
|
|
|
|
local function finish(result)
|
|
if not Battle._active then return end
|
|
stop_low_hp_song()
|
|
Battle._active = false
|
|
Battle._phase = nil
|
|
Battle._residualEvents = nil
|
|
Battle._residualIndex = 1
|
|
Battle._residualStepState = nil
|
|
Battle._pendingChoice = nil
|
|
D.reset()
|
|
local st = Battle._st
|
|
close_turn(st)
|
|
if st then
|
|
st.over = true
|
|
st.result = result or st.result or "win"
|
|
local Runtime=package.loaded["src.core.game3.runtime"]
|
|
local session=Runtime and Runtime.getSession()
|
|
if session and not Battle._headless then
|
|
require("src.core.game3.quest_log_recorder").battle(session,st)
|
|
end
|
|
end
|
|
AnimSeq.reset()
|
|
CatchSeq.reset()
|
|
ExpSeq.reset()
|
|
EvoSeq.reset()
|
|
IntroSeq.reset()
|
|
LearnMove.reset()
|
|
SwitchSeq.reset()
|
|
Anim.reset({ headless = true })
|
|
local okM, Message = pcall(require, "src.ui.game3.message")
|
|
if okM and Message then
|
|
if Message.setFrame then Message.setFrame("dialogue") end
|
|
if Message.open and Message.close then Message.close() end
|
|
end
|
|
local Field = package.loaded["src.core.game3.field"]
|
|
if Field and Field.unlock then Field.unlock() end
|
|
if Battle._headless then
|
|
local okF, Fade = pcall(require, "src.ui.game3.fade")
|
|
if okF and Fade and Fade.clear then Fade.clear() end
|
|
end
|
|
-- Victory BGM starts in begin_win_award (while awards play). Map BGM is
|
|
-- restored by battle_bridge on exit — do not clobber victory here.
|
|
local cb = Battle._onDone
|
|
Battle._onDone = nil
|
|
if cb then cb(st and st.result or result or "win", st) end
|
|
end
|
|
|
|
function Battle.start(opts)
|
|
opts = opts or {}
|
|
if Battle._active then
|
|
return nil, "battle already active"
|
|
end
|
|
local playerParty = opts.playerParty or {}
|
|
if #playerParty == 0 then
|
|
return nil, "empty party"
|
|
end
|
|
local foeMon = foe_mon_from(opts.foe)
|
|
local foeParty = opts.foeParty
|
|
if not foeParty and opts.foe and opts.foe.party then
|
|
foeParty = {}
|
|
for _, fm in ipairs(opts.foe.party) do
|
|
foeParty[#foeParty + 1] = foe_mon_from(fm)
|
|
end
|
|
end
|
|
if foeParty and foeParty[1] and type(opts.foe) == "table" and opts.foe.species == nil and opts.foe.id == nil then
|
|
foeMon = foeParty[1]
|
|
end
|
|
Moves.loadRomPack(opts.cache)
|
|
local double = (opts.double == true) and not opts.wild
|
|
local st = State.new({
|
|
wild = opts.wild,
|
|
double = double or nil,
|
|
playerIndex = double and State.firstUsable(playerParty) or nil,
|
|
playerParty = playerParty,
|
|
foeMon = foeMon,
|
|
foeParty = foeParty,
|
|
rng = opts.rng,
|
|
})
|
|
D.reset()
|
|
st.ghostBattle = ghost_battle(opts, st)
|
|
if st.ghostBattle then
|
|
-- pokefirered/src/battle_setup.c:326
|
|
st.ghostUnveiled = (opts.ghostUnveiled or (type(opts.foe) == "table" and opts.foe.ghostUnveiled)) and true or nil
|
|
-- pokefirered/src/battle_setup.c:334
|
|
if st.enemy and st.enemy.mon then st.enemy.mon.nickname = "GHOST" end
|
|
end
|
|
Battle._headless = opts.headless and true or false
|
|
Battle._auto = (opts.autoFight == true) or (opts.headless and opts.autoFight ~= false)
|
|
Battle._fade = (opts.fade ~= false) and not Battle._headless
|
|
Ui.reset({ headless = opts.headless })
|
|
do
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = opts.session
|
|
or (Runtime and Runtime.getSession and Runtime.getSession())
|
|
Ui.bindState(st, session)
|
|
st.playerName = session and session.name or "PLAYER"
|
|
-- pokefirered/src/battle_main.c:2618
|
|
if session and session.dex and foeMon and (foeMon.species or foeMon.speciesId)
|
|
and not (st.ghostBattle and not st.ghostUnveiled) then
|
|
local Dex = require("src.core.game3.dex")
|
|
Dex.setSeen(session.dex, foeMon.species or foeMon.speciesId)
|
|
local b3 = st.double and not st.absent[3] and st.battlers[3]
|
|
if b3 and b3.mon then Dex.setSeen(session.dex, b3.mon.species or b3.mon.speciesId) end
|
|
end
|
|
end
|
|
Anim.reset({ headless = opts.headless, double = st.double })
|
|
AnimSeq.reset()
|
|
CatchSeq.reset()
|
|
ExpSeq.reset()
|
|
IntroSeq.reset()
|
|
SwitchSeq.reset()
|
|
-- Align party exp to ROM growth curves before battle display
|
|
for _, mon in ipairs(playerParty) do
|
|
Experience.syncExpToLevel(mon)
|
|
end
|
|
if foeMon then Experience.syncExpToLevel(foeMon) end
|
|
if foeParty then
|
|
for _, fm in ipairs(foeParty) do
|
|
Experience.syncExpToLevel(fm)
|
|
end
|
|
end
|
|
Anim.syncDisplayFromState(st)
|
|
Battle._st = st
|
|
Battle._adapter = Adapter.new(st, function(text) Ui.push(text) end)
|
|
Battle._onDone = opts.onDone
|
|
Battle._active = true
|
|
Battle._lowHpSong = false
|
|
Battle._phase = "intro"
|
|
Battle._actions = nil
|
|
Battle._actionI = 1
|
|
Battle._pendingEnd = nil
|
|
Battle._metaAct = nil
|
|
Battle._pendingChoice = nil
|
|
|
|
local BattleBg = require("src.core.game3.battle.bg")
|
|
local terrain = opts.terrain
|
|
if terrain == nil and opts.mapKind then
|
|
terrain = BattleBg.resolveFromMapKind(opts.mapKind)
|
|
end
|
|
if terrain == nil then
|
|
terrain = BattleBg.TERRAIN.BUILDING
|
|
end
|
|
BattleBg.setTerrain(terrain)
|
|
st.terrain = terrain
|
|
|
|
-- Trainer presentation identity
|
|
local trainerId = opts.trainerId
|
|
or (opts.foe and opts.foe.trainerId)
|
|
or (foeMon and foeMon.trainerId)
|
|
local rivalName = opts.rivalName
|
|
local playerGender = opts.playerGender or 0
|
|
local trainerInfo = nil
|
|
if trainerId and not st.wild then
|
|
trainerInfo = Trainers.info(trainerId, { rivalName = rivalName })
|
|
end
|
|
st.trainerId = trainerId
|
|
st.trainerClassName = trainerInfo and trainerInfo.className
|
|
st.trainerName = trainerInfo and trainerInfo.name
|
|
st.trainerPicId = (opts.trainerPicId)
|
|
or (trainerInfo and trainerInfo.pic)
|
|
st.trainerPartySize = trainerInfo and trainerInfo.partySize
|
|
st.defeatText = opts.defeatText
|
|
st.victoryText = opts.victoryText
|
|
-- pret gTrainers[].aiFlags / items[4] — drive battle AI scripts + item use.
|
|
st.aiFlags = opts.aiFlags
|
|
or (trainerInfo and trainerInfo.aiFlags)
|
|
or (st.wild and 0 or 1) -- wild: no scripts; fallback trainer: CHECK_BAD_MOVE
|
|
st.trainerItems = opts.trainerItems
|
|
or (trainerInfo and trainerInfo.items)
|
|
or { 0, 0, 0, 0 }
|
|
st.playerGender = playerGender
|
|
st.overworldWeather = opts.overworldWeather or overworld_weather()
|
|
do
|
|
-- pokefirered/src/battle_controllers.c:59
|
|
local okAi, Ai = pcall(require, "src.core.game3.battle.ai")
|
|
if okAi and Ai and Ai.battleStart then Ai.battleStart(st) end
|
|
end
|
|
|
|
-- Battle BGM (if not already playing from transition start)
|
|
do
|
|
local Audio = require("src.core.game3.audio")
|
|
local song = opts.song
|
|
if not song then
|
|
if st.wild then
|
|
local foeSpecies = foeMon and (foeMon.species or foeMon.speciesId or foeMon.id)
|
|
if foeSpecies == 150 then
|
|
song = Audio.role("battleMewtwo") or 340
|
|
elseif foeSpecies == 386 then
|
|
song = Audio.role("battleDeoxys") or 339
|
|
elseif foeSpecies == 144 or foeSpecies == 145 or foeSpecies == 146 or foeSpecies == 249 or foeSpecies == 250 then
|
|
song = Audio.role("battleLegend") or 341
|
|
else
|
|
song = Audio.role("battleWild") or 298
|
|
end
|
|
else
|
|
local classId = trainerInfo and trainerInfo.classId
|
|
if classId == 90 then
|
|
song = Audio.role("battleChampion") or 299
|
|
elseif classId == 84 or classId == 87 then
|
|
song = Audio.role("battleGymLeader") or 296
|
|
else
|
|
song = Audio.role("battleTrainer") or 297
|
|
end
|
|
end
|
|
end
|
|
if song then
|
|
Audio.playSong(song)
|
|
end
|
|
end
|
|
|
|
local Field = package.loaded["src.core.game3.field"]
|
|
if Field and Field.lock then Field.lock() end
|
|
|
|
local introOpts = {
|
|
pushMsg = function(text) Ui.push(text) end,
|
|
headless = opts.headless,
|
|
trainerId = trainerId,
|
|
trainerPicId = st.trainerPicId,
|
|
playerGender = playerGender,
|
|
rivalName = rivalName,
|
|
}
|
|
if not IntroSeq.begin(st, introOpts) then
|
|
-- Headless / skip: push FRLG strings for log parity.
|
|
local ename = State.displayName(st.enemy)
|
|
if st.ghostBattle then
|
|
for _, t in ipairs(IntroSeq.headlessGhostIntro(st)) do Ui.push(t) end
|
|
elseif st.wild then
|
|
Ui.push("Wild " .. ename .. " appeared!")
|
|
elseif st.double then
|
|
for _, t in ipairs(D.headlessIntro(st, trainerId, rivalName)) do Ui.push(t) end
|
|
else
|
|
local strings = Trainers.introStrings(trainerId, ename, { rivalName = rivalName })
|
|
Ui.push(strings.wants)
|
|
Ui.push(strings.sentOut)
|
|
end
|
|
if not st.double then
|
|
Ui.push("Go! " .. State.displayName(st.player) .. "!")
|
|
end
|
|
end
|
|
|
|
if opts.onStarted then opts.onStarted(st) end
|
|
|
|
if opts.headless and opts.autoFight ~= false then
|
|
Battle._auto = true
|
|
Battle.runToEnd()
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function end_if_over()
|
|
local st = Battle._st
|
|
if not st or not st.over then return false end
|
|
if not st.endReason and not AnimSeq.ended() then return false end
|
|
Battle._actions = {}
|
|
Battle._metaAct = nil
|
|
Battle._pendingEnd = st.result or "run"
|
|
Battle._phase = "ending"
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3682
|
|
local function focus_punch_prelude()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local list = st._focusPunchSetup
|
|
st._focusPunchSetup = nil
|
|
if not list then
|
|
list = {}
|
|
for _, a in ipairs(Battle._actions or {}) do
|
|
local u = a.user
|
|
if type(u) == "string" then u = st[u] end
|
|
local mv = a.move and Moves.get(a.move)
|
|
if u and mv and tonumber(mv.effect) == 170 and not u.expLockedMove and not ad:hasStatus(u, "SLP") then
|
|
list[#list + 1] = u
|
|
end
|
|
end
|
|
end
|
|
if #list == 0 then return nil end
|
|
table.sort(list, function(a, b) return (a.expTurnOrder or 9) < (b.expTurnOrder or 9) end)
|
|
local mark = ad:eventMark()
|
|
local prev = ad._say
|
|
ad._say = function() end
|
|
for _, b in ipairs(list) do
|
|
if not ad:isFainted(b) then
|
|
ad:playAnim("general", "FOCUS_PUNCH_SETUP", b, b)
|
|
ad:say(ad:displayName(b) .. " is tightening\nits focus!")
|
|
end
|
|
end
|
|
ad._say = prev
|
|
return ad:eventsSince(mark)
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:2856
|
|
local function begin_start_effects()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
if not (st and ad and Engine.battleStartEffects) or st._startEffectsDone then return false end
|
|
st._startEffectsDone = true
|
|
local mark = ad:eventMark()
|
|
local prev = ad._say
|
|
ad._say = function() end
|
|
local ok = pcall(Engine.battleStartEffects, st, ad)
|
|
ad._say = prev
|
|
if not ok then return false end
|
|
local evs = ad:eventsSince(mark)
|
|
if #evs == 0 then return false end
|
|
if Battle._headless then
|
|
for _, e in ipairs(evs) do
|
|
if e.kind == "msg" then Ui.push(e.text) end
|
|
end
|
|
Anim.syncDisplayFromState(st)
|
|
return false
|
|
end
|
|
AnimSeq.beginEvents(evs, seq_push)
|
|
Battle._phase = "startfx"
|
|
return true
|
|
end
|
|
|
|
local function begin_turn_with(playerAct)
|
|
local st = Battle._st
|
|
local ad = Battle._adapter
|
|
if st and st.double then return D.startSelection() end
|
|
st.turn = st.turn + 1
|
|
local enemyAct = Commands.enemyAction(st)
|
|
local actions, meta = Engine.planTurnFromActions(st, ad, playerAct, enemyAct)
|
|
open_turn(st, playerAct, enemyAct, nil)
|
|
Battle._actions = actions
|
|
Battle._actionI = 1
|
|
Battle._metaAct = meta
|
|
Battle._phase = "actions"
|
|
local evs = focus_punch_prelude()
|
|
if evs and #evs > 0 then
|
|
if Battle._headless then
|
|
for _, e in ipairs(evs) do
|
|
if e.kind == "msg" then Ui.push(e.text) end
|
|
end
|
|
else
|
|
AnimSeq.beginEvents(evs, seq_push)
|
|
Battle._phase = "preturn"
|
|
end
|
|
end
|
|
end
|
|
|
|
local function choice_hooks()
|
|
return {
|
|
pushMsg = function(text, cb) Ui.push(text, cb) end,
|
|
askYesNo = function(a, b) Ui.askYesNo(a, b) end,
|
|
askForget = function(labels, cb, ctx) Ui.askForget(labels, cb, ctx) end,
|
|
headless = Battle._headless,
|
|
battleText = true,
|
|
}
|
|
end
|
|
Battle._choiceHooksForTests = choice_hooks
|
|
|
|
local function begin_evo_or_end()
|
|
local st = Battle._st
|
|
Battle._pendingEnd = Battle._pendingEnd or "win"
|
|
if Battle._pendingEnd ~= "win" or not st then
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
local leveled = Battle._leveledUp or ExpSeq.leveledSet()
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = (Runtime and Runtime.getSession and Runtime.getSession()) or (Battle._st and Battle._st.session)
|
|
local pending = Evolution.pending(st.playerParty, leveled, session)
|
|
if Battle._headless then
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
for _, entry in ipairs(pending) do
|
|
local fromName = Pokemon.displayMonName(entry.mon)
|
|
local intoName = Pokemon.name(entry.toSpecies) or "?"
|
|
Ui.push("What?\n" .. fromName .. " is evolving!")
|
|
Evolution.apply(entry.mon, entry.toSpecies, session)
|
|
Ui.push("Congratulations! Your " .. fromName
|
|
.. "\nevolved into " .. intoName .. "!")
|
|
end
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
local hooks = choice_hooks()
|
|
hooks.session = session
|
|
local started = EvoSeq.begin(pending, hooks)
|
|
if started then
|
|
Battle._phase = "evolving"
|
|
else
|
|
Battle._phase = "ending"
|
|
end
|
|
end
|
|
|
|
local function send_out_enemy_next(nextEnemyIdx)
|
|
local st = Battle._st
|
|
if not st then return end
|
|
local pushFn = function(text) Ui.push(text) end
|
|
local onDone = function()
|
|
Battle._phase = "command"
|
|
if Battle._auto then
|
|
begin_turn_with(Commands.playerAction(st, 1, 1))
|
|
else
|
|
Ui.openMenu()
|
|
end
|
|
end
|
|
if Battle._headless or Battle._auto then
|
|
SwitchSeq.beginSendOut(st, "enemy", nextEnemyIdx, {
|
|
headless = true,
|
|
pushMsg = pushFn,
|
|
onDone = onDone,
|
|
})
|
|
else
|
|
SwitchSeq.beginSendOut(st, "enemy", nextEnemyIdx, {
|
|
headless = false,
|
|
pushMsg = pushFn,
|
|
onDone = onDone,
|
|
})
|
|
Battle._phase = "switching"
|
|
end
|
|
end
|
|
|
|
local function handle_player_faint(opts)
|
|
opts = opts or {}
|
|
local st = Battle._st
|
|
if not st then return end
|
|
if st.player and st.playerParty then
|
|
State.syncBattlerToParty(st.player, st.playerParty)
|
|
end
|
|
local hasLiving = Engine.hasLivingMons(st.playerParty)
|
|
if not hasLiving then
|
|
Battle._pendingEnd = "lose"
|
|
Battle._phase = "ending"
|
|
Ui.push("You have no more\nPOKéMON left!")
|
|
Ui.push(string.format("%s blacked out!", (st.playerName or "PLAYER")))
|
|
return
|
|
end
|
|
|
|
if Battle._headless or Battle._auto then
|
|
local nextI = Engine.nextLivingMonIndex(st.playerParty, st.player and st.player.partyIndex) or 1
|
|
SwitchSeq.beginSendOut(st, "player", nextI, {
|
|
headless = true,
|
|
pushMsg = function(t) Ui.push(t) end,
|
|
onDone = function()
|
|
Battle._phase = "command"
|
|
if Battle._auto then
|
|
begin_turn_with(Commands.playerAction(st, 1, 1))
|
|
else
|
|
Ui.openMenu()
|
|
end
|
|
end,
|
|
})
|
|
return
|
|
end
|
|
|
|
local PartyMenu = require("src.ui.game3.party_menu")
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
State.syncBattlerToParty(st.player, st.playerParty)
|
|
if Ui.clearLinger then Ui.clearLinger() end
|
|
Battle._phase = "switching"
|
|
PartyMenu.show(st.playerParty or (session and session.party), session and session.move_overlay, {
|
|
mode = "battle_faint",
|
|
session = session,
|
|
activeSlot = st.player.partyIndex,
|
|
battle = true,
|
|
validate = function(slot) return Commands.switchError(st, slot, true) end,
|
|
onSelect = function(slot)
|
|
SwitchSeq.beginSendOut(st, "player", slot, {
|
|
headless = false,
|
|
pushMsg = function(t) Ui.push(t) end,
|
|
onDone = function()
|
|
Battle._phase = "command"
|
|
Ui.openMenu()
|
|
end,
|
|
})
|
|
Battle._phase = "switching"
|
|
end,
|
|
})
|
|
end
|
|
|
|
local function begin_trainer_win(st)
|
|
Battle._pendingEnd = "win"
|
|
local Audio = require("src.core.game3.audio")
|
|
local role = (st and st.wild) and "victoryWild" or "victoryTrainer"
|
|
local song = Audio.role(role) or ((st and st.wild) and 311 or 310)
|
|
Audio.playSong(song)
|
|
|
|
local pname = st.playerName or "PLAYER"
|
|
local function push_defeated()
|
|
local trName = (st.trainerClassName and st.trainerClassName ~= "")
|
|
and (st.trainerClassName .. " " .. (st.trainerName or ""))
|
|
or (st.trainerName or "TRAINER")
|
|
-- pokefirered/data/battle_scripts_1.s:2912
|
|
Ui.push(string.format("%s defeated\n%s!", pname, trName))
|
|
end
|
|
local function push_lose_text_and_money()
|
|
local Trainers = require("src.core.game3.scripting.trainers")
|
|
local dialogs = Trainers.dialogs(st.trainerId)
|
|
local defeatSpeech = st.defeatText or (dialogs and dialogs.defeat)
|
|
-- pokefirered/data/battle_scripts_1.s:2915
|
|
if defeatSpeech and defeatSpeech ~= "" then
|
|
Ui.push(defeatSpeech)
|
|
end
|
|
local Prize = require("src.core.game3.battle.prize")
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
if session then
|
|
local info = Trainers.info(st.trainerId)
|
|
local lastLevel = info and tonumber(info.lastLevel)
|
|
if not lastLevel or lastLevel < 1 then
|
|
lastLevel = st.enemy and st.enemy.mon and tonumber(st.enemy.mon.level) or 1
|
|
end
|
|
local gained = Prize.awardTrainerWin(session, st.trainerId, {
|
|
lastLevel = lastLevel,
|
|
double = st.double or false,
|
|
moneyMultiplier = st.moneyMultiplier or 1,
|
|
})
|
|
if gained > 0 then
|
|
Ui.push(Prize.moneyMessage(session.name or pname, gained))
|
|
end
|
|
end
|
|
end
|
|
|
|
local function give_payday_money_and_pickup()
|
|
local Prize = require("src.core.game3.battle.prize")
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = (Runtime and Runtime.getSession and Runtime.getSession()) or st.session
|
|
-- pokefirered/data/battle_scripts_1.s:2920
|
|
local bonus = Prize.payDay(session, st.payDayCoins, { moneyMultiplier = st.moneyMultiplier or 1 })
|
|
if bonus > 0 then
|
|
Ui.push(Prize.payDayMessage((session and session.name) or pname, bonus))
|
|
end
|
|
Prize.pickup((session and session.party) or st.playerParty)
|
|
end
|
|
|
|
if not st.wild and st.trainerId and not Battle._headless then
|
|
push_defeated()
|
|
SwitchSeq.beginTrainerSlideIn(st, {
|
|
headless = false,
|
|
onDone = function()
|
|
push_lose_text_and_money()
|
|
give_payday_money_and_pickup()
|
|
begin_evo_or_end()
|
|
end,
|
|
})
|
|
Battle._phase = "switching"
|
|
else
|
|
if not st.wild and st.trainerId then
|
|
push_defeated()
|
|
push_lose_text_and_money()
|
|
end
|
|
-- pokefirered/src/battle_main.c:3764
|
|
give_payday_money_and_pickup()
|
|
begin_evo_or_end()
|
|
end
|
|
end
|
|
|
|
local function push_awards_headless(awards)
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
for _, entry in ipairs(awards or {}) do
|
|
local r = entry.result or {}
|
|
if (r.gained or 0) > 0 then
|
|
local name = entry.battler and State.displayName(entry.battler)
|
|
or Pokemon.displayMonName(entry.mon)
|
|
Ui.push(name .. " gained" .. (entry.boosted and " a boosted" or "") .. "\n" .. tostring(r.gained) .. " EXP. Points!")
|
|
for _, lv in ipairs(r.levels or {}) do
|
|
Ui.push(name .. " grew to\nLV. " .. tostring(lv) .. "!")
|
|
Battle._leveledUp[entry.partyIndex or 1] = true
|
|
end
|
|
for _, lv in ipairs(r.levels or {}) do
|
|
for _, mv in ipairs(Pokemon.movesLearnedAt(
|
|
tonumber(entry.mon and entry.mon.species), lv)) do
|
|
if Pokemon.teachMove(entry.mon, mv) then
|
|
Ui.push(name .. " learned\n" .. Pokemon.moveName(mv) .. "!")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function handle_enemy_faint(opts)
|
|
opts = opts or {}
|
|
local st = Battle._st
|
|
if not st then return end
|
|
stop_low_hp_song()
|
|
if st.enemy and st.foeParty then
|
|
State.syncBattlerToParty(st.enemy, st.foeParty)
|
|
end
|
|
local awards = {}
|
|
if st and st.enemy then
|
|
local partIndices = {}
|
|
if st.enemy.participants then
|
|
for pi, _ in pairs(st.enemy.participants) do
|
|
partIndices[#partIndices + 1] = pi
|
|
end
|
|
table.sort(partIndices)
|
|
end
|
|
awards = Experience.awardFoe(st, st.enemy, {
|
|
trainer = not st.wild,
|
|
partyIndices = (#partIndices > 0) and partIndices or nil,
|
|
})
|
|
end
|
|
Battle._leveledUp = {}
|
|
local nextEnemyIdx = (not st.wild) and Engine.nextLivingMonIndex(st.foeParty, st.enemy and st.enemy.partyIndex)
|
|
if nextEnemyIdx then
|
|
-- pokefirered/src/battle_controller_opponent.c:1416
|
|
local okS, pick = pcall(Engine.mostSuitableMon, st, Battle._adapter, "enemy")
|
|
if okS and pick and st.foeParty[pick] and (tonumber(st.foeParty[pick].hp) or 0) > 0 then
|
|
nextEnemyIdx = pick
|
|
end
|
|
end
|
|
|
|
local onAwardsFinished = function()
|
|
if not nextEnemyIdx then
|
|
begin_trainer_win(st)
|
|
else
|
|
if opts.onFinished then
|
|
send_out_enemy_next(nextEnemyIdx)
|
|
opts.onFinished()
|
|
return
|
|
end
|
|
local okO, Options = pcall(require, "src.core.game3.options")
|
|
local okR, Runtime = pcall(require, "src.core.game3.runtime")
|
|
local session = okR and Runtime.getSession and Runtime.getSession()
|
|
local battleStyle = (okO and session and Options.battleStyle and Options.battleStyle(session)) or "shift"
|
|
if Battle._headless or Battle._auto or opts.mutual or battleStyle == "set" or State.isFainted(st.player) then
|
|
send_out_enemy_next(nextEnemyIdx)
|
|
else
|
|
local nextMon = st.foeParty[nextEnemyIdx]
|
|
local nextSp = nextMon and (nextMon.species or nextMon.speciesId)
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local nextName = Pokemon.name(nextSp)
|
|
local trName = (st.trainerClassName and st.trainerClassName ~= "")
|
|
and (st.trainerClassName .. " " .. (st.trainerName or ""))
|
|
or (st.trainerName or "TRAINER")
|
|
Ui.push(string.format("%s is\nabout to use %s.\\pWill %s change\nPOKéMON?", trName, nextName, (st.playerName or "PLAYER")))
|
|
Battle._shiftEnemyIdx = nextEnemyIdx
|
|
Battle._shiftAsked = false
|
|
Battle._phase = "shift_prompt"
|
|
end
|
|
end
|
|
end
|
|
|
|
local hooks = choice_hooks()
|
|
if Battle._headless then
|
|
push_awards_headless(awards)
|
|
onAwardsFinished()
|
|
return
|
|
end
|
|
|
|
local started = ExpSeq.begin(awards, hooks.pushMsg, nil, hooks)
|
|
if started then
|
|
Battle._leveledUp = ExpSeq.leveledSet() or {}
|
|
Battle._onExpDone = onAwardsFinished
|
|
Battle._phase = "awarding"
|
|
else
|
|
onAwardsFinished()
|
|
end
|
|
end
|
|
|
|
local function check_faints_and_end()
|
|
local st = Battle._st
|
|
local ad = Battle._adapter
|
|
if not st then return false end
|
|
|
|
local pFainted = State.isFainted(st.player)
|
|
local eFainted = State.isFainted(st.enemy)
|
|
|
|
if pFainted and eFainted then
|
|
local playerHasLiving = Engine.hasLivingMons(st.playerParty)
|
|
local enemyHasLiving = Engine.hasLivingMons(st.foeParty)
|
|
if not playerHasLiving then
|
|
handle_player_faint()
|
|
return true
|
|
elseif not enemyHasLiving then
|
|
handle_enemy_faint({ mutual = true })
|
|
return true
|
|
else
|
|
handle_enemy_faint({ mutual = true, onFinished = function()
|
|
handle_player_faint()
|
|
end })
|
|
return true
|
|
end
|
|
elseif eFainted then
|
|
handle_enemy_faint()
|
|
return true
|
|
elseif pFainted then
|
|
handle_player_faint()
|
|
return true
|
|
end
|
|
|
|
local endResult = Engine.checkEnd(st, ad)
|
|
if endResult == "win" then
|
|
handle_enemy_faint()
|
|
return true
|
|
elseif endResult == "lose" then
|
|
handle_player_faint()
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function begin_win_award()
|
|
handle_enemy_faint()
|
|
end
|
|
|
|
local function after_actions()
|
|
local st = Battle._st
|
|
local ad = Battle._adapter
|
|
if st and st.double then return D.afterActions() end
|
|
if end_if_over() then return end
|
|
local events = Engine.collectResidualEvents(st, ad)
|
|
close_turn(st)
|
|
Battle._residualEvents = events
|
|
Battle._residualIndex = 1
|
|
Battle._residualStepState = "start"
|
|
|
|
if Battle._headless then
|
|
for _, evt in ipairs(events or {}) do
|
|
push_msgs(evt.msgs)
|
|
end
|
|
Ui.pump()
|
|
if check_faints_and_end() then
|
|
return
|
|
end
|
|
Battle._phase = "command"
|
|
if Battle._auto then
|
|
begin_turn_with(Commands.playerAction(Battle._st, 1, 1))
|
|
end
|
|
return
|
|
end
|
|
|
|
local stream = {}
|
|
for _, evt in ipairs(events or {}) do
|
|
for _, e in ipairs(evt.events or {}) do stream[#stream + 1] = e end
|
|
end
|
|
AnimSeq.beginEvents(stream, seq_push)
|
|
Battle._phase = "residuals"
|
|
end
|
|
|
|
local function step_action()
|
|
local st = Battle._st
|
|
local ad = Battle._adapter
|
|
if st and st.double then return D.stepAction() end
|
|
if end_if_over() then return end
|
|
|
|
if Battle._metaAct then
|
|
local meta = Battle._metaAct
|
|
Battle._metaAct = nil
|
|
if meta.kind == "run" then
|
|
local mark = ad:eventMark()
|
|
local prev = ad._say
|
|
ad._say = function() end
|
|
local fled = Commands.tryFlee(st, ad)
|
|
ad._say = prev
|
|
local evs = ad:eventsSince(mark)
|
|
if fled then
|
|
st.over = true
|
|
st.result = "run"
|
|
st.endReason = "flee"
|
|
end
|
|
-- pokefirered/src/battle_message.c:320
|
|
local function flee_push(text)
|
|
if fled then
|
|
pcall(function()
|
|
require("src.core.game3.audio").playSe(require("src.core.game3.se_ids").SE_FLEE)
|
|
end)
|
|
end
|
|
seq_push(text)
|
|
end
|
|
if Battle._headless then
|
|
for _, e in ipairs(evs) do
|
|
if e.kind == "msg" then Ui.push(e.text) end
|
|
end
|
|
if fled then
|
|
Battle._pendingEnd = "run"
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
else
|
|
AnimSeq.beginEvents(evs, flee_push)
|
|
Battle._phase = "animating"
|
|
return
|
|
end
|
|
elseif meta.kind == "bag" then
|
|
local Catching = require("src.core.game3.battle.catching")
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
local bag = session and session.bag
|
|
|
|
if Catching.isBall(meta.itemId) and st.ghostBattle then
|
|
-- pokefirered/src/battle_script_commands.c:9473
|
|
local Bag = require("src.core.game3.bag")
|
|
if bag and Bag.has(bag, meta.itemId, 1) then Bag.remove(bag, meta.itemId, 1) end
|
|
local pushFn = function(text, wait)
|
|
if wait then Ui.pushTimed(text, wait) else Ui.push(text) end
|
|
end
|
|
local headless = Battle._headless or (Ui and Ui._headless)
|
|
CatchSeq.begin(st, meta.itemId, false, 0, {
|
|
pushMsg = pushFn, headless = headless, session = session, ghostDodge = true,
|
|
})
|
|
if not headless then
|
|
Battle._phase = "catching"
|
|
return
|
|
end
|
|
elseif Catching.isBall(meta.itemId) then
|
|
if not st.wild then
|
|
Ui.push("The TRAINER blocked\nthe BALL!")
|
|
Battle._actions = {}
|
|
Battle._phase = "command"
|
|
Ui.openMenu()
|
|
return
|
|
end
|
|
local Bag = require("src.core.game3.bag")
|
|
if not bag or not Bag.has(bag, meta.itemId, 1) then
|
|
Ui.push("You don't have that item.")
|
|
Battle._actions = {}
|
|
Battle._phase = "command"
|
|
Ui.openMenu()
|
|
return
|
|
end
|
|
Bag.remove(bag, meta.itemId, 1)
|
|
local rng = ad and ad.rng and ad:rng() or st.rng
|
|
local caught, shakes = Catching.tryCatch(meta.itemId, st.enemy, st, session, rng)
|
|
local pushFn = function(text) Ui.push(text) end
|
|
if Battle._headless or (Ui and Ui._headless) then
|
|
CatchSeq.begin(st, meta.itemId, caught, shakes, {
|
|
pushMsg = pushFn,
|
|
headless = true,
|
|
session = session,
|
|
})
|
|
if caught then
|
|
Battle._actions = {}
|
|
st.over = true
|
|
st.result = "catch"
|
|
Battle._pendingEnd = "catch"
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
else
|
|
CatchSeq.begin(st, meta.itemId, caught, shakes, {
|
|
pushMsg = pushFn,
|
|
headless = false,
|
|
session = session,
|
|
})
|
|
Battle._phase = "catching"
|
|
return
|
|
end
|
|
else
|
|
local BattleItems = require("src.core.game3.battle.items")
|
|
local result, _msgs, endsTurn, endsBattle = BattleItems.use(
|
|
st, ad, bag, session, meta.itemId, meta.partySlot)
|
|
if endsBattle then
|
|
Battle._actions = {}
|
|
if result == "catch" then
|
|
st.over = true
|
|
st.result = "catch"
|
|
Battle._pendingEnd = "catch"
|
|
else
|
|
st.over = true
|
|
st.result = "run"
|
|
Battle._pendingEnd = "run"
|
|
end
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
if not endsTurn or result == "error" then
|
|
Battle._actions = {}
|
|
Battle._phase = "command"
|
|
Ui.openMenu()
|
|
return
|
|
end
|
|
if result == "heal" and st.player and st.player.partyIndex == meta.partySlot then
|
|
local p = Anim.present("player")
|
|
local logical = tonumber(st.player.mon and st.player.mon.hp) or 0
|
|
if p and p.displayHp ~= nil and math.abs(logical - p.displayHp) >= 1 then
|
|
Anim.tweenHp("player", p.displayHp, logical, st.player.mon.maxHp)
|
|
end
|
|
end
|
|
end
|
|
elseif meta.kind == "switch" then
|
|
-- Pursuit interrupt check
|
|
local enemyAct = nil
|
|
local enemyActIdx = nil
|
|
for idx, a in ipairs(Battle._actions or {}) do
|
|
if a.user == "enemy" and a.kind == "move" then
|
|
enemyAct = a
|
|
enemyActIdx = idx
|
|
break
|
|
end
|
|
end
|
|
if enemyAct and Engine.isPursuit(enemyAct.move) and not State.isFainted(st.enemy) then
|
|
table.remove(Battle._actions, enemyActIdx)
|
|
local out = {}
|
|
Engine.resolveMove(enemyAct.user, st.player, enemyAct.move, enemyAct.slot, ad, st, out, { pursuitSwitch = true })
|
|
local animMeta = out._anim
|
|
if Battle._headless or not animMeta then
|
|
push_msgs(out)
|
|
if State.isFainted(st.player) then
|
|
Battle._actions = {}
|
|
handle_player_faint()
|
|
return
|
|
end
|
|
else
|
|
AnimSeq.begin(animMeta, seq_push)
|
|
Battle._phase = "animating"
|
|
return
|
|
end
|
|
end
|
|
|
|
local newSlot = meta.slot or 1
|
|
local pushFn = function(text) Ui.push(text) end
|
|
local onDone = function()
|
|
Battle._phase = "actions"
|
|
if not Battle._actions or not Battle._actions[Battle._actionI] then
|
|
after_actions()
|
|
end
|
|
end
|
|
if Battle._headless then
|
|
SwitchSeq.beginPlayerSwitch(st, newSlot, {
|
|
headless = true,
|
|
pushMsg = pushFn,
|
|
onDone = onDone,
|
|
})
|
|
-- Fall through to enemy action list
|
|
else
|
|
SwitchSeq.beginPlayerSwitch(st, newSlot, {
|
|
headless = false,
|
|
pushMsg = pushFn,
|
|
onDone = onDone,
|
|
})
|
|
Battle._phase = "switching"
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
local act = Battle._actions and Battle._actions[Battle._actionI]
|
|
if not act then
|
|
after_actions()
|
|
return
|
|
end
|
|
Battle._actionI = Battle._actionI + 1
|
|
|
|
if act.meta then
|
|
if not Battle._actions[Battle._actionI] then after_actions() end
|
|
return
|
|
end
|
|
if act.kind == "switch" and act.battler == 1 then return D.singleEnemySwitch(act) end
|
|
if act.kind == "item" then return D.enemyItem(act) end
|
|
|
|
local uBattler = (type(act.user) == "string" and st and st[act.user]) or act.user
|
|
local tBattler = (type(act.target) == "string" and st and st[act.target]) or act.target
|
|
if uBattler and uBattler.side and st and st[uBattler.side] then uBattler = st[uBattler.side] end
|
|
if tBattler and tBattler.side and st and st[tBattler.side] then tBattler = st[tBattler.side] end
|
|
|
|
if State.isFainted(uBattler) or State.isFainted(tBattler) then
|
|
if not Battle._actions[Battle._actionI] then after_actions() end
|
|
return
|
|
end
|
|
|
|
local out = {}
|
|
st.interactiveChoices = not (Battle._headless or Battle._auto)
|
|
Engine.resolveMove(act.user, act.target, act.move, act.slot, ad, st, out)
|
|
st.interactiveChoices = nil
|
|
Battle._pendingChoice = out.pendingChoice
|
|
local animMeta = out._anim
|
|
if Battle._headless or not animMeta then
|
|
push_msgs(out)
|
|
if end_if_over() then return end
|
|
else
|
|
AnimSeq.begin(animMeta, seq_push)
|
|
Battle._phase = "animating"
|
|
return
|
|
end
|
|
|
|
if check_faints_and_end() then
|
|
return
|
|
end
|
|
if not Battle._actions[Battle._actionI] then
|
|
after_actions()
|
|
end
|
|
end
|
|
|
|
-- pokefirered/src/battle_script_commands.c:4626
|
|
local function open_pending_choice()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local req = Battle._pendingChoice
|
|
Battle._pendingChoice = nil
|
|
local pc = st.pendingChoice
|
|
local uid = (pc and pc.M and pc.M.user and pc.M.user.id) or 0
|
|
local function resume(slot)
|
|
st.interactiveChoices = true
|
|
local out = Engine.resumeChoice(st, ad, slot)
|
|
st.interactiveChoices = nil
|
|
Battle._pendingChoice = out and out.pendingChoice
|
|
if out and out._anim and not Battle._headless then
|
|
AnimSeq.begin(out._anim, seq_push)
|
|
Battle._phase = "animating"
|
|
else
|
|
for _, t in ipairs(out or {}) do Ui.push(t) end
|
|
Battle._phase = "actions"
|
|
if st.double and not Battle._pendingChoice then D.afterEach() end
|
|
end
|
|
end
|
|
if Battle._headless or Battle._auto or not (req and req.kind == "baton_pass") then
|
|
resume(req and req.candidates and req.candidates[1])
|
|
return
|
|
end
|
|
local PartyMenu = require("src.ui.game3.party_menu")
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
State.syncBattlerToParty(st.player, st.playerParty)
|
|
if Ui.clearLinger then Ui.clearLinger() end
|
|
Battle._phase = "switching"
|
|
if st.double and Ui.openPartyMenu then
|
|
return Ui.openPartyMenu(st, uid, {
|
|
forced = true,
|
|
validate = function(slot) return Commands.switchError(st, slot, true, uid) end,
|
|
onSelect = function(slot) resume(slot) end,
|
|
})
|
|
end
|
|
PartyMenu.show(st.playerParty or (session and session.party), session and session.move_overlay, {
|
|
mode = "battle_faint",
|
|
session = session,
|
|
activeSlot = st.player.partyIndex,
|
|
activeSlots = st.double and D.activeSlots(st) or nil,
|
|
battlerId = st.double and uid or nil,
|
|
battle = true,
|
|
validate = function(slot) return Commands.switchError(st, slot, true, st.double and uid or nil) end,
|
|
onSelect = function(slot) resume(slot) end,
|
|
})
|
|
end
|
|
Battle._openPendingChoiceForTests = open_pending_choice
|
|
|
|
local function after_anim_sequence()
|
|
if Battle._pendingChoice then
|
|
open_pending_choice()
|
|
return
|
|
end
|
|
if Battle._singleAfterAnim then
|
|
local scont = Battle._singleAfterAnim
|
|
Battle._singleAfterAnim = nil
|
|
if end_if_over() then return end
|
|
return scont()
|
|
end
|
|
if Battle._st and Battle._st.double then
|
|
local cont = Battle._dblAfterAnim
|
|
Battle._dblAfterAnim = nil
|
|
if cont then return cont() end
|
|
if end_if_over() then return end
|
|
return D.afterEach()
|
|
end
|
|
if end_if_over() then return end
|
|
if check_faints_and_end() then
|
|
return
|
|
end
|
|
Battle._phase = "actions"
|
|
if not Battle._actions or not Battle._actions[Battle._actionI] then
|
|
after_actions()
|
|
end
|
|
end
|
|
|
|
local SEL_ORDER = { 0, 2 }
|
|
|
|
local function has_flag(t, f)
|
|
return math.floor((tonumber(t) or 0) / f) % 2 == 1
|
|
end
|
|
|
|
function D.reset()
|
|
Battle._dblSel = nil
|
|
Battle._dblFaint = nil
|
|
Battle._dblSwitch = nil
|
|
Battle._dblAfterAnim = nil
|
|
Battle._dblLeveled = nil
|
|
Battle._singleAfterAnim = nil
|
|
end
|
|
|
|
function D.activeSlots(st)
|
|
local out = {}
|
|
for _, id in ipairs(SEL_ORDER) do
|
|
local b = State.battler(st, id)
|
|
if b and not State.isAbsent(st, id) then out[#out + 1] = b.partyIndex end
|
|
end
|
|
return out
|
|
end
|
|
|
|
function D.capture(fn)
|
|
local ad = Battle._adapter
|
|
if not ad then return {} end
|
|
local mark = ad:eventMark()
|
|
local prev = ad._say
|
|
ad._say = function() end
|
|
pcall(fn)
|
|
ad._say = prev
|
|
return ad:eventsSince(mark)
|
|
end
|
|
|
|
-- pokefirered/src/battle_message.c:1591
|
|
function D.headlessIntro(st, trainerId, rivalName)
|
|
local strings = Trainers.introStrings(trainerId, State.displayName(st.enemy), { rivalName = rivalName })
|
|
local out = { strings.wants }
|
|
local b3 = not State.isAbsent(st, 3) and State.battler(st, 3)
|
|
if b3 then
|
|
local who = strings.sentOut:match("^(.-) sent\n") or ""
|
|
out[#out + 1] = who .. " sent\nout " .. State.displayName(st.enemy) .. " and " .. State.displayName(b3) .. "!"
|
|
else
|
|
out[#out + 1] = strings.sentOut
|
|
end
|
|
local b2 = not State.isAbsent(st, 2) and State.battler(st, 2)
|
|
if b2 then
|
|
out[#out + 1] = "Go! " .. State.displayName(st.player) .. " and\n" .. State.displayName(b2) .. "!"
|
|
else
|
|
out[#out + 1] = "Go! " .. State.displayName(st.player) .. "!"
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3125
|
|
function D.lockedAction(st, id)
|
|
local b = State.battler(st, id)
|
|
if not b or not (b.expLockedMove or b.expMustRecharge) then return nil end
|
|
local mon = b.mon or {}
|
|
local slot = b.expLockedSlot or 1
|
|
local mv = b.expLockedMove or b.lastMoveId or b.lastMove or (mon.moves and mon.moves[slot])
|
|
return { kind = "move", battler = id, move = mv, slot = slot, target = st.moveTarget and st.moveTarget[id] }
|
|
end
|
|
|
|
function D.autoAction(st, id)
|
|
local act = Commands.fightShortcut(st, id)
|
|
if act then
|
|
act.battler = id
|
|
return act
|
|
end
|
|
for slot = 1, 4 do
|
|
if Commands.moveUsable(st, slot, id) then return Commands.playerAction(st, 1, slot, id) end
|
|
end
|
|
return Commands.playerAction(st, 1, 1, id)
|
|
end
|
|
|
|
-- pokefirered/src/battle_controller_player.c:447
|
|
function D.targetPlan(st, id, cmd)
|
|
local b = State.battler(st, id)
|
|
local mv = Moves.get(cmd.move)
|
|
local tt = tonumber(mv and mv.target) or 0
|
|
local num = tonumber(cmd.move) or tonumber(mv and mv.id)
|
|
if num == 174 then
|
|
local Types = require("src.core.game3.battle.types")
|
|
local ad = Battle._adapter
|
|
local ghost = ad and b and Types.ID and Types.ID.GHOST and ad:hasType(b, Types.ID.GHOST)
|
|
tt = ghost and 0 or 0x10
|
|
end
|
|
local opposing = (id % 2 == 0) and 1 or 0
|
|
local cursor = has_flag(tt, 0x10) and id or opposing
|
|
local can = not (has_flag(tt, 0x04) or has_flag(tt, 0x08) or has_flag(tt, 0x01)
|
|
or has_flag(tt, 0x20) or has_flag(tt, 0x40) or has_flag(tt, 0x10))
|
|
local pp = b and b.mon and b.mon.pp and cmd.slot and tonumber(b.mon.pp[cmd.slot])
|
|
if pp == 0 then
|
|
can = false
|
|
elseif not (has_flag(tt, 0x10) or has_flag(tt, 0x02)) then
|
|
local others = 0
|
|
for oid = 0, 3 do
|
|
if oid ~= id and State.isPresent(st, oid) then others = others + 1 end
|
|
end
|
|
if others <= 1 then
|
|
-- pokefirered/src/pokemon.c:2700
|
|
cursor = State.isAbsent(st, opposing) and State.PARTNER(opposing) or opposing
|
|
can = false
|
|
end
|
|
end
|
|
local start = cursor
|
|
if can then
|
|
if has_flag(tt, 0x10) or has_flag(tt, 0x02) then
|
|
start = id
|
|
elseif State.isAbsent(st, opposing) then
|
|
start = State.PARTNER(opposing)
|
|
else
|
|
start = opposing
|
|
end
|
|
end
|
|
return can, cursor, start, tt
|
|
end
|
|
|
|
function D.startSelection()
|
|
local st = Battle._st
|
|
st.monToSwitchInto = {}
|
|
Battle._phase = "command"
|
|
Battle._dblSel = { chosen = {}, pos = 1 }
|
|
return D.advanceSelection()
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3097
|
|
function D.advanceSelection()
|
|
local st, sel = Battle._st, Battle._dblSel
|
|
if not sel then return end
|
|
while sel.pos <= #SEL_ORDER do
|
|
local id = SEL_ORDER[sel.pos]
|
|
local b = State.battler(st, id)
|
|
if not b or State.isAbsent(st, id) then
|
|
sel.pos = sel.pos + 1
|
|
else
|
|
local locked = D.lockedAction(st, id)
|
|
if locked then
|
|
sel.chosen[id] = locked
|
|
sel.pos = sel.pos + 1
|
|
elseif Battle._auto then
|
|
sel.chosen[id] = D.autoAction(st, id)
|
|
sel.pos = sel.pos + 1
|
|
else
|
|
sel.active = id
|
|
st.activeBattler = id
|
|
return D.openMenu(id)
|
|
end
|
|
end
|
|
end
|
|
return D.finishSelection()
|
|
end
|
|
|
|
-- pokefirered/src/battle_controller_player.c:286
|
|
function D.cancelPartner()
|
|
local st, sel = Battle._st, Battle._dblSel
|
|
local c0 = sel and sel.chosen[0]
|
|
if c0 and c0.kind == "bag" then
|
|
local Catching = require("src.core.game3.battle.catching")
|
|
if not Catching.isBall(c0.itemId) then return end
|
|
end
|
|
pcall(function()
|
|
require("src.core.game3.audio").playSe(require("src.core.game3.se_ids").SE_SELECT)
|
|
end)
|
|
sel.chosen[0] = nil
|
|
if st.monToSwitchInto then st.monToSwitchInto[0] = nil end
|
|
sel.pos = 1
|
|
return D.advanceSelection()
|
|
end
|
|
|
|
function D.openMenu(id)
|
|
local sel = Battle._dblSel
|
|
return Ui.openMenu(id, { partnerAction = (id == 2 and sel) and sel.chosen[0] or nil })
|
|
end
|
|
|
|
function D.commit(cmd)
|
|
local sel = Battle._dblSel
|
|
if not sel then return end
|
|
sel.chosen[cmd.battler] = cmd
|
|
sel.pos = sel.pos + 1
|
|
return D.advanceSelection()
|
|
end
|
|
|
|
function D.onCommand(cmd)
|
|
local st, sel = Battle._st, Battle._dblSel
|
|
if not sel or not sel.active then return end
|
|
local id = sel.active
|
|
if cmd.kind == "cancel_partner" then
|
|
if id == 2 and not State.isAbsent(st, 0) then return D.cancelPartner() end
|
|
return D.openMenu(id)
|
|
end
|
|
local b = State.battler(st, id)
|
|
if cmd.battler ~= id then
|
|
if cmd.kind == "move" and cmd.slot and b and b.mon and b.mon.moves and b.mon.moves[cmd.slot] then
|
|
cmd.move = b.mon.moves[cmd.slot]
|
|
end
|
|
cmd.battler = id
|
|
end
|
|
if cmd.kind == "move" then
|
|
if cmd.target == nil and cmd.move ~= "STRUGGLE" then
|
|
local can, cursor, start = D.targetPlan(st, id, cmd)
|
|
if not can then
|
|
cmd.target = cursor
|
|
elseif Ui.chooseTarget then
|
|
sel.targeting = true
|
|
Ui.chooseTarget(st, id, cmd.slot, function(targetId)
|
|
sel.targeting = false
|
|
if targetId == nil then
|
|
if Ui.openMoveMenu then return Ui.openMoveMenu(id) end
|
|
return D.openMenu(id)
|
|
end
|
|
cmd.target = targetId
|
|
return D.commit(cmd)
|
|
end)
|
|
return
|
|
else
|
|
cmd.target = start
|
|
end
|
|
end
|
|
elseif cmd.kind == "switch" then
|
|
local err = cmd.slot and Commands.switchError(st, cmd.slot, false, id)
|
|
if err or not cmd.slot then
|
|
if err then Ui.push(err, function() D.openMenu(id) end) else D.openMenu(id) end
|
|
return
|
|
end
|
|
st.monToSwitchInto[id] = cmd.slot
|
|
end
|
|
return D.commit(cmd)
|
|
end
|
|
|
|
function D.commandUpdate(input)
|
|
local sel = Battle._dblSel
|
|
if not sel then return D.startSelection() end
|
|
if sel.targeting then
|
|
if input then Ui.handleInput(input) end
|
|
return
|
|
end
|
|
local BagMenu = require("src.ui.game3.bag_menu")
|
|
if BagMenu.isOpen and BagMenu.isOpen() then
|
|
if input then BagMenu.handleInput(input) end
|
|
return
|
|
end
|
|
local PartyMenu = require("src.ui.game3.party_menu")
|
|
if PartyMenu.isOpen and PartyMenu.isOpen() then
|
|
if input then party_menu_input(PartyMenu, input) end
|
|
return
|
|
end
|
|
if Ui._mode == "bag" or Ui._mode == "party" then
|
|
Ui._mode = "menu"
|
|
end
|
|
if Ui.selectionPump() then
|
|
local scmd = Ui.takeCommand()
|
|
if scmd then D.onCommand(scmd) end
|
|
return
|
|
end
|
|
if input then Ui.handleInput(input) end
|
|
local cmd = Ui.takeCommand()
|
|
if cmd then D.onCommand(cmd) end
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3532
|
|
function D.finishSelection()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local sel = Battle._dblSel
|
|
local chosen = sel and sel.chosen or {}
|
|
Battle._dblSel = nil
|
|
st.activeBattler = nil
|
|
for _, id in ipairs({ 1, 3 }) do
|
|
if State.battler(st, id) and not State.isAbsent(st, id) then
|
|
chosen[id] = Commands.enemyAction(st, id)
|
|
end
|
|
end
|
|
st.turn = st.turn + 1
|
|
Battle._actions = Engine.planTurnActions(st, ad, chosen)
|
|
open_turn(st, chosen[0], chosen[1], chosen)
|
|
Battle._actionI = 1
|
|
Battle._metaAct = nil
|
|
Battle._phase = "actions"
|
|
local evs = focus_punch_prelude()
|
|
if evs and #evs > 0 then
|
|
if Battle._headless then
|
|
for _, e in ipairs(evs) do
|
|
if e.kind == "msg" then Ui.push(e.text) end
|
|
end
|
|
else
|
|
AnimSeq.beginEvents(evs, seq_push)
|
|
Battle._phase = "preturn"
|
|
end
|
|
end
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3704
|
|
function D.stepAction()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
if end_if_over() then return end
|
|
local acts = Battle._actions or {}
|
|
local act
|
|
while true do
|
|
act = acts[Battle._actionI]
|
|
if not act then return D.afterActions() end
|
|
Battle._actionI = Battle._actionI + 1
|
|
if Engine.actionRunnable(st, act) then break end
|
|
end
|
|
act.done = true
|
|
local user = State.battler(st, act.battler)
|
|
if act.kind == "move" then
|
|
if not user or State.isFainted(user) then return D.afterEach() end
|
|
local out = {}
|
|
st.interactiveChoices = not (Battle._headless or Battle._auto)
|
|
Engine.resolveMove(act.battler, act.target, act.move, act.slot, ad, st, out)
|
|
st.interactiveChoices = nil
|
|
Battle._pendingChoice = out.pendingChoice
|
|
if Battle._headless or not out._anim then
|
|
push_msgs(out)
|
|
if Battle._pendingChoice then return open_pending_choice() end
|
|
return D.afterEach()
|
|
end
|
|
AnimSeq.begin(out._anim, seq_push)
|
|
Battle._phase = "animating"
|
|
return
|
|
elseif act.kind == "switch" then
|
|
return D.beginSwitch(act)
|
|
elseif act.kind == "bag" then
|
|
return D.useBag(act)
|
|
elseif act.kind == "run" then
|
|
return D.run(act)
|
|
elseif act.kind == "item" then
|
|
return D.enemyItem(act)
|
|
end
|
|
return D.afterEach()
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:4433
|
|
function D.afterEach()
|
|
if end_if_over() then return end
|
|
return D.faintFlow(function()
|
|
Battle._phase = "actions"
|
|
end)
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:2953
|
|
function D.afterActions()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
if end_if_over() then return end
|
|
local events = Engine.collectResidualEvents(st, ad)
|
|
close_turn(st)
|
|
if Battle._headless then
|
|
for _, evt in ipairs(events or {}) do push_msgs(evt.msgs) end
|
|
Ui.pump()
|
|
return D.endTurn()
|
|
end
|
|
local stream = {}
|
|
for _, evt in ipairs(events or {}) do
|
|
for _, e in ipairs(evt.events or {}) do stream[#stream + 1] = e end
|
|
end
|
|
AnimSeq.beginEvents(stream, seq_push)
|
|
Battle._phase = "residuals"
|
|
end
|
|
|
|
function D.endTurn()
|
|
return D.faintFlow(function()
|
|
Battle._st.monToSwitchInto = {}
|
|
return D.startSelection()
|
|
end)
|
|
end
|
|
|
|
-- pokefirered/src/battle_util.c:1144
|
|
function D.faintFlow(onDone)
|
|
Engine.refreshAbsent(Battle._st)
|
|
Battle._dblFaint = { stage = "exp", onDone = onDone }
|
|
return D.faintStep()
|
|
end
|
|
|
|
function D.presentAwards(awards)
|
|
if not awards or #awards == 0 then return false end
|
|
Battle._dblLeveled = Battle._dblLeveled or {}
|
|
Battle._leveledUp = Battle._dblLeveled
|
|
if Battle._headless then
|
|
push_awards_headless(awards)
|
|
return false
|
|
end
|
|
local hooks = choice_hooks()
|
|
hooks.double = true
|
|
if not ExpSeq.begin(awards, hooks.pushMsg, nil, hooks) then return false end
|
|
Battle._onExpDone = function()
|
|
for k, v in pairs(ExpSeq.leveledSet() or {}) do Battle._dblLeveled[k] = v end
|
|
Battle._leveledUp = Battle._dblLeveled
|
|
Battle._phase = "actions"
|
|
return D.faintStep()
|
|
end
|
|
Battle._phase = "awarding"
|
|
return true
|
|
end
|
|
|
|
function D.faintStep()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local F = Battle._dblFaint
|
|
if not F or not st then return end
|
|
if F.stage == "exp" then
|
|
for _, id in ipairs(Engine.expAwardOrder(st)) do
|
|
local foe = State.battler(st, id)
|
|
if foe and not foe._expGiven then
|
|
foe._expGiven = true
|
|
if st.foeParty then State.syncBattlerToParty(foe, st.foeParty) end
|
|
local awards = Experience.awardFoe(st, foe, { trainer = not st.wild })
|
|
-- pokefirered/src/battle_util.c:1181
|
|
State.opponentSwitchInResetSentPokes(st, foe)
|
|
if D.presentAwards(awards) then return end
|
|
end
|
|
end
|
|
F.stage = "check"
|
|
end
|
|
if F.stage == "check" then
|
|
-- pokefirered/data/battle_scripts_1.s:2825
|
|
local res = Engine.checkEnd(st, ad)
|
|
if res == "win" or res == "lose" then
|
|
Battle._dblFaint = nil
|
|
Battle._actions = {}
|
|
stop_low_hp_song()
|
|
if res == "win" then
|
|
Battle._leveledUp = Battle._dblLeveled or {}
|
|
return begin_trainer_win(st)
|
|
end
|
|
return D.lose()
|
|
end
|
|
F.stage = "repl"
|
|
end
|
|
if F.stage == "repl" then
|
|
for id = 0, 3 do
|
|
local b = State.battler(st, id)
|
|
if b and not State.isAbsent(st, id) and State.isFainted(b) then
|
|
local cands = Engine.replacementCandidates(st, id)
|
|
if #cands == 0 then
|
|
-- pokefirered/src/battle_script_commands.c:4870
|
|
Engine.markAbsent(st, id)
|
|
else
|
|
return D.pickReplacement(id, cands)
|
|
end
|
|
end
|
|
end
|
|
F.stage = "after"
|
|
end
|
|
Battle._dblFaint = nil
|
|
-- pokefirered/src/battle_util.c:1208
|
|
local evs = D.capture(function() Engine.afterAction(st, ad) end)
|
|
if #evs > 0 and not Battle._headless then
|
|
AnimSeq.beginEvents(evs, seq_push)
|
|
Battle._phase = "animating"
|
|
Battle._dblAfterAnim = F.onDone
|
|
return
|
|
end
|
|
for _, e in ipairs(evs) do
|
|
if e.kind == "msg" then Ui.push(e.text) end
|
|
end
|
|
if F.onDone then return F.onDone() end
|
|
end
|
|
|
|
function D.lose()
|
|
Battle._pendingEnd = "lose"
|
|
Battle._phase = "ending"
|
|
Ui.push("You have no more\nPOKéMON left!")
|
|
Ui.push(string.format("%s blacked out!", (Battle._st and Battle._st.playerName or "PLAYER")))
|
|
end
|
|
|
|
-- pokefirered/src/battle_script_commands.c:4855
|
|
function D.pickReplacement(id, cands)
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local function go(slot)
|
|
st.monToSwitchInto[id] = slot
|
|
return D.sendOut(id, slot)
|
|
end
|
|
if State.sideOf(id) == "enemy" then
|
|
-- pokefirered/src/battle_controller_opponent.c:1410
|
|
local ok, pick = pcall(Engine.mostSuitableMon, st, ad, id)
|
|
for _, c in ipairs(cands) do
|
|
if ok and c == pick then return go(pick) end
|
|
end
|
|
return go(cands[1])
|
|
end
|
|
if Battle._headless or Battle._auto then return go(cands[1]) end
|
|
local PartyMenu = require("src.ui.game3.party_menu")
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
for _, pid in ipairs(SEL_ORDER) do
|
|
local pb = State.battler(st, pid)
|
|
if pb then State.syncBattlerToParty(pb, st.playerParty) end
|
|
end
|
|
if Ui.clearLinger then Ui.clearLinger() end
|
|
Battle._phase = "switching"
|
|
if Ui.openPartyMenu then
|
|
return Ui.openPartyMenu(st, id, {
|
|
forced = true,
|
|
validate = function(slot) return Commands.switchError(st, slot, true, id) end,
|
|
onSelect = function(slot)
|
|
if slot == nil then return D.pickReplacement(id, cands) end
|
|
return go(slot)
|
|
end,
|
|
})
|
|
end
|
|
local b0 = State.battler(st, 0)
|
|
PartyMenu.show(st.playerParty or (session and session.party), session and session.move_overlay, {
|
|
mode = "battle_faint",
|
|
session = session,
|
|
activeSlot = b0 and b0.partyIndex,
|
|
activeSlots = D.activeSlots(st),
|
|
battlerId = id,
|
|
battle = true,
|
|
validate = function(slot) return Commands.switchError(st, slot, true, id) end,
|
|
onSelect = function(slot)
|
|
if slot == nil then return D.pickReplacement(id, cands) end
|
|
return go(slot)
|
|
end,
|
|
})
|
|
end
|
|
|
|
function D.sendOut(id, slot)
|
|
local started = SwitchSeq.beginDoubleSwitch(Battle._st, id, slot, {
|
|
headless = Battle._headless or Battle._auto,
|
|
reason = "replace",
|
|
pushMsg = function(t) Ui.push(t) end,
|
|
onDone = function() return D.faintStep() end,
|
|
})
|
|
if started then Battle._phase = "switching" end
|
|
end
|
|
|
|
-- pokefirered/data/battle_scripts_1.s:3046
|
|
function D.beginSwitch(act)
|
|
local st = Battle._st
|
|
local id = act.battler
|
|
local slot = act.slot or (st.monToSwitchInto and st.monToSwitchInto[id])
|
|
if not slot then return D.afterEach() end
|
|
Ui.push(SwitchSeq.returnText(st, id))
|
|
Battle._dblSwitch = { id = id, slot = slot, pursuers = (State.sideOf(id) == "player") and { 3, 1 } or { 2, 0 }, i = 1 }
|
|
return D.switchStep()
|
|
end
|
|
|
|
-- pokefirered/src/battle_script_commands.c:8337
|
|
function D.pursuitRow(st, pid, targetId)
|
|
if State.isAbsent(st, pid) then return nil end
|
|
local ad = Battle._adapter
|
|
local user = State.battler(st, pid)
|
|
local target = State.battler(st, targetId)
|
|
if not user or State.isFainted(user) or not target or State.isFainted(target) then return nil end
|
|
for _, row in ipairs(st.turnActions or {}) do
|
|
if row.battler == pid and row.kind == "move" and not row.done and not row.finished
|
|
and Engine.isPursuit(row.move) then
|
|
local tgt = row.target
|
|
if type(tgt) == "table" then tgt = tgt.id end
|
|
if tgt == nil and st.moveTarget then tgt = st.moveTarget[pid] end
|
|
if tgt == targetId and not ad:hasStatus(user, "SLP") and not ad:hasStatus(user, "FRZ")
|
|
and (tonumber(user.expTruantCounter) or 0) == 0 then
|
|
return row
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function D.switchStep()
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local sw = Battle._dblSwitch
|
|
if not sw then return end
|
|
while sw.i <= #sw.pursuers do
|
|
local pid = sw.pursuers[sw.i]
|
|
sw.i = sw.i + 1
|
|
local row = D.pursuitRow(st, pid, sw.id)
|
|
if row then
|
|
row.done = true
|
|
local out = {}
|
|
Engine.resolveMove(pid, sw.id, row.move, row.slot, ad, st, out, { pursuitSwitch = true })
|
|
if Battle._headless or not out._anim then
|
|
push_msgs(out)
|
|
else
|
|
AnimSeq.begin(out._anim, seq_push)
|
|
Battle._phase = "animating"
|
|
Battle._dblAfterAnim = D.switchStep
|
|
return
|
|
end
|
|
end
|
|
end
|
|
Battle._dblSwitch = nil
|
|
local b = State.battler(st, sw.id)
|
|
if not b or State.isFainted(b) then
|
|
if st.monToSwitchInto then st.monToSwitchInto[sw.id] = nil end
|
|
return D.afterEach()
|
|
end
|
|
local started = SwitchSeq.beginDoubleSwitch(st, sw.id, sw.slot, {
|
|
withdraw = true,
|
|
noWithdrawMsg = true,
|
|
reason = "switch",
|
|
headless = Battle._headless,
|
|
pushMsg = function(t) Ui.push(t) end,
|
|
onDone = function() return D.afterEach() end,
|
|
})
|
|
if started then Battle._phase = "switching" end
|
|
end
|
|
|
|
-- pokefirered/data/battle_scripts_2.s:134
|
|
function D.enemyItem(act)
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local evs = D.capture(function() Engine.performEnemyItem(st, ad, act) end)
|
|
if Battle._headless or #evs == 0 then
|
|
for _, e in ipairs(evs) do
|
|
if e.kind == "msg" then Ui.push(e.text) end
|
|
end
|
|
Anim.syncDisplayFromState(st)
|
|
if st.double then return D.afterEach() end
|
|
Battle._phase = "actions"
|
|
if not Battle._actions[Battle._actionI] then after_actions() end
|
|
return
|
|
end
|
|
AnimSeq.beginEvents(evs, seq_push)
|
|
Battle._phase = "animating"
|
|
end
|
|
|
|
-- pokefirered/data/battle_scripts_1.s:3046
|
|
function D.singleEnemySwitch(act)
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local slot = act.slot or (st.monToSwitchInto and st.monToSwitchInto[1])
|
|
local function cont()
|
|
Battle._phase = "actions"
|
|
if not (Battle._actions and Battle._actions[Battle._actionI]) then after_actions() end
|
|
end
|
|
if not slot or State.isFainted(st.enemy) then return cont() end
|
|
Ui.push(SwitchSeq.returnText(st, 1))
|
|
local function do_switch()
|
|
if State.isFainted(st.enemy) then
|
|
if st.monToSwitchInto then st.monToSwitchInto[1] = nil end
|
|
if check_faints_and_end() then return end
|
|
return cont()
|
|
end
|
|
local started = SwitchSeq.beginDoubleSwitch(st, 1, slot, {
|
|
withdraw = true,
|
|
noWithdrawMsg = true,
|
|
reason = "switch",
|
|
headless = Battle._headless,
|
|
pushMsg = function(t) Ui.push(t) end,
|
|
onDone = cont,
|
|
})
|
|
if started then Battle._phase = "switching" end
|
|
end
|
|
local prow = D.pursuitRow(st, 0, 1)
|
|
if not prow then return do_switch() end
|
|
for i = #(Battle._actions or {}), 1, -1 do
|
|
if Battle._actions[i] == prow then table.remove(Battle._actions, i) end
|
|
end
|
|
prow.done = true
|
|
local out = {}
|
|
Engine.resolveMove(st.player, st.enemy, prow.move, prow.slot, ad, st, out, { pursuitSwitch = true })
|
|
if Battle._headless or not out._anim then
|
|
push_msgs(out)
|
|
return do_switch()
|
|
end
|
|
AnimSeq.begin(out._anim, seq_push)
|
|
Battle._phase = "animating"
|
|
Battle._singleAfterAnim = do_switch
|
|
end
|
|
|
|
function D.useBag(act)
|
|
local st, ad = Battle._st, Battle._adapter
|
|
if State.sideOf(act.battler) ~= "player" then return D.afterEach() end
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
local bag = session and session.bag
|
|
local Catching = require("src.core.game3.battle.catching")
|
|
if Catching.isBall(act.itemId) then
|
|
local Bag = require("src.core.game3.bag")
|
|
if bag and Bag.has(bag, act.itemId, 1) then Bag.remove(bag, act.itemId, 1) end
|
|
local okI, Items = pcall(require, "src.core.game3.items")
|
|
local iname = okI and Items.displayName and Items.displayName(act.itemId) or "POKé BALL"
|
|
-- pokefirered/data/battle_scripts_2.s:54
|
|
Ui.push(string.format("%s used\n%s!", st.playerName or "PLAYER", iname))
|
|
-- pokefirered/data/battle_scripts_2.s:116
|
|
Ui.push("The TRAINER blocked the BALL!")
|
|
Ui.push("Don't be a thief!")
|
|
return D.afterEach()
|
|
end
|
|
local BattleItems = require("src.core.game3.battle.items")
|
|
local result = BattleItems.use(st, ad, bag, session, act.itemId, act.partySlot, act.battler)
|
|
if result == "heal" then
|
|
for _, id in ipairs(SEL_ORDER) do
|
|
local b = State.battler(st, id)
|
|
if b and b.partyIndex == act.partySlot and not State.isAbsent(st, id) then
|
|
local p = Anim.present(id)
|
|
local logical = tonumber(b.mon and b.mon.hp) or 0
|
|
if p and p.displayHp ~= nil and math.abs(logical - p.displayHp) >= 1 then
|
|
Anim.tweenHp(id, p.displayHp, logical, b.mon.maxHp)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return D.afterEach()
|
|
end
|
|
|
|
function D.run(act)
|
|
local st, ad = Battle._st, Battle._adapter
|
|
local fled = false
|
|
local evs = D.capture(function()
|
|
fled = Engine.tryFlee(st, ad, State.battler(st, act.battler)) and true or false
|
|
end)
|
|
for _, e in ipairs(evs) do
|
|
if e.kind == "msg" then Ui.push(e.text) end
|
|
end
|
|
if fled then
|
|
st.over = true
|
|
st.result = "run"
|
|
st.endReason = "flee"
|
|
Battle._pendingEnd = "run"
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
return D.afterEach()
|
|
end
|
|
|
|
local function finish_catch_flow(catchRes, ename)
|
|
if catchRes and catchRes.location == "pc" then
|
|
Battle._phase = "catch_pc_msg"
|
|
local name = (catchRes.mon and (catchRes.mon.nickname ~= "" and catchRes.mon.nickname or catchRes.mon.name))
|
|
or ename or "POKéMON"
|
|
Ui.push(name .. " was transferred\nto the PC.")
|
|
return
|
|
end
|
|
Battle._actions = {}
|
|
Battle._pendingEnd = "catch"
|
|
Battle._phase = "ending"
|
|
end
|
|
|
|
local function start_post_catch_flow(catchRes)
|
|
if Battle._headless then
|
|
Battle._actions = {}
|
|
Battle._pendingEnd = "catch"
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
|
|
local enemy = Battle._st and Battle._st.enemy
|
|
local mon = (catchRes and catchRes.mon) or (enemy and enemy.mon)
|
|
local sp = (enemy and enemy.mon and (enemy.mon.species or enemy.mon.speciesId))
|
|
or (catchRes and catchRes.mon and (catchRes.mon.species or catchRes.mon.speciesId))
|
|
or (enemy and enemy.species) or 1
|
|
local ename = (mon and (mon.nickname ~= "" and mon.nickname or mon.name))
|
|
or Pokemon.name(sp) or "POKéMON"
|
|
local gender = (mon and (mon.gender or (mon.isFemale and 1))) or (enemy and enemy.gender) or 0
|
|
local personality = (mon and mon.personality) or 0
|
|
|
|
local function prompt_nickname()
|
|
Battle._phase = "catch_nickname_prompt"
|
|
-- pokefirered/src/battle_message.c:477
|
|
Ui.askYesNo("Give a nickname to the\ncaptured " .. ename .. "?", function(yes)
|
|
if yes then
|
|
local okN, Naming = pcall(require, "src.ui.game3.naming")
|
|
if okN and Naming and Naming.open then
|
|
Battle._phase = "catch_naming"
|
|
Naming.open({
|
|
template = "CAUGHT_MON",
|
|
maxLen = 10,
|
|
species = sp,
|
|
gender = gender,
|
|
personality = personality,
|
|
seed = ename,
|
|
title = "YOUR POKEMON'S NICKNAME?",
|
|
onDone = function(nick)
|
|
if nick and nick ~= "" and nick ~= ename then
|
|
if mon then mon.nickname = nick end
|
|
end
|
|
finish_catch_flow(catchRes, ename)
|
|
end,
|
|
})
|
|
return
|
|
end
|
|
end
|
|
finish_catch_flow(catchRes, ename)
|
|
end)
|
|
end
|
|
|
|
if catchRes and catchRes.firstTimeCaught and sp then
|
|
local okP, Pokedex = pcall(require, "src.ui.game3.pokedex")
|
|
if okP and Pokedex and Pokedex.showRegistration then
|
|
Battle._phase = "pokedex_reg"
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
Pokedex.showRegistration(sp, {
|
|
session = session,
|
|
onDone = function()
|
|
prompt_nickname()
|
|
end,
|
|
})
|
|
return
|
|
end
|
|
end
|
|
|
|
prompt_nickname()
|
|
end
|
|
|
|
Battle.startPostCatchFlow = start_post_catch_flow
|
|
Battle.finishCatchFlow = finish_catch_flow
|
|
|
|
function Battle.update(dt, game)
|
|
if not Battle._active then return end
|
|
|
|
local input = game and game.input
|
|
local Pokedex = package.loaded["src.ui.game3.pokedex"]
|
|
if Pokedex and Pokedex.isOpen and Pokedex.isOpen() then
|
|
if input then Pokedex.handleInput(input) end
|
|
return
|
|
end
|
|
|
|
local Naming = package.loaded["src.ui.game3.naming"]
|
|
if Naming and Naming.isOpen and Naming.isOpen() then
|
|
if input then Naming.update(input, dt or (1 / 60)) end
|
|
return
|
|
end
|
|
|
|
if not Battle._headless then
|
|
Anim.update(dt or 0)
|
|
local okA, Audio = pcall(require, "src.core.game3.audio")
|
|
if okA and Audio and Audio.tickCry then Audio.tickCry(dt or 1 / 60) end
|
|
update_low_hp_music()
|
|
local PartyMenu = package.loaded["src.ui.game3.party_menu"]
|
|
if PartyMenu and PartyMenu.isOpen and PartyMenu.isOpen() and PartyMenu.update then
|
|
PartyMenu.update(dt or (1 / 60))
|
|
end
|
|
end
|
|
|
|
if Battle._phase == "command" and not Battle._auto and Battle._st and Battle._st.double then
|
|
D.commandUpdate(input)
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "command" and not Battle._auto then
|
|
local BagMenu = require("src.ui.game3.bag_menu")
|
|
if BagMenu.isOpen and BagMenu.isOpen() then
|
|
if input then BagMenu.handleInput(input) end
|
|
return
|
|
end
|
|
local PartyMenu = require("src.ui.game3.party_menu")
|
|
if PartyMenu.isOpen and PartyMenu.isOpen() then
|
|
if input then party_menu_input(PartyMenu, input) end
|
|
return
|
|
end
|
|
if Ui._mode == "bag" or Ui._mode == "party" then
|
|
Ui._mode = "menu"
|
|
end
|
|
if Ui.selectionPump() then
|
|
local scmd = Ui.takeCommand()
|
|
if scmd then begin_turn_with(scmd) end
|
|
return
|
|
end
|
|
if input then Ui.handleInput(input) end
|
|
local cmd = Ui.takeCommand()
|
|
if cmd then
|
|
begin_turn_with(cmd)
|
|
end
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "preturn" then
|
|
if Anim.busy() then return end
|
|
if not Ui.pump() then return end
|
|
if AnimSeq.update() then
|
|
Battle._phase = "actions"
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Choice input during award / shift prompt / evolution learn-move prompts / catch nickname prompt / evolving
|
|
if (Battle._phase == "awarding" or Battle._phase == "evolving" or Battle._phase == "switching"
|
|
or Battle._phase == "shift_prompt" or Battle._phase == "catch_nickname_prompt")
|
|
and not Battle._auto and game and game.input then
|
|
local EvolutionScene = package.loaded["src.ui.game3.evolution_scene"]
|
|
if EvolutionScene and EvolutionScene.isOpen and EvolutionScene.isOpen() then
|
|
EvolutionScene.handleInput(game.input)
|
|
return
|
|
end
|
|
local StatGrowth = package.loaded["src.ui.game3.stat_growth"]
|
|
if StatGrowth and StatGrowth.isOpen and StatGrowth.isOpen() then
|
|
if StatGrowth.handleInput(game.input) then
|
|
return
|
|
end
|
|
end
|
|
local SummaryMenu = package.loaded["src.ui.game3.summary_menu"]
|
|
local PartyMenu = package.loaded["src.ui.game3.party_menu"]
|
|
if (Battle._phase == "awarding" or Battle._phase == "evolving")
|
|
and SummaryMenu and SummaryMenu.isOpen and SummaryMenu.isOpen()
|
|
and not (PartyMenu and PartyMenu.isOpen and PartyMenu.isOpen()) then
|
|
SummaryMenu.handleInput(game.input)
|
|
return
|
|
end
|
|
if Ui.choiceActive and Ui.choiceActive() then
|
|
Ui.handleInput(game.input)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Intro: pump dialogs even while slide tweens run
|
|
if Battle._phase == "intro" then
|
|
if not Ui.pump() then return end
|
|
if IntroSeq.update() then
|
|
if begin_start_effects() then return end
|
|
Battle._phase = "command"
|
|
if Battle._auto then
|
|
begin_turn_with(Commands.playerAction(Battle._st, 1, 1))
|
|
else
|
|
Ui.openMenu()
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "startfx" then
|
|
if Anim.busy() then return end
|
|
if not Ui.pump() then return end
|
|
if AnimSeq.update() then
|
|
Battle._phase = "command"
|
|
if Battle._auto then
|
|
begin_turn_with(Commands.playerAction(Battle._st, 1, 1))
|
|
else
|
|
Ui.openMenu()
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Mid-turn switch-in during faint / pursuit
|
|
if Battle._phase == "faint_switch" then
|
|
local PartyMenu = package.loaded["src.ui.game3.party_menu"]
|
|
if PartyMenu and PartyMenu.isOpen and PartyMenu.isOpen() then
|
|
if input then party_menu_input(PartyMenu, input) end
|
|
return
|
|
end
|
|
if Anim.busy() then return end
|
|
if Ui.choiceActive and Ui.choiceActive() then
|
|
return
|
|
end
|
|
if not Ui.pump() then return end
|
|
if SwitchSeq.update() then
|
|
Battle._phase = "actions"
|
|
if not Battle._actions or not Battle._actions[Battle._actionI] then
|
|
after_actions()
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Shift prompt: after dialog dismissed, open Yes/No box
|
|
if Battle._phase == "shift_prompt" then
|
|
if not Ui.pump() then return end
|
|
if Ui.choiceActive and Ui.choiceActive() then
|
|
if input then Ui.handleInput(input) end
|
|
return
|
|
end
|
|
if not Battle._shiftAsked then
|
|
Battle._shiftAsked = true
|
|
Ui.askYesNo(function(yes)
|
|
Battle._shiftAsked = false
|
|
local nextEnemyIdx = Battle._shiftEnemyIdx
|
|
Battle._shiftEnemyIdx = nil
|
|
local st = Battle._st
|
|
if yes and st then
|
|
local PartyMenu = require("src.ui.game3.party_menu")
|
|
local Runtime = package.loaded["src.core.game3.runtime"]
|
|
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
|
State.syncBattlerToParty(st.player, st.playerParty)
|
|
PartyMenu.show(st.playerParty or (session and session.party), session and session.move_overlay, {
|
|
mode = "battle_switch",
|
|
session = session,
|
|
activeSlot = st.player.partyIndex,
|
|
battle = true,
|
|
validate = function(slot)
|
|
if slot == st.player.partyIndex then return nil end
|
|
return Commands.switchError(st, slot, true)
|
|
end,
|
|
onSelect = function(pSlot)
|
|
if pSlot == nil or pSlot == st.player.partyIndex then
|
|
send_out_enemy_next(nextEnemyIdx)
|
|
else
|
|
SwitchSeq.beginShiftSwitch(st, pSlot, nextEnemyIdx, {
|
|
headless = false,
|
|
pushMsg = function(t) Ui.push(t) end,
|
|
onDone = function()
|
|
Battle._phase = "command"
|
|
Ui.openMenu()
|
|
end,
|
|
})
|
|
Battle._phase = "switching"
|
|
end
|
|
end,
|
|
onClose = function()
|
|
send_out_enemy_next(nextEnemyIdx)
|
|
end,
|
|
})
|
|
Battle._phase = "switching"
|
|
else
|
|
send_out_enemy_next(nextEnemyIdx)
|
|
end
|
|
end)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Switch / send-out presentation
|
|
if Battle._phase == "switching" then
|
|
local PartyMenu = package.loaded["src.ui.game3.party_menu"]
|
|
if PartyMenu and PartyMenu.isOpen and PartyMenu.isOpen() then
|
|
if input then party_menu_input(PartyMenu, input) end
|
|
return
|
|
end
|
|
if Anim.busy() then return end
|
|
if Ui.choiceActive and Ui.choiceActive() then
|
|
return
|
|
end
|
|
if not Ui.pump() then return end
|
|
SwitchSeq.update()
|
|
return
|
|
end
|
|
|
|
-- Anim sequence owns presentation pacing
|
|
if Battle._phase == "animating" then
|
|
if Anim.busy() then return end
|
|
if not Ui.pump() then return end
|
|
local done = AnimSeq.update()
|
|
if done then
|
|
after_anim_sequence()
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Poké Ball catch presentation
|
|
if Battle._phase == "catching" then
|
|
if Anim.busy() then return end
|
|
if not Ui.pump() then return end
|
|
local done = CatchSeq.update()
|
|
if done then
|
|
local res = CatchSeq.result()
|
|
if res == "catch" then
|
|
local catchRes = CatchSeq.catchResult and CatchSeq.catchResult()
|
|
start_post_catch_flow(catchRes)
|
|
else
|
|
Battle._phase = "actions"
|
|
if not Battle._actions or not Battle._actions[Battle._actionI] then
|
|
after_actions()
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "pokedex_reg" then
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "catch_nickname_prompt" then
|
|
if not Ui.pump() then return end
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "catch_naming" then
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "catch_pc_msg" then
|
|
if not Ui.pump() then return end
|
|
Battle._actions = {}
|
|
Battle._pendingEnd = "catch"
|
|
Battle._phase = "ending"
|
|
return
|
|
end
|
|
|
|
-- EXP award / level-up / learn-move
|
|
if Battle._phase == "awarding" then
|
|
if Anim.busy() then return end
|
|
if Ui.choiceActive and Ui.choiceActive() then return end
|
|
if not Ui.pump() then return end
|
|
local done = ExpSeq.update()
|
|
if done then
|
|
Battle._leveledUp = ExpSeq.leveledSet() or Battle._leveledUp
|
|
local cb = Battle._onExpDone
|
|
Battle._onExpDone = nil
|
|
if cb then
|
|
cb()
|
|
else
|
|
begin_evo_or_end()
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Post-battle evolution (EVO_LEVEL)
|
|
if Battle._phase == "evolving" then
|
|
local EvolutionScene = package.loaded["src.ui.game3.evolution_scene"]
|
|
if EvolutionScene and EvolutionScene.isOpen and EvolutionScene.isOpen() then
|
|
if not Battle._auto and game and game.input then
|
|
EvolutionScene.handleInput(game.input)
|
|
end
|
|
return
|
|
end
|
|
if Ui.choiceActive and Ui.choiceActive() then return end
|
|
if not Ui.pump() then return end
|
|
local done = EvoSeq.update()
|
|
if done then
|
|
Battle._phase = "ending"
|
|
end
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "residuals" then
|
|
if Anim.busy() then return end
|
|
if not Ui.pump() then return end
|
|
if not AnimSeq.update() then return end
|
|
Battle._residualEvents = nil
|
|
Battle._residualIndex = 1
|
|
Battle._residualStepState = nil
|
|
if Battle._st and Battle._st.double then
|
|
D.endTurn()
|
|
return
|
|
end
|
|
if check_faints_and_end() 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
|
|
|
|
if Anim.busy() then return end
|
|
if not Ui.pump() then return end
|
|
|
|
if Battle._phase == "actions" then
|
|
step_action()
|
|
if (Battle._headless or not Battle._fade) and Battle._phase == "ending" then
|
|
finish(Battle._pendingEnd or "win")
|
|
end
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "ending" then
|
|
if Battle._headless or not Battle._fade then
|
|
finish(Battle._pendingEnd or "win")
|
|
else
|
|
Battle._phase = "fade_out"
|
|
local okA, Audio = pcall(require, "src.core.game3.audio")
|
|
if okA and Audio and Audio.fadeOutBgm then
|
|
Audio.fadeOutBgm(5)
|
|
end
|
|
local okF, Fade = pcall(require, "src.ui.game3.fade")
|
|
if okF and Fade and Fade.begin then
|
|
Fade.begin(Fade.MODE.TO_BLACK, 1, function()
|
|
finish(Battle._pendingEnd or "win")
|
|
end)
|
|
else
|
|
finish(Battle._pendingEnd or "win")
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "fade_out" then
|
|
return
|
|
end
|
|
|
|
if Battle._phase == "command" and Battle._auto then
|
|
begin_turn_with(Commands.playerAction(Battle._st, 1, 1))
|
|
end
|
|
end
|
|
|
|
function Battle.runToEnd()
|
|
if not Battle._active then return Battle.getResult() end
|
|
Battle._auto = true
|
|
Battle._headless = true
|
|
local savedLog = Ui.log and Ui.log() or {}
|
|
Ui.reset({ headless = true })
|
|
Ui.bindState(Battle._st)
|
|
if savedLog and #savedLog > 0 then
|
|
for _, t in ipairs(savedLog) do
|
|
Ui._log[#Ui._log + 1] = t
|
|
end
|
|
end
|
|
Anim.reset({ headless = true, double = Battle._st and Battle._st.double })
|
|
AnimSeq.reset()
|
|
CatchSeq.reset()
|
|
ExpSeq.reset()
|
|
EvoSeq.reset()
|
|
IntroSeq.reset()
|
|
LearnMove.reset()
|
|
local guard = 0
|
|
while Battle._active and guard < 800 do
|
|
guard = guard + 1
|
|
Battle.update(0, nil)
|
|
end
|
|
return Battle.getResult()
|
|
end
|
|
|
|
function Battle.draw(_game, w, h)
|
|
if not Battle._active then return end
|
|
Ui.draw(w, h)
|
|
end
|
|
|
|
function Battle.abort(result)
|
|
if Battle._active then finish(result or "run") end
|
|
end
|
|
|
|
return Battle
|