-- 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). local TITLE_Y_ON, TITLE_Y_OFF = -118, 4 -- closed board: user saw the title riding the top edge (+16) -- +8 px: user asked the title slightly lower -- 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 ` 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 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 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 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 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