mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user