mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
CLOSES #1478, CLOSES #1479, CLOSES #1488, CLOSES #1510, CLOSES #1511, CLOSES #1514, CLOSES #1521, CLOSES #1522, CLOSES #1545, CLOSES #1557, CLOSES #1563, CLOSES #1565, CLOSES #1566, CLOSES #1577, CLOSES #1594, CLOSES #1596, CLOSES #1606, CLOSES #1608
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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)")
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user