CLOSES #1478, CLOSES #1479, CLOSES #1488, CLOSES #1510, CLOSES #1511, CLOSES #1514, CLOSES #1521, CLOSES #1522, CLOSES #1545, CLOSES #1557, CLOSES #1563, CLOSES #1565, CLOSES #1566, CLOSES #1577, CLOSES #1594, CLOSES #1596, CLOSES #1606, CLOSES #1608

This commit is contained in:
bryanthaboi
2026-08-20 17:30:18 -04:00
parent 0f8f6d0e4f
commit dbecc345e3
53 changed files with 881 additions and 225 deletions
+1
View File
@@ -8,6 +8,7 @@ local BattleSafety = {}
local BATTLE_BUSY_FIELDS = {
"current", "afterQueue", "nextInsert", "pendingHit", "waitingUI",
"waitingSound", "waitFrames", "draining", "animPlaying", "growIn",
"shrinkOut",
"introSlide", "ghostReveal", "mimicCtx", "mimicMoves", "result",
}
+45 -14
View File
@@ -2495,9 +2495,12 @@ function BattleState:openOldManBag()
-- POKé BALLs; pokeyellow's SimulatedInputBattleItemList, shared by
-- the Viridian tutorial and Oak's catch, has one.
local qty = require("src.core.GameVersion").isYellow() and "x1" or "x50"
-- the tutorial bag rides DisplayBagMenu's LIST_MENU_BOX over the battle
-- screen (engine/battle/core.asm:2210)
list = ListMenu.new(game, "ITEMS", {
{ value = "POKE_BALL", label = Strings("POKé BALL"), right = qty },
}, {
itemBox = true,
script = function(l)
l.scriptTimer = (l.scriptTimer or 0) + 1
if l.scriptTimer == 81 then
@@ -3329,15 +3332,18 @@ end
function BattleState:queueRetreatAnim()
if self:starterPikachuSendOut() then
self:actNext(function() self:slidePic("playerMon", 0, -64, 8, 3) end)
self.nextInsert = (self.nextInsert or 0) + 1
table.insert(self.queue, self.nextInsert, { wait = 24 })
self:actNext(function() self:slidePic("playerMon") end)
self:waitNext(24)
self:actNext(function()
-- .clearScreenArea keeps the 7x7 area blank until the swap
-- (pokeyellow core.asm:1867-1871) (#1545)
self.sendingOut = true
self:slidePic("playerMon")
end)
else
self:actNext(function()
self.shrinkOut = { battler = self.player, frame = 0 }
end)
self.nextInsert = (self.nextInsert or 0) + 1
table.insert(self.queue, self.nextInsert, { wait = 7 })
self:waitNext(7)
end
end
@@ -3558,10 +3564,10 @@ function BattleState:updateFx()
if self.growIn.frame >= 12 then self.growIn = nil end
end
-- the retreat shrink (AnimateRetreatingPlayerMon): 4+3 frames, then the
-- 7x7 area is cleared for good
-- 7x7 area holds cleared (scale 0) until the swap replaces the battler
if self.shrinkOut then
self.shrinkOut.frame = self.shrinkOut.frame + 1
if self.shrinkOut.frame >= 7 then self.shrinkOut = nil end
if self.shrinkOut.battler ~= self.player then self.shrinkOut = nil end
end
-- low-HP alarm (audio/low_health_alarm.asm): the two-tone siren
-- loops while the player's bar is red; see lowHealthAlarmActive
@@ -3939,7 +3945,9 @@ function BattleState:performMove(user, target, moveInst, isCalled)
-- = false) queues it from applyMimic after a successful copy
if not (record and record.announceAnim == false) then
self.nextInsert = (self.nextInsert or 0) + 1
self.moveAnimRow = { anim = move.id, attackerIsPlayer = user.isPlayer }
-- ld a, THRASH / ld [wPlayerMoveNum] (core.asm:3534-3535, :5909-5910) #1577
self.moveAnimRow = { anim = thrashing and "THRASH" or move.id,
attackerIsPlayer = user.isPlayer }
table.insert(self.queue, self.nextInsert, self.moveAnimRow)
end
Runtime.emit("battle.move_used", {
@@ -4225,7 +4233,9 @@ function BattleState:awardExp()
local playerId = self.game.save.player and self.game.save.player.id
-- GainExperience (engine/battle/experience.asm:69-88) compares the
-- stored MON_OTID against wPlayerID every award; no persistent flag
local traded = mon.otId ~= nil and playerId ~= nil and mon.otId ~= playerId
-- mon.traded covers otId-less mons (repairTradedOtIds, old link peers) #1488
local traded = playerId ~= nil and ((mon.otId ~= nil and mon.otId ~= playerId)
or (mon.otId == nil and mon.traded == true))
local levels, gained = Experience.apply(self.data, mon, self.enemy.def,
self.enemy.mon.level, self.kind == "trainer",
split, traded)
@@ -4397,7 +4407,8 @@ function BattleState:enemyMonFainted()
forceSwitch = true,
onSwitch = function(mon)
if mon == self.player.mon then
reopenShift(Strings("%s is\nalready out!", self.player.name))
reopenShift(self:romText("_AlreadyOutText",
"%s is\nalready out!", self.player.name))
elseif mon.hp <= 0 then
reopenShift(self:romText("_NoWillText", "There's no will\nto fight!"))
else
@@ -4524,9 +4535,22 @@ function BattleState:enemyMonFainted()
-- TrainerNamePointers aims those entries at wTrainerName). The tag
-- prints once, so a `para` page carries no second copy (#566).
local tag = self.trainer and self.trainer.name
-- the badge jingle (sound_get_item_1 and friends) rides the armed
-- line's first page, as the script's text command would (#1606)
local sfx = self.endBattleSound
local data = self.data
for page in (self.endBattleText .. "\f"):gmatch("(.-)\f") do
if page ~= "" then
self:sayNext(tag and (tag .. ": " .. page) or page)
local line = tag and (tag .. ": " .. page) or page
if sfx then
local id = sfx
self:sayNextWaitSfx(line, function()
return require("src.core.Sound").play(data, id)
end)
sfx = nil
else
self:sayNext(line)
end
tag = nil
end
end
@@ -5144,7 +5168,8 @@ function BattleState:openParty()
onSwitch = function(mon)
-- PartyMenuOrRockOrRun's SWITCH .partyMonDeselected (core.asm:2396-2408)
if mon == self.player.mon then
self:say(Strings("%s is\nalready out!", self.player.name))
self:say(self:romText("_AlreadyOutText",
"%s is\nalready out!", self.player.name))
self:act(function() self:openParty() end)
elseif mon.hp <= 0 then
self:say(self:romText("_NoWillText", "There's no will\nto fight!"))
@@ -5297,7 +5322,9 @@ end
function BattleState:shrinkOutScale(battler)
local shrink = self.shrinkOut
if not shrink or shrink.battler ~= battler then return nil end
return shrink.frame < 4 and 5 / 7 or 3 / 7
-- scale 0 past Delay3: the area stays cleared until the swap
-- (core.asm:1790-1796) (#1563)
return shrink.frame < 4 and 5 / 7 or shrink.frame < 7 and 3 / 7 or 0
end
-- battler hidden this frame? (damage blink)
@@ -5942,8 +5969,12 @@ function BattleState:drawPicsLayer(slide, sx, sy, onlySide, skipMenuClip)
-- the AnimateRetreatingPlayerMon shrink (core.asm:1769-1796)
local eff = s * gs
if eff > 0 then
-- the retreat stages sit one tile right of the grow-in's
-- (hlcoord 3,7 / 4,9 vs 2,7 / 3,9, core.asm:1770-1788) (#1563)
local shrinkX = self.shrinkOut
and self.shrinkOut.battler == self.player and 8 or 0
love.graphics.draw(img,
8 - padL * s + img:getWidth() * s * (1 - gs) / 2 + sx,
8 + shrinkX - padL * s + img:getWidth() * s * (1 - gs) / 2 + sx,
96 - (img:getHeight() - pad) * eff + sy, 0, eff, eff)
end
else
+5 -3
View File
@@ -108,13 +108,17 @@ end
-- The damaging pipeline, extracted from the performMove monolith: every
-- stage keeps the original's exact check order and rng consumption
-- (invulnerability -> gate -> hit count -> pre-accuracy -> accuracy ->
-- (pre-accuracy -> invulnerability -> gate -> hit count -> accuracy ->
-- damage choice -> hits -> messages -> after-damage -> secondary run).
function EffectRegistry.runDamaging(battle, ctx, record)
local user, target = ctx.user, ctx.target
local move, moveInst = ctx.move, ctx.moveInst
local neverMiss = record and record.neverMiss
-- SpecialEffectsCont's JumpMoveEffect (core.asm:3129-3133) runs before
-- MoveHitTest's INVULNERABLE test (:3150), mid-Fly/Dig included (#1565)
if record and record.beforeAccuracy then record.beforeAccuracy(ctx) end
-- Swift ignores semi-invulnerability (MoveHitTest returns hit for
-- SWIFT_EFFECT before the INVULNERABLE check)
if target.invulnerable and not neverMiss then
@@ -143,8 +147,6 @@ function EffectRegistry.runDamaging(battle, ctx, record)
local hits = hitCount(ctx, record)
if record and record.beforeAccuracy then record.beforeAccuracy(ctx) end
if not neverMiss then
if not battle:accuracyRoll(move, user, target) then
-- Explosion/Selfdestruct still animate on a miss (HandleIfPlayerMoveMissed)
+12 -4
View File
@@ -892,10 +892,12 @@ Battle.PRIORITY = {
EFFECT_ENDURE = 3,
EFFECT_COUNTER = -1,
EFFECT_MIRROR_COAT = -1,
EFFECT_FORCE_SWITCH = -1, -- Whirlwind, Roar: priority 0, below BASE
}
function Battle:movePriority(moveId)
-- engine/battle/core.asm:786 GetMovePriority, `cp VITAL_THROW / ld a, 0`.
-- GetMovePriority `cp VITAL_THROW / ld a, 0 / ret z`
-- (engine/battle/core.asm:787-789).
if moveId == "VITAL_THROW" then return -1 end
local def = self:moveDef(moveId)
return (def and Battle.PRIORITY[def.effect]) or 0
@@ -2410,6 +2412,7 @@ Battle.MOVE_EFFECTS.EFFECT_BATON_PASS = function(self, attacker)
local sent = side == "player" and self.player or self.enemy
self:emit({ kind = "send", side = side, mon = sent,
hp = sent.hp or 0, status = sent.status or false,
level = sent.level, experience = sent.experience,
text = "Go! " .. self:monName(sent) .. "!" })
end
@@ -2670,6 +2673,7 @@ Battle.MOVE_EFFECTS.EFFECT_FORCE_SWITCH = function(self, attacker, defender,
end
self:emit({ kind = "send", side = self:sideOf(incoming), mon = incoming,
hp = incoming.hp or 0, status = incoming.status or false,
level = incoming.level, experience = incoming.experience,
text = self:monName(incoming) .. " was dragged out!" })
self:breakTrapsOnSend(incoming)
self:spikesDamage(incoming)
@@ -3117,6 +3121,7 @@ function Battle:resolveFaints()
self:emit({ kind = "send", side = "enemy", mon = self.enemy,
replacement = true,
hp = self.enemy.hp or 0, status = self.enemy.status or false,
level = self.enemy.level, experience = self.enemy.experience,
text = (self.trainer and self.trainer.name or "Foe") .. " sent out "
.. self:monName(self.enemy) .. "!" })
Runtime.emit("battle.battler_switched", {
@@ -3196,11 +3201,12 @@ function Battle:printWinLossText(result)
if not trainer then return end
-- The DEBUG_BATTLE_F skip sits in front of PrintWinLossText alone, behind
-- the slide (engine/battle/core.asm:2310, :2320-2323).
self:emit({ kind = "trainer-return" })
-- The CANLOSE loss arm runs ClearBox first (:2770-2773).
self:emit({ kind = "trainer-return", cleared = result == "lose" or nil })
local text = (result == "lose") and trainer.lossText or trainer.winText
if type(text) ~= "string" or text == "" then return end
-- FarPrintText prints the pointer alone: no trainer-name tag in front of it,
-- unlike Gen 1's TrainerEndBattleText (pokered home/trainers.asm:341).
-- FarPrintText prints the pointer alone: no trainer-name tag in front of
-- it, unlike Gen 1's TrainerEndBattleText (pokered home/trainers.asm:355).
self:emit({ kind = "win-text", text = text })
end
@@ -3526,6 +3532,7 @@ function Battle:switch(index)
self.stages.player = Battle.newStages()
self:emit({ kind = "send", side = "player", mon = mon,
hp = mon.hp or 0, status = mon.status or false,
level = mon.level, experience = mon.experience,
text = "Go! " .. self:monName(mon) .. "!" })
-- battle.battler_switched, the payload BattleState:resolveSwitch emits on
-- Gen 1: the side record, whoever walked in, and whoever walked out.
@@ -3991,6 +3998,7 @@ function Battle:enemyTrySwitchOrItem()
self.stages.enemy = Battle.newStages()
self:emit({ kind = "send", side = "enemy", mon = self.enemy,
hp = self.enemy.hp or 0, status = self.enemy.status or false,
level = self.enemy.level, experience = self.enemy.experience,
text = (self.trainer.name or "TRAINER") .. " sent out "
.. self:monName(self.enemy) .. "!" })
Runtime.emit("battle.battler_switched", {
+4 -3
View File
@@ -609,9 +609,10 @@ function Game:draw()
-- ...and for the same reason the UI's own scale has to know the world is
-- still the backdrop while an opaque menu covers it. Renderer:uiScale
-- steps the UI down with the survey zoom only while a world is behind it,
-- gated on this frame's world pass -- which the party menu and the bag end
-- by being opaque. Without this hold they lose the step-down and blit at
-- full fit scale over a battle drawn at the zoomed-out one.
-- gated on this frame's world pass -- which the party menu ends by being
-- opaque (the bag's item box shows the map around it, #1521). Without
-- this hold it loses the step-down and blits at full fit scale over a
-- battle drawn at the zoomed-out one.
Renderer.uiWorldHold = Renderer.battleDim ~= nil
-- ...and a battle keeps its dialogue box and YES/NO inside its own screen
-- instead of letting them dock to the window edge.
+9
View File
@@ -220,6 +220,9 @@ function Save.newGame(opts)
phoneContacts = {},
tradeFlags = {},
pokedex = { seen = {}, caught = {} },
-- wLastDexMode (engine/pokedex/pokedex.asm:59-61): the sort mode the
-- #DEX reopens in. NEW_MODE is the cart's zero byte.
lastDexMode = "NEW",
-- wUnownDex: the distinct Unown FORMS caught, in catching order. A second
-- record beside the #DEX because the #DEX knows only the species
-- (src/core/gen2/Unown.lua).
@@ -685,6 +688,12 @@ function Save.validate(save)
scrubEvents(save, report)
scrubMapScenes(save, report)
scrubPlayerState(save, report)
-- wLastDexMode: only the three modes the #DEX has (PokedexMenu MODES);
-- a hand-edited value falls back to NEW_MODE, the cart's zero byte
if save.lastDexMode ~= "NEW" and save.lastDexMode ~= "OLD"
and save.lastDexMode ~= "A-Z" then
save.lastDexMode = "NEW"
end
-- The `mailmsg` structs get the same treatment for the same reason: their
-- `type` byte is an item id nothing else in the save vouches for, and a
-- party key outside 1..6 or a MAILBOX past MAILBOX_CAPACITY is a region the
+25 -7
View File
@@ -161,16 +161,34 @@ end
-- Headless (no real graphics) falls back to the plain text flow.
function Evolution.evolve(game, mon, newSpecies, onDone, via)
local oldName = mon.nickname or game.data.pokemon[mon.species].name
-- IsEvolvingText, DelayFrames 50, then ClearScreenArea before EvolveMon
-- (engine/pokemon/evos_moves.asm:120-134)
-- IsEvolvingText, DelayFrames 50; ClearScreenArea then wipes rows 0-11
-- ONLY, so the box rides through EvolveMon (evos_moves.asm:120-134)
local isEvolving = romText(game.data, "_IsEvolvingText",
"What?\n%s is\nevolving!", oldName)
if love.image and love.image.newImageData then
game.stack:push(TextBox.new(game, isEvolving, function()
-- forward `via` so EvolutionState can keep trade evolutions
-- non-cancelable (LINK_STATE_TRADING) while others accept B (#213)
Screens.push(game, "EvolutionState", mon, newSpecies, onDone, via)
end, { auto = { delay = EVOLVING_TEXT_FRAMES } }))
local intro
intro = TextBox.new(game, isEvolving, nil, { stay = {
onShown = function()
-- DelayFrames 50 with the box and the old screen still up
-- (evos_moves.asm:122-123)
local hold = { t = 0 }
hold.update = function()
hold.t = hold.t + 1
if hold.t < EVOLVING_TEXT_FRAMES then return end
game.stack:pop() -- this hold
-- forward `via` so trade evolutions stay non-cancelable while
-- others accept B (evos_moves.asm:72-75) (#213)
Screens.push(game, "EvolutionState", mon, newSpecies, function()
-- the result/cancel box owns the intro box's pop (#1596)
if game.stack:top() == intro then game.stack:pop() end
if onDone then onDone() end
end, via)
end
hold.draw = function() end
game.stack:push(hold)
end,
} })
game.stack:push(intro)
return
end
Music.play(game.data, Music.special(game.data, "evolution"))
+13 -2
View File
@@ -44,7 +44,8 @@ local NAME_DELAYS = { FAST = 1, MID = 3, SLOW = 5 }
-- waits for nothing, shows no blinking arrow, and never pops itself --
-- whoever pushed it owns the pop. stay.onShown fires once, on the frame
-- the last page finishes typing, which is where the caller pushes whatever
-- goes on top of it (#591).
-- goes on top of it (#591). stay.prompt waits out one arrowed A/B press
-- first (TextCommand_PROMPT_BUTTON, home/text.asm:434-444) (#1511).
function TextBox.new(game, text, onDone, opts)
local self = setmetatable({}, TextBox)
self.game = game
@@ -296,6 +297,15 @@ function TextBox:update(dt)
-- exactly once (#591)
if self.stay then
if not self.stayShown then
-- stay.prompt: arrowed A/B wait, then the box stays up
-- (TextCommand_PROMPT_BUTTON, home/text.asm:434-444)
if self.stay.prompt
and not (input:wasPressed("a") or input:wasPressed("b")) then
return
end
if self.stay.prompt then
require("src.core.Sound").play(self.game.data, "Press_AB")
end
self.stayShown = true
if self.stay.onShown then self.stay.onShown() end
end
@@ -494,7 +504,8 @@ function TextBox:draw()
Font.draw(money, 152 - Font.width(money), 8)
end
if (self.waiting or (self.done and not self.choice and not self.auto
and not self.stay))
and (not self.stay
or (self.stay.prompt and not self.stayShown))))
and self.blink < 30 then
-- page-advance cursor: glyph $EE by default, the blinking down arrow
-- the original prints via `ld a, "▼"` (home/text.asm)
+2 -1
View File
@@ -1694,7 +1694,8 @@ H.RandomPhoneWildMon = function(vm)
local entry = contact and contact.map and grass and grass[contact.map]
local slots = entry and entry.slots
if not slots then return end
local daytime = (w and w.daytime) or "DAY"
-- wTimeOfDay, not the palette pin (wildmons.asm:861)
local daytime = (w and (w.tod or w.daytime)) or "DAY"
if daytime == "DARK" then daytime = "NITE" end
local slot = (slots[daytime] or slots.DAY or {})[Specials.random(4)]
if slot and slot.species then nameSpecies(vm, slot.species) end
+8 -3
View File
@@ -1118,7 +1118,9 @@ local function runCmd(self, cmd, op)
-- really does run here.
if self.reloadMapFn then self.reloadMapFn(true) end
elseif op == "winlosstext" then
-- Overrides the struct's win/loss text for this battle only.
-- Overrides the struct's win/loss text for this battle only; a 0
-- argument zeroes that pointer (engine/overworld/scripting.asm:651)
self.winLossArmed = true
self.winTextOverride = cmd.winText
self.lossTextOverride = cmd.lossText
elseif op == "trainertext" then
@@ -1126,9 +1128,11 @@ local function runCmd(self, cmd, op)
local obj = self.trainerObject or {}
local key
if which == 1 then
key = self.winTextOverride or obj.winText
key = self.winLossArmed and self.winTextOverride
or (not self.winLossArmed and obj.winText or nil)
elseif which == 2 then
key = self.lossTextOverride or obj.lossText
key = self.winLossArmed and self.lossTextOverride
or (not self.winLossArmed and obj.lossText or nil)
else
key = obj.seenText
end
@@ -2429,6 +2433,7 @@ function Vm:start(scriptKey)
self.battleOutcome = nil
self.winTextOverride = nil
self.lossTextOverride = nil
self.winLossArmed = nil
-- The whiteout abort is per-run too: a script that ended because the player
-- was wiped must not stop the next one before it starts.
self.aborted = false
+4 -2
View File
@@ -339,9 +339,11 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker)
{ kind = "levelup" })
-- the party menu stays up through TryEvolvingMon and only
-- comes down at RemoveUsedItem (item_effects.asm:1392-1418)
closePicker()
if evoTo then
Evolution.evolve(game, target, evoTo, nil, evo and evo.method)
Evolution.evolve(game, target, evoTo, closePicker,
evo and evo.method)
else
closePicker()
end
return
end
+4 -3
View File
@@ -13,9 +13,10 @@
local Music = require("src.core.Music")
local romText = require("src.core.RomText")
-- Not opaque: ClearScreenArea wipes rows 0-11 only (evos_moves.asm:126-128),
-- so the "is evolving!" box beneath stays visible through the flash (#1596).
local EvolutionState = {}
EvolutionState.__index = EvolutionState
EvolutionState.isOpaque = true
-- SGB: SetPal_PokemonWholeScreen for the mon on display
function EvolutionState:sgbPalettes(game)
@@ -147,7 +148,8 @@ end
function EvolutionState:draw()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
-- rows 0-11 only (hlcoord 0,0 / lb bc, 12, 20, evos_moves.asm:126-128)
love.graphics.rectangle("fill", 0, 0, 160, 96)
-- accelerating flash between the two forms
local sprite, spriteTrueColor
@@ -172,7 +174,6 @@ function EvolutionState:draw()
require("src.render.PaletteFX").markTrueColor(x, y, sprite:getDimensions())
end
end
love.graphics.setColor(1, 1, 1, 1)
end
return EvolutionState
+12 -3
View File
@@ -94,7 +94,15 @@ function ListMenu.new(game, title, items, opts)
-- the bag's item list: a partial box the map stays visible around, not a
-- screen of its own (home/list_menu.asm:29-31)
self.itemBox = opts.itemBox or false
if self.itemBox then self.isOpaque = false end
if self.itemBox then
self.isOpaque = false
-- keep RunDefaultPaletteCommand's last palette: ItemMenuLoop never sets
-- its own (engine/menus/start_sub_menus.asm:300)
self.sgbPalettes = false
-- wMaxMenuItem is 2 for item lists; the fourth printed row is a
-- look-ahead the cursor cannot reach (home/list_menu.asm:46-48)
self.cursorRows = 3
end
self.rows = opts.rows or (self.itemBox and ITEM_ROWS)
or ((opts.dialogue or opts.messageBox) and 4 or ROWS)
return self
@@ -113,8 +121,9 @@ local function moveIndex(self, delta)
end
local function syncScroll(self)
if self.index - self.scroll > self.rows then
self.scroll = self.index - self.rows
local maxRow = self.cursorRows or self.rows
if self.index - self.scroll > maxRow then
self.scroll = self.index - maxRow
end
if self.index - self.scroll < 1 then self.scroll = self.index - 1 end
end
+7 -1
View File
@@ -119,8 +119,14 @@ function Menu:draw()
self.tw * 8, self.th * 8, self.anchor)
end
Font.drawBox(self.tx, self.ty, self.tw, self.th)
-- PlaceString at hlcoord 3,0 writes over the border row it was just
-- drawn on (oak_speech2.asm:162-170)
if self.title then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", (self.tx + 3) * 8, self.ty * 8,
#Font.split(self.title) * 8, 8)
end
love.graphics.setColor(0, 0, 0, 1)
-- PlaceString at hlcoord 3,0 writes over the border row it was just drawn on
if self.title then
Font.draw(self.title, (self.tx + 3) * 8, self.ty * 8)
end
+5 -3
View File
@@ -96,15 +96,17 @@ function NamingScreen:enter()
onSelect = function()
-- the menu already popped itself; pop the naming screen too
self.game.stack:pop()
if self.onDone then self.onDone(preset) end
if self.onDone then self.onDone(preset, false) end
end,
})
end
if self.introBox then
-- DisplayIntroNameTextBox (oak_speech2.asm:162): TextBoxBorder at
-- hlcoord 0,0 with b=$a c=$9, "NAME" at hlcoord 3,0, list at hlcoord 2,2
-- TextBoxBorder's b = $a is a fixed 12-row box, whatever the preset
-- list's length (oak_speech2.asm:163-166)
self.game.stack:push(Menu.new(self.game, items, {
tx = 0, ty = 0, tw = 11, th = #items * 2 + 4,
tx = 0, ty = 0, tw = 11, th = 12,
itemY = 2, title = Strings("NAME"), cancelable = false,
}))
else
@@ -135,7 +137,7 @@ function NamingScreen:confirm()
end
Sound.play(self.game.data, "Press_AB")
self.game.stack:pop()
if self.onDone then self.onDone(name) end
if self.onDone then self.onDone(name, true) end
end
function NamingScreen:grid()
+43 -9
View File
@@ -43,6 +43,12 @@ local PicSlide = {}
PicSlide.__index = PicSlide
function PicSlide:update(dt)
-- OakSpeechSlidePicLeft: ClearScreenArea, ld c, 10 / DelayFrames, Delay3
-- before the first slide step (oak_speech2.asm:69-78)
if (self.delay or 0) > 0 then
self.delay = self.delay - 1
return
end
self.t = self.t + 1
local tiles = math.min(SLIDE_TILES, math.floor(self.t / SLIDE_FRAMES))
self.speech.picSlide = (self.dir > 0 and tiles or (SLIDE_TILES - tiles)) * 8
@@ -172,8 +178,8 @@ function OakSpeech.defaultSteps(speech)
kind = "say",
textKey = "_IntroducePlayerText",
pic = "player",
-- oak_speech.asm:89-92: MovePicLeft, then IntroducePlayerText ends in
-- text_end, so PrintText returns with the box still up under the names
-- oak_speech.asm:89-92: MovePicLeft, then IntroducePlayerText's
-- `prompt` (text_2.asm:1730) waits for A and leaves the box up
reveal = "wipe",
stay = true,
},
@@ -192,6 +198,9 @@ function OakSpeech.defaultSteps(speech)
id = "confirm_player_name",
kind = "say",
textKey = "_YourNameIsText",
-- _YourNameIsText's `prompt` (text_2.asm:1766), then GBFadeOutToWhite
-- / ClearScreen with the box still up (oak_speech.asm:93-94)
fadeOut = true,
},
{
id = "ask_rival_name",
@@ -199,7 +208,7 @@ function OakSpeech.defaultSteps(speech)
textKey = "_IntroduceRivalText",
pic = "rival",
-- oak_speech.asm:98-101: FadeInIntroPic, then IntroduceRivalText's
-- text_end leaves the box up for ChooseRivalName
-- `prompt` (text_2.asm:1740) leaves the box up for ChooseRivalName
reveal = "fade",
stay = true,
},
@@ -216,6 +225,9 @@ function OakSpeech.defaultSteps(speech)
id = "confirm_rival_name",
kind = "say",
textKey = "_HisNameIsText",
-- _HisNameIsText's `prompt` (text_2.asm:1772) then the .skipSpeech
-- fade with the box up (oak_speech.asm:103-104)
fadeOut = true,
},
{
id = "legend",
@@ -403,9 +415,20 @@ function OakSpeech:runStep(step)
self:applyPic(step)
self:afterReveal(step, function()
self:runCry(step)
if step.stay then
if step.stay or step.fadeOut then
local box = TextBox.new(self.game, self:stepText(step), nil,
{ stay = { onShown = function() self:advance() end } })
{ stay = { prompt = true, onShown = function()
if step.fadeOut then
-- GBFadeOutToWhite / ClearScreen (oak_speech.asm:93-94)
self.game.stack:push(require("src.render.Transition")
.whiteFlash(self.game, nil, function()
self:closeHoldBox()
self:advance()
end))
else
self:advance()
end
end } })
self.holdBox = box
self.game.stack:push(box)
else
@@ -432,16 +455,26 @@ function OakSpeech:runStep(step)
presets = presets,
introBox = true,
maxLen = step.maxLen or self.nameLen,
onDone = function(name)
onDone = function(name, custom)
if who == "rival" then
self.game.save.player.rival = name
else
self.game.save.player.name = name
end
self:recordAnswer(step, 1, name, name)
-- YourNameIsText/HisNameIsText print into the box this one held
-- YourNameIsText / HisNameIsText print into the box this one
-- held (oak_speech2.asm:26-28, :59-61)
self:closeHoldBox()
self:slidePic(-1, function() self:advance() end)
if custom then
-- .customName: ClearScreen / Delay3 / pic recentered, no
-- slide-back (oak_speech2.asm:21-25)
self.picSlide = 0
self:advance()
else
-- OakSpeechSlidePicLeft's 13-frame pre-slide beat
-- (oak_speech2.asm:69-78)
self:slidePic(-1, function() self:advance() end, 13)
end
end,
})
end
@@ -557,10 +590,11 @@ function OakSpeech:revealPic(kind, next)
end
-- ..(engine/movie/oak_speech/oak_speech2.asm ln 67)
function OakSpeech:slidePic(dir, onDone)
function OakSpeech:slidePic(dir, onDone, delay)
self.picSlide = (dir > 0 and 0 or SLIDE_TILES * 8)
self.game.stack:push(setmetatable({
game = self.game, speech = self, dir = dir, t = 0, onDone = onDone,
delay = delay,
}, PicSlide))
end
+51 -33
View File
@@ -11,6 +11,7 @@ local Renderer = require("src.render.Renderer")
local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens")
local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local StartMenu = {}
@@ -19,6 +20,7 @@ local function sameItems(_, items) return items end
function StartMenu.new(game)
local flags = game.save.flags or {}
local items = {}
local menu
-- vanilla start submenus return here on B (RedisplayStartMenu): the
-- generic Menu pops the start menu when a row is selected, so each
@@ -51,20 +53,31 @@ function StartMenu.new(game)
end })
-- SAVE shows the player/badges/dex/time panel then asks to confirm
-- (PrintSaveScreenText)
table.insert(items, { label = Strings("SAVE"), onSelect = function()
-- (PrintSaveScreenText); StartMenu_SaveReset never clears the START menu
-- box, so it stays on screen beside the panel (start_sub_menus.asm:641-647)
table.insert(items, { label = Strings("SAVE"), keepOpen = true,
onSelect = function()
local TextBox = require("src.render.TextBox")
local badges = require("src.inventory.Badges").count(game.data, game.save)
local owned = 0
for _ in pairs(game.save.pokedex and game.save.pokedex.owned or {}) do
owned = owned + 1
end
-- the panel is a static snapshot; the cart prints it once
-- (main_menu.asm:390-401)
local t = math.floor(game.save.playTime or 0)
-- PrintSaveScreenText draws its own border at hlcoord 4,0 (b=8, c=$e) and
-- leaves it up under the prompt -- engine/menus/main_menu.asm:381-405
local panel = {
update = function() end,
local panel
panel = {
delay = 0,
update = function()
-- ld c, 30 / jp DelayFrames: the bare panel holds before the
-- prompt (main_menu.asm:404-405)
panel.delay = panel.delay + 1
if panel.delay == 30 then panel.openPrompt() end
end,
draw = function()
local t = math.floor(game.save.playTime or 0)
Font.drawBox(4, 0, 16, 10)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings("PLAYER"), 5 * 8, 2 * 8)
@@ -81,35 +94,40 @@ function StartMenu.new(game)
}
local function closePanel()
if game.stack:top() == panel then game.stack:pop() end
-- SaveMenu returns into HoldTextDisplayOpen, not RedisplayStartMenu
-- (start_sub_menus.asm:645-647): the kept-open START menu goes too
if menu and game.stack:top() == menu then game.stack:pop() end
end
panel.openPrompt = function()
game.stack:push(TextBox.new(game,
Strings("Would you like to\nSAVE the game?"), nil, {
-- SaveTheGame_YesOrNo pins its TWO_OPTION_MENU at hlcoord 0, 7 rather
-- than the shared right-hand one -- engine/menus/save.asm:186-192
choiceBox = Theme.saveBox,
choice = function(yes)
if not yes then closePanel() return end
-- SaveMenu .save (engine/menus/save.asm:164-181): "Now saving..."
-- is a bare PlaceString held by DelayFrames 120, then GameSavedText,
-- which ends in `done` and so never reaches TX_PROMPT_BUTTON.
-- Neither page takes a button press (#765); the second waits on
-- SFX_SAVE (PlaySoundWaitForCurrent + WaitForSoundToFinish) and then
-- DelayFrames 30. The write itself is invisible either side of the
-- "Now saving..." hold, so it stays on that box's onDone.
game.stack:push(TextBox.new(game, Strings("Now saving..."), function()
game:writeSave()
game.stack:push(TextBox.new(game,
Strings("%s saved\nthe game!", game.save.player.name or "RED"),
closePanel, { auto = {
sound = function()
return require("src.core.Sound").play(game.data, "Save")
end,
delay = 30,
} }))
end, { auto = { delay = 120 } }))
end,
}))
end
game.stack:push(panel)
game.stack:push(TextBox.new(game,
Strings("Would you like to\nSAVE the game?"), nil, {
-- SaveTheGame_YesOrNo pins its TWO_OPTION_MENU at hlcoord 0, 7 rather
-- than the shared right-hand one -- engine/menus/save.asm:186-192
choiceBox = { tx = 0, ty = 7, tw = 6, th = 5 },
choice = function(yes)
if not yes then closePanel() return end
-- SaveMenu .save (engine/menus/save.asm:164-181): "Now saving..."
-- is a bare PlaceString held by DelayFrames 120, then GameSavedText,
-- which ends in `done` and so never reaches TX_PROMPT_BUTTON.
-- Neither page takes a button press (#765); the second waits on
-- SFX_SAVE (PlaySoundWaitForCurrent + WaitForSoundToFinish) and then
-- DelayFrames 30. The write itself is invisible either side of the
-- "Now saving..." hold, so it stays on that box's onDone.
game.stack:push(TextBox.new(game, Strings("Now saving..."), function()
game:writeSave()
game.stack:push(TextBox.new(game,
Strings("%s saved\nthe game!", game.save.player.name or "RED"),
closePanel, { auto = {
sound = function()
return require("src.core.Sound").play(game.data, "Save")
end,
delay = 30,
} }))
end, { auto = { delay = 120 } }))
end,
}))
end })
table.insert(items, { label = Strings("OPTION"), onSelect = function()
@@ -165,7 +183,7 @@ function StartMenu.new(game)
-- with Menu's moreArrow showing while there's more below.
local rowStep = 2
local maxVisible = math.floor((Renderer.HEIGHT / 8 - 2) / rowStep)
local menu = Menu.new(game, items,
menu = Menu.new(game, items,
-- the START menu hugs the top-right corner of the SCREEN, not of a
-- centred letterbox: at 9,0 x 11 it is already flush with the top and
-- right of the 20x18 grid, so the anchor keeps it flush when the view
+3
View File
@@ -23,6 +23,9 @@ local Theme = {
-- EnemySendOutFirstMon inlines its own TWO_OPTION_MENU at hlcoord 0, 7
-- instead of the shared right-hand one -- engine/battle/core.asm:1378-1384
trainerSwitchBox = { tx = 0, ty = 7, tw = 6, th = 5 },
-- SaveTheGame_YesOrNo pins its TWO_OPTION_MENU at hlcoord 0, 7 too --
-- engine/menus/save.asm:186-192
saveBox = { tx = 0, ty = 7, tw = 6, th = 5 },
}
function Theme.load(data)
+56 -11
View File
@@ -392,6 +392,17 @@ function TitleState:updateSequence()
self.phase = "loop"
self.blinkTimer = 0
end
elseif self.phase == "exitCry" then
-- .finishedWaiting: PlayCry then WaitForSoundToFinish before the
-- white-out (engine/movie/title.asm:241-243)
self.timer = self.timer + 1
local playing = self.exitCrySrc and self.exitCrySrc.isPlaying
and self.exitCrySrc:isPlaying()
if self.timer >= 3 and (not playing or self.timer > 180) then
self.exitCrySrc = nil
self.phase = "loop"
self:toMenu()
end
end
end
@@ -460,8 +471,8 @@ function ContinueInfo:update(dt)
self.game.stack:pop()
if self.title.onContinue then self.title.onContinue() end
elseif input:wasPressed("b") then
-- the CONTINUE / NEW GAME menu is still open underneath (main_menu.asm:91-92)
self.game.stack:pop()
self.title:openMenu()
end
end
@@ -497,7 +508,10 @@ function TitleState:openMenu()
local game = self.game
local items = {}
if hasSave() then
table.insert(items, { label = Strings("CONTINUE"), onSelect = function()
-- DisplayContinueGameInfo leaves the menu box up behind the info window
-- (engine/menus/main_menu.asm:36-39, :91-92)
table.insert(items, { label = Strings("CONTINUE"), keepOpen = true,
onSelect = function()
-- peek at the save for the info window; fall through if the
-- file can't be read
local ok, loaded = pcall(require("src.core.SaveData").load)
@@ -513,8 +527,11 @@ function TitleState:openMenu()
end })
-- DisplayOptionMenu returns to .mainMenuLoop, which redraws the box
-- (engine/menus/main_menu.asm ln 87-90)
local menu
table.insert(items, { label = Strings("OPTION"), keepOpen = true,
onSelect = function()
-- .mainMenuLoop re-zeroes wCurrentMenuItem on re-entry (main_menu.asm:56-57)
if menu then menu.index = 1 end
require("src.ui.Screens").push(game, "OptionsMenu")
end })
table.insert(items, { label = Strings("EXIT GAME"), onSelect = function()
@@ -532,12 +549,13 @@ function TitleState:openMenu()
type(hooked))
end
local th = #items * 2 + 2
local menu = Menu.new(game, items, { tx = 0, ty = 0, tw = 13, th = th })
menu = Menu.new(game, items, { tx = 0, ty = 0, tw = 13, th = th })
-- .mainMenuLoop's B branch jumps back to DisplayTitleScreen, which opens
-- with GBPalWhiteOut (engine/menus/main_menu.asm:69, title.asm:29)
-- with GBPalWhiteOut and reruns the whole boot cinematic
-- (engine/menus/main_menu.asm:69-70, title.asm:29)
menu.onCancel = function()
game.stack:push(require("src.render.Transition").whiteFlash(game, nil,
function() self.menuOpen = false end))
function() self:restartSequence() end))
end
-- full-width title LOGO zones would recolor this box; see sgbPalettes.
-- Menu.new may have grown tw for longer (e.g. localized) labels, so the
@@ -546,6 +564,27 @@ function TitleState:openMenu()
game.stack:push(menu)
end
-- .mainMenuLoop's B branch: DisplayTitleScreen from the top
-- (main_menu.asm:70, title.asm:39-222)
function TitleState:restartSequence()
self.menuOpen = false
pcall(Music.stop)
self.scy = 0x40
self.phase = "drop"
self.dropStep, self.dropLeft = 1, nil
self.showBubble = not self.yellowLayout
self.timer = 0
self.blinkTimer = 0
self.blinkAt = nil
self.cycleIndex = 1
self.scrollPhase = "hold"
self.scrollFrame = 1
self.monOffset = 0
self.ballY = BALL_REST
self.ribbonOffset = nil
self.whooshSrc, self.crySrc, self.exitCrySrc = nil, nil, nil
end
-- .finishedWaiting: GBPalWhiteOutWithDelay3 then ClearScreen before MainMenu,
-- which clears again itself (engine/movie/title.asm ln 243, main_menu.asm ln 26)
function TitleState:toMenu()
@@ -609,6 +648,11 @@ function TitleState:updateCycle()
end
function TitleState:update(dt)
-- an onSelect that handed control back without a new state (a failed
-- CONTINUE load, a mod row) re-runs DisplayTitleScreen (main_menu.asm:70)
if self.menuOpen and self.game.stack:top() == self then
self:restartSequence()
end
if self.phase ~= "loop" then
self:updateSequence()
return
@@ -619,10 +663,10 @@ function TitleState:update(dt)
if input:wasPressed("start") or input:wasPressed("a") then
-- .go_to_main_menu voices PikachuCry11 on the way out
local Sound = require("src.core.Sound")
if not Sound.playPikaCry(self.game.data, 11) then
Sound.playCry(self.game.data, "PIKACHU")
end
self:toMenu()
self.exitCrySrc = Sound.playPikaCry(self.game.data, 11)
or Sound.playCry(self.game.data, "PIKACHU")
self.phase = "exitCry"
self.timer = 0
end
return
end
@@ -633,10 +677,11 @@ function TitleState:update(dt)
if input:wasPressed("start") or input:wasPressed("a") then
-- the title mon cries when you leave the title (.finishedWaiting);
-- Yellow's fixed Pikachu title always cries Pikachu.
require("src.core.Sound").playCry(self.game.data,
self.exitCrySrc = require("src.core.Sound").playCry(self.game.data,
self.yellowLayout and "PIKACHU"
or self.cycleSpecies[self.cycleIndex])
self:toMenu()
self.phase = "exitCry"
self.timer = 0
end
end
+17 -10
View File
@@ -38,6 +38,7 @@ local Sound = require("src.core.Sound")
-- Only for playerPic: the player.sprite raiser both generations share.
local Sprites = require("src.pokemon.Sprites")
local Strings = require("src.core.Strings")
local SummaryMenu = require("src.ui.gen2.SummaryMenu")
-- Only for TextBox.substitute: the {PLAYER} / {RIVAL} markers a map text
-- carries into the battle box (PrintWinLossText, home/trainers.asm:230).
local TextBox = require("src.render.TextBox")
@@ -161,7 +162,7 @@ local CONTEST_MENU_COL_SPACING = 12
-- PrintMoveType prints the type table's own names; only these two differ from
-- the constant (data/types/names.asm).
local TYPE_NAMES = { PSYCHIC_TYPE = "PSYCHIC", CURSE_TYPE = "???" }
local TYPE_NAMES = SummaryMenu.TYPE_NAMES
-- charmap.asm's quantity glyph, spelled the way MartMenu spells it.
local CONTEST_BALL_LABEL = "PARKBALL\xc3\x97"
@@ -1451,12 +1452,12 @@ function BattleState:advanceQueue()
-- inside SendOutPlayerMon and nothing on the enemy's path touches them.
self.menuIndex = 1
self.moveIndex = 1
-- The incoming mon's own level and exp bar: SendOutPlayerMon reloads
-- wBattleMon* from the party slot and UpdatePlayerHUD draws them at its
-- tail (:3838), so both snap here the way shownHp does above.
self.shownLevel = event.mon.level or 1
self.shownExp = self:expPixels(event.mon, event.mon.level,
event.mon.experience)
-- SendOutPlayerMon reloads wBattleMon* from the party slot (:3838):
-- snap from the emit-time snapshot, not the live table (#1514).
local level = event.level or event.mon.level or 1
self.shownLevel = level
self.shownExp = self:expPixels(event.mon, level,
event.experience or event.mon.experience)
self.expAnim = nil
end
end
@@ -1470,6 +1471,12 @@ function BattleState:advanceQueue()
-- BattleWinSlideInEnemyTrainerFrontpic and the DelayFrames 40 behind it
-- (engine/battle/core.asm:2310-2312)
if event.kind == "trainer-return" then
-- LostBattle's ClearBox wipes the live foe pic and HUD before the slide
-- (engine/battle/core.asm:2770-2773)
if event.cleared then
self.showEnemyHud = false
self.ballRows.enemy = false
end
if not self.enemyTrainerImage then return self:advanceQueue() end
self.showEnemyTrainer = true
self.picHidden.enemy = false
@@ -3577,11 +3584,11 @@ function BattleState:drawPanel()
-- MoveSelectionScreen type 0 is two boxes: the name-only list
-- (engine/battle/core.asm:5074-5084) and MoveInfoBox's (:5407-5410).
local moveMenu = self.phase == "moves"
Chrome.box(0, 12, 20, 6)
if moveMenu then
Chrome.box(0, 8, 11, 5)
-- List box first (core.asm:5074-5084), MoveInfoBox on top (:5157).
Chrome.box(4, 12, 16, 6)
else
Chrome.box(0, 12, 20, 6)
Chrome.box(0, 8, 11, 5)
end
if self.phase == "menu" then
self:printMessage()
+2 -1
View File
@@ -1382,7 +1382,8 @@ end
-- wTimeOfDay, as the cart numbers it: MORN 0, DAY 1, NITE 2, DARK 3.
function Pokegear:timeOfDayIndex()
local world = self.game and self.game.world
local daytime = (world and world.daytime)
-- the unpinned clock split, not the palette pin (pokegear.asm:1456, :1957)
local daytime = (world and (world.tod or world.daytime))
or Palettes.clockDaytime(self.clock and self.clock.hour or nil)
return (Palettes.DAYTIME_ID[daytime] or 2) - 1
end
+3 -1
View File
@@ -886,7 +886,8 @@ function SummaryMenu:drawPageSquare(tx, ty, large, colors)
local first = large and TILE_SQUARE_LARGE or TILE_SQUARE_SMALL
local sheet = self:statsTiles()
if sheet and sheet.quads[first] then
-- [hli] / [hld], a row down, [hli] / [hl]: the four tiles in that order.
-- [hli] / [hld], a row down, [hli] / [hl]: the four tiles in that
-- order (engine/pokemon/stats_screen.asm:841-853).
local function body()
G.setColor(1, 1, 1, 1)
G.draw(sheet.image, sheet.quads[first], px, py)
@@ -1184,6 +1185,7 @@ end
SummaryMenu.STAT_LABELS = STAT_LABELS
SummaryMenu.STAT_KEYS = STAT_KEYS
SummaryMenu.TYPE_NAMES = TYPE_NAMES
SummaryMenu.PAGE_PALETTES = PAGE_PALETTES
SummaryMenu.levelText = levelText
return SummaryMenu
+28 -21
View File
@@ -2967,25 +2967,24 @@ function OverworldState:openPC(onDone)
self:openOaksPC(done)
end,
})
end
-- PKMN LEAGUE sits between PROF.OAK's PC and LOG OFF once wNumHoFTeams
-- is nonzero (engine/pokemon/bills_pc.asm:5, :49)
if #(Game.save.hallOfFame or {}) > 0 then
table.insert(items, {
label = Strings("<PK><MN>LEAGUE"),
keepOpen = true,
onSelect = function()
-- pc.asm PKMNLeague plays SFX_ENTER_PC, then PKMNLeaguePC prints
-- AccessedHoFPCText (engine/menus/pc.asm:67, league_pc.asm:2)
require("src.core.Sound").play(Game.data, "Enter_PC")
Game.stack:push(TextBox.new(Game,
romText(Game.data, "_AccessedHoFPCText",
"Accessed POKéMON\nLEAGUE's site.\fAccessed the HALL\nOF FAME List."),
function() Screens.push(Game, "LeaguePC") end))
done()
end,
})
-- engine/pokemon/bills_pc.asm:48-60 PKMN LEAGUE row (#1566)
if #(Game.save.hallOfFame or {}) > 0 then
table.insert(items, {
label = Strings("<PK><MN>LEAGUE"),
keepOpen = true,
onSelect = function()
-- pc.asm PKMNLeague plays SFX_ENTER_PC, then PKMNLeaguePC prints
-- AccessedHoFPCText (engine/menus/pc.asm:67, league_pc.asm:2)
require("src.core.Sound").play(Game.data, "Enter_PC")
Game.stack:push(TextBox.new(Game,
romText(Game.data, "_AccessedHoFPCText",
"Accessed POKéMON\nLEAGUE's site.\fAccessed the HALL\nOF FAME List."),
function() Screens.push(Game, "LeaguePC") end))
done()
end,
})
end
end
local hooked = Runtime.call("ui.pc.items", sameItems, Game, items)
@@ -3320,7 +3319,8 @@ end
-- the approach walk and then EngageMapTrainer with no further text: the
-- caller already showed the box, so the battle starts without a second
-- one (#869).
function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText)
function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText,
endBattleSound, endBattleIsReward)
local d = npc.def
Runtime.emit("world.trainer_engaged", { npc = npc, trainerClass = d.trainerClass,
partyIndex = d.trainerParty })
@@ -3376,6 +3376,13 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
-- cuts (#282). Substituted here because BattleState:say takes finished
-- text, while TextBox expanded the {PLAYER}/{RIVAL} tokens itself.
battle.endBattleText = wonText and TextBox.substitute(Game, wonText) or nil
-- the badge jingle rides the armed line's first page on the battle
-- screen (sound_get_item_1 in _TX_PRE dialogue; see gyms.lua) (#1606)
battle.endBattleSound = endBattleText ~= nil and endBattleSound or nil
-- one truth for both checkVictoryRewards call sites; endBattleIsReward
-- = false marks an armed line that is NOT the victories dialogue (#1606)
battle.rewardDialogueShown = endBattleText ~= nil
and endBattleIsReward ~= false
battle.onFinish = function(result)
if result == "win" then
Game.save.defeatedTrainers[npc.id] = true
@@ -3386,7 +3393,7 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
-- onVictory script UNDER whatever runs next, so the player still sees
-- EndBattle (now inside the battle), then the reward, then AfterBattle
self:checkVictoryRewards(d.trainerClass, d.trainerParty,
endBattleText ~= nil)
battle.rewardDialogueShown)
self:afterBattle(result, battle)
if onDone then onDone() end
else
@@ -4449,7 +4456,7 @@ function OverworldState:restoreBattleContinuation(battle, origin)
game.save.defeatedTrainers[origin.npcId] = true
if origin.event then game.save.flags[origin.event] = true end
self:checkVictoryRewards(battle.oppClass, battle.partyIndex,
battle.endBattleText ~= nil)
battle.rewardDialogueShown)
end
self:afterBattle(result, battle)
self.engaging = false
+8 -2
View File
@@ -6017,8 +6017,14 @@ function World:trainerWinLossText()
if not vm then return nil, nil end
local obj = vm.trainerObject or {}
local text = self.text or {}
local winKey = vm.winTextOverride or obj.winText
local lossKey = vm.lossTextOverride or obj.lossText
-- `winlosstext` writes BOTH pointers; a 0 argument destroys the struct
-- value rather than falling back to it (engine/overworld/scripting.asm:651)
local winKey, lossKey
if vm.winLossArmed then
winKey, lossKey = vm.winTextOverride, vm.lossTextOverride
else
winKey, lossKey = obj.winText, obj.lossText
end
return winKey and text[winKey] or nil, lossKey and text[lossKey] or nil
end