mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-06 09:05:48 -04:00
fix(game3): battle UI, Pyramid battles, Daycare, Faraway Mew, status timing, Solar Beam, and Seagallop cutscene
- Fixes #2667: Align FireRed battle UI text and cursor positions with vanilla layout. - Fixes #2663: Fix Battle Pyramid double trainer sight crash and party resolution. - Fixes #2651: Fix crash when picking up Pokemon at the daycare (native step-counting and exp/level sync). - Fixes #2668: Fix Faraway Island Mew encounter and grass rustle collision/despawn state. - Fixes #2647: Fix status display and Leech Seed absorption/healing sequence timing. - Fixes #2673: Set animTurn = 1 for instant Solar Beam execution in sunlight. - Fix Cinnabar Island Seagallop boat cutscene off-map culling and sprite fallback.
This commit is contained in:
@@ -248,6 +248,12 @@ 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
|
||||
self:pushEvent({
|
||||
kind = "status_apply",
|
||||
battler = id_of(battler),
|
||||
side = side_of(battler),
|
||||
status = status,
|
||||
})
|
||||
-- pokefirered/src/battle_script_commands.c:2110
|
||||
if ModRuntime.wants("battle.status_inflicted") then
|
||||
ModRuntime.emit("battle.status_inflicted", {
|
||||
@@ -264,6 +270,11 @@ function Adapter.new(battleState, sayFn)
|
||||
battler.toxicCounter = nil
|
||||
battler.sleepTurns = nil
|
||||
if battler.mon then battler.mon.status = nil end
|
||||
self:pushEvent({
|
||||
kind = "status_clear",
|
||||
battler = id_of(battler),
|
||||
side = side_of(battler),
|
||||
})
|
||||
end
|
||||
function a:stages(battler) return battler and battler.stages end
|
||||
function a:changeStages(battler, changes)
|
||||
|
||||
@@ -138,6 +138,7 @@ local function default_present(id)
|
||||
displayMaxHp = nil,
|
||||
displayExp = nil,
|
||||
displayLevel = nil,
|
||||
displayStatus = nil,
|
||||
flash = 0,
|
||||
}
|
||||
end
|
||||
@@ -504,6 +505,7 @@ function Anim.syncDisplayFromState(st)
|
||||
p.displayHp = tonumber(b.mon.hp) or 0
|
||||
p.displayMaxHp = tonumber(b.mon.maxHp) or 1
|
||||
p.displayLevel = tonumber(b.mon.level) or 1
|
||||
p.displayStatus = b.status or (b.mon and (b.mon.status or b.mon.status1))
|
||||
local prog = Experience.progress(b.mon)
|
||||
p.displayExp = prog.progressPercent or 0
|
||||
end
|
||||
|
||||
@@ -315,6 +315,10 @@ function AnimSeq.buildSteps(events, meta)
|
||||
else
|
||||
add("switch_in", d)
|
||||
end
|
||||
elseif k == "status_apply" then
|
||||
add("status_apply", { side = ev.side, battler = ev_id(ev, "battler", "side"), status = ev.status })
|
||||
elseif k == "status_clear" then
|
||||
add("status_clear", { side = ev.side, battler = ev_id(ev, "battler", "side") })
|
||||
elseif k == "end" then
|
||||
add("end", { result = ev.result, reason = ev.reason })
|
||||
end
|
||||
@@ -660,6 +664,7 @@ local function run_switch_in(d)
|
||||
p.displayHp = tonumber(d.hp) or tonumber(b.mon.hp) or 0
|
||||
p.displayMaxHp = tonumber(b.mon.maxHp) or 1
|
||||
p.displayLevel = tonumber(b.mon.level) or 1
|
||||
p.displayStatus = b.status or (b.mon and (b.mon.status or b.mon.status1))
|
||||
end
|
||||
end
|
||||
local SwitchSeq = require("src.core.game3.battle.switch_seq")
|
||||
@@ -733,12 +738,27 @@ local function run_step(step)
|
||||
launch_done()
|
||||
return
|
||||
end
|
||||
if kind == "status_apply" then
|
||||
local id = ev_id(d, "battler", "side")
|
||||
local p = Anim.present(id)
|
||||
if p then p.displayStatus = d.status end
|
||||
advance()
|
||||
return
|
||||
end
|
||||
if kind == "status_clear" then
|
||||
local id = ev_id(d, "battler", "side")
|
||||
local p = Anim.present(id)
|
||||
if p then p.displayStatus = false end
|
||||
advance()
|
||||
return
|
||||
end
|
||||
if kind == "faint" then
|
||||
local id = ev_id(d, "battler", "side") or 1
|
||||
local p = Anim.present(id)
|
||||
if p then
|
||||
Anim.setSubstitute(id, false)
|
||||
p.blinkHidden = false
|
||||
p.displayStatus = false
|
||||
end
|
||||
wait_anim()
|
||||
Anim.faintMon(id, {
|
||||
|
||||
@@ -78,10 +78,7 @@ function Healing.rest(ctx)
|
||||
if hp >= maxHp then return hp_full(ctx, user) end
|
||||
local hadStatus = ad:status(user) ~= nil
|
||||
ad:clearStatus(user)
|
||||
user.status = "SLP"
|
||||
if user.mon then user.mon.status = "SLP" end
|
||||
-- pokefirered/src/battle_script_commands.c:6480
|
||||
user.sleepTurns = 3
|
||||
ad:applyStatus(user, "SLP", user, { force = true, turns = 3 })
|
||||
if hadStatus then
|
||||
ad:sayText("STRINGID_PKMNSLEPTHEALTHY", { atk = user })
|
||||
else
|
||||
|
||||
@@ -935,6 +935,7 @@ local function charge_turn(M)
|
||||
if eff == E.SOLAR_BEAM and Rules.weather.effective(M.st, ad) == "SUN" then
|
||||
M:ppReduce()
|
||||
M.noPP = true
|
||||
M.animTurn = 1
|
||||
return false
|
||||
end
|
||||
if ModRuntime.wantsHook("battle.charge_required") then
|
||||
@@ -946,6 +947,7 @@ local function charge_turn(M)
|
||||
if required == false then
|
||||
M:ppReduce()
|
||||
M.noPP = true
|
||||
M.animTurn = 1
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -371,7 +371,9 @@ local function draw_doubles(id, battler, st, opts)
|
||||
|
||||
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 p = Anim.present and Anim.present(id)
|
||||
local stObj = (p and p.displayStatus ~= nil) and p.displayStatus or (battler.status or (battler.mon and (battler.mon.status or battler.mon.status1)))
|
||||
if stObj == false or stObj == 0 then stObj = nil end
|
||||
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)
|
||||
@@ -388,9 +390,7 @@ local function draw_doubles(id, battler, st, opts)
|
||||
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 lv = (p and p.displayLevel) or (battler.mon and battler.mon.level) or 1
|
||||
local ty = tlY + TEXT_Y
|
||||
local gender = healthbox_gender(battler.mon)
|
||||
-- pokefirered/src/battle_interface.c:1531
|
||||
@@ -450,12 +450,21 @@ function Healthbox.draw(side, battler, opts)
|
||||
return
|
||||
end
|
||||
|
||||
local p = nil
|
||||
do
|
||||
local ok, AnimP = pcall(require, "src.core.game3.battle.anim")
|
||||
if ok and AnimP and AnimP.present then
|
||||
p = AnimP.present(side)
|
||||
end
|
||||
end
|
||||
|
||||
local barCx, barCy = hp_bar_center(side, c.x + ox, c.y)
|
||||
local bx, by = hp_bar_top_left(barCx, barCy)
|
||||
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 st1 = (p and p.displayStatus ~= nil) and p.displayStatus or (battler.status or (battler.mon and (battler.mon.status or battler.mon.status1)))
|
||||
if st1 == false or st1 == 0 then st1 = nil end
|
||||
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
|
||||
@@ -464,14 +473,7 @@ function Healthbox.draw(side, battler, opts)
|
||||
BattleChrome.drawHpBar(bx, by, hpNow, hpMax, statusBorder)
|
||||
|
||||
local name = State.displayName(battler)
|
||||
local lv = battler.mon and battler.mon.level or 1
|
||||
do
|
||||
local ok, AnimP = pcall(require, "src.core.game3.battle.anim")
|
||||
if ok and AnimP and AnimP.present then
|
||||
local p = AnimP.present(side)
|
||||
if p and p.displayLevel then lv = p.displayLevel end
|
||||
end
|
||||
end
|
||||
local lv = (p and p.displayLevel) or (battler.mon and battler.mon.level) or 1
|
||||
|
||||
local ty = tlY + TEXT_Y
|
||||
local lvlX = isPlayer and PLAYER_LVL_X or ENEMY_LVL_X
|
||||
@@ -493,7 +495,8 @@ function Healthbox.draw(side, battler, opts)
|
||||
|
||||
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 stObj = (p and p.displayStatus ~= nil) and p.displayStatus or (battler.status or (battler.mon and (battler.mon.status or battler.mon.status1)))
|
||||
if stObj == false or stObj == 0 then stObj = nil end
|
||||
local ailment = SummaryData.statusAilment({ status = stObj, hp = battler.mon and battler.mon.hp })
|
||||
|
||||
if isPlayer then
|
||||
|
||||
@@ -2291,13 +2291,13 @@ local function draw_action_menu(st)
|
||||
local labels = menu_labels((st and st.safari) and "gText_SafariZoneMenu" or "gText_BattleMenu")
|
||||
draw_prompt_text(action_prompt(st, ab), 10, 122)
|
||||
local positions = {
|
||||
{ 136, 122 }, { 184, 122 },
|
||||
{ 136, 138 }, { 184, 138 },
|
||||
{ 136, 122 }, { 192, 122 },
|
||||
{ 136, 138 }, { 192, 138 },
|
||||
}
|
||||
local c = Ui._menuIndex - 1
|
||||
local cursorPos = {
|
||||
{ 128, 122 }, { 176, 122 },
|
||||
{ 128, 138 }, { 176, 138 },
|
||||
{ 128, 122 }, { 184, 122 },
|
||||
{ 128, 138 }, { 184, 138 },
|
||||
}
|
||||
local cp = cursorPos[c + 1] or cursorPos[1]
|
||||
Window.cursorPx(cp[1], cp[2], { colors = FrlgFont.COLOR.NORMAL })
|
||||
|
||||
@@ -383,10 +383,10 @@ function BattleBridge.start(mod, game, foe, opts)
|
||||
end
|
||||
if #battleParty == 0 then return nil, "empty party" end
|
||||
local foeHalf
|
||||
if opts.twoOpponents and opts.trainerIdB and not opts.wild then
|
||||
foe, foeHalf = BattleBridge.twoOpponentFoe(foe, opts.trainerIdB)
|
||||
elseif opts.twoOpponents and opts.frontierFoeHalf and not opts.wild then
|
||||
if opts.twoOpponents and opts.frontierFoeHalf and not opts.wild then
|
||||
foeHalf = opts.frontierFoeHalf
|
||||
elseif opts.twoOpponents and opts.trainerIdB and not opts.wild then
|
||||
foe, foeHalf = BattleBridge.twoOpponentFoe(foe, opts.trainerIdB)
|
||||
end
|
||||
local isDouble = (not opts.wild) and (opts.double or foeHalf or (foe and foe.doubleBattle)) and true or false
|
||||
if isDouble and Party.monsStateToDoubles(linkParty or session.party) ~= Party.PLAYER_HAS_TWO_USABLE_MONS then
|
||||
|
||||
+32
-14
@@ -166,25 +166,43 @@ function Daycare.teachMove(mon, moveId)
|
||||
mon.moves = mon.moves or {}
|
||||
mon.pp = mon.pp or {}
|
||||
mon.maxPp = mon.maxPp or {}
|
||||
for i = 1, #mon.moves do
|
||||
if mon.moves[i] == moveId then return false end
|
||||
|
||||
local moves = {}
|
||||
local pps = {}
|
||||
local maxPps = {}
|
||||
for i = 1, MAX_MON_MOVES do
|
||||
local m = tonumber(mon.moves[i]) or 0
|
||||
if m > 0 then
|
||||
if m == moveId then return false end
|
||||
moves[#moves + 1] = m
|
||||
pps[#pps + 1] = tonumber(mon.pp[i]) or 0
|
||||
maxPps[#maxPps + 1] = tonumber(mon.maxPp[i]) or 0
|
||||
end
|
||||
end
|
||||
|
||||
local Pokemon = pokemonMod()
|
||||
local maxPp = 0
|
||||
if Pokemon.movePp then maxPp = tonumber(Pokemon.movePp(moveId)) or 0 end
|
||||
if #mon.moves < MAX_MON_MOVES then
|
||||
mon.moves[#mon.moves + 1] = moveId
|
||||
mon.pp[#mon.moves] = maxPp
|
||||
mon.maxPp[#mon.moves] = maxPp
|
||||
return true
|
||||
|
||||
if #moves < MAX_MON_MOVES then
|
||||
moves[#moves + 1] = moveId
|
||||
pps[#pps + 1] = maxPp
|
||||
maxPps[#maxPps + 1] = maxPp
|
||||
else
|
||||
-- pokefirered/src/daycare.c:495 DeleteFirstMoveAndGiveMoveToMon
|
||||
table.remove(moves, 1)
|
||||
table.remove(pps, 1)
|
||||
table.remove(maxPps, 1)
|
||||
moves[MAX_MON_MOVES] = moveId
|
||||
pps[MAX_MON_MOVES] = maxPp
|
||||
maxPps[MAX_MON_MOVES] = maxPp
|
||||
end
|
||||
|
||||
for i = 1, MAX_MON_MOVES do
|
||||
mon.moves[i] = moves[i] or 0
|
||||
mon.pp[i] = pps[i] or 0
|
||||
mon.maxPp[i] = maxPps[i] or 0
|
||||
end
|
||||
-- pokefirered/src/daycare.c:495 DeleteFirstMoveAndGiveMoveToMon
|
||||
table.remove(mon.moves, 1)
|
||||
table.remove(mon.pp, 1)
|
||||
table.remove(mon.maxPp, 1)
|
||||
mon.moves[MAX_MON_MOVES] = moveId
|
||||
mon.pp[MAX_MON_MOVES] = maxPp
|
||||
mon.maxPp[MAX_MON_MOVES] = maxPp
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
@@ -34,13 +34,22 @@ local function counter()
|
||||
end
|
||||
|
||||
local function mewGfx()
|
||||
local Constants = lazyReq("src.core.game3.constants")
|
||||
return Constants.of(Constants.versionOf(session())):require("event_objects", "OBJ_EVENT_GFX_MEW")
|
||||
local ok, res = pcall(function()
|
||||
local Constants = lazyReq("src.core.game3.constants")
|
||||
return Constants.of(Constants.versionOf(session())):require("event_objects", "OBJ_EVENT_GFX_MEW")
|
||||
end)
|
||||
return ok and res or nil
|
||||
end
|
||||
|
||||
-- pokeemerald/src/faraway_island.c:335
|
||||
function FarawayIsland.isMew(eo)
|
||||
return onMap() and eo ~= nil and tonumber(eo.graphicsId) == mewGfx()
|
||||
if not (onMap() and eo) then return false end
|
||||
local mgfx = mewGfx()
|
||||
local gid = tonumber(eo.graphicsId)
|
||||
local dgid = eo.def and tonumber(eo.def.graphicsId or eo.def.gfx)
|
||||
if mgfx and (gid == mgfx or dgid == mgfx) then return true end
|
||||
if eo.graphicsId == "OBJ_EVENT_GFX_MEW" or (eo.def and eo.def.graphicsId == "OBJ_EVENT_GFX_MEW") then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
-- pokeemerald/src/faraway_island.c:347
|
||||
|
||||
@@ -901,6 +901,14 @@ function Field.interact(game)
|
||||
-- 1) EventObject (nurse behind counter uses doubled cell; Cut tree / Rock / Boulder)
|
||||
local ox, oy = facing_object_cell(fx, fy, P.facing)
|
||||
local eo = Objects.at(ox, oy)
|
||||
if not eo then
|
||||
local under = Objects.at(P.cellX, P.cellY)
|
||||
local Faraway = package.loaded["src.core.game3.faraway_island"]
|
||||
if under and (under.copy or (Faraway and Faraway.isMew and Faraway.isMew(under))) then
|
||||
eo = under
|
||||
ox, oy = P.cellX, P.cellY
|
||||
end
|
||||
end
|
||||
if eo and eo.def then
|
||||
local gfx = eo.def.graphicsId or eo.def.gfx
|
||||
local FP = lazyReq("src.core.game3.profile").forSession(Field._session)
|
||||
|
||||
@@ -418,8 +418,8 @@ local function collectNeighborActors(actors, baseIndex, hostMapId, hostDef, camX
|
||||
local p = nb.perm[lid]
|
||||
local ox = p and p.x or tonumber(obj.x) or 0
|
||||
local oy = p and p.y or tonumber(obj.y) or 0
|
||||
local out = bounds and (ox < 0 or oy < 0
|
||||
or ox >= bounds.w or oy >= bounds.h)
|
||||
local out = bounds and (ox < -16 or oy < -16
|
||||
or ox >= bounds.w + 16 or oy >= bounds.h + 16)
|
||||
-- src/event_object_movement.c:8014
|
||||
if tonumber(obj.movementType) == 0x4C then out = true end
|
||||
local gid = obj.graphicsId or obj.graphics
|
||||
|
||||
@@ -844,7 +844,7 @@ function Objects.forDraw()
|
||||
local eo = Objects._byId[lid]
|
||||
-- src/event_object_movement.c:8014
|
||||
if eo and eo.visible and not eo.hidden and not eo.invisible
|
||||
and (eo.foreignMap ~= nil or not offMap(Objects._bounds, eo)) then
|
||||
and (eo.foreignMap ~= nil or eo.moving or eo.scriptBusy or (Objects._tracks and Objects._tracks[eo.localId] ~= nil) or not offMap(Objects._bounds, eo)) then
|
||||
n = n + 1
|
||||
list[n] = eo
|
||||
end
|
||||
|
||||
@@ -44,6 +44,8 @@ GfxIds.TO_SPRITE = {
|
||||
[88] = "SPRITE_MOM", -- player's PC / mom-adjacent
|
||||
[89] = "SPRITE_SUPER_NERD", -- CELIO
|
||||
[92] = "SPRITE_POKE_BALL",
|
||||
[108] = "SPRITE_SAILOR", -- OBJ_EVENT_GFX_SEAGALLOP fallback
|
||||
[109] = "SPRITE_SAILOR", -- OBJ_EVENT_GFX_SS_ANNE fallback
|
||||
}
|
||||
|
||||
local S, N, W, E = "down", "up", "left", "right"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
local Strings = require("src.core.Strings")
|
||||
local Std = require("src.core.game3.scripting.stdscripts")
|
||||
local Model = require("src.core.game3.daycare")
|
||||
local Breeding = require("src.core.game3.breeding")
|
||||
@@ -202,7 +201,10 @@ Daycare.BY_NAME = {
|
||||
return false, SPECIES_NONE
|
||||
end
|
||||
setStringVar(ctx, adapters, 1, nicknameOf(mon))
|
||||
return false, Model.take(session, index)
|
||||
local species = Model.take(session, index)
|
||||
local speciesId = tonumber(species) or tonumber(mon.species or mon.speciesId) or 0
|
||||
setResult(ctx, speciesId)
|
||||
return false, speciesId
|
||||
end,
|
||||
-- pokefirered/src/daycare.c:1588 TakePokemonFromRoute5Daycare
|
||||
TakePokemonFromRoute5Daycare = function(ctx, adapters)
|
||||
@@ -219,7 +221,10 @@ Daycare.BY_NAME = {
|
||||
return false, SPECIES_NONE
|
||||
end
|
||||
setStringVar(ctx, adapters, 1, nicknameOf(mon))
|
||||
return false, Model.takeRoute5(session)
|
||||
local species = Model.takeRoute5(session)
|
||||
local speciesId = tonumber(species) or tonumber(mon.species or mon.speciesId) or 0
|
||||
setResult(ctx, speciesId)
|
||||
return false, speciesId
|
||||
end,
|
||||
-- pokefirered/src/daycare.c:594 GetDaycareCost
|
||||
GetDaycareCost = function(ctx, adapters)
|
||||
|
||||
@@ -560,14 +560,61 @@ function TrainerSight.engagePair(game, a, b)
|
||||
local Sp = Space()
|
||||
F.locked = true
|
||||
a.eo.frozen, b.eo.frozen = true, true
|
||||
local tidA, tidB = TrainerSight.getTrainerId(a.eo), TrainerSight.getTrainerId(b.eo)
|
||||
|
||||
local okPyr, Pyramid = pcall(require, "src.core.game3.rse.frontier.pyramid")
|
||||
local inPyramid = okPyr and Pyramid and Pyramid.inPyramid and Pyramid.inPyramid()
|
||||
|
||||
local tidA, tidB
|
||||
local sess = nil
|
||||
local D = nil
|
||||
local partyA, partyB = {}, {}
|
||||
local combinedParty = {}
|
||||
local half = 0
|
||||
local foeAInfo, foeBInfo = {}, {}
|
||||
local nameA, nameB = "", ""
|
||||
local defeatTextA, defeatTextB = nil, nil
|
||||
|
||||
if inPyramid then
|
||||
local Rse = require("src.core.game3.rse.init")
|
||||
sess = Rse.session()
|
||||
D = require("src.core.game3.rse.frontier.data")
|
||||
local F_FACILITY = D.FACILITY
|
||||
tidA = Pyramid.localIdToTrainerId(sess, a.eo.localId)
|
||||
tidB = Pyramid.localIdToTrainerId(sess, b.eo.localId)
|
||||
sess.frontierOpponentA = tidA
|
||||
sess.frontierOpponentB = tidB
|
||||
D.fillTrainerParty(sess, tidA, 0, 1, partyA, { facility = F_FACILITY.PYRAMID })
|
||||
D.fillTrainerParty(sess, tidB, 0, 1, partyB, { facility = F_FACILITY.PYRAMID })
|
||||
for _, m in ipairs(partyA) do combinedParty[#combinedParty + 1] = m end
|
||||
half = #combinedParty
|
||||
for _, m in ipairs(partyB) do combinedParty[#combinedParty + 1] = m end
|
||||
foeAInfo = D.trainerClass(sess, tidA, F_FACILITY.PYRAMID) or {}
|
||||
foeBInfo = D.trainerClass(sess, tidB, F_FACILITY.PYRAMID) or {}
|
||||
nameA = D.trainerName(sess, tidA, F_FACILITY.PYRAMID) or ""
|
||||
nameB = D.trainerName(sess, tidB, F_FACILITY.PYRAMID) or ""
|
||||
defeatTextA = Pyramid.speech(sess, tidA, 1)
|
||||
defeatTextB = Pyramid.speech(sess, tidB, 1)
|
||||
else
|
||||
tidA = TrainerSight.getTrainerId(a.eo)
|
||||
tidB = TrainerSight.getTrainerId(b.eo)
|
||||
end
|
||||
|
||||
local rowA, rowB = battleRow(a.eo), battleRow(b.eo)
|
||||
TrainerSight._pair = { a = tidA, b = tidB }
|
||||
|
||||
local function finishBattle(result)
|
||||
local store = Sp and Sp.store
|
||||
local ctx = Sp and Sp.vm and Sp.vm.ctx
|
||||
TrainerSight._pair = nil
|
||||
F.locked = false
|
||||
a.eo.frozen, b.eo.frozen = false, false
|
||||
if inPyramid then
|
||||
Pyramid.markBattled(sess, tidA, a.eo.localId)
|
||||
Pyramid.markBattled(sess, tidB, b.eo.localId)
|
||||
local N = require("src.core.game3.scripting.natives")
|
||||
sess.battleOutcome = N.outcome_to_code(result or "win")
|
||||
return
|
||||
end
|
||||
if result == "lose" or result == "whiteout" or result == "blackout" then return end
|
||||
-- pokeemerald/src/battle_setup.c:1245
|
||||
for _, tid in ipairs({ tidB, tidA }) do
|
||||
@@ -593,41 +640,102 @@ function TrainerSight.engagePair(game, a, b)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function startBattle()
|
||||
local okT, Trainers = pcall(require, "src.core.game3.scripting.trainers")
|
||||
local foe = okT and Trainers.foeFromId(tidA)
|
||||
local dlgA, dlgB = Trainers.dialogs(tidA) or {}, Trainers.dialogs(tidB) or {}
|
||||
local function text(row, key, dlg)
|
||||
if row and row[key] and Sp and Sp.vm and Sp.vm.getText then
|
||||
local t = Sp.vm:getText(row[key])
|
||||
if t then return t end
|
||||
end
|
||||
return dlg
|
||||
end
|
||||
local Runtime = package.loaded["src.core.game3.runtime"] or require("src.core.game3.runtime")
|
||||
local foe
|
||||
local battleOpts
|
||||
if inPyramid then
|
||||
foe = {
|
||||
party = combinedParty,
|
||||
trainerId = tidA,
|
||||
trainerClass = foeAInfo.class,
|
||||
trainerClassName = foeAInfo.className,
|
||||
trainerName = nameA,
|
||||
trainerPicId = foeAInfo.pic,
|
||||
}
|
||||
battleOpts = {
|
||||
wild = false,
|
||||
trainerId = tidA,
|
||||
trainerIdB = tidB,
|
||||
twoOpponents = true,
|
||||
double = true,
|
||||
frontierFoeHalf = half,
|
||||
pyramid = true,
|
||||
frontier = true,
|
||||
frontierTrainer = { class = foeAInfo.class, className = foeAInfo.className, name = nameA, pic = foeAInfo.pic },
|
||||
frontierTrainerB = { class = foeBInfo.class, className = foeBInfo.className, name = nameB, pic = foeBInfo.pic },
|
||||
defeatText = defeatTextA,
|
||||
defeatTextB = defeatTextB,
|
||||
transitionId = D and D.specialTransition(sess, "B_PYRAMID", combinedParty) or nil,
|
||||
done = finishBattle,
|
||||
}
|
||||
else
|
||||
local okT, Trainers = pcall(require, "src.core.game3.scripting.trainers")
|
||||
foe = okT and Trainers.foeFromId(tidA)
|
||||
local dlgA, dlgB = Trainers.dialogs(tidA) or {}, Trainers.dialogs(tidB) or {}
|
||||
local function text(row, key, dlg)
|
||||
if row and row[key] and Sp and Sp.vm and Sp.vm.getText then
|
||||
local t = Sp.vm:getText(row[key])
|
||||
if t then return t end
|
||||
end
|
||||
return dlg
|
||||
end
|
||||
battleOpts = {
|
||||
wild = false,
|
||||
trainerId = tidA,
|
||||
trainerIdB = tidB,
|
||||
twoOpponents = true,
|
||||
double = true,
|
||||
defeatText = text(rowA, "defeatText", dlgA.defeat),
|
||||
defeatTextB = text(rowB, "defeatText", dlgB.defeat),
|
||||
done = finishBattle,
|
||||
}
|
||||
end
|
||||
-- pokeemerald/src/battle_setup.c:1272
|
||||
local ok, err = require("src.core.game3.battle_bridge").start(Runtime._mod, game or Runtime._game, foe, {
|
||||
wild = false,
|
||||
trainerId = tidA,
|
||||
trainerIdB = tidB,
|
||||
twoOpponents = true,
|
||||
double = true,
|
||||
defeatText = text(rowA, "defeatText", dlgA.defeat),
|
||||
defeatTextB = text(rowB, "defeatText", dlgB.defeat),
|
||||
done = finishBattle,
|
||||
})
|
||||
local ok, err = require("src.core.game3.battle_bridge").start(Runtime._mod, game or Runtime._game, foe, battleOpts)
|
||||
if not ok then
|
||||
print("[game3/trainer_sight] two-trainer battle did not start: " .. tostring(err))
|
||||
finishBattle("lose")
|
||||
end
|
||||
end
|
||||
playEncounterMusic(tidA, rowA)
|
||||
|
||||
local function showIntroA(onDone)
|
||||
if inPyramid then
|
||||
local introA = Pyramid.speech(sess, tidA, 0)
|
||||
require("src.ui.game3.hud").openMessage(nil, introA, { done = onDone })
|
||||
else
|
||||
introSpeech(a.eo, tidA, rowA, onDone)
|
||||
end
|
||||
end
|
||||
|
||||
local function showIntroB(onDone)
|
||||
if inPyramid then
|
||||
local introB = Pyramid.speech(sess, tidB, 0)
|
||||
require("src.ui.game3.hud").openMessage(nil, introB, { done = onDone })
|
||||
else
|
||||
introSpeech(b.eo, tidB, rowB, onDone)
|
||||
end
|
||||
end
|
||||
|
||||
if inPyramid then
|
||||
local Audio = require("src.core.game3.audio")
|
||||
Audio.playSong(Pyramid.encounterMusic(sess, tidA))
|
||||
else
|
||||
playEncounterMusic(tidA, rowA)
|
||||
end
|
||||
|
||||
approach(a.eo, a.dist, a.facing, function()
|
||||
introSpeech(a.eo, tidA, rowA, function()
|
||||
showIntroA(function()
|
||||
-- pokeemerald/src/trainer_see.c:666
|
||||
playEncounterMusic(tidB, rowB)
|
||||
if inPyramid then
|
||||
local Audio = require("src.core.game3.audio")
|
||||
Audio.playSong(Pyramid.encounterMusic(sess, tidB))
|
||||
else
|
||||
playEncounterMusic(tidB, rowB)
|
||||
end
|
||||
approach(b.eo, b.dist, b.facing, function()
|
||||
introSpeech(b.eo, tidB, rowB, startBattle)
|
||||
showIntroB(startBattle)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -776,6 +776,33 @@ do
|
||||
check(st.player.twoTurnMove == nil, "twoTurnMove is cleared after unleash")
|
||||
end
|
||||
|
||||
do
|
||||
-- Solarbeam in Sun (e.g. Groudon Drought) fires in 1 turn and uses unleash animation (turn=1)
|
||||
local sbId = Moves.numForName("SOLARBEAM") or Moves.numForName("SOLAR_BEAM")
|
||||
local st = State.new({
|
||||
wild = true,
|
||||
weather = "SUN",
|
||||
playerParty = { { species = 383, level = 50, hp = 200, maxHp = 200, moves = { sbId }, pp = { 10 },
|
||||
attack = 150, defense = 140, spAtk = 100, spDef = 90, speed = 90 } },
|
||||
foeMon = { species = 19, level = 20, hp = 100, maxHp = 100, moves = { 33 }, pp = { 35 },
|
||||
attack = 20, defense = 50, spAtk = 20, spDef = 50, speed = 20 },
|
||||
})
|
||||
local ad = setup_test_battle(st)
|
||||
local out = {}
|
||||
Engine.resolveMove(st.player, st.enemy, sbId, 1, ad, st, out)
|
||||
check(ad:hp(st.enemy) < 100, "Foe takes damage immediately in Sun without a charge turn")
|
||||
check(st.player.twoTurnMove == nil, "No twoTurnMove locked in Sun")
|
||||
local moveEvents = {}
|
||||
for _, ev in ipairs(ad:events()) do
|
||||
if ev.kind == "move" and ev.moveId == sbId then
|
||||
moveEvents[#moveEvents + 1] = ev
|
||||
end
|
||||
end
|
||||
check(#moveEvents == 1, "Exactly one move event generated for instant Solar Beam")
|
||||
check(moveEvents[1] and moveEvents[1].turn == 1,
|
||||
"Solar Beam animation event uses turn=1 (unleash beam), got turn=" .. tostring(moveEvents[1] and moveEvents[1].turn))
|
||||
end
|
||||
|
||||
do
|
||||
-- Skull Bash raises defense on charge turn 1
|
||||
local sbashId = Moves.numForName("SKULL_BASH")
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env luajit
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
|
||||
print("[test] 1. GfxIds maps OBJ_EVENT_GFX_SEAGALLOP (108)")
|
||||
local sprName = GfxIds.spriteFor(108)
|
||||
check(sprName == "SPRITE_SAILOR", "108 maps to SPRITE_SAILOR fallback (got " .. tostring(sprName) .. ")")
|
||||
|
||||
print("[test] 2. offMap bounds checking")
|
||||
local bounds = { w = 24, h = 20 }
|
||||
check(not Objects.offMap(bounds, { cellX = 10, cellY = 12 }), "x=10 is on-map")
|
||||
check(not Objects.offMap(bounds, { cellX = 0, cellY = 0 }), "x=0, y=0 is on-map")
|
||||
check(Objects.offMap(bounds, { cellX = 24, cellY = 12 }), "x=24 is off-map on 24x20 map")
|
||||
check(Objects.offMap(bounds, { cellX = -1, cellY = 12 }), "x=-1 is off-map")
|
||||
|
||||
print("[test] 3. Cinnabar Island Seagallop spawn & arrival movement")
|
||||
local cinnabarMapDef = {
|
||||
midLayout = { width = 24, height = 20 },
|
||||
objects = {
|
||||
{ localId = 1, graphicsId = 23, x = 14, y = 6, movementType = 5, flag = 0 },
|
||||
{ localId = 2, graphicsId = 32, x = 11, y = 11, movementType = 1, flag = 0 },
|
||||
{ localId = 3, graphicsId = 73, x = 20, y = 7, movementType = 10, flag = 0x27 }, -- Bill
|
||||
{ localId = 4, graphicsId = 108, x = 23, y = 7, elevation = 1, movementType = 9, flag = 0x28 }, -- Seagallop
|
||||
}
|
||||
}
|
||||
|
||||
local store = Flags.newStore()
|
||||
Flags.setFlag(store, nil, 0x28, true) -- FLAG_HIDE_CINNABAR_SEAGALLOP = true
|
||||
Space.store = store
|
||||
Space.mapId = "CinnabarIsland"
|
||||
|
||||
Objects.clear()
|
||||
Objects.loadMap(nil, "CinnabarIsland", cinnabarMapDef)
|
||||
|
||||
-- Initially Seagallop is hidden by flag 0x28
|
||||
local drawList = Objects.forDraw()
|
||||
local foundSeagallop = false
|
||||
for _, eo in ipairs(drawList) do
|
||||
if eo.localId == 4 then foundSeagallop = true end
|
||||
end
|
||||
check(not foundSeagallop, "Seagallop is initially hidden before addobject")
|
||||
|
||||
-- Script runs: setobjectxyperm 4, 30, 12 then addobject 4
|
||||
Objects.setObjectXY(4, 30, 12)
|
||||
Objects.addObject(4)
|
||||
local boatEo = Objects.find(4)
|
||||
check(boatEo and boatEo.cellX == 30 and boatEo.cellY == 12, "Seagallop coordinates set to (30, 12)")
|
||||
|
||||
-- Script runs: applymovement 4, CinnabarIsland_Movement_BoatArrive (5 steps left: 30 -> 25)
|
||||
local arriveBytes = {
|
||||
0x1B, -- delay_16
|
||||
0x1B, -- delay_16
|
||||
0x1F, -- walk_fast_left
|
||||
0x1F, -- walk_fast_left
|
||||
0x12, -- walk_left
|
||||
0x0A, -- walk_slower_left
|
||||
0x0A, -- walk_slower_left
|
||||
0xFE -- step_end
|
||||
}
|
||||
|
||||
local movementFinished = false
|
||||
Objects.applyMovement(4, arriveBytes, function()
|
||||
movementFinished = true
|
||||
end)
|
||||
|
||||
drawList = Objects.forDraw()
|
||||
foundSeagallop = false
|
||||
for _, eo in ipairs(drawList) do
|
||||
if eo.localId == 4 then foundSeagallop = true end
|
||||
end
|
||||
check(foundSeagallop, "Seagallop moving from off-map (30, 12) is included in forDraw() while tracked")
|
||||
|
||||
-- Step through frames until movement completes
|
||||
local maxFrames = 300
|
||||
local frames = 0
|
||||
while not movementFinished and frames < maxFrames do
|
||||
Objects.update(nil)
|
||||
frames = frames + 1
|
||||
end
|
||||
|
||||
check(movementFinished, "Boat arrival movement completed in " .. frames .. " frames")
|
||||
check(boatEo and boatEo.cellX == 25 and boatEo.cellY == 12,
|
||||
"Boat arrived at shoreline (25, 12), got (" .. tostring(boatEo and boatEo.cellX) .. ", " .. tostring(boatEo and boatEo.cellY) .. ")")
|
||||
|
||||
drawList = Objects.forDraw()
|
||||
foundSeagallop = false
|
||||
for _, eo in ipairs(drawList) do
|
||||
if eo.localId == 4 then foundSeagallop = true end
|
||||
end
|
||||
check(foundSeagallop, "Seagallop at (25, 12) is visible and drawn at shoreline")
|
||||
|
||||
if failed > 0 then
|
||||
print("\nFAIL: " .. failed .. " assertions failed")
|
||||
os.exit(1)
|
||||
else
|
||||
print("\nPASS: all Cinnabar Seagallop tests passed")
|
||||
end
|
||||
Reference in New Issue
Block a user