Files
gen1recomp/tests/gen2_clock_test.lua
T
thibautbus 70f7d5c028 Translate the clock-setting screens' day names and time-of-day word
DAYS (SUNDAY..SATURDAY), the MORN/DAY/NITE word PrintHour prints, and the
"o'clock"/"min." suffixes bypassed src/core/Strings.lua entirely -- they
were plain Lua literals with no lookup, so a translation mod's `strings`
registry had nothing to catch and Oak's clock screens, the day-of-week
wheel, the main menu clock box and the Pokegear's clock card kept printing
English regardless of the loaded language (reported from a real Spanish
Gold build).

Both live in src/core/gen2/Clock.lua, which already owns weekday/hour
arithmetic and is already required by InitClock.lua, MainMenu.lua and
Pokegear.lua: Clock.DAY_NAMES + Clock.weekdayName(day) is the one place the
three screens read a weekday's name from, so a fix to it cannot land on one
screen and silently miss the other two. Clock.daytimeLabel(hour) is the
translated counterpart to Palettes.clockDaytime, which keeps answering the
untranslated MORN/DAY/NITE key every FORCED_DAYTIME lookup in Palettes.lua
compares against -- src/world/gen2/Palettes.lua itself is untouched, so
that module stays pure table/color math with no Strings coupling.
2026-08-16 22:45:03 +02:00

321 lines
12 KiB
Lua

-- Gen 2 clock setup: the InitClock screens and the RTC base they write.
--
-- The cart does not store "the time": InitClock stores the RTC reading at the
-- moment the player answered (wStartHour / wStartMinute / wStartDay) and every
-- later read goes through it. src/core/gen2/Clock.lua is that arithmetic and
-- src/ui/gen2/InitClock.lua is both screens that write it.
-- luajit tests/gen2_clock_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 clock")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
-- No font is loaded here (these are layout and arithmetic assertions), so
-- Font.encode would warn once per unknown glyph and bury a real failure.
require("src.core.Logger").warn = function() end
local Clock = require("src.core.gen2.Clock")
local InitClock = require("src.ui.gen2.InitClock")
local Strings = require("src.core.Strings")
-- A stub input the screen drives off, the same shape Input:wasPressed has.
local function fakeInput()
local pressed = {}
return {
press = function(self, button) pressed[button] = true end,
wasPressed = function(_self, button)
if pressed[button] then
pressed[button] = nil
return true
end
return false
end,
}
end
-- --------------------------------------------------------------- the offsets
do
local save = {}
check(not Clock.isSet(save), "a fresh save has no base")
eq(Clock.hour(save), math.floor(Clock.hostMinutes() / 60),
"so it reads the host clock straight through")
Clock.setTime(save, 10, 0)
check(Clock.isSet(save), "answering Oak writes the base")
eq(Clock.hour(save), 10, "and the clock reads back what was set")
eq(Clock.minute(save), 0, "minutes too")
Clock.setTime(save, 23, 45)
eq(Clock.hour(save), 23, "a second answer re-anchors it")
eq(Clock.minute(save), 45, "minutes and all")
-- The base is an OFFSET, not a frozen reading: the clock keeps running.
local base = save.rtc.startMinute
local moved = (Clock.hostMinutes() + 90 + base) % Clock.MINUTES_PER_DAY
eq(math.floor(moved / 60), 1,
"an hour and a half later it is 1:15, not still 23:45")
eq(moved % 60, 15, "minutes and all")
Clock.setTime(save, 0, 0)
eq(Clock.hour(save), 0, "midnight is hour 0")
Clock.setTime(save, 12, 30)
eq(Clock.hour(save), 12, "noon is hour 12")
end
do
local save = {}
eq(Clock.weekday(save), Clock.hostWeekday(),
"with no base the weekday is the host's")
for day = 0, 6 do
Clock.setWeekday(save, day)
eq(Clock.weekday(save), day, ("SetDayOfWeek pins day %d"):format(day))
end
eq(save.rtc.dayOfWeek, 6,
"and leaves wCurDay where the daily rollovers read it")
end
-- ------------------------------------------------------------ the hour screen
do
local save = {}
local input = fakeInput()
local done = {}
local screen = InitClock.new({ input = input }, {
save = save,
onDone = function(hour, minute) done = { hour = hour, minute = minute } end,
})
eq(screen.phase, "intro", "the screen opens on Oak waking up")
eq(screen.hour, 10, "with InitClock's own default of 10 AM")
eq(screen.minute, 0, "and no minutes")
-- OakTimeWokeUpText is two `para` pages, so A turns the page before it takes
-- the box down.
eq(#screen:pages(), 2, "Oak's opening is two pages")
input:press("a")
screen:update(0)
eq(screen.phase, "intro", "the first A turns the page")
check(screen:pageText():find("clock", 1, true) ~= nil,
"onto the one that asks about the clock")
input:press("a")
screen:update(0)
eq(screen.phase, "hour", "and the second opens the hour picker")
-- DisplayHourOClock is PrintHour + String_oclock, so the word rides along
eq(screen:display(), "DAY 10 o'clock", "which shows the hour DisplayHourOClock does")
input:press("up")
screen:update(0)
eq(screen.hour, 11, "up walks the hour forward")
input:press("down")
screen:update(0)
input:press("down")
screen:update(0)
eq(screen.hour, 9, "down walks it back")
-- .DecreaseThroughMidnight / .AdvanceThroughMidnight: both ends wrap.
screen.hour = 0
input:press("down")
screen:update(0)
eq(screen.hour, 23, "and midnight wraps to 11 PM")
input:press("up")
screen:update(0)
eq(screen.hour, 0, "and back again")
screen.hour = 7
input:press("a")
screen:update(0)
eq(screen.phase, "confirm-hour", "A confirms the hour")
check(screen:question():find("o'clock", 1, true) ~= nil,
"and the question reads it back")
-- NO drops back to the picker (`jr c, .loop`).
screen.yesNo = 2
input:press("a")
screen:update(0)
eq(screen.phase, "hour", "NO goes back to the picker")
input:press("a")
screen:update(0)
input:press("a")
screen:update(0)
eq(screen.phase, "minute", "YES moves on to the minutes")
input:press("up")
screen:update(0)
eq(screen.minute, 1, "up walks the minutes")
screen.minute = 0
input:press("down")
screen:update(0)
eq(screen.minute, 59, "and they wrap at the hour")
screen.minute = 30
eq(screen:display(), "30 min.", "DisplayMinutesWithMinString's own string")
input:press("a")
screen:update(0)
eq(screen.phase, "confirm-minute", "A confirms them")
input:press("a")
screen:update(0)
eq(screen.phase, "response", "and Oak answers with the time")
-- "MORN 7:30", not "7 AM:30": OakText_ResponseToSetTime is PrintHour (the
-- time-of-day word then the 1-12 hour) then ':' then two-digit minutes, so
-- the meridiem never appears and cannot land between the hour and them.
check(screen:question():find("MORN 7:30", 1, true) ~= nil,
"which is the pair the player just set")
input:press("a")
screen:update(0)
eq(done.hour, 7, "the screen hands the hour back")
eq(done.minute, 30, "and the minutes")
eq(Clock.hour(save), 7, "and the save now reads that hour")
eq(Clock.minute(save), 30, "and those minutes")
end
-- OakText_ResponseToSetTime's own ladder.
eq(InitClock.responseKey(2), "soDark", "before MORN_HOUR it is still dark")
eq(InitClock.responseKey(4), "overslept", "MORN_HOUR is 'I overslept'")
-- DAY_HOUR is 10 (constants/misc_constants.asm:38), and
-- OakText_ResponseToSetTime's `cp DAY_HOUR + 1 / jr c, .morn` keeps the hour
-- ITSELF in the plain "I overslept!" arm; "Yikes!" starts at 11. This read 9
-- when InitClock did, so both were an hour early together.
eq(InitClock.responseKey(10), "overslept", "and so is DAY_HOUR itself")
eq(InitClock.responseKey(11), "yikes", "past it Oak yikes")
eq(InitClock.responseKey(18), "soDark", "and NITE_HOUR is dark again")
-- PrintHour (engine/rtc/timeset.asm:672) is GetTimeOfDayString + PlaceString
-- and THEN the 1-12 hour, so the cart prints the time-of-day word ahead of the
-- number and no meridiem at all. These read "12 AM" / "1 PM" while InitClock
-- built the string that way, which is what let InitClock.timeString append
-- ":mm" to a meridiem and have Oak say "5 AM:30".
eq(InitClock.hourString(0), "NITE 12", "PrintHour shows midnight as NITE 12")
eq(InitClock.hourString(12), "DAY 12", "and noon as DAY 12")
eq(InitClock.hourString(13), "DAY 1", "and the afternoon on a 12-hour clock")
eq(InitClock.timeString(5, 30), "MORN 5:30",
"and OakText_ResponseToSetTime's line is PrintHour, ':', two-digit minutes")
-- --------------------------------------------------------- the weekday wheel
do
local save = {}
local input = fakeInput()
local picked
local screen = InitClock.new({ input = input }, {
mode = "day", save = save,
onDone = function(day) picked = day end,
})
eq(screen.phase, "day", "the wheel opens on its picker, with no preamble")
eq(screen.day, 0, "`xor a / ld [wTempDayOfWeek], a`: SUNDAY")
eq(screen:display(), "SUNDAY", "which is what the box shows")
for _ = 1, 2 do
input:press("up")
screen:update(0)
end
eq(screen:display(), "TUESDAY", "up walks the wheel forward")
input:press("down")
screen:update(0)
input:press("down")
screen:update(0)
input:press("down")
screen:update(0)
eq(screen:display(), "SATURDAY", "and it wraps past SUNDAY")
input:press("a")
screen:update(0)
eq(screen.phase, "confirm-day", "A confirms it")
input:press("b")
screen:update(0)
eq(screen.phase, "day", "B is NO and drops back to the wheel")
input:press("a")
screen:update(0)
input:press("a")
screen:update(0)
eq(picked, 6, "YES hands the day back")
eq(Clock.weekday(save), 6, "and the save reads it")
end
-- The driven path: a screen this new must not stall a scripted run, so it
-- walks itself to the end on its defaults.
do
local save = {}
local finished = false
local screen = InitClock.new({}, {
save = save, autoConfirm = true,
onDone = function() finished = true end,
})
for _ = 1, 20 do
if finished then break end
screen:update(0)
end
check(finished, "autoConfirm reaches the end on its own")
eq(Clock.hour(save), Clock.DEFAULT_HOUR, "taking InitClock's 10 AM default")
eq(Clock.minute(save), Clock.DEFAULT_MINUTE, "and no minutes")
end
-- Drawing must not throw with the stub canvas: the layout is transcribed from
-- hlcoord calls, so a bad coordinate is a crash rather than a wrong pixel.
do
local screen = InitClock.new({}, { save = {} })
for _, phase in ipairs({ "intro", "hour", "confirm-hour", "minute",
"confirm-minute", "response", "day", "confirm-day" }) do
screen.phase = phase
local ok, err = pcall(function() screen:drawPanel() end)
check(ok, ("phase %s draws (%s)"):format(phase, tostring(err)))
end
end
-- Clock.DAY_NAMES / Clock.weekdayName / Clock.daytimeLabel: the single home
-- InitClock, MainMenu and the Pokegear clock card all share, so a weekday
-- cannot be named one way on one screen and another way on the next.
do
eq(Clock.weekdayName(1), "SUNDAY", "1-based, SUNDAY first")
eq(Clock.weekdayName(6), "FRIDAY", "and the rest in wCurDay's order")
check(Clock.weekdayName(0) == nil, "day 0 is out of range")
check(Clock.weekdayName(8) == nil, "and so is day 8")
eq(Clock.daytimeLabel(4), "MORN", "daytimeLabel matches clockDaytime's word")
eq(Clock.daytimeLabel(10), "DAY", "for every hour band")
eq(Clock.daytimeLabel(20), "NITE", "including the wrap back to NITE")
local MainMenu = require("src.ui.gen2.MainMenu")
check(MainMenu.DAYS == Clock.DAY_NAMES,
"MainMenu reuses the same table InitClock and the Pokegear do")
end
-- ------------------------------------------------- a translation mod's turn
--
-- DAYS, the clockDaytime word and the "o'clock"/"min." suffixes used to
-- bypass Strings entirely, so a translation mod's `strings` registry had no
-- seam to catch them: the picker kept printing the English day name and
-- "o'clock" no matter the catalog (reported from a real Gold build).
do
Strings.load({
strings = {
SUNDAY = "DIMANCHE",
MORN = "MATIN",
["%s o'clock"] = "%s heures",
["%d min."] = "%d min",
},
})
local wheel = InitClock.new({ input = fakeInput() }, { mode = "day", save = {} })
eq(wheel:display(), "DIMANCHE", "a translated catalog reaches the day wheel")
eq(InitClock.hourString(4), "MATIN 4",
"and the clockDaytime word, through Clock.daytimeLabel")
eq(InitClock.oclockString(4), "MATIN 4 heures",
"and the o'clock suffix, template and all")
local minutePicker = InitClock.new({ input = fakeInput() }, { save = {} })
minutePicker.phase = "minute"
minutePicker.minute = 30
eq(minutePicker:display(), "30 min", "and the minutes picker's own suffix")
-- Palettes.clockDaytime itself must stay untranslated even with a catalog
-- loaded: FORCED_DAYTIME and the rest of Palettes.lua's own lookups
-- compare against its return value as an internal key, not display text.
local Palettes = require("src.world.gen2.Palettes")
eq(Palettes.clockDaytime(4), "MORN",
"the internal palette key is untouched by the loaded catalog")
-- Module state is process-global and tests/run_tests.lua runs every suite
-- in one process (see tests/mod_strings_tests.lua's own note): leaving the
-- catalog loaded would translate the day/hour of every suite after this
-- one.
Strings.load({})
check(not Strings.active(), "the catalog is unloaded for the suites after this one")
end
S.finish()