Files
YataiDONNijiiro/Scripts/entry/box.lua
T
2026-09-19 13:45:48 -04:00

456 lines
25 KiB
Lua

-- YataiDON-HSS override of PyTaikoGreen/entry/box.lua
-- Arcade (reference cabinet) mode-select board list, ported from
-- script_lua/entry/mode_select.lua + ModeSelectBorad.lua and lumen entry/mode_select.
-- The C++ BoxManager still owns selection/input; this script only draws. It ignores the
-- x / left_x / right_x / y_pos the engine hands it and lays the boards out the way the
-- arcade list does: selected board open ("on" label) at the screen centre, the others
-- closed ("off" label) stacked above/below at the arcade offsets
-- (mode_select_list_instance "wait": +-305, +-496, +-687 in y, +-50/90/130 in x).
-- Everything here is 1080p pixels (child skin, screen_scale-independent).
EntryBox = {}
EntryBox.__index = EntryBox
-- shared list state (one Lua state for all boxes). Entry.new() (entry.lua) sets
-- __hss_entry_reset so the next batch of EntryBox.new calls starts a fresh list.
__hss_entry_boxes = __hss_entry_boxes or {}
__hss_entry_mode = __hss_entry_mode or { sel = 1, t0 = 0 }
local CENTER_X, CENTER_Y = 960, 535 -- arcade open-board centre (350..720)
-- ROUND 6 — every timing below is a frame count read straight off the the reference build lumens
-- (60 fps, `Common.FPS`); see Graphics/entry/MAPPING.md "ROUND 6" for the decoded tables.
local F = 1000 / 60 -- one arcade frame in ms
local SLIDE_F = 9 -- entry/entry mode_select_list_instance list_anim_up/down: 9 frames, LINEAR
local TITLE_SIZE = 72 -- text_title DefineEditText font size
-- text_info DefineEditText (id 66): font 32, white #ffffff, outline 5, box 478x192, align centre.
-- Nominal 32 reads smaller than the arcade MS P-Gothic atlas ink, so 34 (user's measurement).
local INFO_SIZE = 34
-- text_title_instance sits at y=-106 (on) / 0 (off); inside it text_instance_1/2 at -84.5 hold the
-- 758x126 text box at (-378,2) -> box centre -20.5 (off) / -126.5 (on). Lumen dynamic text is
-- vertically centred in its box (the reference research notes 3a).
-- TITLE_Y_ON: the open board's title sits in the middle of the dark band at the top of the
-- plate (plate rows 64..187 -> centre 104.5 above the board centre). The text object's
-- box is taller than its ink (descender room), so its ink centre lands ~16 px above the
-- nominal y; -97 nominal puts the ink on the band centre (tuned on screen).
local TITLE_Y_ON, TITLE_Y_OFF = -97, 4 -- closed board: user saw the title riding the top edge (+16)
-- text_info_instance at y=57, text_info rect (-2,-2,478,192) at (-238,-92.5) -> box -37.5..156.5,
-- centre 59.5. The cabinet renderer centres the text block in the box and advances lines by the
-- atlas line height (jp_32 fontSize 53 x scale 32/32 = 53 px) -> line centres ~33 / ~86.
local INFO_CY, INFO_LINE_H = 59.5, 53
-- the title is two stacked DefineEditTexts: text_instance_2 (outline 9, renderer default black)
-- under text_instance_1 (outline 3, colour-add = mode colour from text_title_instance's label
-- frame). create_raw_text thickness is multiplied by screen_scale (1.5). In the arcade capture the
-- visible ring is ~6 px mode colour with a thin black edge outside it -> inner 4 (=6 px),
-- outer 5 (=7.5 px).
local TITLE_OUTER_T, TITLE_INNER_T = 8, 4 -- user: thicker black rim, as the arcade 9-under-3
local INFO_COLOR = { 255, 255, 255, 255 } -- text_info colour #ffffff
local INFO_OUTLINE = { 0, 0, 0, 255 } -- outline 5 (renderer default black) -> 3 (=4.5 px)
-- ROUND 16 (r16-outline): the record's border 5.0 is already in 1080p stage px and
-- OutlinedText multiplies the thickness by screen_scale (1.5); dividing it out makes
-- the ring exactly 5.0 px instead of the 4.5 px the rounded-down 3 drew.
local INFO_BORDER = 5.0
-- OutlinedText pads both sides by outline+2 (symmetric), but raylib's MeasureTextEx adds the glyph
-- spacing after the LAST glyph too, so the ink sits spacing/2 left of the texture centre.
local TITLE_SPACING, INFO_SPACING = 2.0, 1.0
-- mode table: frame = index of the "on" frame in entry/mode_select/box (frame+1 = "off";
-- the board art is baked into the parent's fixed texture name "box" as a framed texture
-- because the engine's TexID table is generated from PyTaikoGreen); titles = what BoxManager passes
-- (skin_config entry_* text) so we can recognise the mode and the language.
local MODES = {
game = {
frame = 0, outline = {251, 1, 29, 255},
title_key = "entry_game",
comment_keys = { "entry_game_comment_1", "entry_game_comment_2" },
},
practice = {
frame = 2, outline = {34, 138, 117, 255},
title_key = "entry_practice",
comment_keys = { "entry_practice_comment_1", "entry_practice_comment_2" },
},
costume = {
frame = 4, outline = {200, 120, 222, 255},
title_key = "entry_costume",
comment_keys = { "entry_costume_comment_1", "entry_costume_comment_2" },
},
ai = {
frame = 2, outline = {34, 138, 117, 255},
title_key = "entry_ai_battle",
comment_keys = { "entry_ai_battle_comment_1", "entry_ai_battle_comment_2" },
},
-- ROUND 83 (r83-dandojo-as-mode) — 段位道場 is a MODE on the cabinet
-- (`mode_select.lua` CreateBoardList l.289-296 appends GameMode.kDani right after
-- kEnso), not a folder in the song wheel. Its board is the arcade `dani` style —
-- frames 6/7, the dark blue the user pointed at — and its title colour is the
-- colour-add on `text_title_instance` depth 1 at label `dani` (frame 5) = (76,60,151).
-- Strings are the cabinet's own: wordlist keys mode_select_dani /
-- mode_select_comment_dani (three lines; every other board here has two).
dan = {
frame = 6, outline = {76, 60, 151, 255},
title_key = "mode_dan",
comment_keys = { "mode_dan_comment_1", "mode_dan_comment_2", "mode_dan_comment_3" },
},
-- ROUND 83: ゲーム設定 has no cabinet counterpart and was wearing 段位道場's board
-- (dani, frames 6/7) since ROUND 6. Moved onto `aprilfool`, baked as frames 9/10 by
-- bake_entry.py's EXTRA list; its title colour-add at label `aprilfool` is (0,132,212).
settings = {
frame = 9, outline = {0, 132, 212, 255},
title_key = "entry_settings",
comment_keys = { "entry_settings_comment_1", "entry_settings_comment_2" },
},
}
-- ROUND 83: 段位道場's ja and zh titles are the SAME string, so a bare scan over
-- `pairs(m.titles)` could resolve either language and hand back the wrong comment lines
-- (pairs() order is unspecified). Ask the engine which language it is actually running
-- (`tex.language()`, script.cpp:693 -> config.general.language) and try that key first;
-- the old scan stays as the fallback for a language the table has no column for.
-- Titles and comments live in skin_config (entry_* / entry_*_comment_N); the engine hands
-- us the title it drew, so the mode is the one whose skin_config title matches.
local LANGS = { "ja", "en", "zh", "ko" }
local function find_mode(text_str)
local ok, cur = pcall(function() return tex.language() end)
if ok and type(cur) == "string" then
for key, m in pairs(MODES) do
if tex.skin_text(m.title_key, cur) == text_str then return m, cur end
end
end
for key, m in pairs(MODES) do
for _, lang in ipairs(LANGS) do
if tex.skin_text(m.title_key, lang) == text_str then return m, lang end
end
end
return MODES.settings, "ja", true
end
local function ease_out(t)
if t <= 0 then return 0 end
if t >= 1 then return 1 end
return 1 - (1 - t) * (1 - t)
end
local function clamp01(v)
if v <= 0 then return 0 end
if v >= 1 then return 1 end
return v
end
-- ---------------------------------------------------------------------------
-- ROUND 6 — arcade board timelines (lumen entry/mode_select, board_main_instance,
-- MC 128: labels init 0 / off 1 / inside 4 / select_on 27 / on 42 / select_off 52 /
-- in 67 / choose 117). All functions take a FRAME count relative to the label.
--
-- The board is a 3-slice in the arcade (top cap ty -26..-140, centre band scaleY
-- 0.186..1.0, bottom cap +26..+140) and this skin's art is two baked frames
-- ("off" 964x157 / "on" 1050x436), so the openness `p` below drives a crossfade
-- instead of the band growing. Every other channel is reproduced exactly.
-- ---------------------------------------------------------------------------
-- ROUND 7 — every one of the eight channels below is now SAMPLED out of the cabinet's
-- own timeline rather than fitted by hand: `Scripts/anim/mode_board.lua` is exported by
-- scratchpad/anim_export.py straight from `entry/mode_select.nulm` sprite 128.
-- See ANIM_PIPELINE.md. Re-checking round 6's hand-derived constants against the export
-- found NO numeric error in this rig — select_on, select_off, `in` and `choose` were all
-- frame-exact — so this change buys provenance and re-exportability, not a visible fix.
-- (The one deviation that remains is deliberate and is NOT taken from the table:
-- TITLE_Y_ON / TITLE_Y_OFF above, which the user asked to sit 8-12 px lower than the
-- arcade's 0 / -106.)
-- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL
-- clip when a generated table is missing or malformed, so every reader below falls
-- through to its `or <constant>` default instead of taking the screen down.
local _ba_ok, box_anim = pcall(require, "anim/sampler")
if not _ba_ok then box_anim = { safe = function()
-- sampler.lua itself is missing: hand back the same NULL clip shape it would
local n = {}
n.at = function() return nil end; n.get = function() return nil end
n.label = function() return nil end; n.has = function() return false end
n.first = function() return 0 end; n.last = function() return 0 end
return n
end } end
local MB = box_anim.safe("anim/mode_board")
-- the selected board's cursor glow pulse (shared with song select -- see draw)
local CURSOR_GLOW = box_anim.safe("anim/cursor_glow")
local CURSOR_GLOW_LEN = math.max(1, CURSOR_GLOW:last() - CURSOR_GLOW:first() + 1) -- 60 frames = 1 s
local ML = box_anim.safe("anim/mode_list")
local ML_WAIT = ML:label("wait") or 13
local ML_IN = ML:label("in") or 87
local MB_SELECT_ON = MB:label("select_on") or 27
local MB_ON = MB:label("on") or 42
local MB_SELECT_OFF = MB:label("select_off") or 52
local MB_IN = MB:label("in") or 67
local MB_CHOOSE = MB:label("choose") or 117
-- the centre band's scaleY at the two rest states -> the 0..1 openness the rest of the
-- file speaks (our art is two baked plates, so `o` drives a crossfade, not a 3-slice)
local MB_SY_CLOSED, MB_SY_OPEN = 0.1857, 1.0
-- a label's segment ends one frame before the next label starts; clamping there keeps a
-- caller that passes a huge `u` ("this board has been settled for ages") on the segment's
-- own last frame instead of walking into the next animation.
local function mb_seg(base, u, limit)
if u < 0 then u = 0 elseif u > limit then u = limit end
return base + u
end
local function mb_open(f)
local sy = MB:get("board_bg_center_instance", f, "sy") or MB_SY_CLOSED
return clamp01((sy - MB_SY_CLOSED) / (MB_SY_OPEN - MB_SY_CLOSED))
end
-- select_on (label 27): board grows f30..f36, cursor alpha with it, title y 0 -> -106,
-- comment alpha f36..f41.
local ON_LIMIT = MB_SELECT_OFF - MB_SELECT_ON - 1
local OFF_LIMIT = MB_IN - MB_SELECT_OFF - 1
local IN_LIMIT = MB_CHOOSE - MB_IN - 1
local function on_p(u) return mb_open(mb_seg(MB_SELECT_ON, u, ON_LIMIT)) end
local function on_info(u) return MB:get("text_info_instance", mb_seg(MB_SELECT_ON, u, ON_LIMIT), "a") or 0 end
-- select_off (label 52): comment fades f52..f57, board closes f55..f61.
local function off_p(u) return mb_open(mb_seg(MB_SELECT_OFF, u, OFF_LIMIT)) end
local function off_info(u) return MB:get("text_info_instance", mb_seg(MB_SELECT_OFF, u, OFF_LIMIT), "a") or 0 end
-- in (label 67): the board fades UP while still closed, then opens, then its contents
-- fade in, then the cursor glow ramps -- four staggered ramps, all read from the table.
local function in_board_fade(t) return MB:get("board_bg_center_instance", mb_seg(MB_IN, t, IN_LIMIT), "a") or 0 end
local function in_p(t) return mb_open(mb_seg(MB_IN, t, IN_LIMIT)) end
local function in_contents(t) return MB:get("text_info_instance", mb_seg(MB_IN, t, IN_LIMIT), "a") or 0 end
local function in_cursor(t) return MB:get("cursor_center", mb_seg(MB_IN, t, IN_LIMIT), "a") or 0 end
local IN_TOTAL_F = IN_LIMIT
-- the list itself flies in (mode_select_list_instance label "in" 87): the closed
-- boards start 3 slots out (dy -+878, dx -+170) and settle on their slot with a
-- quadratic ease-out over frames 98..109 (12 frames) after an 11-frame hold.
-- ROUND 49: 11 -> 10. The exported plateau's last held frame is mode_list f97 = t_in 10
-- (label `in` 87 + 10), so the ease starts at t_in 10 and settles at f109, not f110.
local LIST_IN_DELAY_F, LIST_IN_F = 10, 12
-- the parked position at label `in` (held f87..f97, then eased onto the slot by f109)
local LIST_IN_DX, LIST_IN_DY = 170, 878
do
local tx, ty = ML:at("kanban_3", ML_IN)
if tx then LIST_IN_DX, LIST_IN_DY = tx, ty end
end
-- choose (label 117): the white board-shaped flash is MC 22, placed unnamed at depth 6 --
-- hence the track key `#22@6`. Peaks at +4/+12/+20/+28, troughs between, then out over 6.
local CHOOSE_LIMIT = MB:last() - MB_CHOOSE
local function choose_flash(c)
if c < 0 then return 0 end
if c > CHOOSE_LIMIT then return 0 end
return MB:get("#22@6", MB_CHOOSE + c, "a") or 0
end
-- ROUND 7 — the slot offsets and the `in` fly-in start are read out of
-- `Scripts/anim/mode_list.lua` (entry/entry.nulm `mode_select_list_instance`, sprite 23,
-- labels init 0 / wait 13 / list_anim_up 27 / list_anim_down 36 / outside 45 / inside 66 /
-- in 87) instead of being the arithmetic series round 6 fitted to them. The export
-- confirmed that series exactly -- +-50/305, +-90/496, +-130/687, and the 9-frame LINEAR
-- slide -- so again no visible change, but the numbers now have a provenance and a
-- re-export path.
-- kanban_1 is the selected slot; the pairs above/below it are kanban_2/3, 4/5, 6/7, 8/-.
local function kanban_of(rel)
if rel == 0 then return "kanban_1" end
local n = math.abs(rel)
return "kanban_" .. (2 * n + (rel < 0 and 0 or 1))
end
local function slot_offset(rel)
if rel == 0 then return 0, 0 end
local tx, ty = ML:at(kanban_of(rel), ML_WAIT)
if not tx then -- more slots than the arcade list carries
local n, s = math.abs(rel), (rel < 0 and -1 or 1)
return s * (50 + (n - 1) * 40), s * (305 + (n - 1) * 191)
end
return tx, ty
end
function EntryBox.new(text_str, font_size)
local self = setmetatable({}, EntryBox)
if __hss_entry_reset then
__hss_entry_boxes = {}
__hss_entry_mode = { sel = 1, t0 = 0, in_t0 = nil }
__hss_entry_reset = false
end
__hss_entry_boxes[#__hss_entry_boxes + 1] = self
self.index = #__hss_entry_boxes
local mode, lang, unknown = find_mode(text_str)
self.mode = mode
self.title_outer = text.create_raw_text(text_str, TITLE_SIZE, { 0, 0, 0, 255 }, { 0, 0, 0, 255 }, false, TITLE_OUTER_T, TITLE_SPACING)
-- a single wide outline pass leaves star-shaped gaps between the 8 offset copies; a second,
-- thinner black pass fills them so the rim reads smooth.
self.title_outer2 = text.create_raw_text(text_str, TITLE_SIZE, { 0, 0, 0, 255 }, { 0, 0, 0, 255 }, false, TITLE_OUTER_T - 3, TITLE_SPACING)
self.title = text.create_raw_text(text_str, TITLE_SIZE, { 255, 255, 255, 255 }, mode.outline, false, TITLE_INNER_T, TITLE_SPACING)
self.info = {}
if not unknown then
local lines = {}
for i, key in ipairs(mode.comment_keys) do lines[i] = tex.skin_text(key, lang) end
for i, line in ipairs(lines) do
self.info[i] = text.create_raw_text(line, INFO_SIZE, INFO_COLOR, INFO_OUTLINE, false,
INFO_BORDER / tex.get_screen_scale(), INFO_SPACING)
end
end
self.board = tex.get_texture("entry/mode_select/box") -- framed: see MAPPING.md
self.cursor = tex.get_texture("entry/mode_select/box_highlight_center") -- yellow glow ring
-- ROUND 6: frame 8 is a white silhouette of the "on" board (its own alpha, RGB 255),
-- baked from box/2.png — the stand-in for the arcade `choose` flash overlay (MC 22).
self.frame_flash = 8
self.frame_on, self.frame_off = mode.frame, mode.frame + 1
-- animated state (screen px relative to the list centre)
self.cur_x, self.cur_y = nil, nil
self.from_x, self.from_y = 0, 0
self.tgt_x, self.tgt_y = 0, 0
self.t0 = 0
-- open/close timeline: `open_t0` = ms of the last selection change for THIS board,
-- `opening` = which of select_on / select_off is running.
self.open_t0, self.opening, self.was_selected = nil, false, false
self.choose_t0 = nil
return self
end
-- List slide: entry/entry `mode_select_list_instance` list_anim_up / list_anim_down move
-- every board to its next slot LINEARLY over 9 frames (150 ms). `vis` (the >1-slot fade)
-- rides the same ramp.
function EntryBox:_retarget(now, tx, ty, tvis)
if self.cur_x == nil then
self.cur_x, self.cur_y, self.cur_vis = tx, ty, tvis
self.tgt_x, self.tgt_y, self.tgt_vis = tx, ty, tvis
self.from_x, self.from_y, self.from_vis = tx, ty, tvis
self.t0 = now - SLIDE_F * F
return
end
if tx ~= self.tgt_x or ty ~= self.tgt_y or tvis ~= self.tgt_vis then
self.from_x, self.from_y, self.from_vis = self.cur_x, self.cur_y, self.cur_vis
self.tgt_x, self.tgt_y, self.tgt_vis = tx, ty, tvis
self.t0 = now
end
local p = clamp01((now - self.t0) / (SLIDE_F * F))
self.cur_x = self.from_x + (self.tgt_x - self.from_x) * p
self.cur_y = self.from_y + (self.tgt_y - self.from_y) * p
self.cur_vis = self.from_vis + (self.tgt_vis - self.from_vis) * p
end
-- Box::draw(x, left_x, right_x, is_selected, move_finished, open_attr, fade, y_pos)
-- `fade` is BoxManager's entry animation 9 (1 -> 0 over 160 ms) and is started only when a
-- board is picked, so `fade < 1` is the one decide signal Lua can see: it drives `choose`.
function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr, fade, y_pos)
local now = tex.get_current_ms()
local shared = __hss_entry_mode
if shared.in_t0 == nil then shared.in_t0 = now end -- first frame the list is drawn
-- ModeSelect: the cabinet's guide shows `all` (rim to choose + face to decide) when the
-- list has more than one board, `decide` alone otherwise (entry_main.lua ChangeMode)
__hss_entry_guide = { ms = now, phase = "mode", count = #__hss_entry_boxes }
if is_selected and shared.sel ~= self.index then
shared.sel = self.index
shared.t0 = now
end
-- per-board select_on / select_off timeline
-- ROUND 49 (r49-lua-chn-crosscheck): the arcade SEQUENCES the open after the slide —
-- mode_select.lua MoveCursor only plays list_anim_up/down + Close(); state MenuMove then
-- waits `list_mc_:IsPlay() == false`, does SetAllBoard() (all Off) and ONLY THEN
-- kanban:Open() -> select_on. Opening concurrently with the 9-frame slide (as before)
-- had the incoming board part-open while still moving. Delaying select_on by the slide
-- (SLIDE_F * F = 150 ms) reproduces the cabinet order; on_p/on_info clamp negative u.
if is_selected ~= self.was_selected then
self.was_selected = is_selected
self.open_t0 = now + (is_selected and SLIDE_F * F or 0)
self.opening = is_selected
end
local t_in = (now - shared.in_t0) / F -- frames since the list appeared
local in_running = t_in <= IN_TOTAL_F
local rel = self.index - shared.sel
local ox, oy = slot_offset(rel)
-- The arcade list places kanban_5/7 at dy +496/+687 too, but the user's arcade capture (list
-- = AI + enso) shows only ONE closed board under the open one; a second one (952..1110) would
-- sit on the nameplate / free-play row, so boards further than one slot away fade out.
self:_retarget(now, ox, oy, (math.abs(rel) <= 1) and 1 or 0)
local dx, dy = self.cur_x, self.cur_y
-- list fly-in: the closed boards start 3 slots out and ease-out-quad onto their slot
if in_running and rel ~= 0 then
local q = ease_out(clamp01((t_in - LIST_IN_DELAY_F) / LIST_IN_F))
local s = (rel < 0) and -1 or 1
dx = dx + (1 - q) * (s * LIST_IN_DX - ox)
dy = dy + (1 - q) * (s * LIST_IN_DY - oy)
end
-- openness (0 = "off" board, 1 = "on"): the `in` timeline owns it while it runs,
-- otherwise select_on / select_off do.
local o, info_a, cursor_a
if in_running then
o = is_selected and in_p(t_in) or 0
info_a = is_selected and in_contents(t_in) or 0
cursor_a = is_selected and in_cursor(t_in) or 0
fade = (fade or 1.0) * in_board_fade(t_in)
else
local u = self.open_t0 and ((now - self.open_t0) / F) or 999
if self.opening then
o, info_a = on_p(u), on_info(u)
else
o, info_a = off_p(u), off_info(u)
end
cursor_a = o
fade = fade or 1.0
end
local vis_fade = fade * self.cur_vis
if vis_fade <= 0.001 then return end
-- The arcade stacks the cursor glow UNDER the board (mode_select.nulm: cursor_* at
-- depths 3-5, board_bg_* at 7-9, board_chara_l/r at 16-17), so the characters that
-- lean out of the board stand in front of the glow, not behind it.
if cursor_a > 0.001 then
-- ROUND 8: the cursor pulse is the SAME 60-frame MC the song-select boards use
-- (`anim/cursor_glow`, char 16 of common_song_select_main_song_board): checked
-- against `entry/mode_select.nulm` `cursor_*/#7@0`, which reads 0.6797 at board
-- frame 42, 0.75 at 44 and 0.9297 at 51 -- the table's own values at those frames.
-- Round 6's `0.75 + 0.25*sin(2*pi*t)` was neither the right shape nor the right
-- range: the gradient our plate is a bake of swings all the way to 0.
local pulse = CURSOR_GLOW:get("#12@0", (now * 0.06) % CURSOR_GLOW_LEN, "a") or 1.0
tex.draw_texture(self.cursor, { x = dx, y = dy, fade = vis_fade * cursor_a * pulse })
end
if o < 0.999 then
tex.draw_texture(self.board, { frame = self.frame_off, x = dx, y = dy, fade = vis_fade * (1 - o) })
end
if o > 0.001 then
tex.draw_texture(self.board, { frame = self.frame_on, x = dx, y = dy, fade = vis_fade * o })
end
if info_a > 0.001 and #self.info > 0 then
local n = #self.info
for i, t in ipairs(self.info) do
local cy = CENTER_Y + dy + INFO_CY + (i - 1 - (n - 1) / 2) * INFO_LINE_H
t:draw({ x = CENTER_X + dx - (t.width - INFO_SPACING) / 2, y = cy - t.height / 2, fade = vis_fade * info_a })
end
end
-- title: rides `o` (arcade text_title_instance y 0 -> -106) and, during `in`, fades in
-- with the comment (arcade frames 78..84)
local title_a = vis_fade
if in_running and is_selected then title_a = vis_fade * in_contents(t_in) end
local ty = TITLE_Y_OFF + (TITLE_Y_ON - TITLE_Y_OFF) * o
-- both passes are the same string/font, so ink centre = (width - spacing)/2 for each;
-- centring each on its own width keeps them on the same x.
self.title_outer:draw({
x = CENTER_X + dx - (self.title_outer.width - TITLE_SPACING) / 2,
y = CENTER_Y + dy + ty - self.title_outer.height / 2,
fade = title_a,
})
self.title_outer2:draw({
x = CENTER_X + dx - (self.title_outer2.width - TITLE_SPACING) / 2,
y = CENTER_Y + dy + ty - self.title_outer2.height / 2,
fade = title_a,
})
self.title:draw({
x = CENTER_X + dx - (self.title.width - TITLE_SPACING) / 2,
y = CENTER_Y + dy + ty - self.title.height / 2,
fade = title_a,
})
-- `choose`: the picked board flashes white. The engine only gives 160 ms (animation 9)
-- before the screen changes, so one blink of the arcade's 8-frame cycle is what fits.
if fade < 0.999 and is_selected then
if self.choose_t0 == nil then self.choose_t0 = now end
local fl = choose_flash((now - self.choose_t0) / F)
if fl > 0.001 then
tex.draw_texture(self.board, { frame = self.frame_flash, x = dx, y = dy, fade = fl })
end
end
end