CLOSES #455, CLOSES #487, CLOSES #501, CLOSES #540, CLOSES #585, CLOSES #591, CLOSES #593, CLOSES #595, CLOSES #597, CLOSES #599, CLOSES #600, CLOSES #606, CLOSES #607, CLOSES #610, CLOSES #613, CLOSES #616, CLOSES #620, CLOSES #626, CLOSES #632, CLOSES #633, CLOSES #647

This commit is contained in:
bryanthaboi
2026-08-02 08:17:16 -04:00
parent ebbc55c4d0
commit 35b3fa6d9c
87 changed files with 6349 additions and 285 deletions
+99 -19
View File
@@ -9,7 +9,8 @@
-- hidden_text_predef spends the facing byte on the tx_pre id, so neither
-- tile gates on facing.
local Menu = require("src.ui.Menu")
local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local TextBox = require("src.render.TextBox")
-- ViridianSchoolBlackboard (engine/events/hidden_events/school_blackboard.asm):
@@ -25,33 +26,112 @@ local STATUS_LABELS = {
{ " FRZ", "_ViridianBlackboardFrozenText" },
}
-- The headings list is a two-column menu, which src/ui/Menu.lua does not do
-- (it stacks one column), so the layout lives here (#591). .blackboardLoop:
-- TextBoxBorder at hlcoord 0, 0 with `lb bc, 6, 10` is the 12x8 box,
-- StatusAilmentText1 (" SLP"/" PSN"/" PAR") is placed at hlcoord 1, 2 and
-- StatusAilmentText2 (" BRN"/" FRZ"/" QUIT") at hlcoord 6, 2. LEFT/RIGHT
-- move wTopMenuItemX between those two columns and swap wMenuItemOffset
-- between 0 and 3 while leaving wCurrentMenuItem (the row) alone; UP/DOWN
-- are not in wMenuWatchedKeys, so they only slide the cursor and loop.
local BOARD_LABELS = {}
for i, row in ipairs(STATUS_LABELS) do BOARD_LABELS[i] = row[1] end
BOARD_LABELS[#BOARD_LABELS + 1] = " QUIT"
local BOARD_COL_X = { 1, 6 }
local BOARD_ROW_Y = 2
local BOARD_ROWS = 3
local StatusBoard = {}
StatusBoard.__index = StatusBoard
function StatusBoard.new(game, onPick, onQuit)
return setmetatable({ game = game, col = 1, row = 1, labels = BOARD_LABELS,
onPick = onPick, onQuit = onQuit }, StatusBoard)
end
-- flat index = pokered's wMenuItemOffset (0 or 3) + wCurrentMenuItem (0..2),
-- so 1..5 are the statuses in ViridianBlackboardStatusPointers order and 6
-- is QUIT
function StatusBoard:selection()
return (self.col - 1) * BOARD_ROWS + self.row
end
function StatusBoard:update()
local input = self.game.input
if input:wasPressed("up") then
-- wMenuWrappingEnabled is never set here, so both ends are hard stops
if self.row > 1 then self.row = self.row - 1 end
elseif input:wasPressed("down") then
if self.row < BOARD_ROWS then self.row = self.row + 1 end
elseif input:wasPressed("left") then
self.col = 1
elseif input:wasPressed("right") then
self.col = 2
elseif input:wasPressed("a") or input:wasPressed("b") then
-- HandleMenuInput_ (home/window.asm) beeps for the PAD_A | PAD_B branch,
-- and B and QUIT share .exitBlackboard
require("src.core.Sound").play(self.game.data, "Press_AB")
local sel = self:selection()
if input:wasPressed("b") or sel > #STATUS_LABELS then
self.onQuit()
else
self.onPick(sel)
end
end
end
function StatusBoard:draw()
Font.drawBox(0, 0, 12, 8)
love.graphics.setColor(0, 0, 0, 1)
for i, label in ipairs(BOARD_LABELS) do
local col = i <= BOARD_ROWS and 1 or 2
local row = i - (col - 1) * BOARD_ROWS
Font.draw(label, BOARD_COL_X[col] * 8, (BOARD_ROW_Y + row - 1) * 8)
end
-- wTopMenuItemX equals the column PlaceString started at, so the cursor
-- covers the blank each label leads with
Font.drawCode(Theme.cursor, BOARD_COL_X[self.col] * 8,
(BOARD_ROW_Y + self.row - 1) * 8)
love.graphics.setColor(1, 1, 1, 1)
end
local function blackboard(game)
local text = game.data.text or {}
local items, showMenu, askHeading
function showMenu()
game.stack:push(Menu.new(game, items,
{ tx = 0, ty = 0, tw = 12, th = 8, rowStep = 1 }))
local openBoard
-- wCurrentMenuItem / wMenuItemOffset are zeroed once, above .blackboardLoop,
-- and nothing inside the loop clears them again: after a status blurb
-- `jp .blackboardLoop` comes back with the cursor still on the row and
-- column the player just picked. One StatusBoard lives for the whole
-- reading and is re-pushed each pass, so only entering the blackboard
-- resets to the left column / top row (#591).
local board
-- .blackboardLoop reprints ViridianSchoolBlackboardText2 and only then
-- calls HandleMenuInput, so the prompt is on screen for exactly as long as
-- the headings list is. That text ends in `done`, not `prompt`
-- (data/text/text_2.asm:646), so PrintText returns with the box still up
-- and never waits for a button: TextBox opts.stay holds it open under the
-- list and these callbacks pop the pair together (#591).
local function closeBoard()
game.stack:pop() -- the headings list
game.stack:pop() -- the held "Which heading" box under it
end
-- ViridianSchoolBlackboardText2 is reprinted on every .blackboardLoop
-- pass, immediately before HandleMenuInput
function askHeading()
local function pick(i)
closeBoard()
game.stack:push(TextBox.new(game,
text[STATUS_LABELS[i][2]] or STATUS_LABELS[i][1], openBoard))
end
function openBoard()
game.stack:push(TextBox.new(game,
text._ViridianSchoolBlackboardText2 or "Which heading do\nyou want to read?",
showMenu))
nil, { stay = { onShown = function()
board = board or StatusBoard.new(game, pick, closeBoard)
game.stack:push(board)
end } }))
end
items = {}
for i, row in ipairs(STATUS_LABELS) do
local label, key = row[1], row[2]
items[i] = { label = label, onSelect = function()
game.stack:push(TextBox.new(game, text[key] or label, askHeading))
end }
end
-- no onSelect: Menu's own pop closes the box, matching .exitBlackboard
items[#items + 1] = { label = " QUIT" }
game.stack:push(TextBox.new(game,
text._ViridianSchoolBlackboardText1
or "The blackboard\ndescribes POKéMON\vSTATUS changes\vduring battles.",
askHeading))
openBoard))
end
-- ViridianSchoolNotebook (engine/events/hidden_events/school_notebooks.asm):
+27 -4
View File
@@ -25,7 +25,7 @@ local function starterBall(askText, species, choseFlag, ownBall,
{ "jump_if_true", 20 }, -- 2
-- no picking until Oak has walked you in (OaksLabScript gating)
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
{ "jump_if_false", 22 }, -- 4
{ "jump_if_false", 23 }, -- 4
-- the Pokédex "new species" entry shows before the ask (predef
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
-- sets the owned bits so ShowPokedexData prints height/weight/text;
@@ -63,7 +63,10 @@ local function starterBall(askText, species, choseFlag, ownBall,
-- out as Pokémon here.
{ "face_object", 5, "down" }, -- 20
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 21
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 22
-- OaksLabLastMonScript ends at TextScriptEnd; the port used to fall
-- through into the pre-pick line below (#601 remnant, reported on #600)
{ "jump", "end" }, -- 22
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 23
}
end
@@ -71,10 +74,22 @@ return {
talk = {
-- Oak: OaksLabOak1Text. Parcel delivery kicks SCRIPT_OAKSLAB_RIVAL_
-- ARRIVES_AT_OAKS_REQUEST + OaksLabOakGivesPokedexScript (rival walk-
-- in, full Pokédex speech, rival exit, Route 22 arm). Dex-rating
-- (DisplayDexRating) is still skipped.
-- in, full Pokédex speech, rival exit, Route 22 arm).
TEXT_OAKSLAB_OAK1 = {
{ "face_player" },
-- OaksLabOak1Text leads with the dex-rating branch (#600): with
-- EVENT_PALLET_AFTER_GETTING_POKEBALLS set (converted saves), or
-- 2+ species owned once the Pokédex is in hand, Oak asks how it is
-- coming and rates it (predef DisplayDexRating). Red keeps the
-- GOT_POKEDEX gate that Yellow's copy of this text drops
-- (data/scripts/oaks_lab_yellow.lua).
{ "check_flag", "EVENT_PALLET_AFTER_GETTING_POKEBALLS" },
{ "jump_if_true", "dex_rating" },
{ "check_dex_owned", 2 },
{ "jump_if_false", "no_rating" },
{ "check_flag", "EVENT_GOT_POKEDEX" },
{ "jump_if_true", "dex_rating" },
{ "label", "no_rating" },
{ "check_item", "POKE_BALL" },
{ "jump_if_true", "come_see" },
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" },
@@ -160,6 +175,14 @@ return {
{ "label", "come_see" },
{ "show_text", "_OaksLabOak1ComeSeeMeSometimesText" },
{ "jump", "end" },
-- .HowIsYourPokedexComingText ends on `prompt` and OaksLabOak1Text
-- sets wDoNotWaitForButtonPressAfterDisplayingText, so the seen/owned
-- tally follows with no button wait (engine/events/pokedex_rating.asm)
{ "label", "dex_rating" },
{ "show_text", "_OaksLabOak1HowIsYourPokedexComingText" },
{ "dex_rating" },
},
TEXT_OAKSLAB_CHARMANDER_POKE_BALL =
+71 -29
View File
@@ -4,9 +4,11 @@
-- (.PlayerNextToSafariZoneWorker1CoordsArray). Paying ¥500 hands over
-- 30 SAFARI BALLs and starts the 502-step game
-- (SafariZoneGateWouldYouLikeToJoinScript: wSafariSteps = 502,
-- wNumSafariBalls = SAFARI_BALLS_RECEIVED). Declining walks you back
-- so you can't slip past. Returning to the gate ends the game and the
-- worker takes the leftover balls back.
-- wNumSafariBalls = SAFARI_BALLS_RECEIVED), then auto-walks the player
-- up through the north warp into the zone. Declining walks you back
-- so you can't slip past. Returning to the gate ends the game, the
-- worker takes the leftover balls back and the auto-walk drops you 3
-- cells below the warp you came in by (#540).
--
-- Step/ball bookkeeping lives in src/world/OverworldController.lua
-- (safariStep/safariGameOver, from
@@ -19,7 +21,33 @@ local FEE = 500
local BALLS = 30
local STEPS = 502
local function startGame(game, t, done, balls, introText)
-- SafariZoneGateSafariZoneWorker1WouldYouLikeToJoinText .success closes with
-- `ld a, PAD_UP / ld c, 3 / SafariZoneEntranceAutoWalk`: paying walks the
-- player up out of the gate and through the north warp, it is never left to
-- the player. EVENT_IN_SAFARI_ZONE is already set when that walk runs, so
-- the two gate steps taken before the warp fires are charged against
-- wSafariSteps (home/overworld.asm:307-310) -- which is why the counter
-- reads 500/500 on arrival even though the script wrote 502 (#540). The
-- port's counter only runs on the nine interior maps (FieldDefaults
-- safari.stepMaps, OverworldState:inSafariStepZone), so charge those two
-- steps here instead.
local function walkIntoZone(game, ow)
local p = ow.player
-- only from the two trigger cells in front of the worker, which are the
-- columns the north warps sit on; a player who paid after TALKING to him
-- from somewhere else walks in on their own, as they do today
local w = p.cellY == 2 and ow.map:warpAtCell(p.cellX, 0) or nil
if not w then return end
ow:scriptMove(p, "up", 2, function()
local st = game.save.safari
if st then st.steps = st.steps - 2 end
-- scripted steps skip onStepComplete (and with it CheckWarpsNoCollision),
-- so take that warp explicitly once the walk lands on it
ow:takeWarp(w.def)
end)
end
local function startGame(game, ow, t, done, balls, introText)
game.save.safari = { balls = balls or BALLS, steps = STEPS }
game.save.safariNags = nil
local TextBox = require("src.render.TextBox")
@@ -31,7 +59,10 @@ local function startGame(game, t, done, balls, introText)
local pa = t._SafariZoneGateSafariZoneWorker1CallYouOnThePAText
or "\fWe'll call you on\nthe PA when you\nrun out of time\nor SAFARI BALLs!"
local luck = t._SafariZoneGateSafariZoneWorker1GoodLuckText or "Good Luck!"
game.stack:push(TextBox.new(game, paid .. pa .. "\f" .. luck, done))
game.stack:push(TextBox.new(game, paid .. pa .. "\f" .. luck, function()
if done then done() end
walkIntoZone(game, ow)
end))
end
-- Yellow's soft-lock fix (scripts/SafariZoneGate_2.asm): a player short of
@@ -52,7 +83,7 @@ local function yellowLowCost(game, ow, t, done, back)
or "\fOh, all right, pay\nme what you have.")
.. "\f" .. (t._SafariZoneLowCostText2
or "But, I can't give\nyou all 30 BALLs.")
startGame(game, t, done, balls, intro)
startGame(game, ow, t, done, balls, intro)
return
end
local nag = game.save.safariNags or 0
@@ -62,7 +93,7 @@ local function yellowLowCost(game, ow, t, done, back)
(t._SafariZoneLowCostText8 or "Read my lips, NO!\nGet it?")
.. (t._SafariZoneLowCostText3
or "\fYou're persistent,\naren't you?\fOK, you can go in\nfor free, but\njust this once!")
startGame(game, t, done, 1, intro)
startGame(game, ow, t, done, 1, intro)
return
end
local nags = {
@@ -100,7 +131,7 @@ local function joinPrompt(game, ow, done)
end
else
game.save.money = game.save.money - FEE
startGame(game, t, done)
startGame(game, ow, t, done)
end
end))
end))
@@ -135,27 +166,38 @@ M.SAFARI_ZONE_GATE = {
-- no walks you back into the zone
onEnter = function(game, ow)
if not game.save.safari or ow.player.cellY > 1 then return end
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
local t = game.data.text
game.stack:push(TextBox.new(game,
t._SafariZoneGateSafariZoneWorker1LeavingEarlyText or "Leaving early?",
function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then
-- back into the zone through the entrance warp
local w = game.data.maps.SAFARI_ZONE_CENTER.warps[1]
ow:startWarpTo("SAFARI_ZONE_CENTER", w.x, w.y, "up")
return
end
game.save.safari = nil
game.stack:push(TextBox.new(game,
(t._SafariZoneGateSafariZoneWorker1ReturnSafariBallsText
or "Please return any\nSAFARI BALLs you\nhave left.")
.. "\f" .. (t._SafariZoneGateSafariZoneWorker1GoodHaulComeAgainText
or "Did you get a\ngood haul?\fCome again!")))
end))
end))
-- QUEUED, never pushed: onEnter runs inside the arriving warp's
-- Transition midpoint, and Transition:finish pops whatever is on top
-- the same frame (Timing.WARP_FADE_IN is 0) -- so a box pushed here is
-- swallowed, and on a build where it survived it drew over a screen
-- still faded to black (#540). Same contract as M.HALL_OF_FAME in
-- data/scripts/story.lua.
--
-- SafariZoneGateLeavingSafariScript .leaving_early: YES prints the
-- return-balls text, faces the player down and runs
-- SafariZoneEntranceAutoWalk with `PAD_DOWN, c = 3`, landing on the
-- counter row 3 cells below the warp you came in by; NO prints
-- "Good Luck!" and walks one step back up through that same warp.
local rightSide = ow.player.cellX ~= 3
local dest = game.data.maps.SAFARI_ZONE_CENTER.warps[rightSide and 2 or 1]
ow:queueScript({
{ "ask", "_SafariZoneGateSafariZoneWorker1LeavingEarlyText" },
{ "jump_if_false", "stay" },
-- the port never reaches SafariZoneGateLeavingSafariScript's own
-- GOOD_HAUL_COME_AGAIN branch (safariGameOver warps straight to the
-- counter), so the sign-off rides on this path
{ "show_text", "_SafariZoneGateSafariZoneWorker1ReturnSafariBallsText" },
{ "show_text", "_SafariZoneGateSafariZoneWorker1GoodHaulComeAgainText" },
-- no value: set_field assigns nil, which is how save.safari is cleared
{ "set_field", "safari" },
-- move_player runs through scriptMove, which skips onStepComplete, so
-- walking back down past (x,2) cannot re-fire the join trigger
{ "move_player", "down", 3 },
{ "jump", "end" },
{ "label", "stay" },
{ "show_text", "_SafariZoneGateSafariZoneWorker1GoodLuckText" },
{ "warp", "SAFARI_ZONE_CENTER", dest.x, dest.y, "up" },
})
end,
}
+42 -1
View File
@@ -11,7 +11,7 @@
-- field.seafoam (SEAFOAM_ISLANDS_1F/B1F/B3F holes+holeDestination and
-- B3F's pluggedByHolesOn) plus the generic
-- OverworldState:boulderIntoHole in src/world/OverworldController.lua;
-- no per-map onEnter hook is needed here.
-- no per-map onEnter hook is needed for the boulders.
local M = {}
@@ -23,4 +23,45 @@ M.VERMILION_GYM = {
end,
}
-- The PLAYER falling down those same holes (#599). field.seafoam's holes
-- carry only the BOULDER's object cell on the floor below (landsAt), not
-- the player's landing, so the player's landing is spelled out here: it
-- comes from data/maps/special_warps.asm DungeonWarpList/DungeonWarpData,
-- which the importer does not extract. Same shape as MANSION_HOLES in
-- data/scripts/story6.lua and VICTORY_ROAD_3F.onStep in
-- data/scripts/story.lua; CAVERN $22 is a walkable tile, so the fall has
-- to be an onStep, not a collision block.
--
-- Sources: scripts/SeafoamIslands1F.asm Seafoam1HolesCoords (17,6)/(24,6),
-- B1F.asm Seafoam2HolesCoords (18,6)/(23,6), B2F.asm Seafoam3HolesCoords
-- (19,6)/(22,6), B3F.asm Seafoam4HolesCoords (3,16)/(6,16). Each floor
-- sets wDungeonWarpDestinationMap and calls IsPlayerOnDungeonWarp, and
-- wCoordIndex picks that floor's DungeonWarpData row. Unconditional in
-- the original: a plugged hole still drops the player. The B3F and B4F
-- landings are water; setMap's CheckForceBikeOrSurf pass
-- (OverworldState:checkForcedMovement) mounts SURF on arrival.
local HOLE_FALLS = {
SEAFOAM_ISLANDS_1F = { { 17, 6, "SEAFOAM_ISLANDS_B1F", 18, 7 },
{ 24, 6, "SEAFOAM_ISLANDS_B1F", 23, 7 } },
SEAFOAM_ISLANDS_B1F = { { 18, 6, "SEAFOAM_ISLANDS_B2F", 19, 7 },
{ 23, 6, "SEAFOAM_ISLANDS_B2F", 22, 7 } },
SEAFOAM_ISLANDS_B2F = { { 19, 6, "SEAFOAM_ISLANDS_B3F", 18, 7 },
{ 22, 6, "SEAFOAM_ISLANDS_B3F", 19, 7 } },
SEAFOAM_ISLANDS_B3F = { { 3, 16, "SEAFOAM_ISLANDS_B4F", 4, 14 },
{ 6, 16, "SEAFOAM_ISLANDS_B4F", 5, 14 } },
}
for mapId, holes in pairs(HOLE_FALLS) do
M[mapId] = M[mapId] or {}
M[mapId].onStep = function(game, ow, x, y)
for _, h in ipairs(holes) do
if x == h[1] and y == h[2] then
ow:startWarpTo(h[3], h[4], h[5], ow.player.facing)
return true
end
end
return false
end
end
return M
+30 -1
View File
@@ -188,7 +188,12 @@ M.BILLS_HOUSE = {
end
if ow.player.facing == "down" then
-- the player is standing on his straight path: walk around
-- (.PokemonWalkAroundPlayerMovement)
-- (.PokemonWalkAroundPlayerMovement). BillsHouseScript2 runs
-- BillsHousePikachuWatchPlayer first on this branch, so a
-- Pikachu that is still following steps clear of Bill's detour
-- and turns to watch the player (#455).
require("src.world.PikachuFollower")
.onBillWalksAroundPlayer(game, ow)
ow:scriptMove(npc, "right", 1, function()
ow:scriptMove(npc, "up", 2, function()
ow:scriptMove(npc, "left", 1, function()
@@ -600,12 +605,34 @@ local function snorlaxWake(mapId, objName, beatFlag, wokeUpText, calmedText)
}
end
-- A beaten Snorlax is gone for good: Route12/Route16DefaultScript run
-- HideObject in the same breath as the battle that sets
-- EVENT_BEAT_ROUTEnn_SNORLAX, so "event set, object still on the map" is a
-- state the asm cannot produce. Here it can (a mod's world:toggleObject, a
-- save edited or migrated from a build older than the flag), and it is a
-- dead end: ItemEffects' adjacentSleepingSnorlax refuses to wake a Snorlax
-- whose beat flag is set (ItemUsePokeFlute's CheckEvent, engine/items/
-- item_effects.asm), so the sleeper sits in the road forever and Cycling
-- Road is unreachable (#585). Reconcile the toggle from the flag on every
-- entry -- the mirror of SaveData.lua's toggle -> flag backfill, and the
-- same repair shape the Silph Co. floors use below.
local function hideBeatenSnorlax(mapId, objName, beatFlag)
return function(game, ow)
if not game.save.flags[beatFlag] then return end
local Commands = require("src.script.Commands")
Commands.hide_object({ game = game, save = game.save, overworld = ow },
mapId, objName)
end
end
-- snorlaxWake is looked up by ItemEffects.lua/BagMenu.lua (via
-- data/scripts/init.lua's M.get) and run when the flute wakes Snorlax;
-- objName/beatFlag let ItemEffects find the NPC and check whether it's
-- already been beaten before allowing the wake.
M.ROUTE_12 = {
talk = { TEXT_ROUTE12_SNORLAX = { { "show_text", "_Route12SnorlaxText" } } },
onEnter = hideBeatenSnorlax("ROUTE_12", "ROUTE12_SNORLAX",
"EVENT_BEAT_ROUTE12_SNORLAX"),
snorlaxWake = {
objName = "ROUTE12_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE12_SNORLAX",
script = snorlaxWake("ROUTE_12", "ROUTE12_SNORLAX", "EVENT_BEAT_ROUTE12_SNORLAX",
@@ -614,6 +641,8 @@ M.ROUTE_12 = {
}
M.ROUTE_16 = {
talk = { TEXT_ROUTE16_SNORLAX = { { "show_text", "_Route16Text7" } } },
onEnter = hideBeatenSnorlax("ROUTE_16", "ROUTE16_SNORLAX",
"EVENT_BEAT_ROUTE16_SNORLAX"),
snorlaxWake = {
objName = "ROUTE16_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE16_SNORLAX",
script = snorlaxWake("ROUTE_16", "ROUTE16_SNORLAX", "EVENT_BEAT_ROUTE16_SNORLAX",