mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 06:55:29 +02:00
fad7443f94
MainMenu.lua's row labels (CONTINUE/NEW GAME/OPTION/EXIT GAME), its clock box's AM/PM half, and its CONTINUE save-summary panel (PLAYER <name>/ BADGES/POKéDEX/TIME, or NO SAVE FILE) were bare literals, invisible to a translation mod's strings registry. SaveMenu.lua's confirm/overwrite/ saving/saved prompts, its YES/NO choice, and its own copy of the same summary panel had the same gap. Both mirror the Gen 1 port's already- translated equivalents (src/ui/TitleState.lua, src/ui/StartMenu.lua), which route every one of these rows through Strings(). SaveMenu's two-line prompts (module-level OVERWRITE_PROMPT/SAVING_PROMPT, plus the dynamically-built "%s saved\nthe game." and the confirm/failed messages) are now single Strings()-resolved strings with an embedded "\n", matching the Gen 1 port's own single-call convention for two-line messages, split into the two-slot table drawPanel's fixed-position Chrome.print calls expect only at draw time -- so a translation sees one whole sentence to reorder, not two independently-translated fragments. Added tests/engine/gen2_main_menu_translation_test.lua and gen2_save_menu_translation_test.lua: drive both screens' drawPanel()/ drawSavePanel() with a mod-loaded Strings catalog and check the translated text reaches Font.draw, plus a vanilla no-mod case proving the fallback is unchanged. Confirmed both catch the regression: reverting either file to its pre-fix content fails the corresponding suite (7 and 15 checks respectively).
278 lines
10 KiB
Lua
278 lines
10 KiB
Lua
-- SAVE from the start menu (engine/menus/save.asm SaveMenu).
|
|
--
|
|
-- Transcribed rather than laid out by eye. SaveMenu is four calls:
|
|
--
|
|
-- DisplayNormalContinueData with `lb de, 4, 0`, which is the *same* panel
|
|
-- CONTINUE shows, moved: _OffsetMenuHeader keeps the header's 15x9 size
|
|
-- but puts its left edge at 4 and its top at 0, so MenuBox draws a 16x10
|
|
-- box over (4,0)..(19,9). GetMenuTextStartCoord then lands the four
|
|
-- labels at (5,2), (5,4), (5,6), (5,8) -- border + 1, plus one more row
|
|
-- because the header does not set STATICMENU_NO_TOP_SPACING, and no extra
|
|
-- column because it does not set STATICMENU_CURSOR.
|
|
-- SpeechTextbox, the ordinary 18x4 box over rows 12-17.
|
|
-- SaveTheGame_yesorno, which prints into that box and puts the yes/no at
|
|
-- `lb bc, 0, 7` -- left 0, top 7, so a 6x5 box at (0,7) with YES at (2,8)
|
|
-- and NO at (2,10). YesNoMenuHeader sets STATICMENU_CURSOR and
|
|
-- STATICMENU_NO_TOP_SPACING, which is what puts the labels one column in
|
|
-- from the cursor and skips the blank row.
|
|
-- SavingDontTurnOffThePower, which is a timed sequence and not a prompt:
|
|
-- "SAVING… DON'T TURN / OFF THE POWER." for 16 frames, the write, 32
|
|
-- frames, "<PLAYER> saved / the game.", SFX_SAVE, then 30 more.
|
|
--
|
|
-- Overwriting an existing file gets a second yes/no first
|
|
-- (AskOverwriteSaveFile), which is the whole reason this is a state and not a
|
|
-- one-line call.
|
|
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local Save = require("src.core.gen2.Save")
|
|
local Sound = require("src.core.Sound")
|
|
local Strings = require("src.core.Strings")
|
|
|
|
local SaveMenu = {}
|
|
SaveMenu.__index = SaveMenu
|
|
SaveMenu.isOpaque = true
|
|
|
|
-- SFX_SAVE ($25, constants/sfx_constants.asm:40) is what plays as the file is
|
|
-- written: `ld de, SFX_SAVE / call PlaySFX` right after ResumeGameLogic in
|
|
-- SaveGameData (engine/menus/save.asm:110), and again under SavedTheGameText
|
|
-- (:265). It is an index into the sfx pointer table, so an id that is off by
|
|
-- anything plays a different sound rather than nothing -- $1f is
|
|
-- SFX_ENTER_DOOR, which is what saving used to creak with.
|
|
local SFX_SAVE = 0x25
|
|
|
|
-- SavingDontTurnOffThePower's DelayFrames counts, at the 60 Hz logic clock.
|
|
local SAVING_FRAMES = 16
|
|
local SAVED_FRAMES = 32 + 30
|
|
|
|
-- MenuBox coordinates after _OffsetMenuHeader(4, 0).
|
|
local PANEL_X, PANEL_Y, PANEL_W, PANEL_H = 4, 0, 16, 10
|
|
local LABEL_X, LABEL_Y = 5, 2
|
|
-- Continue_DisplayBadgesDex / Continue_PrintGameTime add these to the box's
|
|
-- own origin, so they are (4,0) + (13,4) / (12,6) / (9,8).
|
|
local BADGES_X, BADGES_Y = 17, 4
|
|
local DEX_X, DEX_Y = 16, 6
|
|
local TIME_X, TIME_Y = 13, 8
|
|
|
|
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
|
|
|
|
-- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and
|
|
-- SavingDontTurnOffThePower's own line, shared with the PC's CHANGE BOX save
|
|
-- (src/ui/gen2/PcMenu.lua:savePrompt(), which returns this two-slot table
|
|
-- straight through to its own lines[1]/lines[2] Chrome.print calls, so the
|
|
-- table shape here is a cross-file contract that must not change).
|
|
local OVERWRITE_PROMPT = { "There is already a", "save file. Is it" }
|
|
local SAVING_PROMPT = { "SAVING… DON'T TURN", "OFF THE POWER." }
|
|
|
|
-- The same two prompts as a single \n-joined Strings.source() key, used only
|
|
-- by this screen's own prompt() below (PcMenu keeps reading the untranslated
|
|
-- table above unchanged). One key lets a translation reorder the whole
|
|
-- sentence rather than two independently-translated fragments, and lets a
|
|
-- cart whose own translation shows it on ONE line (German's SAVING prompt
|
|
-- has no second line at all) say so directly -- the per-line "{RAM:...}"-
|
|
-- style split load_engine_overrides uses elsewhere requires a non-empty
|
|
-- override for every line, so it cannot express "this line is blank" the
|
|
-- way an embedded "\n"-less string can.
|
|
--
|
|
-- Written as literals, not `table.concat(OVERWRITE_PROMPT, "\n")`: tools/
|
|
-- modkit.py's STRINGS_CALL harvester matches a quoted string literal
|
|
-- immediately inside Strings.source(...)/Strings(...), not an arbitrary
|
|
-- expression, so a computed argument here would be invisible to every
|
|
-- translator's `modkit.py translation ... --refresh` scaffold despite the
|
|
-- runtime lookup working fine -- caught by an independent review. Keep
|
|
-- these two byte-for-byte in sync with OVERWRITE_PROMPT/SAVING_PROMPT
|
|
-- above (checked by tests/engine/gen2_save_menu_translation_test.lua).
|
|
local OVERWRITE_PROMPT_SOURCE = Strings.source("There is already a\nsave file. Is it")
|
|
local SAVING_PROMPT_SOURCE = Strings.source("SAVING… DON'T TURN\nOFF THE POWER.")
|
|
|
|
-- Splits a Strings()-resolved "line one\nline two" into the two-slot table
|
|
-- drawPanel's fixed-position Chrome.print calls expect; a translation with no
|
|
-- "\n" at all (single-line messages like "Could not save.") lands whole on
|
|
-- the first slot, matching the untranslated code's own { text, "" } shape.
|
|
local function twoLines(text)
|
|
local first, second = text:match("^(.-)\n(.*)$")
|
|
return { first or text, second or "" }
|
|
end
|
|
|
|
function SaveMenu:wantsFillScale() return true end
|
|
function SaveMenu:drawsWidescreen() return true end
|
|
|
|
-- opts: save, onDone(saved), existed (override), writer (injected for tests)
|
|
function SaveMenu.new(game, opts)
|
|
opts = opts or {}
|
|
local self = setmetatable({}, SaveMenu)
|
|
self.game = game
|
|
self.save = opts.save or (game and game.save)
|
|
self.onDone = opts.onDone
|
|
self.writer = opts.writer or Save.save
|
|
local existed = opts.existed
|
|
if existed == nil then existed = Save.exists() end
|
|
self.existed = existed
|
|
-- confirm -> overwrite (only when a file exists) -> saving -> done
|
|
self.phase = "confirm"
|
|
self.choice = 1 -- 1 YES, 2 NO
|
|
self.timer = 0
|
|
return self
|
|
end
|
|
|
|
function SaveMenu.playSaveSfx(game, id)
|
|
local data = game and game.data
|
|
local audio = data and data.audio
|
|
if not (audio and audio.sfxOrder) then return end
|
|
local name = audio.sfxOrder[id + 1]
|
|
if name and audio.sfx and audio.sfx[name] then Sound.play(data, name) end
|
|
end
|
|
|
|
function SaveMenu:playSfx(id)
|
|
SaveMenu.playSaveSfx(self.game, id)
|
|
end
|
|
|
|
function SaveMenu:finish(saved)
|
|
if self.onDone then self.onDone(saved) end
|
|
end
|
|
|
|
function SaveMenu:playerName()
|
|
return (self.save and self.save.player and self.save.player.name) or "GOLD"
|
|
end
|
|
|
|
-- The write itself, which the cart does between the two messages.
|
|
function SaveMenu:writeNow()
|
|
local ok = self.writer(self.save)
|
|
self.saved = ok and true or false
|
|
if ok then self:playSfx(SFX_SAVE) end
|
|
end
|
|
|
|
function SaveMenu:accept()
|
|
if self.phase == "confirm" then
|
|
if self.choice == 2 then
|
|
self:finish(false)
|
|
return
|
|
end
|
|
if self.existed then
|
|
self.phase = "overwrite"
|
|
self.choice = 1
|
|
return
|
|
end
|
|
self.phase = "saving"
|
|
self.timer = 0
|
|
return
|
|
end
|
|
if self.phase == "overwrite" then
|
|
if self.choice == 2 then
|
|
self:finish(false)
|
|
return
|
|
end
|
|
self.phase = "saving"
|
|
self.timer = 0
|
|
end
|
|
end
|
|
|
|
function SaveMenu:update(_dt)
|
|
-- The saving and saved messages are DelayFrames, not prompts: no button
|
|
-- does anything until the sequence runs out.
|
|
if self.phase == "saving" then
|
|
self.timer = self.timer + 1
|
|
if self.timer >= SAVING_FRAMES then
|
|
self:writeNow()
|
|
self.phase = "done"
|
|
self.timer = 0
|
|
end
|
|
return
|
|
end
|
|
if self.phase == "done" then
|
|
self.timer = self.timer + 1
|
|
if self.timer >= SAVED_FRAMES then self:finish(self.saved) end
|
|
return
|
|
end
|
|
|
|
local input = self.game and self.game.input
|
|
if not input then return end
|
|
if input:wasPressed("up") or input:wasPressed("down") then
|
|
self.choice = self.choice == 1 and 2 or 1
|
|
return
|
|
end
|
|
if input:wasPressed("a") then
|
|
self:accept()
|
|
elseif input:wasPressed("b") then
|
|
-- B out of a yes/no is NO (InterpretTwoOptionMenu returns carry).
|
|
self:finish(false)
|
|
end
|
|
end
|
|
|
|
-- The two lines the speech box holds, in the cart's own wording. `line` puts
|
|
-- the second one on the box's lower line; `cont` scrolls, which the overwrite
|
|
-- prompt uses for its third line and which this shows as a second page.
|
|
function SaveMenu:prompt()
|
|
if self.phase == "overwrite" then
|
|
-- AlreadyASaveFileText when the file is this player's; AnotherSaveFileText
|
|
-- when the ID differs. Only the first can happen here.
|
|
return twoLines(Strings(OVERWRITE_PROMPT_SOURCE))
|
|
end
|
|
if self.phase == "saving" then
|
|
return twoLines(Strings(SAVING_PROMPT_SOURCE))
|
|
end
|
|
if self.phase == "done" then
|
|
if self.saved then
|
|
return twoLines(Strings("%s saved\nthe game.", self:playerName()))
|
|
end
|
|
return twoLines(Strings("Could not save."))
|
|
end
|
|
return twoLines(Strings("Would you like to\nsave the game?"))
|
|
end
|
|
|
|
function SaveMenu:drawPanel()
|
|
Chrome.clear()
|
|
local summary = Save.summary(self.save)
|
|
Chrome.box(PANEL_X, PANEL_Y, PANEL_W, PANEL_H)
|
|
if summary then
|
|
Chrome.print(Strings("PLAYER %s", summary.name), LABEL_X, LABEL_Y)
|
|
Chrome.print(Strings("BADGES"), LABEL_X, LABEL_Y + 2)
|
|
Chrome.print(Strings("POKéDEX"), LABEL_X, LABEL_Y + 4)
|
|
Chrome.print(Strings("TIME"), LABEL_X, LABEL_Y + 6)
|
|
-- PrintNum fills its field from the left, space padded.
|
|
Chrome.print(Chrome.number(summary.badges, 2), BADGES_X, BADGES_Y)
|
|
Chrome.print(Chrome.number(summary.caught, 3), DEX_X, DEX_Y)
|
|
Chrome.print(Chrome.number(summary.hours, 3), TIME_X, TIME_Y)
|
|
Chrome.print(":", TIME_X + 3, TIME_Y)
|
|
Chrome.print(Chrome.number(summary.minutes, 2, true), TIME_X + 4, TIME_Y)
|
|
end
|
|
|
|
-- SpeechTextbox: interior 18x4 at (0,12), so the two lines are at (1,14)
|
|
-- and (1,16) -- `line` is the box's lower line, two rows down.
|
|
Chrome.textbox(0, 12, 18, 4)
|
|
local lines = self:prompt()
|
|
Chrome.print(lines[1] or "", 1, 14)
|
|
Chrome.print(lines[2] or "", 1, 16)
|
|
|
|
if self.phase == "confirm" or self.phase == "overwrite" then
|
|
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
|
|
Chrome.print(Strings("YES"), YESNO_X + 2, YESNO_Y + 1)
|
|
Chrome.print(Strings("NO"), YESNO_X + 2, YESNO_Y + 3)
|
|
Chrome.cursor(YESNO_X + 1, YESNO_Y + (self.choice == 1 and 1 or 3))
|
|
end
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
function SaveMenu:draw()
|
|
self:drawPanel()
|
|
end
|
|
|
|
function SaveMenu:drawWidescreen(winW, winH)
|
|
local G = love.graphics
|
|
G.setColor(1, 1, 1, 1)
|
|
G.rectangle("fill", 0, 0, winW, winH)
|
|
local scale = Chrome.fitScale(winW, winH)
|
|
G.push()
|
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
|
G.scale(scale, scale)
|
|
self:drawPanel()
|
|
G.pop()
|
|
end
|
|
|
|
SaveMenu.SFX_SAVE = SFX_SAVE
|
|
SaveMenu.SAVING_FRAMES = SAVING_FRAMES
|
|
SaveMenu.SAVED_FRAMES = SAVED_FRAMES
|
|
SaveMenu.OVERWRITE_PROMPT = OVERWRITE_PROMPT
|
|
SaveMenu.SAVING_PROMPT = SAVING_PROMPT
|
|
|
|
return SaveMenu
|