forked from Yonokid/YataiDONNijiiro
- 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>
476 lines
26 KiB
Lua
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
|