Files
YataiDONNijiiro/Scripts/entry/box.lua
TaikoZhongandClaude Fable 5.1 44e0a5e3b2 Entry: control-guide mode per phase, mode board title centred on its band
- The top-left control guide follows the cabinet's ChangeMode: `decide` alone on
  the credit screen, the `all` loop (rim to choose + face to decide) on mode
  select when the list has more than one board, hidden during the drum select.
  entry.lua / entry/box.lua publish the phase, global/indicator.lua reads it.
- The open mode board's title sits in the middle of the dark band at the top of
  the plate; the text object's box is taller than its ink, so the nominal y is
  set for the ink (-118 -> -97).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 00:40:52 +08:00

476 lines
26 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},
titles = { ja = "演奏ゲーム", en = "Game", zh = "节奏游戏" },
comment = {
ja = { "すきな曲や、むずかしさを", "えらんであそべるよ!" },
en = { "Select a song and", "difficulty to play!" },
zh = { "可選擇喜愛的", "歌曲、難度遊玩哦!" },
},
},
practice = {
frame = 2, outline = {34, 138, 117, 255},
titles = { ja = "特訓モード", en = "Practice", zh = "练习游戏" },
comment = {
ja = { "曲の一部をくりかえし練習して", "腕をみがこう!" },
en = { "Practice a section of a song", "over and over to improve!" },
zh = { "反覆練習歌曲的片段", "磨練技巧吧!" },
},
},
costume = {
frame = 4, outline = {200, 120, 222, 255},
titles = { ja = "きせかえ", en = "Costume", zh = "装扮" },
comment = {
ja = { "どんちゃんのきせかえを", "えらんであそべるよ!" },
en = { "Choose an outfit", "for your Don-chan!" },
zh = { "可為咚醬選擇", "服飾遊玩哦!" },
},
},
ai = {
frame = 2, outline = {34, 138, 117, 255},
titles = { ja = "AIバトル", en = "AI Battle", zh = "AI斗争" },
comment = {
ja = { "「AIどん」と勝つか負けるか", "ギリギリのバトルをたのしもう!" },
en = { "See if you can beat AI-DON", "in this neck-and-neck competition!" },
zh = { "與「AI咚」來一場", "不到最後難分勝負的對戰吧!" },
},
},
-- 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},
titles = { ja = "段位道場", en = "Dan-i Dojo", zh = "段位道場" },
comment = {
ja = { "3曲連続して演奏しながら、", "お題の合格を目指そう!", "合格すると腕前が認定されるよ!" },
en = { "Play three songs in a row", "and aim to clear the task!", "Your skills will be rewarded!" },
zh = { "連續演奏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},
titles = { ja = "ゲーム設定", en = "Settings", zh = "游戏設定" },
comment = {
ja = { "ゲームのせっていを", "かえられるよ!" },
en = { "Change the game's", "settings here!" },
zh = { "可以變更", "遊戲設定哦!" },
},
},
}
-- 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.
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 m.titles[cur] == text_str then return m, cur end
end
end
for key, m in pairs(MODES) do
for lang, t in pairs(m.titles) do
if t == 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 = mode.comment[lang] or mode.comment.ja
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.load_texture("entry/mode_select/box") -- framed: see MAPPING.md
self.cursor = tex.load_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