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
+7 -5
View File
@@ -38,8 +38,8 @@ local function retryTmGive(game, ow, victoryKey, done)
return true return true
end end
-- The leader's badge line, armed for the battle screen the way -- The badge line + its jingle, armed for the battle screen the way
-- scripts/PewterGym.asm:117 / CeruleanGym.asm:111 SaveEndBattleTextPointers do -- SaveEndBattleTextPointers does (PewterGym.asm:117-119) (#1606)
local function badgeEndBattleText(game, victoryKey) local function badgeEndBattleText(game, victoryKey)
local reward = victoryKey and require("data.scripts.victories")[victoryKey] local reward = victoryKey and require("data.scripts.victories")[victoryKey]
if not (reward and reward.dialogue) then return nil end if not (reward and reward.dialogue) then return nil end
@@ -51,7 +51,7 @@ local function badgeEndBattleText(game, victoryKey)
end end
end end
if #pages == 0 then return nil end if #pages == 0 then return nil end
return table.concat(pages, "\f") return table.concat(pages, "\f"), reward.badgeSound
end end
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent -- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
@@ -74,7 +74,8 @@ M.PEWTER_GYM.talk = {
game.data.text._PewterGymBrockPostBattleAdviceText game.data.text._PewterGymBrockPostBattleAdviceText
or "Go to the GYM in\nCERULEAN and test\nyour abilities!", done)) or "Go to the GYM in\nCERULEAN and test\nyour abilities!", done))
else else
ow:engageTrainer(npc, done, badgeEndBattleText(game, "OPP_BROCK#1")) local text, sound = badgeEndBattleText(game, "OPP_BROCK#1")
ow:engageTrainer(npc, done, text, nil, sound)
end end
end, end,
} }
@@ -107,7 +108,8 @@ local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice, victoryK
game.stack:push(TextBox.new(game, game.stack:push(TextBox.new(game,
game.data.text[adviceLabel] or fallback, finish)) game.data.text[adviceLabel] or fallback, finish))
else else
ow:engageTrainer(npc, done, badgeEndBattleText(game, victoryKey)) local text, sound = badgeEndBattleText(game, victoryKey)
ow:engageTrainer(npc, done, text, nil, sound)
end end
end end
end end
+5 -4
View File
@@ -837,13 +837,14 @@ M.SILPH_CO_11F = {
-- every Silph rocket leaves off-screen (the street rockets are -- every Silph rocket leaves off-screen (the street rockets are
-- handled by M.SAFFRON_CITY.onEnter in story4.lua). Queued, not -- handled by M.SAFFRON_CITY.onEnter in story4.lua). Queued, not
-- run here: the battle's own callbacks are still unwinding, so -- run here: the battle's own callbacks are still unwinding, so
-- queueScript starts it on the first idle overworld frame -- -- queueScript starts it on the first idle overworld frame (#722).
-- after the end-battle "Arrgh!!" box victories.lua OPP_GIOVANNI#2
-- pushes (#722).
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
ow:queueScript(silphAftermathRows()) ow:queueScript(silphAftermathRows())
end end
end, nil, true) end,
-- "Arrgh!!" is armed for the battle screen, not the map
-- (scripts/SilphCo11F.asm:264-266 SaveEndBattleTextPointers) #1606
game.data.text._SilphCo10FGiovanniILostAgainText, true)
end) end)
end)) end))
return true return true
+4 -1
View File
@@ -216,7 +216,10 @@ local function dojoMasterGate(game, ow, x, y)
if not master or ow:trainerDefeated(master) then return false end if not master or ow:trainerDefeated(master) then return false end
ow.player.facing = "right" ow.player.facing = "right"
master:facePlayer(ow.player) master:facePlayer(ow.player)
ow:engageTrainer(master) -- scripts/FightingDojo.asm:117-119 SaveEndBattleTextPointers (#1606)
ow:engageTrainer(master, nil,
((game.data or {}).text or {})._FightingDojoKarateMasterDefeatedText,
nil, nil, false)
return true return true
end end
+1 -1
View File
@@ -651,7 +651,7 @@ local rocketRows = {
{ "check_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 4 { "check_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 4
{ "jump_if_true", 10 }, -- 5 { "jump_if_true", 10 }, -- 5
{ "show_text", "_CeruleanCityRocketText" }, -- 6 { "show_text", "_CeruleanCityRocketText" }, -- 6
-- scripts/CeruleanCity.asm:295 SaveEndBattleTextPointers -- scripts/CeruleanCity.asm:297 SaveEndBattleTextPointers
{ "save_end_battle_text", "_CeruleanCityRocketIGiveUpText" }, -- 7 { "save_end_battle_text", "_CeruleanCityRocketIGiveUpText" }, -- 7
{ "start_battle", "trainer", "OPP_ROCKET", 5 }, -- 8 { "start_battle", "trainer", "OPP_ROCKET", 5 }, -- 8
{ "jump_if_false", "end" }, -- 9 { "jump_if_false", "end" }, -- 9
+1
View File
@@ -8,6 +8,7 @@ local BattleSafety = {}
local BATTLE_BUSY_FIELDS = { local BATTLE_BUSY_FIELDS = {
"current", "afterQueue", "nextInsert", "pendingHit", "waitingUI", "current", "afterQueue", "nextInsert", "pendingHit", "waitingUI",
"waitingSound", "waitFrames", "draining", "animPlaying", "growIn", "waitingSound", "waitFrames", "draining", "animPlaying", "growIn",
"shrinkOut",
"introSlide", "ghostReveal", "mimicCtx", "mimicMoves", "result", "introSlide", "ghostReveal", "mimicCtx", "mimicMoves", "result",
} }
+45 -14
View File
@@ -2495,9 +2495,12 @@ function BattleState:openOldManBag()
-- POKé BALLs; pokeyellow's SimulatedInputBattleItemList, shared by -- POKé BALLs; pokeyellow's SimulatedInputBattleItemList, shared by
-- the Viridian tutorial and Oak's catch, has one. -- the Viridian tutorial and Oak's catch, has one.
local qty = require("src.core.GameVersion").isYellow() and "x1" or "x50" 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", { list = ListMenu.new(game, "ITEMS", {
{ value = "POKE_BALL", label = Strings("POKé BALL"), right = qty }, { value = "POKE_BALL", label = Strings("POKé BALL"), right = qty },
}, { }, {
itemBox = true,
script = function(l) script = function(l)
l.scriptTimer = (l.scriptTimer or 0) + 1 l.scriptTimer = (l.scriptTimer or 0) + 1
if l.scriptTimer == 81 then if l.scriptTimer == 81 then
@@ -3329,15 +3332,18 @@ end
function BattleState:queueRetreatAnim() function BattleState:queueRetreatAnim()
if self:starterPikachuSendOut() then if self:starterPikachuSendOut() then
self:actNext(function() self:slidePic("playerMon", 0, -64, 8, 3) end) self:actNext(function() self:slidePic("playerMon", 0, -64, 8, 3) end)
self.nextInsert = (self.nextInsert or 0) + 1 self:waitNext(24)
table.insert(self.queue, self.nextInsert, { wait = 24 }) self:actNext(function()
self:actNext(function() self:slidePic("playerMon") end) -- .clearScreenArea keeps the 7x7 area blank until the swap
-- (pokeyellow core.asm:1867-1871) (#1545)
self.sendingOut = true
self:slidePic("playerMon")
end)
else else
self:actNext(function() self:actNext(function()
self.shrinkOut = { battler = self.player, frame = 0 } self.shrinkOut = { battler = self.player, frame = 0 }
end) end)
self.nextInsert = (self.nextInsert or 0) + 1 self:waitNext(7)
table.insert(self.queue, self.nextInsert, { wait = 7 })
end end
end end
@@ -3558,10 +3564,10 @@ function BattleState:updateFx()
if self.growIn.frame >= 12 then self.growIn = nil end if self.growIn.frame >= 12 then self.growIn = nil end
end end
-- the retreat shrink (AnimateRetreatingPlayerMon): 4+3 frames, then the -- 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 if self.shrinkOut then
self.shrinkOut.frame = self.shrinkOut.frame + 1 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 end
-- low-HP alarm (audio/low_health_alarm.asm): the two-tone siren -- low-HP alarm (audio/low_health_alarm.asm): the two-tone siren
-- loops while the player's bar is red; see lowHealthAlarmActive -- 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 -- = false) queues it from applyMimic after a successful copy
if not (record and record.announceAnim == false) then if not (record and record.announceAnim == false) then
self.nextInsert = (self.nextInsert or 0) + 1 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) table.insert(self.queue, self.nextInsert, self.moveAnimRow)
end end
Runtime.emit("battle.move_used", { Runtime.emit("battle.move_used", {
@@ -4225,7 +4233,9 @@ function BattleState:awardExp()
local playerId = self.game.save.player and self.game.save.player.id local playerId = self.game.save.player and self.game.save.player.id
-- GainExperience (engine/battle/experience.asm:69-88) compares the -- GainExperience (engine/battle/experience.asm:69-88) compares the
-- stored MON_OTID against wPlayerID every award; no persistent flag -- 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, local levels, gained = Experience.apply(self.data, mon, self.enemy.def,
self.enemy.mon.level, self.kind == "trainer", self.enemy.mon.level, self.kind == "trainer",
split, traded) split, traded)
@@ -4397,7 +4407,8 @@ function BattleState:enemyMonFainted()
forceSwitch = true, forceSwitch = true,
onSwitch = function(mon) onSwitch = function(mon)
if mon == self.player.mon then 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 elseif mon.hp <= 0 then
reopenShift(self:romText("_NoWillText", "There's no will\nto fight!")) reopenShift(self:romText("_NoWillText", "There's no will\nto fight!"))
else else
@@ -4524,9 +4535,22 @@ function BattleState:enemyMonFainted()
-- TrainerNamePointers aims those entries at wTrainerName). The tag -- TrainerNamePointers aims those entries at wTrainerName). The tag
-- prints once, so a `para` page carries no second copy (#566). -- prints once, so a `para` page carries no second copy (#566).
local tag = self.trainer and self.trainer.name 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 for page in (self.endBattleText .. "\f"):gmatch("(.-)\f") do
if page ~= "" then 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 tag = nil
end end
end end
@@ -5144,7 +5168,8 @@ function BattleState:openParty()
onSwitch = function(mon) onSwitch = function(mon)
-- PartyMenuOrRockOrRun's SWITCH .partyMonDeselected (core.asm:2396-2408) -- PartyMenuOrRockOrRun's SWITCH .partyMonDeselected (core.asm:2396-2408)
if mon == self.player.mon then 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) self:act(function() self:openParty() end)
elseif mon.hp <= 0 then elseif mon.hp <= 0 then
self:say(self:romText("_NoWillText", "There's no will\nto fight!")) self:say(self:romText("_NoWillText", "There's no will\nto fight!"))
@@ -5297,7 +5322,9 @@ end
function BattleState:shrinkOutScale(battler) function BattleState:shrinkOutScale(battler)
local shrink = self.shrinkOut local shrink = self.shrinkOut
if not shrink or shrink.battler ~= battler then return nil end 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 end
-- battler hidden this frame? (damage blink) -- 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) -- the AnimateRetreatingPlayerMon shrink (core.asm:1769-1796)
local eff = s * gs local eff = s * gs
if eff > 0 then 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, 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) 96 - (img:getHeight() - pad) * eff + sy, 0, eff, eff)
end end
else else
+5 -3
View File
@@ -108,13 +108,17 @@ end
-- The damaging pipeline, extracted from the performMove monolith: every -- The damaging pipeline, extracted from the performMove monolith: every
-- stage keeps the original's exact check order and rng consumption -- 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). -- damage choice -> hits -> messages -> after-damage -> secondary run).
function EffectRegistry.runDamaging(battle, ctx, record) function EffectRegistry.runDamaging(battle, ctx, record)
local user, target = ctx.user, ctx.target local user, target = ctx.user, ctx.target
local move, moveInst = ctx.move, ctx.moveInst local move, moveInst = ctx.move, ctx.moveInst
local neverMiss = record and record.neverMiss 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 ignores semi-invulnerability (MoveHitTest returns hit for
-- SWIFT_EFFECT before the INVULNERABLE check) -- SWIFT_EFFECT before the INVULNERABLE check)
if target.invulnerable and not neverMiss then if target.invulnerable and not neverMiss then
@@ -143,8 +147,6 @@ function EffectRegistry.runDamaging(battle, ctx, record)
local hits = hitCount(ctx, record) local hits = hitCount(ctx, record)
if record and record.beforeAccuracy then record.beforeAccuracy(ctx) end
if not neverMiss then if not neverMiss then
if not battle:accuracyRoll(move, user, target) then if not battle:accuracyRoll(move, user, target) then
-- Explosion/Selfdestruct still animate on a miss (HandleIfPlayerMoveMissed) -- Explosion/Selfdestruct still animate on a miss (HandleIfPlayerMoveMissed)
+12 -4
View File
@@ -892,10 +892,12 @@ Battle.PRIORITY = {
EFFECT_ENDURE = 3, EFFECT_ENDURE = 3,
EFFECT_COUNTER = -1, EFFECT_COUNTER = -1,
EFFECT_MIRROR_COAT = -1, EFFECT_MIRROR_COAT = -1,
EFFECT_FORCE_SWITCH = -1, -- Whirlwind, Roar: priority 0, below BASE
} }
function Battle:movePriority(moveId) 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 if moveId == "VITAL_THROW" then return -1 end
local def = self:moveDef(moveId) local def = self:moveDef(moveId)
return (def and Battle.PRIORITY[def.effect]) or 0 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 local sent = side == "player" and self.player or self.enemy
self:emit({ kind = "send", side = side, mon = sent, self:emit({ kind = "send", side = side, mon = sent,
hp = sent.hp or 0, status = sent.status or false, hp = sent.hp or 0, status = sent.status or false,
level = sent.level, experience = sent.experience,
text = "Go! " .. self:monName(sent) .. "!" }) text = "Go! " .. self:monName(sent) .. "!" })
end end
@@ -2670,6 +2673,7 @@ Battle.MOVE_EFFECTS.EFFECT_FORCE_SWITCH = function(self, attacker, defender,
end end
self:emit({ kind = "send", side = self:sideOf(incoming), mon = incoming, self:emit({ kind = "send", side = self:sideOf(incoming), mon = incoming,
hp = incoming.hp or 0, status = incoming.status or false, hp = incoming.hp or 0, status = incoming.status or false,
level = incoming.level, experience = incoming.experience,
text = self:monName(incoming) .. " was dragged out!" }) text = self:monName(incoming) .. " was dragged out!" })
self:breakTrapsOnSend(incoming) self:breakTrapsOnSend(incoming)
self:spikesDamage(incoming) self:spikesDamage(incoming)
@@ -3117,6 +3121,7 @@ function Battle:resolveFaints()
self:emit({ kind = "send", side = "enemy", mon = self.enemy, self:emit({ kind = "send", side = "enemy", mon = self.enemy,
replacement = true, replacement = true,
hp = self.enemy.hp or 0, status = self.enemy.status or false, 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 " text = (self.trainer and self.trainer.name or "Foe") .. " sent out "
.. self:monName(self.enemy) .. "!" }) .. self:monName(self.enemy) .. "!" })
Runtime.emit("battle.battler_switched", { Runtime.emit("battle.battler_switched", {
@@ -3196,11 +3201,12 @@ function Battle:printWinLossText(result)
if not trainer then return end if not trainer then return end
-- The DEBUG_BATTLE_F skip sits in front of PrintWinLossText alone, behind -- The DEBUG_BATTLE_F skip sits in front of PrintWinLossText alone, behind
-- the slide (engine/battle/core.asm:2310, :2320-2323). -- 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 local text = (result == "lose") and trainer.lossText or trainer.winText
if type(text) ~= "string" or text == "" then return end if type(text) ~= "string" or text == "" then return end
-- FarPrintText prints the pointer alone: no trainer-name tag in front of it, -- FarPrintText prints the pointer alone: no trainer-name tag in front of
-- unlike Gen 1's TrainerEndBattleText (pokered home/trainers.asm:341). -- it, unlike Gen 1's TrainerEndBattleText (pokered home/trainers.asm:355).
self:emit({ kind = "win-text", text = text }) self:emit({ kind = "win-text", text = text })
end end
@@ -3526,6 +3532,7 @@ function Battle:switch(index)
self.stages.player = Battle.newStages() self.stages.player = Battle.newStages()
self:emit({ kind = "send", side = "player", mon = mon, self:emit({ kind = "send", side = "player", mon = mon,
hp = mon.hp or 0, status = mon.status or false, hp = mon.hp or 0, status = mon.status or false,
level = mon.level, experience = mon.experience,
text = "Go! " .. self:monName(mon) .. "!" }) text = "Go! " .. self:monName(mon) .. "!" })
-- battle.battler_switched, the payload BattleState:resolveSwitch emits on -- battle.battler_switched, the payload BattleState:resolveSwitch emits on
-- Gen 1: the side record, whoever walked in, and whoever walked out. -- 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.stages.enemy = Battle.newStages()
self:emit({ kind = "send", side = "enemy", mon = self.enemy, self:emit({ kind = "send", side = "enemy", mon = self.enemy,
hp = self.enemy.hp or 0, status = self.enemy.status or false, 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 " text = (self.trainer.name or "TRAINER") .. " sent out "
.. self:monName(self.enemy) .. "!" }) .. self:monName(self.enemy) .. "!" })
Runtime.emit("battle.battler_switched", { 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 -- ...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 -- 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, -- 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 -- gated on this frame's world pass -- which the party menu ends by being
-- by being opaque. Without this hold they lose the step-down and blit at -- opaque (the bag's item box shows the map around it, #1521). Without
-- full fit scale over a battle drawn at the zoomed-out one. -- 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 Renderer.uiWorldHold = Renderer.battleDim ~= nil
-- ...and a battle keeps its dialogue box and YES/NO inside its own screen -- ...and a battle keeps its dialogue box and YES/NO inside its own screen
-- instead of letting them dock to the window edge. -- instead of letting them dock to the window edge.
+9
View File
@@ -220,6 +220,9 @@ function Save.newGame(opts)
phoneContacts = {}, phoneContacts = {},
tradeFlags = {}, tradeFlags = {},
pokedex = { seen = {}, caught = {} }, 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 -- wUnownDex: the distinct Unown FORMS caught, in catching order. A second
-- record beside the #DEX because the #DEX knows only the species -- record beside the #DEX because the #DEX knows only the species
-- (src/core/gen2/Unown.lua). -- (src/core/gen2/Unown.lua).
@@ -685,6 +688,12 @@ function Save.validate(save)
scrubEvents(save, report) scrubEvents(save, report)
scrubMapScenes(save, report) scrubMapScenes(save, report)
scrubPlayerState(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 -- 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 -- `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 -- 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. -- Headless (no real graphics) falls back to the plain text flow.
function Evolution.evolve(game, mon, newSpecies, onDone, via) function Evolution.evolve(game, mon, newSpecies, onDone, via)
local oldName = mon.nickname or game.data.pokemon[mon.species].name local oldName = mon.nickname or game.data.pokemon[mon.species].name
-- IsEvolvingText, DelayFrames 50, then ClearScreenArea before EvolveMon -- IsEvolvingText, DelayFrames 50; ClearScreenArea then wipes rows 0-11
-- (engine/pokemon/evos_moves.asm:120-134) -- ONLY, so the box rides through EvolveMon (evos_moves.asm:120-134)
local isEvolving = romText(game.data, "_IsEvolvingText", local isEvolving = romText(game.data, "_IsEvolvingText",
"What?\n%s is\nevolving!", oldName) "What?\n%s is\nevolving!", oldName)
if love.image and love.image.newImageData then if love.image and love.image.newImageData then
game.stack:push(TextBox.new(game, isEvolving, function() local intro
-- forward `via` so EvolutionState can keep trade evolutions intro = TextBox.new(game, isEvolving, nil, { stay = {
-- non-cancelable (LINK_STATE_TRADING) while others accept B (#213) onShown = function()
Screens.push(game, "EvolutionState", mon, newSpecies, onDone, via) -- DelayFrames 50 with the box and the old screen still up
end, { auto = { delay = EVOLVING_TEXT_FRAMES } })) -- (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 return
end end
Music.play(game.data, Music.special(game.data, "evolution")) 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 -- -- waits for nothing, shows no blinking arrow, and never pops itself --
-- whoever pushed it owns the pop. stay.onShown fires once, on the frame -- 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 -- 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) function TextBox.new(game, text, onDone, opts)
local self = setmetatable({}, TextBox) local self = setmetatable({}, TextBox)
self.game = game self.game = game
@@ -296,6 +297,15 @@ function TextBox:update(dt)
-- exactly once (#591) -- exactly once (#591)
if self.stay then if self.stay then
if not self.stayShown 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 self.stayShown = true
if self.stay.onShown then self.stay.onShown() end if self.stay.onShown then self.stay.onShown() end
end end
@@ -494,7 +504,8 @@ function TextBox:draw()
Font.draw(money, 152 - Font.width(money), 8) Font.draw(money, 152 - Font.width(money), 8)
end end
if (self.waiting or (self.done and not self.choice and not self.auto 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 and self.blink < 30 then
-- page-advance cursor: glyph $EE by default, the blinking down arrow -- page-advance cursor: glyph $EE by default, the blinking down arrow
-- the original prints via `ld a, "▼"` (home/text.asm) -- 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 entry = contact and contact.map and grass and grass[contact.map]
local slots = entry and entry.slots local slots = entry and entry.slots
if not slots then return end 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 if daytime == "DARK" then daytime = "NITE" end
local slot = (slots[daytime] or slots.DAY or {})[Specials.random(4)] local slot = (slots[daytime] or slots.DAY or {})[Specials.random(4)]
if slot and slot.species then nameSpecies(vm, slot.species) end 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. -- really does run here.
if self.reloadMapFn then self.reloadMapFn(true) end if self.reloadMapFn then self.reloadMapFn(true) end
elseif op == "winlosstext" then 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.winTextOverride = cmd.winText
self.lossTextOverride = cmd.lossText self.lossTextOverride = cmd.lossText
elseif op == "trainertext" then elseif op == "trainertext" then
@@ -1126,9 +1128,11 @@ local function runCmd(self, cmd, op)
local obj = self.trainerObject or {} local obj = self.trainerObject or {}
local key local key
if which == 1 then 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 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 else
key = obj.seenText key = obj.seenText
end end
@@ -2429,6 +2433,7 @@ function Vm:start(scriptKey)
self.battleOutcome = nil self.battleOutcome = nil
self.winTextOverride = nil self.winTextOverride = nil
self.lossTextOverride = nil self.lossTextOverride = nil
self.winLossArmed = nil
-- The whiteout abort is per-run too: a script that ended because the player -- 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. -- was wiped must not stop the next one before it starts.
self.aborted = false self.aborted = false
+4 -2
View File
@@ -339,9 +339,11 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker)
{ kind = "levelup" }) { kind = "levelup" })
-- the party menu stays up through TryEvolvingMon and only -- the party menu stays up through TryEvolvingMon and only
-- comes down at RemoveUsedItem (item_effects.asm:1392-1418) -- comes down at RemoveUsedItem (item_effects.asm:1392-1418)
closePicker()
if evoTo then 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 end
return return
end end
+4 -3
View File
@@ -13,9 +13,10 @@
local Music = require("src.core.Music") local Music = require("src.core.Music")
local romText = require("src.core.RomText") 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 = {} local EvolutionState = {}
EvolutionState.__index = EvolutionState EvolutionState.__index = EvolutionState
EvolutionState.isOpaque = true
-- SGB: SetPal_PokemonWholeScreen for the mon on display -- SGB: SetPal_PokemonWholeScreen for the mon on display
function EvolutionState:sgbPalettes(game) function EvolutionState:sgbPalettes(game)
@@ -147,7 +148,8 @@ end
function EvolutionState:draw() function EvolutionState:draw()
love.graphics.setColor(1, 1, 1, 1) 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 -- accelerating flash between the two forms
local sprite, spriteTrueColor local sprite, spriteTrueColor
@@ -172,7 +174,6 @@ function EvolutionState:draw()
require("src.render.PaletteFX").markTrueColor(x, y, sprite:getDimensions()) require("src.render.PaletteFX").markTrueColor(x, y, sprite:getDimensions())
end end
end end
love.graphics.setColor(1, 1, 1, 1)
end end
return EvolutionState 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 -- 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) -- screen of its own (home/list_menu.asm:29-31)
self.itemBox = opts.itemBox or false 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) self.rows = opts.rows or (self.itemBox and ITEM_ROWS)
or ((opts.dialogue or opts.messageBox) and 4 or ROWS) or ((opts.dialogue or opts.messageBox) and 4 or ROWS)
return self return self
@@ -113,8 +121,9 @@ local function moveIndex(self, delta)
end end
local function syncScroll(self) local function syncScroll(self)
if self.index - self.scroll > self.rows then local maxRow = self.cursorRows or self.rows
self.scroll = self.index - self.rows if self.index - self.scroll > maxRow then
self.scroll = self.index - maxRow
end end
if self.index - self.scroll < 1 then self.scroll = self.index - 1 end if self.index - self.scroll < 1 then self.scroll = self.index - 1 end
end end
+7 -1
View File
@@ -119,8 +119,14 @@ function Menu:draw()
self.tw * 8, self.th * 8, self.anchor) self.tw * 8, self.th * 8, self.anchor)
end end
Font.drawBox(self.tx, self.ty, self.tw, self.th) 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) 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 if self.title then
Font.draw(self.title, (self.tx + 3) * 8, self.ty * 8) Font.draw(self.title, (self.tx + 3) * 8, self.ty * 8)
end end
+5 -3
View File
@@ -96,15 +96,17 @@ function NamingScreen:enter()
onSelect = function() onSelect = function()
-- the menu already popped itself; pop the naming screen too -- the menu already popped itself; pop the naming screen too
self.game.stack:pop() self.game.stack:pop()
if self.onDone then self.onDone(preset) end if self.onDone then self.onDone(preset, false) end
end, end,
}) })
end end
if self.introBox then if self.introBox then
-- DisplayIntroNameTextBox (oak_speech2.asm:162): TextBoxBorder at -- 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 -- 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, { 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, itemY = 2, title = Strings("NAME"), cancelable = false,
})) }))
else else
@@ -135,7 +137,7 @@ function NamingScreen:confirm()
end end
Sound.play(self.game.data, "Press_AB") Sound.play(self.game.data, "Press_AB")
self.game.stack:pop() self.game.stack:pop()
if self.onDone then self.onDone(name) end if self.onDone then self.onDone(name, true) end
end end
function NamingScreen:grid() function NamingScreen:grid()
+43 -9
View File
@@ -43,6 +43,12 @@ local PicSlide = {}
PicSlide.__index = PicSlide PicSlide.__index = PicSlide
function PicSlide:update(dt) 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 self.t = self.t + 1
local tiles = math.min(SLIDE_TILES, math.floor(self.t / SLIDE_FRAMES)) 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 self.speech.picSlide = (self.dir > 0 and tiles or (SLIDE_TILES - tiles)) * 8
@@ -172,8 +178,8 @@ function OakSpeech.defaultSteps(speech)
kind = "say", kind = "say",
textKey = "_IntroducePlayerText", textKey = "_IntroducePlayerText",
pic = "player", pic = "player",
-- oak_speech.asm:89-92: MovePicLeft, then IntroducePlayerText ends in -- oak_speech.asm:89-92: MovePicLeft, then IntroducePlayerText's
-- text_end, so PrintText returns with the box still up under the names -- `prompt` (text_2.asm:1730) waits for A and leaves the box up
reveal = "wipe", reveal = "wipe",
stay = true, stay = true,
}, },
@@ -192,6 +198,9 @@ function OakSpeech.defaultSteps(speech)
id = "confirm_player_name", id = "confirm_player_name",
kind = "say", kind = "say",
textKey = "_YourNameIsText", 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", id = "ask_rival_name",
@@ -199,7 +208,7 @@ function OakSpeech.defaultSteps(speech)
textKey = "_IntroduceRivalText", textKey = "_IntroduceRivalText",
pic = "rival", pic = "rival",
-- oak_speech.asm:98-101: FadeInIntroPic, then IntroduceRivalText's -- 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", reveal = "fade",
stay = true, stay = true,
}, },
@@ -216,6 +225,9 @@ function OakSpeech.defaultSteps(speech)
id = "confirm_rival_name", id = "confirm_rival_name",
kind = "say", kind = "say",
textKey = "_HisNameIsText", 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", id = "legend",
@@ -403,9 +415,20 @@ function OakSpeech:runStep(step)
self:applyPic(step) self:applyPic(step)
self:afterReveal(step, function() self:afterReveal(step, function()
self:runCry(step) self:runCry(step)
if step.stay then if step.stay or step.fadeOut then
local box = TextBox.new(self.game, self:stepText(step), nil, 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.holdBox = box
self.game.stack:push(box) self.game.stack:push(box)
else else
@@ -432,16 +455,26 @@ function OakSpeech:runStep(step)
presets = presets, presets = presets,
introBox = true, introBox = true,
maxLen = step.maxLen or self.nameLen, maxLen = step.maxLen or self.nameLen,
onDone = function(name) onDone = function(name, custom)
if who == "rival" then if who == "rival" then
self.game.save.player.rival = name self.game.save.player.rival = name
else else
self.game.save.player.name = name self.game.save.player.name = name
end end
self:recordAnswer(step, 1, name, name) 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: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,
}) })
end end
@@ -557,10 +590,11 @@ function OakSpeech:revealPic(kind, next)
end end
-- ..(engine/movie/oak_speech/oak_speech2.asm ln 67) -- ..(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.picSlide = (dir > 0 and 0 or SLIDE_TILES * 8)
self.game.stack:push(setmetatable({ self.game.stack:push(setmetatable({
game = self.game, speech = self, dir = dir, t = 0, onDone = onDone, game = self.game, speech = self, dir = dir, t = 0, onDone = onDone,
delay = delay,
}, PicSlide)) }, PicSlide))
end end
+51 -33
View File
@@ -11,6 +11,7 @@ local Renderer = require("src.render.Renderer")
local Runtime = require("src.mods.Runtime") local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens") local Screens = require("src.ui.Screens")
local Strings = require("src.core.Strings") local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local StartMenu = {} local StartMenu = {}
@@ -19,6 +20,7 @@ local function sameItems(_, items) return items end
function StartMenu.new(game) function StartMenu.new(game)
local flags = game.save.flags or {} local flags = game.save.flags or {}
local items = {} local items = {}
local menu
-- vanilla start submenus return here on B (RedisplayStartMenu): the -- vanilla start submenus return here on B (RedisplayStartMenu): the
-- generic Menu pops the start menu when a row is selected, so each -- generic Menu pops the start menu when a row is selected, so each
@@ -51,20 +53,31 @@ function StartMenu.new(game)
end }) end })
-- SAVE shows the player/badges/dex/time panel then asks to confirm -- SAVE shows the player/badges/dex/time panel then asks to confirm
-- (PrintSaveScreenText) -- (PrintSaveScreenText); StartMenu_SaveReset never clears the START menu
table.insert(items, { label = Strings("SAVE"), onSelect = function() -- 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 TextBox = require("src.render.TextBox")
local badges = require("src.inventory.Badges").count(game.data, game.save) local badges = require("src.inventory.Badges").count(game.data, game.save)
local owned = 0 local owned = 0
for _ in pairs(game.save.pokedex and game.save.pokedex.owned or {}) do for _ in pairs(game.save.pokedex and game.save.pokedex.owned or {}) do
owned = owned + 1 owned = owned + 1
end 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 -- 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 -- leaves it up under the prompt -- engine/menus/main_menu.asm:381-405
local panel = { local panel
update = function() end, 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() draw = function()
local t = math.floor(game.save.playTime or 0)
Font.drawBox(4, 0, 16, 10) Font.drawBox(4, 0, 16, 10)
love.graphics.setColor(0, 0, 0, 1) love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings("PLAYER"), 5 * 8, 2 * 8) Font.draw(Strings("PLAYER"), 5 * 8, 2 * 8)
@@ -81,35 +94,40 @@ function StartMenu.new(game)
} }
local function closePanel() local function closePanel()
if game.stack:top() == panel then game.stack:pop() end 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 end
game.stack:push(panel) 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 }) end })
table.insert(items, { label = Strings("OPTION"), onSelect = function() 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. -- with Menu's moreArrow showing while there's more below.
local rowStep = 2 local rowStep = 2
local maxVisible = math.floor((Renderer.HEIGHT / 8 - 2) / rowStep) 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 -- 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 -- 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 -- 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 -- EnemySendOutFirstMon inlines its own TWO_OPTION_MENU at hlcoord 0, 7
-- instead of the shared right-hand one -- engine/battle/core.asm:1378-1384 -- instead of the shared right-hand one -- engine/battle/core.asm:1378-1384
trainerSwitchBox = { tx = 0, ty = 7, tw = 6, th = 5 }, 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) function Theme.load(data)
+56 -11
View File
@@ -392,6 +392,17 @@ function TitleState:updateSequence()
self.phase = "loop" self.phase = "loop"
self.blinkTimer = 0 self.blinkTimer = 0
end 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
end end
@@ -460,8 +471,8 @@ function ContinueInfo:update(dt)
self.game.stack:pop() self.game.stack:pop()
if self.title.onContinue then self.title.onContinue() end if self.title.onContinue then self.title.onContinue() end
elseif input:wasPressed("b") then elseif input:wasPressed("b") then
-- the CONTINUE / NEW GAME menu is still open underneath (main_menu.asm:91-92)
self.game.stack:pop() self.game.stack:pop()
self.title:openMenu()
end end
end end
@@ -497,7 +508,10 @@ function TitleState:openMenu()
local game = self.game local game = self.game
local items = {} local items = {}
if hasSave() then 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 -- peek at the save for the info window; fall through if the
-- file can't be read -- file can't be read
local ok, loaded = pcall(require("src.core.SaveData").load) local ok, loaded = pcall(require("src.core.SaveData").load)
@@ -513,8 +527,11 @@ function TitleState:openMenu()
end }) end })
-- DisplayOptionMenu returns to .mainMenuLoop, which redraws the box -- DisplayOptionMenu returns to .mainMenuLoop, which redraws the box
-- (engine/menus/main_menu.asm ln 87-90) -- (engine/menus/main_menu.asm ln 87-90)
local menu
table.insert(items, { label = Strings("OPTION"), keepOpen = true, table.insert(items, { label = Strings("OPTION"), keepOpen = true,
onSelect = function() 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") require("src.ui.Screens").push(game, "OptionsMenu")
end }) end })
table.insert(items, { label = Strings("EXIT GAME"), onSelect = function() table.insert(items, { label = Strings("EXIT GAME"), onSelect = function()
@@ -532,12 +549,13 @@ function TitleState:openMenu()
type(hooked)) type(hooked))
end end
local th = #items * 2 + 2 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 -- .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() menu.onCancel = function()
game.stack:push(require("src.render.Transition").whiteFlash(game, nil, game.stack:push(require("src.render.Transition").whiteFlash(game, nil,
function() self.menuOpen = false end)) function() self:restartSequence() end))
end end
-- full-width title LOGO zones would recolor this box; see sgbPalettes. -- 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 -- 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) game.stack:push(menu)
end 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, -- .finishedWaiting: GBPalWhiteOutWithDelay3 then ClearScreen before MainMenu,
-- which clears again itself (engine/movie/title.asm ln 243, main_menu.asm ln 26) -- which clears again itself (engine/movie/title.asm ln 243, main_menu.asm ln 26)
function TitleState:toMenu() function TitleState:toMenu()
@@ -609,6 +648,11 @@ function TitleState:updateCycle()
end end
function TitleState:update(dt) 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 if self.phase ~= "loop" then
self:updateSequence() self:updateSequence()
return return
@@ -619,10 +663,10 @@ function TitleState:update(dt)
if input:wasPressed("start") or input:wasPressed("a") then if input:wasPressed("start") or input:wasPressed("a") then
-- .go_to_main_menu voices PikachuCry11 on the way out -- .go_to_main_menu voices PikachuCry11 on the way out
local Sound = require("src.core.Sound") local Sound = require("src.core.Sound")
if not Sound.playPikaCry(self.game.data, 11) then self.exitCrySrc = Sound.playPikaCry(self.game.data, 11)
Sound.playCry(self.game.data, "PIKACHU") or Sound.playCry(self.game.data, "PIKACHU")
end self.phase = "exitCry"
self:toMenu() self.timer = 0
end end
return return
end end
@@ -633,10 +677,11 @@ function TitleState:update(dt)
if input:wasPressed("start") or input:wasPressed("a") then if input:wasPressed("start") or input:wasPressed("a") then
-- the title mon cries when you leave the title (.finishedWaiting); -- the title mon cries when you leave the title (.finishedWaiting);
-- Yellow's fixed Pikachu title always cries Pikachu. -- 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" self.yellowLayout and "PIKACHU"
or self.cycleSpecies[self.cycleIndex]) or self.cycleSpecies[self.cycleIndex])
self:toMenu() self.phase = "exitCry"
self.timer = 0
end end
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. -- Only for playerPic: the player.sprite raiser both generations share.
local Sprites = require("src.pokemon.Sprites") local Sprites = require("src.pokemon.Sprites")
local Strings = require("src.core.Strings") local Strings = require("src.core.Strings")
local SummaryMenu = require("src.ui.gen2.SummaryMenu")
-- Only for TextBox.substitute: the {PLAYER} / {RIVAL} markers a map text -- Only for TextBox.substitute: the {PLAYER} / {RIVAL} markers a map text
-- carries into the battle box (PrintWinLossText, home/trainers.asm:230). -- carries into the battle box (PrintWinLossText, home/trainers.asm:230).
local TextBox = require("src.render.TextBox") 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 -- PrintMoveType prints the type table's own names; only these two differ from
-- the constant (data/types/names.asm). -- 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. -- charmap.asm's quantity glyph, spelled the way MartMenu spells it.
local CONTEST_BALL_LABEL = "PARKBALL\xc3\x97" 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. -- inside SendOutPlayerMon and nothing on the enemy's path touches them.
self.menuIndex = 1 self.menuIndex = 1
self.moveIndex = 1 self.moveIndex = 1
-- The incoming mon's own level and exp bar: SendOutPlayerMon reloads -- SendOutPlayerMon reloads wBattleMon* from the party slot (:3838):
-- wBattleMon* from the party slot and UpdatePlayerHUD draws them at its -- snap from the emit-time snapshot, not the live table (#1514).
-- tail (:3838), so both snap here the way shownHp does above. local level = event.level or event.mon.level or 1
self.shownLevel = event.mon.level or 1 self.shownLevel = level
self.shownExp = self:expPixels(event.mon, event.mon.level, self.shownExp = self:expPixels(event.mon, level,
event.mon.experience) event.experience or event.mon.experience)
self.expAnim = nil self.expAnim = nil
end end
end end
@@ -1470,6 +1471,12 @@ function BattleState:advanceQueue()
-- BattleWinSlideInEnemyTrainerFrontpic and the DelayFrames 40 behind it -- BattleWinSlideInEnemyTrainerFrontpic and the DelayFrames 40 behind it
-- (engine/battle/core.asm:2310-2312) -- (engine/battle/core.asm:2310-2312)
if event.kind == "trainer-return" then 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 if not self.enemyTrainerImage then return self:advanceQueue() end
self.showEnemyTrainer = true self.showEnemyTrainer = true
self.picHidden.enemy = false self.picHidden.enemy = false
@@ -3577,11 +3584,11 @@ function BattleState:drawPanel()
-- MoveSelectionScreen type 0 is two boxes: the name-only list -- MoveSelectionScreen type 0 is two boxes: the name-only list
-- (engine/battle/core.asm:5074-5084) and MoveInfoBox's (:5407-5410). -- (engine/battle/core.asm:5074-5084) and MoveInfoBox's (:5407-5410).
local moveMenu = self.phase == "moves" local moveMenu = self.phase == "moves"
Chrome.box(0, 12, 20, 6)
if moveMenu then 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) Chrome.box(4, 12, 16, 6)
else Chrome.box(0, 8, 11, 5)
Chrome.box(0, 12, 20, 6)
end end
if self.phase == "menu" then if self.phase == "menu" then
self:printMessage() 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. -- wTimeOfDay, as the cart numbers it: MORN 0, DAY 1, NITE 2, DARK 3.
function Pokegear:timeOfDayIndex() function Pokegear:timeOfDayIndex()
local world = self.game and self.game.world 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) or Palettes.clockDaytime(self.clock and self.clock.hour or nil)
return (Palettes.DAYTIME_ID[daytime] or 2) - 1 return (Palettes.DAYTIME_ID[daytime] or 2) - 1
end 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 first = large and TILE_SQUARE_LARGE or TILE_SQUARE_SMALL
local sheet = self:statsTiles() local sheet = self:statsTiles()
if sheet and sheet.quads[first] then 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() local function body()
G.setColor(1, 1, 1, 1) G.setColor(1, 1, 1, 1)
G.draw(sheet.image, sheet.quads[first], px, py) G.draw(sheet.image, sheet.quads[first], px, py)
@@ -1184,6 +1185,7 @@ end
SummaryMenu.STAT_LABELS = STAT_LABELS SummaryMenu.STAT_LABELS = STAT_LABELS
SummaryMenu.STAT_KEYS = STAT_KEYS SummaryMenu.STAT_KEYS = STAT_KEYS
SummaryMenu.TYPE_NAMES = TYPE_NAMES SummaryMenu.TYPE_NAMES = TYPE_NAMES
SummaryMenu.PAGE_PALETTES = PAGE_PALETTES
SummaryMenu.levelText = levelText SummaryMenu.levelText = levelText
return SummaryMenu return SummaryMenu
+28 -21
View File
@@ -2967,25 +2967,24 @@ function OverworldState:openPC(onDone)
self:openOaksPC(done) self:openOaksPC(done)
end, end,
}) })
end
-- PKMN LEAGUE sits between PROF.OAK's PC and LOG OFF once wNumHoFTeams -- engine/pokemon/bills_pc.asm:48-60 PKMN LEAGUE row (#1566)
-- is nonzero (engine/pokemon/bills_pc.asm:5, :49) if #(Game.save.hallOfFame or {}) > 0 then
if #(Game.save.hallOfFame or {}) > 0 then table.insert(items, {
table.insert(items, { label = Strings("<PK><MN>LEAGUE"),
label = Strings("<PK><MN>LEAGUE"), keepOpen = true,
keepOpen = true, onSelect = function()
onSelect = function() -- pc.asm PKMNLeague plays SFX_ENTER_PC, then PKMNLeaguePC prints
-- pc.asm PKMNLeague plays SFX_ENTER_PC, then PKMNLeaguePC prints -- AccessedHoFPCText (engine/menus/pc.asm:67, league_pc.asm:2)
-- AccessedHoFPCText (engine/menus/pc.asm:67, league_pc.asm:2) require("src.core.Sound").play(Game.data, "Enter_PC")
require("src.core.Sound").play(Game.data, "Enter_PC") Game.stack:push(TextBox.new(Game,
Game.stack:push(TextBox.new(Game, romText(Game.data, "_AccessedHoFPCText",
romText(Game.data, "_AccessedHoFPCText", "Accessed POKéMON\nLEAGUE's site.\fAccessed the HALL\nOF FAME List."),
"Accessed POKéMON\nLEAGUE's site.\fAccessed the HALL\nOF FAME List."), function() Screens.push(Game, "LeaguePC") end))
function() Screens.push(Game, "LeaguePC") end)) done()
done() end,
end, })
}) end
end end
local hooked = Runtime.call("ui.pc.items", sameItems, Game, items) 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 -- the approach walk and then EngageMapTrainer with no further text: the
-- caller already showed the box, so the battle starts without a second -- caller already showed the box, so the battle starts without a second
-- one (#869). -- one (#869).
function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText) function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText,
endBattleSound, endBattleIsReward)
local d = npc.def local d = npc.def
Runtime.emit("world.trainer_engaged", { npc = npc, trainerClass = d.trainerClass, Runtime.emit("world.trainer_engaged", { npc = npc, trainerClass = d.trainerClass,
partyIndex = d.trainerParty }) partyIndex = d.trainerParty })
@@ -3376,6 +3376,13 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
-- cuts (#282). Substituted here because BattleState:say takes finished -- cuts (#282). Substituted here because BattleState:say takes finished
-- text, while TextBox expanded the {PLAYER}/{RIVAL} tokens itself. -- text, while TextBox expanded the {PLAYER}/{RIVAL} tokens itself.
battle.endBattleText = wonText and TextBox.substitute(Game, wonText) or nil 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) battle.onFinish = function(result)
if result == "win" then if result == "win" then
Game.save.defeatedTrainers[npc.id] = true 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 -- onVictory script UNDER whatever runs next, so the player still sees
-- EndBattle (now inside the battle), then the reward, then AfterBattle -- EndBattle (now inside the battle), then the reward, then AfterBattle
self:checkVictoryRewards(d.trainerClass, d.trainerParty, self:checkVictoryRewards(d.trainerClass, d.trainerParty,
endBattleText ~= nil) battle.rewardDialogueShown)
self:afterBattle(result, battle) self:afterBattle(result, battle)
if onDone then onDone() end if onDone then onDone() end
else else
@@ -4449,7 +4456,7 @@ function OverworldState:restoreBattleContinuation(battle, origin)
game.save.defeatedTrainers[origin.npcId] = true game.save.defeatedTrainers[origin.npcId] = true
if origin.event then game.save.flags[origin.event] = true end if origin.event then game.save.flags[origin.event] = true end
self:checkVictoryRewards(battle.oppClass, battle.partyIndex, self:checkVictoryRewards(battle.oppClass, battle.partyIndex,
battle.endBattleText ~= nil) battle.rewardDialogueShown)
end end
self:afterBattle(result, battle) self:afterBattle(result, battle)
self.engaging = false self.engaging = false
+8 -2
View File
@@ -6017,8 +6017,14 @@ function World:trainerWinLossText()
if not vm then return nil, nil end if not vm then return nil, nil end
local obj = vm.trainerObject or {} local obj = vm.trainerObject or {}
local text = self.text or {} local text = self.text or {}
local winKey = vm.winTextOverride or obj.winText -- `winlosstext` writes BOTH pointers; a 0 argument destroys the struct
local lossKey = vm.lossTextOverride or obj.lossText -- 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 return winKey and text[winKey] or nil, lossKey and text[lossKey] or nil
end end
@@ -23,8 +23,13 @@ return function(game)
game.save.party = { mon } game.save.party = { mon }
-- engine/movie/evolution.asm:103 -- engine/movie/evolution.asm:103
Evolution.evolve(game, mon, "RAICHU", nil, "ITEM") Evolution.evolve(game, mon, "RAICHU", nil, "ITEM")
U.wait(12) -- the IsEvolvingText box now holds 50+ frames before the movie (#1596)
local top = game.stack:top() local top
for _ = 1, 300 do
top = game.stack:top()
if top and top.screenId == "EvolutionState" then break end
U.wait(1)
end
check("the evolution screen opened", check("the evolution screen opened",
top and top.screenId == "EvolutionState") top and top.screenId == "EvolutionState")
U.shot(game, DIR .. "/bug1412_evo_old_pikachu.png") U.shot(game, DIR .. "/bug1412_evo_old_pikachu.png")
@@ -39,9 +39,13 @@ return function(game)
local mon = Pokemon.new(game.data, "PIKACHU", 20) local mon = Pokemon.new(game.data, "PIKACHU", 20)
game.save.party = { mon } game.save.party = { mon }
Evolution.evolve(game, mon, "RAICHU") Evolution.evolve(game, mon, "RAICHU")
U.wait(12) -- the IsEvolvingText box now holds 50+ frames before the movie (#1596)
local top
local top = game.stack:top() for _ = 1, 300 do
top = game.stack:top()
if top and top.screenId == "EvolutionState" then break end
U.wait(1)
end
check("the evolution screen opened", top and top.screenId == "EvolutionState") check("the evolution screen opened", top and top.screenId == "EvolutionState")
U.log("Issue #494: true-color sprites during evolutions and trades") U.log("Issue #494: true-color sprites during evolutions and trades")
U.log("Watch this PIKACHU evolve into RAICHU in ADVANCED colors.") U.log("Watch this PIKACHU evolve into RAICHU in ADVANCED colors.")
+53 -14
View File
@@ -50,7 +50,8 @@ return function(game)
U.wait(5) U.wait(5)
local ow = game.stack:top() local ow = game.stack:top()
U.shot(game, DIR .. "/" .. shots.prefix .. "_0_gym.png") U.shot(game, DIR .. "/" .. shots.prefix .. "_0_gym.png")
ow:checkVictoryRewards(class, party) -- true: the badge line rode the battle screen on the real path (#1606)
ow:checkVictoryRewards(class, party, true)
U.wait(10) U.wait(10)
for _, s in ipairs(shots.pages) do for _, s in ipairs(shots.pages) do
advancePages(s.want, shots.prefix .. "_" .. s.name) advancePages(s.want, shots.prefix .. "_" .. s.name)
@@ -63,26 +64,64 @@ return function(game)
game.save.player.name = game.save.player.name or "RED" game.save.player.name = game.save.player.name or "RED"
runLeader("PEWTER_GYM", 4, 3, "OPP_BROCK", 1, { -- The REAL gym path (#1606): the badge line and jingle must ride the
prefix = "brock", -- battle it pushes (scripts/PewterGym.asm:117-119).
pages = { while game.stack:top() do game.stack:pop() end
{ want = "BOULDERBADGE", name = "1_badge" }, local Pokemon = require("src.pokemon.Pokemon")
{ want = "FLASH", name = "2_flash" }, game.save.party = { Pokemon.new(game.data, "CHARIZARD", 50) }
{ want = "Wait!", name = "3_wait" }, game.save.flags = game.save.flags or {}
{ want = "TM34", name = "4_tm34" }, game.save.inventory = game.save.inventory or {}
{ want = "BIDE", name = "5_bide" }, game.save.defeatedTrainers = game.save.defeatedTrainers or {}
}, game.stack:push(OW, "PEWTER_GYM", 4, 3, "up")
}) U.wait(5)
local realOw = game.stack:top()
local brock
for _, npc in ipairs(realOw.npcs or {}) do
if npc.def and npc.def.trainerClass == "OPP_BROCK" then brock = npc end
end
assert(brock, "Brock stands in PEWTER_GYM")
require("data.scripts.gyms").PEWTER_GYM.talk.TEXT_PEWTERGYM_BROCK(
game, realOw, brock, function() end)
local battle
for _ = 1, 600 do
local top = game.stack:top()
if top and top.oppClass == "OPP_BROCK" and top.onFinish then
battle = top
break
end
U.tap(game, "a")
U.wait(2)
end
assert(battle, "the real gym path reaches Brock's BattleState")
assert(type(battle.endBattleText) == "string" and #battle.endBattleText > 0,
"the battle carries the armed badge line (#1606)")
assert(battle.endBattleSound == "Get_Item1",
"and the badge jingle beside it (sound_level_up, PewterGym.asm)")
U.shot(game, DIR .. "/brock_real_battle.png")
U.log("force-finishing Brock's battle to run the reward chain")
battle.onFinish("win")
if game.stack:top() == battle then game.stack:pop() end
U.wait(10)
-- rewardDialogueShown: the badge line rode the battle screen, so the map
-- chain opens on the TM prelude, not on a reprint of the badge line
for _, s in ipairs({
{ want = "Wait!", name = "1_wait" },
{ want = "TM34", name = "2_tm34" },
{ want = "BIDE", name = "3_bide" },
}) do
advancePages(s.want, "brock_" .. s.name)
end
assert(game.save.flags.EVENT_BEAT_BROCK, "EVENT_BEAT_BROCK") assert(game.save.flags.EVENT_BEAT_BROCK, "EVENT_BEAT_BROCK")
assert(game.save.inventory.BOULDERBADGE, "BOULDERBADGE") assert(game.save.inventory.BOULDERBADGE, "BOULDERBADGE")
assert((game.save.inventory.TM_BIDE or 0) >= 1, "TM_BIDE") assert((game.save.inventory.TM_BIDE or 0) >= 1, "TM_BIDE")
-- checkVictoryRewards direct drive, as after a battle whose badge line
-- rode the battle screen (shownOnBattleScreen = true)
runLeader("CERULEAN_GYM", 5, 5, "OPP_MISTY", 1, { runLeader("CERULEAN_GYM", 5, 5, "OPP_MISTY", 1, {
prefix = "misty", prefix = "misty",
pages = { pages = {
{ want = "CASCADEBADGE", name = "1_badge" }, { want = "CUT", name = "1_cut" },
{ want = "CUT", name = "2_cut" }, { want = "TM11", name = "2_tm11" },
{ want = "TM11", name = "3_tm11" },
}, },
}) })
assert(game.save.flags.EVENT_BEAT_MISTY, "EVENT_BEAT_MISTY") assert(game.save.flags.EVENT_BEAT_MISTY, "EVENT_BEAT_MISTY")
+16
View File
@@ -87,6 +87,22 @@ do
"and no title row: the box carries no header text") "and no title row: the box carries no header text")
end end
-- the box keeps the palette beneath and caps the cursor at wMaxMenuItem
do
local list = newList(6)
eq(list.sgbPalettes, false,
"no SET_PAL_GENERIC: ItemMenuLoop keeps RunDefaultPaletteCommand's "
.. "palette (start_sub_menus.asm:300)")
eq(list.cursorRows, 3,
"wMaxMenuItem 2: three cursor rows (home/list_menu.asm:46-48)")
list.game = { input = { wasPressed = function(_, b) return b == "down" end,
isDown = function() return false end } }
for _ = 1, 3 do list:update(1 / 60) end
eq(list.index, 4, "three downs reach the fourth item")
eq(list.index - list.scroll, 3,
"scrolling instead of dropping the cursor onto the look-ahead row")
end
-- a short list stops at its last name, and the terminator's CANCEL row is -- a short list stops at its last name, and the terminator's CANCEL row is
-- what would follow -- never the '▼' -- what would follow -- never the '▼'
do do
@@ -40,6 +40,11 @@ do
T.eq(battle:shrinkOutScale(battle.player), 3 / 7, "wDownscaledMonSize 1 -> 3x3") T.eq(battle:shrinkOutScale(battle.player), 3 / 7, "wDownscaledMonSize 1 -> 3x3")
battle.shrinkOut.frame = 6 battle.shrinkOut.frame = 6
T.eq(battle:shrinkOutScale(battle.player), 3 / 7, "through Delay3") T.eq(battle:shrinkOutScale(battle.player), 3 / 7, "through Delay3")
battle.shrinkOut.frame = 7
T.eq(battle:shrinkOutScale(battle.player), 0,
"then the 7x7 area holds cleared: no full-size flash before the swap")
T.check(battle:shrinkOutScale({}) == nil,
"the swapped-in battler draws normally")
T.check(battle:shrinkOutScale(battle.enemy) == nil, T.check(battle:shrinkOutScale(battle.enemy) == nil,
"AnimateRetreatingPlayerMon is player-side only") "AnimateRetreatingPlayerMon is player-side only")
end end
@@ -62,6 +67,8 @@ do
T.check(battle:shrinkOutScale(battle.player) == nil, "and no downscale stage") T.check(battle:shrinkOutScale(battle.player) == nil, "and no downscale stage")
battle.queue[3].fn() battle.queue[3].fn()
T.check((battle.picOff or {}).playerMon == nil, "the slot clears afterwards") T.check((battle.picOff or {}).playerMon == nil, "the slot clears afterwards")
T.eq(battle.sendingOut, true,
"and stays hidden until the swap's send-out (#1545)")
end end
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
@@ -109,4 +116,62 @@ do
T.check(battle.queue[2] and battle.queue[2].fn ~= nil, "which also reprompts") T.check(battle.queue[2] and battle.queue[2].fn ~= nil, "which also reprompts")
end end
-- ---------------------------------------------------------------------
-- the SHIFT prompt's picker reprompts on a dead or already-out pick too
-- (HasMonFainted's NoWillText, core.asm:1473-1488) (#1608)
-- ---------------------------------------------------------------------
do
local save = SaveData.newGame()
save.player.name = "RED"
save.party = { Pokemon.new(Data, "FIXMON_A", 30),
Pokemon.new(Data, "FIXMON_B", 30) }
save.options = { battleStyle = "shift" }
local game = { data = Data, save = save,
stack = { top = function() return nil end,
push = function() end, pop = function() end } }
local battle = BattleState.newTrainer(game, "OPP_FIX_YOUNGSTER", 1)
battle.participants = {}
battle.buildScreen = function(_, _, opts) return opts end
-- capture the picker opts the SHIFT branch pushes
local captured
Data.screens = Data.screens or {}
Data.screens.PartyMenu = function(_, opts) captured = opts; return {} end
require("src.ui.Screens").invalidate()
battle.enemyParty[1].hp = 0
battle.enemy.mon = battle.enemyParty[1]
battle:enemyMonFainted()
local choiceRow
for _, row in ipairs(battle.queue) do
if row.choice then choiceRow = row end
end
T.check(choiceRow ~= nil, "SHIFT queues the change-POKeMON choice")
choiceRow.choice(true)
T.check(captured ~= nil and captured.forceSwitch == true,
"YES opens the forced party picker")
battle.queue, battle.nextInsert = {}, 0
save.party[2].hp = 0
captured.onSwitch(save.party[2])
T.check(battle.queue[1] and battle.queue[1].text
and battle.queue[1].text:find("no will", 1, true) ~= nil,
"a fainted SHIFT pick prints NoWillText first")
T.check(battle.queue[2] and battle.queue[2].ui ~= nil,
"then the picker goes straight back up, ahead of the send-out")
T.eq(battle.queue[2].ui(), captured, "with the same forced opts")
battle.queue, battle.nextInsert = {}, 0
captured.onSwitch(battle.player.mon)
T.check(battle.queue[1] and battle.queue[1].text
and battle.queue[1].text:find("already out", 1, true) ~= nil,
"an already-out SHIFT pick prints AlreadyOutText")
T.check(battle.queue[2] and battle.queue[2].ui ~= nil, "and reprompts too")
battle.queue, battle.nextInsert = {}, 0
save.party[2].hp = 10
captured.onSwitch(save.party[2])
T.eq(#battle.queue, 0, "a healthy pick queues no reprompt rows")
Data.screens.PartyMenu = nil
require("src.ui.Screens").invalidate()
end
T.finish("retreat animation and switch reprompt (#1563, #1545, #1608)") T.finish("retreat animation and switch reprompt (#1563, #1545, #1608)")
+12 -6
View File
@@ -30,7 +30,9 @@ package.loaded["src.core.Sound"] = {
playCry = function() end, playCry = function() end,
} }
package.loaded["src.render.TextBox"] = { package.loaded["src.render.TextBox"] = {
new = function(_, text, done) return { textBox = true, text = text, done = done } end, new = function(_, text, done, opts)
return { textBox = true, text = text, done = done, opts = opts }
end,
} }
-- BagMenu and PartyMenu bind TextBox at require time, so they load against the -- BagMenu and PartyMenu bind TextBox at require time, so they load against the
-- stub; Screens caches its factory per id and must be told to forget. -- stub; Screens caches its factory per id and must be told to forget.
@@ -126,17 +128,21 @@ local function useStone(game)
game.input.pressed = "a" game.input.pressed = "a"
picker:update(1 / 60) picker:update(1 / 60)
game.input.pressed = nil game.input.pressed = nil
-- IsEvolvingText hands off to the movie when it closes -- IsEvolvingText STAYS up; its onShown starts the DelayFrames 50 hold,
-- (engine/pokemon/evos_moves.asm:120-134) -- which then pushes the movie over it (evos_moves.asm:120-134) (#1596)
local intro = game.stack:top() local intro = game.stack:top()
if not (intro and intro.textBox and intro.done) then if not (intro and intro.textBox and intro.opts and intro.opts.stay) then
return nil, "the \"is evolving!\" box never opened" return nil, "the \"is evolving!\" box never opened"
end end
if not tostring(intro.text):find("evolving") then if not tostring(intro.text):find("evolving") then
return nil, "the box before the movie is not _IsEvolvingText" return nil, "the box before the movie is not _IsEvolvingText"
end end
game.stack:pop() intro.opts.stay.onShown()
intro.done() local hold = game.stack:top()
for _ = 1, 60 do
if pushed then break end
if hold.update then hold.update() end
end
return list return list
end end
@@ -1,5 +1,6 @@
-- The evolution dialogue is the cart's, in the cart's order (#1596): -- The evolution dialogue is the cart's, in the cart's order (#1596):
-- engine/pokemon/evos_moves.asm:120-128, :136-150, :151-153 -- engine/pokemon/evos_moves.asm:120-134 (the clear is rows 0-11 only),
-- :136-150, :151-153
package.path = "./?.lua;./?/init.lua;" .. package.path package.path = "./?.lua;./?/init.lua;" .. package.path
@@ -65,10 +66,9 @@ do
"_IsEvolvingText goes up in a real bordered text box first") then "_IsEvolvingText goes up in a real bordered text box first") then
check(textOf(intro):find("is evolving"), check(textOf(intro):find("is evolving"),
"and it is the cart's line: " .. textOf(intro)) "and it is the cart's line: " .. textOf(intro))
check(intro.auto ~= nil and not intro.auto.wait, check(intro.stay ~= nil and not intro.stay.prompt,
"which waits for no button (IsEvolvingText ends in `done`)") "which waits for no button (IsEvolvingText ends in `done`) "
eq(intro.auto.delay, 50, .. "and stays up under whatever follows")
"and holds DelayFrames 50 before it clears (evos_moves.asm:122)")
end end
-- it hands off to the movie on its own, with no input at all -- it hands off to the movie on its own, with no input at all
local top local top
@@ -78,7 +78,15 @@ do
step(game) step(game)
end end
check(getmetatable(top) == EvolutionState, check(getmetatable(top) == EvolutionState,
"the flash movie opens once that box has cleared itself") "the flash movie opens once the DelayFrames 50 hold has passed")
local underneath = false
for _, s in ipairs(game.stack.states or {}) do
if s == intro then underneath = true end
end
check(underneath, "the 'is evolving!' box is still on the stack under the "
.. "flash (ClearScreenArea wipes rows 0-11 only, evos_moves.asm:126-128)")
check(not EvolutionState.isOpaque,
"and the flash screen is not opaque, so the box beneath draws")
eq(mon.species, "FIXMON_A", eq(mon.species, "FIXMON_A",
"and nothing has evolved yet while the box was up") "and nothing has evolved yet while the box was up")
end end
@@ -122,12 +130,4 @@ do
end end
end end
-- The flash itself carries no text: EvolutionState draws sprites only.
do
local source = assert(io.open("src/ui/EvolutionState.lua")):read("*a")
check(not source:find("Font%.draw"),
"EvolutionState draws no bare text over the flash "
.. "(ClearScreenArea ran before EvolveMon, evos_moves.asm:128)")
end
T.finish() T.finish()
@@ -35,4 +35,17 @@ local menu = PokedexMenu.new(game, { onClose = function() closed = true end })
menu:close() menu:close()
check(closed, "close still runs the caller's onClose") check(closed, "close still runs the caller's onClose")
-- Save.newGame seeds the key and Save.validate clamps a hand-edited value
local Save = require("src.core.gen2.Save")
eq(Save.newGame().lastDexMode, "NEW",
"a brand-new save carries the key from the start (wLastDexMode's zero)")
local edited = Save.newGame()
edited.lastDexMode = "SPICY"
Save.validate(edited)
eq(edited.lastDexMode, "NEW", "validate clamps an out-of-range mode to NEW")
local kept = Save.newGame()
kept.lastDexMode = "A-Z"
Save.validate(kept)
eq(kept.lastDexMode, "A-Z", "and keeps a legal one")
T.finish("gen2 pokedex mode persistence bug 1474") T.finish("gen2 pokedex mode persistence bug 1474")
+25 -1
View File
@@ -34,4 +34,28 @@ T.check(not keys["TILESET_KANTO|ROOF_SILVER"],
T.check(keys["TILESET_JOHTO|ROOF_SILVER"], T.check(keys["TILESET_JOHTO|ROOF_SILVER"],
"while TILESET_JOHTO still takes its group's roof") "while TILESET_JOHTO still takes its group's roof")
print("gen2 Kanto roof gate (#1479): ok") -- World:atlasFor is the copy the bug was filed against; it has its own
-- ROOF_TILESETS gate, so pin it separately from MapPreview's
local realAssets = package.loaded["src.render.Assets"]
package.loaded["src.render.Assets"] = setmetatable({
image = function() return { setFilter = function() end } end,
resolve = function(path) return path end,
}, { __index = realAssets or { register = function() end } })
package.loaded["src.world.gen2.World"] = nil
local World = require("src.world.gen2.World")
local world = setmetatable({
tilesets = baker.tilesets,
roofs = baker.roofs,
atlasCache = {},
}, { __index = World })
world:atlasFor({ tileset = "TILESET_KANTO", group = 19 })
world:atlasFor({ tileset = "TILESET_JOHTO", group = 19 })
T.check(world.atlasCache["TILESET_KANTO"] ~= nil
and world.atlasCache["TILESET_KANTO|ROOF_SILVER"] == nil,
"World:atlasFor bakes Kanto with no map-group roof (home/map.asm:1738-1749)")
T.check(world.atlasCache["TILESET_JOHTO|ROOF_SILVER"] ~= nil,
"and still roofs TILESET_JOHTO by group")
package.loaded["src.render.Assets"] = realAssets
package.loaded["src.world.gen2.World"] = nil
T.finish("gen2 Kanto roof gate (#1479)")
+51
View File
@@ -0,0 +1,51 @@
-- #1558: menu_gfx.stats -> $31..$41 quads (engine/gfx/load_font.asm:90-95);
-- PAGE_PALETTES is gfx/stats/pages.pal (cgb_layouts.asm:199-212)
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local SummaryMenu = require("src.ui.gen2.SummaryMenu")
-- gfx/stats/pages.pal, RGB 5-bit rows in ROM order: pink, green, blue
local ROM_PALS = {
{ { 31, 31, 31 }, { 31, 19, 31 }, { 31, 15, 31 }, { 0, 0, 0 } },
{ { 31, 31, 31 }, { 21, 31, 14 }, { 17, 31, 0 }, { 0, 0, 0 } },
{ { 31, 31, 31 }, { 17, 31, 31 }, { 17, 31, 31 }, { 0, 0, 0 } },
}
local function up(v) return math.floor(v * 255 / 31 + 0.5) end
for p = 1, 3 do
for c = 1, 4 do
for ch = 1, 3 do
T.eq(SummaryMenu.PAGE_PALETTES[p][c][ch], up(ROM_PALS[p][c][ch]),
("pages.pal palette %d color %d channel %d"):format(p, c, ch))
end
end
end
-- the quads: 17 tiles from $31, one 8x8 cell each off the 136x8 sheet
love.graphics = love.graphics or {}
local realNewQuad = love.graphics.newQuad
love.graphics.newQuad = function(x, y, w, h)
return { x = x, y = y, w = w, h = h }
end
local menu = setmetatable({
menuGfx = { stats = { sheet = "stats", tiles = 17, firstTile = 0x31 } },
picImage = function()
return { getDimensions = function() return 136, 8 end }
end,
}, { __index = SummaryMenu })
local sheet = menu:statsTiles()
if T.check(sheet ~= nil, "menu_gfx.stats builds the tile sheet") then
for id = 0x31, 0x41 do
local q = sheet.quads[id]
T.check(q ~= nil and q.x == (id - 0x31) * 8 and q.w == 8 and q.h == 8,
("tile $%02x maps to sheet cell %d"):format(id, id - 0x31))
end
T.check(sheet.quads[0x42] == nil, "and exactly 17 tiles, no more")
end
-- a cache built before menu_gfx.stats existed keeps the hand-drawn fallback
local old = setmetatable({ menuGfx = {} }, { __index = SummaryMenu })
T.check(old:statsTiles() == nil, "no menu_gfx.stats falls back, not crashes")
love.graphics.newQuad = realNewQuad
T.finish("gen2 stats tiles and page palettes (#1558)")
+7 -2
View File
@@ -93,7 +93,7 @@ do
local money = indexOf(events, "money") local money = indexOf(events, "money")
T.check(ret and win and money, "all three win rows are queued: " .. T.check(ret and win and money, "all three win rows are queued: " ..
kinds(events)) kinds(events))
T.check(defeated < ret, "BattleText_EnemyWasDefeated comes first") T.check(defeated < ret, "the last faint event precedes the pic's return")
T.check(ret < win, "the frontpic slides back in before the line") T.check(ret < win, "the frontpic slides back in before the line")
T.check(win < money, "and PrintWinLossText runs before the payout") T.check(win < money, "and PrintWinLossText runs before the payout")
T.eq(winEvent.text, "Th-Thank you!", "the struct's win text is printed") T.eq(winEvent.text, "Th-Thank you!", "the struct's win text is printed")
@@ -153,12 +153,17 @@ do
T.eq(win, "Th-Thank you!", "the struct's win text is decoded") T.eq(win, "Th-Thank you!", "the struct's win text is decoded")
T.eq(loss, "...Too weak...", "and its loss text with it") T.eq(loss, "...Too weak...", "and its loss text with it")
world.vm.winLossArmed = true
world.vm.winTextOverride = "3:4200" world.vm.winTextOverride = "3:4200"
win = World.trainerWinLossText(world) win, loss = World.trainerWinLossText(world)
T.eq(win, "Scripted win.", "`winlosstext` overwrites the pointer") T.eq(win, "Scripted win.", "`winlosstext` overwrites the pointer")
-- winlosstext writes BOTH pointers; its 0 loss argument destroyed the
-- struct's value (engine/overworld/scripting.asm:651)
T.eq(loss, nil, "and a 0 loss argument zeroes the loss pointer with it")
world.vm.trainerObject = nil world.vm.trainerObject = nil
world.vm.winTextOverride = nil world.vm.winTextOverride = nil
world.vm.winLossArmed = nil
win, loss = World.trainerWinLossText(world) win, loss = World.trainerWinLossText(world)
T.eq(win, nil, "a battle with no trainer object has no line") T.eq(win, nil, "a battle with no trainer object has no line")
T.eq(loss, nil, "on either side") T.eq(loss, nil, "on either side")
@@ -35,13 +35,15 @@ end
local fakeGame = { data = { text = text }, save = { flags = {} } } local fakeGame = { data = { text = text }, save = { flags = {} } }
local armed local armed, armedSound
local fakeOw = { local fakeOw = {
engageTrainer = function(_, _, _, endBattleText) armed = endBattleText end, engageTrainer = function(_, _, _, endBattleText, _, endBattleSound)
armed, armedSound = endBattleText, endBattleSound
end,
} }
local function armedFor(mapId, textId, victoryKey) local function armedFor(mapId, textId, victoryKey)
armed = nil armed, armedSound = nil, nil
gyms[mapId].talk[textId](fakeGame, fakeOw, { id = "npc#1" }, function() end) gyms[mapId].talk[textId](fakeGame, fakeOw, { id = "npc#1" }, function() end)
local labels = victories[victoryKey].dialogue local labels = victories[victoryKey].dialogue
local want = {} local want = {}
@@ -63,6 +65,10 @@ local leaders = {
for _, entry in ipairs(leaders) do for _, entry in ipairs(leaders) do
local got, want = armedFor(entry[1], entry[2], entry[3]) local got, want = armedFor(entry[1], entry[2], entry[3])
T.eq(got, want, entry[3] .. " arms its badge line for the battle screen") T.eq(got, want, entry[3] .. " arms its badge line for the battle screen")
-- the dialogue's sound command rides the armed line onto the battle
-- screen too (sound_get_item_1 / sound_get_key_item) (#1606)
T.eq(armedSound, victories[entry[3]].badgeSound,
entry[3] .. " arms its badge jingle beside the line")
end end
-- Brock's armed label is one text chain of two text_far pages -- Brock's armed label is one text chain of two text_far pages
@@ -137,3 +143,5 @@ T.check(afterBattleScreen:find(text["_CeruleanGymMistyCascadeBadgeInfoText"],
1, true) ~= nil, 1, true) ~= nil,
"the TM hand-over still runs on the map") "the TM hand-over still runs on the map")
T.check(rewardGame.save.inventory.CASCADEBADGE == 1, "the badge is still given") T.check(rewardGame.save.inventory.CASCADEBADGE == 1, "the badge is still given")
T.finish("gym end battle text (#1606)")
@@ -75,6 +75,25 @@ T.eq(title.menuOpen, true, "the title art stays down until the blink ends")
run(back, 60) run(back, 60)
T.eq(title.menuOpen, false, "and comes back once the blink is over") T.eq(title.menuOpen, false, "and comes back once the blink is over")
T.eq(game.stack:top(), title, "leaving the menu lands back on the title") T.eq(game.stack:top(), title, "leaving the menu lands back on the title")
-- main_menu.asm:70 jumps back to DisplayTitleScreen: the whole cinematic reruns
T.eq(title.phase, "drop", "cancel reruns the boot cinematic from the logo drop")
-- a stranded menuOpen (an onSelect that handed control straight back) may
-- not leave a blank white title behind
title.menuOpen = true
title:update(1 / 60)
T.eq(title.menuOpen, false, "a stranded menuOpen clears on the next update")
-- .finishedWaiting: PlayCry then WaitForSoundToFinish before the white-out
-- (engine/movie/title.asm:241-243)
while title.phase ~= "loop" do title:updateSequence() end
game.input.wasPressed = function(_, b) return b == "start" end
title:update(1 / 60)
game.input.wasPressed = function() return false end
T.eq(title.phase, "exitCry", "START waits out the cry before the white-out")
T.eq(game.stack:top(), title, "no flash is pushed on the cry frame")
run(title, 10)
T.check(game.stack:top() ~= title, "the flash follows once the cry is done")
-- ------------------------------------------- #1511: the intro NAME box -- ------------------------------------------- #1511: the intro NAME box
@@ -144,11 +163,20 @@ local function pump(frames)
end end
end end
pump(400) pump(400)
T.check(getmetatable(fgame.stack:top()) == Menu, "the preset list opens") -- _IntroducePlayerText ends in `prompt` (text_2.asm:1730): arrowed A wait
T.check(getmetatable(fgame.stack:top()) ~= Menu,
"the question box waits for A before the name list")
T.check(fgame.stack:top() == flow.holdBox and flow.holdBox.done,
"the typed-out question box is on top, waiting for the press")
fgame.input.wasPressed = function(_, b) return b == "a" end
fgame.stack:top():update(1 / 60)
fgame.input.wasPressed = function() return false end
pump(400)
T.check(getmetatable(fgame.stack:top()) == Menu, "A opens the preset list")
T.eq(fgame.stack.states[2], flow.holdBox, T.eq(fgame.stack.states[2], flow.holdBox,
"IntroducePlayerText's box is still on the stack under the name list") "IntroducePlayerText's box is still on the stack under the name list")
T.check(flow.holdBox.done and flow.holdBox.stay, T.check(flow.holdBox.stayShown == true,
"it is a `stay` box: text_end returns from PrintText without a wait") "prompt-then-hold: the box stays up after the press (home/text.asm:434)")
local preset = fgame.stack:top() local preset = fgame.stack:top()
preset.index = 2 preset.index = 2
@@ -157,7 +185,9 @@ preset:update(1 / 60)
fgame.input.wasPressed = function() return false end fgame.input.wasPressed = function() return false end
T.eq(fgame.save.player.name, "RED", "picking a preset names the player") T.eq(fgame.save.player.name, "RED", "picking a preset names the player")
T.eq(flow.holdBox, nil, "and takes the question box down with the list") T.eq(flow.holdBox, nil, "and takes the question box down with the list")
run(fgame.stack:top(), 6 * 3) -- 13-frame ClearScreenArea / DelayFrames beat, then six tiles of slide
-- (oak_speech2.asm:69-78)
run(fgame.stack:top(), 13 + 6 * 3)
T.eq(flow.picSlide, 0, "OakSpeechSlidePicLeft puts the pic back") T.eq(flow.picSlide, 0, "OakSpeechSlidePicLeft puts the pic back")
T.finish("intro_title_naming_bug1510_1511") T.finish("intro_title_naming_bug1510_1511")
+7 -4
View File
@@ -87,9 +87,12 @@ T.eq(box.text, fakeGame.data.text._AccessedHoFPCText,
box.onDone() box.onDone()
T.same(opened, { "LeaguePC" }, "the Hall of Fame roster screen opens") T.same(opened, { "LeaguePC" }, "the Hall of Fame roster screen opens")
-- without the Pokedex the league row still shows (bills_pc.asm checks -- without the Pokedex, .noOaksPC2 skips Oak's PC and the league row alike
-- wNumHoFTeams before EVENT_GOT_POKEDEX) -- (bills_pc.asm:48-49, :68-72); only the box height ignores it (:5-7)
fakeGame.save.flags.EVENT_GOT_POKEDEX = nil fakeGame.save.flags.EVENT_GOT_POKEDEX = nil
local noDex = labels() local noDex = labels()
T.check(indexOf(noDex, "<PK><MN>LEAGUE"), T.eq(indexOf(noDex, "<PK><MN>LEAGUE"), nil,
"the league row does not depend on EVENT_GOT_POKEDEX") "no dex, no PKMN LEAGUE row, HoF teams or not")
T.eq(indexOf(noDex, "PROF.OAK's PC"), nil, "and no PROF.OAK's PC either")
T.finish("pc league row (#1566)")
@@ -159,6 +159,47 @@ do
end end
end end
-- .useRareCandy: TryEvolvingMon runs over the party list
-- (item_effects.asm:1392-1418) (#1594)
do
local evolveCalls = {}
package.loaded["src.pokemon.Evolution"] = {
pendingFor = function() return "FIXMON_B", { method = "LEVEL" } end,
evolve = function(_, _, to, onDone, via)
evolveCalls[#evolveCalls + 1] = { to = to, onDone = onDone, via = via }
end,
}
package.loaded["src.battle.BattleState"] = {
StatBox = { new = function(_, _, cb) return { statBox = true, cb = cb } end },
}
local game = freshGame(3)
local list = useFromBag(game, nil, "RARE_CANDY")
if check(list ~= nil, "the bag reached the picker (evolution case)") then
local box = game.stack:top()
check(isBox(box), "the level line prints first")
game.stack:pop() -- a real TextBox pops itself before onDone
box.done()
local stat = game.stack:top()
if check(stat and stat.statBox, "then the stat window") then
game.stack:pop() -- as does the stat window before its callback
stat.cb()
eq(#evolveCalls, 1, "the pending evolution starts")
check(inStack(game.stack, isPicker),
"with the party picker STILL up: the evolution prints over it, "
.. "not over the bag list (#1594)")
check(type(evolveCalls[1].onDone) == "function",
"closePicker rides the evolution's completion callback")
evolveCalls[1].onDone()
check(not inStack(game.stack, isPicker),
"and the picker comes down once the evolution flow completes")
check(inStack(game.stack, function(s) return s == list end),
"while the bag list survives (#796)")
end
end
package.loaded["src.pokemon.Evolution"] = nil
package.loaded["src.battle.BattleState"] = nil
end
-- The last candy: the row goes away (RemoveUsedItem empties the slot) and the -- The last candy: the row goes away (RemoveUsedItem empties the slot) and the
-- cursor clamps to a real row -- but the list itself still must not close. -- cursor clamps to a real row -- but the list itself still must not close.
do do
+7 -1
View File
@@ -35,6 +35,10 @@ end
T.check(save ~= nil, "the start menu lists SAVE") T.check(save ~= nil, "the start menu lists SAVE")
save.onSelect() save.onSelect()
-- PrintSaveScreenText ends `ld c, 30 / jp DelayFrames`: the bare panel
-- holds 30 frames before the prompt (main_menu.asm:404-405)
T.eq(#game.stack.states, 1, "the panel shows alone first")
for _ = 1, 30 do game.stack:top().update() end
T.eq(#game.stack.states, 2, "the panel and the prompt are two separate states") T.eq(#game.stack.states, 2, "the panel and the prompt are two separate states")
local panel, prompt = game.stack.states[1], game.stack.states[2] local panel, prompt = game.stack.states[1], game.stack.states[2]
T.check(type(panel.draw) == "function" and not panel.isTextBox, T.check(type(panel.draw) == "function" and not panel.isTextBox,
@@ -44,9 +48,11 @@ T.eq(#prompt.pages, 1, "the prompt is one page: no \\f-merged info panel")
T.check(prompt.pages[1][1]:find("Would you like to"), T.check(prompt.pages[1][1]:find("Would you like to"),
"the prompt page is WouldYouLikeToSaveText") "the prompt page is WouldYouLikeToSaveText")
-- save.asm:187 hlcoord 0, 7 -- save.asm:188 hlcoord 0, 7
T.eq(prompt.choiceBox.tx, 0, "the save Yes/No box sits at column 0 (left)") T.eq(prompt.choiceBox.tx, 0, "the save Yes/No box sits at column 0 (left)")
T.eq(prompt.choiceBox.ty, 7, "the save Yes/No box sits at row 7") T.eq(prompt.choiceBox.ty, 7, "the save Yes/No box sits at row 7")
T.eq(prompt.choiceBox, require("src.ui.Theme").saveBox,
"the geometry routes through Theme so field.theme can restyle it")
local choice = ChoiceBox.new(game, function() end, { box = prompt.choiceBox }) local choice = ChoiceBox.new(game, function() end, { box = prompt.choiceBox })
T.eq(choice.tx, 0, "ChoiceBox honours the save-specific left placement") T.eq(choice.tx, 0, "ChoiceBox honours the save-specific left placement")
+21 -4
View File
@@ -80,10 +80,11 @@ do
battle:performMove(battle.player, battle.enemy, slot) battle:performMove(battle.player, battle.enemy, slot)
T.check(indexOf(animRows(battle), "SHRINKING_SQUARE_ANIM") == nil, T.check(indexOf(animRows(battle), "SHRINKING_SQUARE_ANIM") == nil,
"a locked-in Thrash queues no setup animation") "a locked-in Thrash queues no setup animation")
-- .ThrashingAboutCheck falls into PlayerCalcMoveDamage, the same -- .ThrashingAboutCheck (core.asm:3534-3535, enemy mirror :5909-5910) (#1577)
-- animation pipeline every other move uses (core.asm:3540) (#1577) T.check(indexOf(animRows(battle), "THRASH") ~= nil,
T.check(indexOf(animRows(battle), "FIX_THRASH") ~= nil, "a continuation turn animates THRASH, not the locked move's own id")
"but the move's own animation still plays on a continuation turn") T.check(indexOf(animRows(battle), "FIX_THRASH") == nil,
"so the locked move's own animation does not play")
T.check(hasText(battle, "thrashing about"), T.check(hasText(battle, "thrashing about"),
"ThrashingAboutText prints in place of the used-move line") "ThrashingAboutText prints in place of the used-move line")
T.check(not hasText(battle, "used FIX THRASH"), T.check(not hasText(battle, "used FIX THRASH"),
@@ -128,4 +129,20 @@ do
"and the user is locked into Thrash next turn") "and the user is locked into Thrash next turn")
end end
-- ---------------------------------------------------------------------
-- JumpMoveEffect (core.asm:3129-3133) before MoveHitTest INVULNERABLE (:3150) (#1565)
do
local battle = newBattle()
battle.queue, battle.nextInsert = {}, 0
battle.enemy.invulnerable = true
battle:performMove(battle.player, battle.enemy, { id = "FIX_THRASH", pp = 20 })
T.check(indexOf(animRows(battle), "SHRINKING_SQUARE_ANIM") ~= nil,
"the setup animation plays against a mid-Fly/Dig target")
T.eq(battle.player.thrashTurns, 2,
"the 2-3 roll commits against a mid-Fly/Dig target")
T.check(battle:menuLockedAction(battle.player) ~= nil,
"and the user is locked into Thrash next turn")
T.check(hasText(battle, "attack missed"), "while the attack itself misses")
end
T.finish("thrash setup animation (#1532)") T.finish("thrash setup animation (#1532)")
+5
View File
@@ -36,5 +36,10 @@ T.eq(boostedText(function(mon, save)
mon.traded = true mon.traded = true
mon.otId = save.player.id mon.otId = save.player.id
end), false, "and a mon traded back to its original trainer loses it (#1488)") end), false, "and a mon traded back to its original trainer loses it (#1488)")
T.eq(boostedText(function(mon)
-- repairTradedOtIds leaves traded mons with otId nil (#1265, #1461)
mon.traded = true
mon.otId = nil
end), true, "a traded mon with no stored OTID keeps the boost (#1488)")
T.finish("traded exp boost is an OTID comparison (#1488)") T.finish("traded exp boost is an OTID comparison (#1488)")
+5 -3
View File
@@ -149,9 +149,11 @@ do
local events = battle:takeTurn({ kind = "move", move = "ROAR" }) local events = battle:takeTurn({ kind = "move", move = "ROAR" })
check(saidSomethingLike(events, "fled in fear!"), check(saidSomethingLike(events, "fled in fear!"),
"FledInFearText: the wild mon is blown away") "FledInFearText: the wild mon is blown away")
check(not saidSomethingLike(events, "used TACKLE!"), -- EFFECT_FORCE_SWITCH is priority 0, below BASE_PRIORITY
"and the mon that left never takes its half of the turn") -- (data/moves/effects_priorities.asm:5): Roar goes last (#1475)
eq(player.hp, hpBefore, "so nothing came back the other way") check(saidSomethingLike(events, "used TACKLE!"),
"so the wild mon takes its half of the turn first, Speed regardless")
check(player.hp < hpBefore, "and its hit landed before the blow-away")
eq(battle.over, true, "the battle is over") eq(battle.over, true, "the battle is over")
eq(battle.outcome, "fled", "as the cart's DRAW") eq(battle.outcome, "fled", "as the cart's DRAW")
end end
+42 -3
View File
@@ -1307,7 +1307,7 @@ do
end end
-- ---- GetMovePriority's Vital Throw carve-out (#1475) ---------------------- -- ---- GetMovePriority's Vital Throw carve-out (#1475) ----------------------
-- engine/battle/core.asm:786 -- engine/battle/core.asm:787-789
do do
local screen = newScreen() local screen = newScreen()
check(runToMenu(screen), "reached the menu") check(runToMenu(screen), "reached the menu")
@@ -1323,11 +1323,20 @@ do
eq(battle:movePriority("SWIFT"), 0, eq(battle:movePriority("SWIFT"), 0,
"while SWIFT, which shares its effect, keeps BASE_PRIORITY") "while SWIFT, which shares its effect, keeps BASE_PRIORITY")
eq(battle:movePriority("QUICK_ATTACK"), 1, "and the table still reads") eq(battle:movePriority("QUICK_ATTACK"), 1, "and the table still reads")
moves.ROAR_FIX = { id = "ROAR_FIX", name = "ROAR", power = 0,
type = "NORMAL", accuracy = 100, pp = 20, effect = "EFFECT_FORCE_SWITCH" }
-- MoveEffectPriorities: EFFECT_FORCE_SWITCH is 0, below BASE_PRIORITY
-- (data/moves/effects_priorities.asm:5)
eq(battle:movePriority("ROAR_FIX"), -1,
"Whirlwind and Roar sit below BASE_PRIORITY")
eq(battle:orderOf("VITAL_THROW", "ROAR_FIX"), "player",
"VITAL_THROW ties a force-switch move (0 vs 0), so Speed decides")
eq(battle:orderOf("TACKLE", "TACKLE"), "player", eq(battle:orderOf("TACKLE", "TACKLE"), "player",
"the faster mon leads on equal priority") "the faster mon leads on equal priority")
eq(battle:orderOf("VITAL_THROW", "TACKLE"), "enemy", eq(battle:orderOf("VITAL_THROW", "TACKLE"), "enemy",
"but VITAL_THROW loses to a normal move whatever the Speed") "but VITAL_THROW loses to a normal move whatever the Speed")
moves.VITAL_THROW, moves.SWIFT, moves.QUICK_ATTACK = nil, nil, nil moves.VITAL_THROW, moves.SWIFT, moves.QUICK_ATTACK = nil, nil, nil
moves.ROAR_FIX = nil
end end
-- ---- a send-out snapshots HP at send time (#1514) ------------------------- -- ---- a send-out snapshots HP at send time (#1514) -------------------------
@@ -1352,6 +1361,29 @@ do
"and the HUD opens on the snapshot, not on the post-hit value") "and the HUD opens on the snapshot, not on the post-hit value")
end end
-- ---- the send-out snapshots level and exp the same way (#1514) ------------
-- SendOutPlayerMon reloads wBattleMon* from the party slot (core.asm:3796-3838)
do
local lead = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
local bench = Mon.new(DATA, "TOTODILE", 10, { dvs = perfect })
local screen, battle = newScreen({ player = lead, party = { lead, bench } })
check(runToMenu(screen), "reached the menu")
battle:takeEvents()
check(battle:switch(2), "the bench mon comes in")
local send = battle:takeEvents()[1]
eq(send.level, bench.level, "the send carries a level snapshot")
eq(send.experience, bench.experience, "and an experience snapshot")
-- awardExperience mutates the live table before the UI dequeues the send
bench.level = bench.level + 3
bench.experience = (bench.experience or 0) + 5000
screen:push(send)
screen:advanceQueue()
eq(screen.shownLevel, send.level,
"the HUD opens on the send-time level, not the post-award one")
eq(screen.shownExp, screen:expPixels(bench, send.level, send.experience),
"and the exp bar fills from the send-time experience")
end
-- ---- LearnMove finishes before the queued send-out (#1516) ---------------- -- ---- LearnMove finishes before the queued send-out (#1516) ----------------
-- LearnMove inside GiveExperiencePoints (engine/battle/core.asm:1959-2010) -- LearnMove inside GiveExperiencePoints (engine/battle/core.asm:1959-2010)
do do
@@ -1404,8 +1436,15 @@ do
local drawn = table.concat(boxes, " ") local drawn = table.concat(boxes, " ")
check(drawn:find("0,8,11,5", 1, true) ~= nil, "the TYPE/PP box is drawn") check(drawn:find("0,8,11,5", 1, true) ~= nil, "the TYPE/PP box is drawn")
check(drawn:find("4,12,16,6", 1, true) ~= nil, "over the narrow list box") check(drawn:find("4,12,16,6", 1, true) ~= nil, "over the narrow list box")
check(drawn:find("0,12,20,6", 1, true) == nil, -- SafeLoadTempTilemapToTilemap keeps the full battle textbox under the
"and the full-width message box is not") -- move list (core.asm:4689); Textbox then MoveInfoBox over it (:5084, :5157).
check(drawn:find("0,12,20,6", 1, true) ~= nil,
"over the restored full-width message box")
local base = drawn:find("0,12,20,6", 1, true)
local list = drawn:find("4,12,16,6", 1, true)
local info = drawn:find("0,8,11,5", 1, true)
check(base < list and list < info,
"painted base box, then list box, then info box")
local text = table.concat(prints, " ") local text = table.concat(prints, " ")
check(text:find("TACKLE@6,13", 1, true) ~= nil, "names sit at column 6") check(text:find("TACKLE@6,13", 1, true) ~= nil, "names sit at column 6")
check(text:find("cursor@5,13", 1, true) ~= nil, "with the cursor at 5") check(text:find("cursor@5,13", 1, true) ~= nil, "with the cursor at 5")
+21
View File
@@ -137,6 +137,27 @@ do
end end
eq(checktime(4), 1, "checktime NITE is TRUE inside the PALETTE_DAY room") eq(checktime(4), 1, "checktime NITE is TRUE inside the PALETTE_DAY room")
eq(checktime(2), 0, "and checktime DAY is FALSE there") eq(checktime(2), 0, "and checktime DAY is FALSE there")
-- the two out-of-World consumers read wTimeOfDay too (#1557)
local Pokegear = require("src.ui.gen2.Pokegear")
eq(Pokegear.timeOfDayIndex({ game = { world = world } }), 2,
"the radio's program pick answers NITE, not the pin (pokegear.asm:1456)")
local Specials = require("src.script.gen2.Specials")
local buffer
local pvm = { curPhoneCaller = 1,
setStringBuffer = function(_, name) buffer = name end,
specials = { world = {
tod = world.tod, daytime = world.daytime,
encounters = { grass = { PLAYERS_HOUSE_1F = { slots = {
NITE = { { species = "NITEMON" }, { species = "NITEMON" },
{ species = "NITEMON" }, { species = "NITEMON" } },
DAY = { { species = "DAYMON" }, { species = "DAYMON" },
{ species = "DAYMON" }, { species = "DAYMON" } },
} } } },
} } }
Specials.ALL.RandomPhoneWildMon(pvm)
eq(buffer, "NITEMON",
"RandomPhoneWildMon reads the NITE column (wildmons.asm:861)")
end end
-- ---- the hour-window respawn is not eaten by a busy frame ------------------- -- ---- the hour-window respawn is not eaten by a busy frame -------------------
+10 -3
View File
@@ -116,9 +116,9 @@ do
end end
-- Every other givepoke in the game is the flag-FALSE form: no names, and the -- Every other givepoke in the game is the flag-FALSE form: no names, and the
-- nickname prompt still runs. -- nickname prompt still runs (engine/pokemon/move_mon.asm:1753-1757).
do do
local given = nil local given, asked = nil, false
local plainVm = Vm.new({ generation = 2, local plainVm = Vm.new({ generation = 2,
["s:eevee"] = { ["s:eevee"] = {
{ op = "givepoke", species = 133, level = 20, item = 0, trainer = 0 }, { op = "givepoke", species = 133, level = 20, item = 0, trainer = 0 },
@@ -127,13 +127,20 @@ do
}, {}, Events.new(), { }, {}, Events.new(), {
givePoke = function(species, level, item, opts) givePoke = function(species, level, item, opts)
given = { opts = opts } given = { opts = opts }
return nil return { species = "EEVEE" }
end,
showText = function(_, onDone) onDone() end,
-- GiveANickname_YesNo (move_mon.asm:1753-1757): the prompt's yes/no
yesorno = function(onChoose)
asked = true
onChoose(false)
end, end,
}) })
plainVm:start("s:eevee") plainVm:start("s:eevee")
for _ = 1, 10 do plainVm:update() end for _ = 1, 10 do plainVm:update() end
check(given ~= nil and given.opts == nil, check(given ~= nil and given.opts == nil,
"the flag-FALSE form hands givePoke no names") "the flag-FALSE form hands givePoke no names")
check(asked, "and the nickname prompt still runs on it")
end end
-- Phone + verbosegiveitem (Elm directions / aide potion) -- Phone + verbosegiveitem (Elm directions / aide potion)
+11 -4
View File
@@ -1709,14 +1709,21 @@ check(hw:mapSceneOf(3, 4) == nil,
"a map with NO scene_var row answers nil, which the VM turns into $ff") "a map with NO scene_var row answers nil, which the VM turns into $ff")
check(hw:mapSceneOf(9, 9) == nil, "and an unresolvable pair is nil too") check(hw:mapSceneOf(9, 9) == nil, "and an unresolvable pair is nil too")
-- hw.tod is the production read (the unpinned wTimeOfDay split, #1557);
-- hw.daytime is the palette pin it must NOT follow
hw.tod = "DAY"
eq(hw:timeOfDayId(), 1, "DAY is wTimeOfDay 1") eq(hw:timeOfDayId(), 1, "DAY is wTimeOfDay 1")
hw.daytime = "MORN" hw.tod = "MORN"
eq(hw:timeOfDayId(), 0, "MORN is 0") eq(hw:timeOfDayId(), 0, "MORN is 0")
hw.daytime = "NITE" hw.tod = "NITE"
eq(hw:timeOfDayId(), 2, "NITE is 2") eq(hw:timeOfDayId(), 2, "NITE is 2")
hw.tod = "NITE"
hw.daytime = "DARK" hw.daytime = "DARK"
eq(hw:timeOfDayId(), 3, "DARKNESS is 3") eq(hw:timeOfDayId(), 2, "a PALETTE_DARK pin does not leak into wTimeOfDay")
hw.daytime = "DAY" hw.tod = nil
hw.daytime = "DARK"
eq(hw:timeOfDayId(), 3, "DARKNESS is 3 only on the tod-less fallback arm")
hw.tod, hw.daytime = nil, "DAY"
eq(hw:gsVersion(), 0, "checkver: a Gold save is 0") eq(hw:gsVersion(), 0, "checkver: a Gold save is 0")
eq(hookWorld({ version = "silver" }):gsVersion(), 1, "and a Silver save is 1") eq(hookWorld({ version = "silver" }):gsVersion(), 1, "and a Silver save is 1")
+9 -6
View File
@@ -978,14 +978,17 @@ do
game.stack:pop() game.stack:pop()
battle.onFinish("win") battle.onFinish("win")
for _ = 1, 12 do -- the "is evolving!" box types out and holds DelayFrames 50 before the
-- movie (evos_moves.asm:120-134) (#1596), so drive frames to reach it
game.input.wasPressed = function() return false end
local evoTop
for _ = 1, 900 do
local t = game.stack:top() local t = game.stack:top()
if not t or t.screenId == "EvolutionState" then break end if not t then break end
game.stack:pop() if t.screenId == "EvolutionState" then evoTop = t break end
if t.onDone then t.onDone() end if t.update then t:update(1 / 60) else break end
end end
check(game.stack:top() and game.stack:top().screenId == "EvolutionState", check(evoTop ~= nil, "the win reaches the evolution screen")
"the win reaches the evolution screen")
end end
do do
+3
View File
@@ -735,6 +735,9 @@ REQUIRED_SYMBOLS = {
# nine fill cells. "HP:" and the ten HP-bar cells come from # nine fill cells. "HP:" and the ten HP-bar cells come from
# FontBattleExtra, which is already extracted. # FontBattleExtra, which is already extracted.
"EnemyHPBarBorderGFX", "HPExpBarBorderGFX", "ExpBarGFX", "EnemyHPBarBorderGFX", "HPExpBarBorderGFX", "ExpBarGFX",
# gfx/stats/stats_tiles.png + gfx/stats/pages.pal, StatsScreen_LoadFont
# and _CGB_StatsScreenHPPals (#1558)
"StatsScreenPageTilesGFX", "StatsScreenPagePals",
# The player's own battle back-pic (gfx/player/chris_back.2bpp.lz). It is # The player's own battle back-pic (gfx/player/chris_back.2bpp.lz). It is
# what stands in the player's pic box for the whole battle intro, before # what stands in the player's pic box for the whole battle intro, before
# SendOutPlayerMon swaps in the mon's backpic. # SendOutPlayerMon swaps in the mon's backpic.