Files
YataiDONNijiiro/Scripts/song_select/file_navigator/box_song.lua
T
2026-10-01 11:28:23 -04:00

427 lines
19 KiB
Lua

-- SongBox rendering (song board, difficulty plates, badges, diff select), split out of
-- song_select.lua. All shared helpers/state come from box_base.
local base = require("song_select/file_navigator/box_base")
local folder_leg = base.folder_leg
local DIFF_BACK, DIFF_MODIFIER = base.DIFF_BACK, base.DIFF_MODIFIER
local DIFF_EASY, DIFF_NORMAL, DIFF_HARD, DIFF_ONI, DIFF_URA =
base.DIFF_EASY, base.DIFF_NORMAL, base.DIFF_HARD, base.DIFF_ONI, base.DIFF_URA
local CROWN_NONE, CROWN_FC, CROWN_DFC = base.CROWN_NONE, base.CROWN_FC, base.CROWN_DFC
local BOARD_W = base.BOARD_W
local CEN_CLOSED_H, CEN_OPEN_H = base.CEN_CLOSED_H, base.CEN_OPEN_H
local SONG_TEXT_BOX_W, DS_TEXT_BOX_W = base.SONG_TEXT_BOX_W, base.DS_TEXT_BOX_W
local SONG_CLOSE_MS, SONG_STALE_MS = base.SONG_CLOSE_MS, base.SONG_STALE_MS
local URA_LOOP, URA_LOOP_F0, URA_LOOP_LEN = base.URA_LOOP, base.URA_LOOP_F0, base.URA_LOOP_LEN
local DS_SLOT_X, DS_BOARD_Y, DS_COURSE_NAME_DY = base.DS_SLOT_X, base.DS_BOARD_Y, base.DS_COURSE_NAME_DY
local URA_ANIM_MS, URA_SWAP_MS, URA_CELLS = base.URA_ANIM_MS, base.URA_SWAP_MS, base.URA_CELLS
local DS_BTN_Y, DS_CURSOR_Y, DS_NEIRO_Y = base.DS_BTN_Y, base.DS_CURSOR_Y, base.DS_NEIRO_Y
local DS_MARK_X, DS_MARK_Y = base.DS_MARK_X, base.DS_MARK_Y
local DS_MARK_DECIDE, DS_MARK_DECIDE_MS = base.DS_MARK_DECIDE, base.DS_MARK_DECIDE_MS
local get_any = base.get_any
local draw_centered, draw_centered_b = base.draw_centered, base.draw_centered_b
local draw_title = base.draw_title
local outline, board_outline = base.outline, base.board_outline
local draw_board, draw_glow, glow_pulse = base.draw_board, base.draw_glow, base.glow_pulse
local box_id = base.box_id
local go_get, leg_f, box_fade, fly_offset = base.go_get, base.leg_f, base.box_fade, base.fly_offset
local wheel_open_t, wheel_close_step = base.wheel_open_t, base.wheel_close_step
local song_open_timeline, song_close_timeline = base.song_open_timeline, base.song_close_timeline
local SONG_TITLE_OUTLINE = 5.0
local SONG_TITLE_SIZE = nil -- resolved once from skin_config song_box_name
local SONG_TITLE_CACHE_MAX = 128 -- bound the per-title texture cache
-- crown / rank badges (72x72) at arcade positions, one column per player.
local ICON_ARTS = nil -- name -> texture id (resolved lazily)
function SongSelect:song_icon(box)
if ICON_ARTS == nil then ICON_ARTS = { icon_new = self._id_new_icon } end
local list = {}
if box.is_new then list[#list + 1] = "icon_new" end
if #list == 0 then return nil end
local pick = list[(math.floor(tex.get_current_ms() / 1000.0) % #list) + 1]
return ICON_ARTS[pick]
end
function SongSelect:badge_diffs(box, pl)
local sel = self._mark_diff and self._mark_diff[pl]
local crown_d, rank_d
if sel and sel >= DIFF_EASY and sel <= DIFF_URA then
local info = box:course_info(sel)
if info.has_course then
if info.crown ~= CROWN_NONE then crown_d = sel end
if (info.rank or 0) > 0 then rank_d = sel end
end
end
if not crown_d or not rank_d then
local cbest, rbest
for d = DIFF_EASY, DIFF_URA do
local info = box:course_info(d)
if info.has_course then
if info.crown ~= CROWN_NONE then cbest = d end
if (info.rank or 0) > 0 then rbest = d end
end
end
crown_d = crown_d or cbest
rank_d = rank_d or rbest
end
return crown_d, rank_d
end
function SongSelect:draw_badges(box, bx, by, p, fade)
local scale = 0.75 + 0.25 * p
local size = 72 * scale
local players = math.min(2, tex.player_num())
for pl = 1, players do
local cx = bx + ((pl == 1) and -425 or 425)
local crown_d, rank_d = self:badge_diffs(box, pl)
if crown_d then
local info = box:course_info(crown_d)
local id = (info.crown == CROWN_DFC) and self._id_crown_dfc
or (info.crown == CROWN_FC) and self._id_crown_fc or self._id_crown_clear
local cy = by + (-27 + (-114 + 27) * p)
draw_centered(id, cx, cy, size, size, { frame = crown_d, x2 = size - 72, y2 = size - 72, fade = fade })
end
if rank_d then
local info = box:course_info(rank_d)
local cy = by + (27 + (-42 - 27) * p)
draw_centered(self._id_score_rank, cx, cy, size, size,
{ frame = (info.rank - 1) * 5 + rank_d, x2 = size - 72, y2 = size - 72, fade = fade })
end
end
end
-- SongBox
-- pb = board progress (0..1), ia = contents alpha, ic = NEW-icon alpha.
function SongSelect:draw_song_board(box, pb, ia, ic)
local bx, by = box:box_x(), box:box_y()
local fdx, fdy = fly_offset(box); bx, by = bx + fdx, by + fdy -- wheel fly-in
local fade = math.min(1.0, box_fade(box))
-- folder-ENTER/EXIT unfurl/furl: while the slab's sx < 1 only the slab is drawn,
-- and the contents alphas come from the clip's own channels.
local gof = leg_f()
local ltitle = 1.0
if gof and folder_leg.kind == "open" and gof < folder_leg.open_swap - 1 and box:draw_state() == "open" then
return -- the new content's board does not exist until the swap frame
end
if gof and box:draw_state() == "open" then
local sx, ba = go_get("song_board_sx", gof, 1), go_get("song_board_sx", gof, 2)
if sx and ba then
if ba <= 0.0 then return end -- board not on screen on this frame
if sx < 1.0 then -- unfurl / furl: the slab alone
draw_board(self._id_bar_genre, box:genre_frame(), bx, by, CEN_OPEN_H,
fade * ba, nil, sx)
return
end
local ia2 = go_get("song_info_a", gof, 1)
if ia2 then ia = math.min(ia or 1.0, ia2) end
ltitle = go_get("song_title_a", gof, 1) or 1.0
local ic2 = go_get("song_icon_a", gof, 1)
if ic2 then ic = math.min(ic or 0.0, ic2) end
end
end
local cen_h = CEN_CLOSED_H + (CEN_OPEN_H - CEN_CLOSED_H) * pb
if pb > 0 then draw_glow(self._id_glow, bx, by, cen_h, fade * pb * glow_pulse()) end
draw_board(self._id_bar_genre, box:genre_frame(), bx, by, cen_h, fade)
if pb <= 0.0 then
draw_title(self:song_title(box), bx, by, fade, SONG_TEXT_BOX_W, 0)
self:draw_badges(box, bx, by, 0.0, fade)
-- instance_icon hangs off the board's left edge (-532)
local icon = (ic > 0) and self:song_icon(box) or nil
if icon then
draw_centered(icon, bx - 532, by + 4, 120, 96, { fade = fade * ic })
end
return
end
local cfade = fade * ia
if cfade > 0 then
self:draw_plates(box, bx, by, cfade)
self:draw_badges(box, bx, by, pb, cfade)
end
-- title rises from the closed position (y=0) to the open one (y=-70) with the growth
draw_title(self:song_title(box), bx, by - 70 * pb, fade * ltitle, SONG_TEXT_BOX_W, 0)
local sub = self:song_subtitle(box)
if sub and cfade > 0 then draw_title(sub, bx, by - 28, cfade, SONG_TEXT_BOX_W) end
if tex.display_bpm() and cfade > 0 then
local bpm_cfg = tex.get_skin_config("song_box_bpm")
box:bpm_text():draw({ x = bx + bpm_cfg.x, y = by + bpm_cfg.y, fade = cfade })
end
local icon = (ic > 0) and self:song_icon(box) or nil
if icon then
draw_centered(icon, bx - 532, by + 4, 120, 96, { fade = fade * ic })
end
-- 新音 / 旧音 / 期間限定 marker, hung off the board's right edge (mirror of the icon)
local ok_exd, exd = pcall(function() return box:ex_data_flag() end)
if ok_exd and exd and exd > 0 and cfade > 0 then
local art = (exd == 1) and get_any("yellow_box", "ex_data_new_audio")
or (exd == 2) and get_any("yellow_box", "ex_data_old_audio")
or get_any("yellow_box", "ex_data_limited_time")
if art then
local s = tex.get_screen_scale()
draw_centered_b(art, { 458 * s, 120 * s }, bx + 532, by + 4,
224 * s, 64 * s, { fade = cfade })
end
end
if box.is_favorite and cfade > 0 then
local fav = get_any("yellow_box", "favorite_" .. tex.player_num() .. "p")
if fav then tex.draw_texture(fav, { x = bx - 480, y = by - 176, fade = cfade }) end
end
end
function SongSelect:draw_song_closed(box)
local key = "s\0" .. box_id(box) -- per-BOX, not per-title
local e = wheel_close_step(key, SONG_CLOSE_MS, SONG_STALE_MS)
if e then
self:draw_song_board(box, song_close_timeline(e))
return
end
self:draw_song_board(box, 0.0, 0.0, 1.0)
end
-- Diff plates row (song_kanban_info, board y +75): plate 184x96 with the course name
-- under the icon, a star and the level number.
function SongSelect:chip_label(diff)
local lbl = self._chip_labels[diff]
if lbl == nil then
local key = "chip_course_name_" .. diff
if tex.get_skin_config(key) then
lbl = text.create_text(key, { 255, 255, 255, 255 }, { 40, 20, 20, 255 }, false, outline(1.5), 1.0)
end
self._chip_labels[diff] = lbl or false
end
return lbl or nil
end
function SongSelect:draw_plates(box, bx, by, fade)
local has_ura = box:has_ura()
local ura_weight = 0.0
local oni_alpha = 1.0
if has_ura then
local f = URA_LOOP_F0 + (tex.get_current_ms() * 0.06) % URA_LOOP_LEN
ura_weight = URA_LOOP:get("ura/text_course_ura", f, "a") or 0.0
oni_alpha = URA_LOOP:get("oni/text_course_oni", f, "a") or 1.0
end
local py = by + 75
local function plate(cx, diff, f)
local info = box:course_info(diff)
if not info.has_course or f <= 0 then return end
draw_centered(self._id_plate, cx, py, 184, 96, { frame = diff, fade = f })
draw_centered(self._id_star, cx + 27, py, 40, 40, { frame = diff, fade = f })
local lv = math.max(1, math.min(11, info.level))
draw_centered(self._id_level_number, cx + 61, py, 48, 48, { frame = diff * 11 + (lv - 1), fade = f })
local name = self:chip_label(diff)
if name then draw_title(name, cx - 42, py + 30, f) end
if info.is_branching then
draw_centered(self._id_branch, cx - 69, py - 23, 40, 40, { fade = f })
end
end
-- charted columns: easy / normal / hard / (oni|ura)
local cols = {}
for d = 0, 2 do if box:course_info(d).has_course then cols[#cols + 1] = d end end
local has_oni = box:course_info(DIFF_ONI).has_course
if has_oni or has_ura then cols[#cols + 1] = DIFF_ONI end
local n = #cols
for i, d in ipairs(cols) do
local cx = bx + (i - 1 - (n - 1) / 2) * 182
if d == DIFF_ONI and has_ura then
if has_oni then
plate(cx, DIFF_ONI, fade * oni_alpha) -- oni below, ura on top
plate(cx, DIFF_URA, fade * ura_weight)
else
plate(cx, DIFF_URA, fade)
end
else
plate(cx, d, fade)
end
end
return ura_weight
end
function SongSelect:draw_song_open(box)
local key = "s\0" .. box_id(box)
local t = wheel_open_t(key)
if t == nil then -- 508 ms pre-growth hold: fully closed
self:draw_song_board(box, 0.0, 0.0, 1.0)
return
end
self:draw_song_board(box, song_open_timeline(t))
end
-- Difficulty select
function SongSelect:draw_diff_mark_decided(p, dec, fade)
local f = math.floor((tex.get_current_ms() - dec.t0) / DS_MARK_DECIDE_MS) + 1
local k = DS_MARK_DECIDE[math.min(f, #DS_MARK_DECIDE)]
local id, id_w = self._id_mark[dec.course], self._id_mark_w[dec.course]
local cx, cy = DS_MARK_X[p], DS_MARK_Y
local function stretched(tid, a, sy)
if a <= 0 then return end
tex.draw_texture(tid, { x = cx, y = cy, y2 = 680 * (sy - 1), origin = { 340, 340 * sy }, fade = a * fade })
end
stretched(id_w, k[5], 1) -- mark_course_white_2: backing
stretched(id, k[1], k[2]) -- mark_course
stretched(id_w, k[3], k[4]) -- mark_course_white: the flash on top
end
function SongSelect:draw_song_diff_select_bg(box)
local fade = box.diff_fade_in.attribute
for p = 1, 2 do
local d = self._mark_diff[p]
local dec = self._mark_decided[p]
if dec and self._id_mark[dec.course] then
self:draw_diff_mark_decided(p, dec, fade)
elseif self._id_diff_mark and d and d >= DIFF_EASY and DS_MARK_X[p] then
draw_centered(self._id_diff_mark, DS_MARK_X[p], DS_MARK_Y, 680, 680, { frame = d, fade = fade })
end
end
tex.draw_texture(self._id_backboard, { frame = box:genre_frame(), x = 960 - 636, y = 440 - 392, fade = fade })
end
function SongSelect:draw_song_diff_select(box)
folder_leg.diff_seen_ms = tex.get_current_ms()
local fade = box.diff_fade_in.attribute
local is_ura = box.is_ura
local side = (tex.player_num() == 2) and 1 or -1
local now = tex.get_current_ms()
-- oni<->ura toggle detection
if fade < 1.0 then
self._ura_prev = is_ura; self._ura_anim = nil
elseif self._ura_prev ~= nil and is_ura ~= self._ura_prev then
self._ura_anim = { t0 = now, to_ura = is_ura }
end
self._ura_prev = is_ura
local ura_elapsed = self._ura_anim and (now - self._ura_anim.t0) or nil
if ura_elapsed and ura_elapsed >= URA_ANIM_MS then self._ura_anim = nil; ura_elapsed = nil end
local ura_mid = (not ura_elapsed) or ura_elapsed >= URA_SWAP_MS
-- what the oni slot shows: before the swap point the previous course, after it the new one
local oni_shown = is_ura
if ura_elapsed and not ura_mid then oni_shown = not is_ura end
-- buttons: return + a small option wrench (the engine cursor can still reach MODIFIER)
draw_centered(self._id_btn_back, DS_SLOT_X[DIFF_BACK], DS_BTN_Y, 128, 128, { fade = fade })
draw_centered(self._id_btn_option, DS_SLOT_X[DIFF_MODIFIER], DS_BTN_Y, 128, 128, { fade = fade })
-- diff boards
for i = 0, 3 do
local cx = DS_SLOT_X[i]
local diff = (i == DIFF_ONI and oni_shown) and DIFF_URA or i
local info = box:course_info(diff)
local f = info.has_course and fade or math.min(fade, 0.4)
draw_centered(self._id_diff_board, cx, DS_BOARD_Y, 200, 360, { frame = diff, fade = f })
if info.has_course and not (i == DIFF_ONI and ura_elapsed) then
local crown = info.crown or 0
draw_centered(self._id_small_crown, cx + side * 58, DS_BOARD_Y - 138, 40, 40, { frame = crown, fade = f })
if (info.rank or 0) > 0 then
draw_centered(self._id_small_rank, cx + side * 20, DS_BOARD_Y - 138, 40, 40, { frame = info.rank - 1, fade = f })
end
local lv = math.max(1, math.min(11, info.level))
tex.draw_texture(self._id_small_star, { frame = 0, x = cx - 40, y = DS_BOARD_Y + 66, fade = f })
tex.draw_texture(self._id_small_star, { frame = lv, x = cx - 4, y = DS_BOARD_Y + 68, fade = f })
draw_centered(self._id_dot_bar, cx, DS_BOARD_Y + 116, 172, 24, { fade = f })
for k = 0, math.min(10, info.level) - 1 do
draw_centered(self._id_dot_star, cx - 67 + k * 15, DS_BOARD_Y + 116, 24, 24, { fade = f })
end
if info.is_branching then
draw_centered(self._id_branch_diff, cx, DS_BOARD_Y + 144, 40, 40, { fade = f })
end
end
end
-- change animation cells over the oni slot
if ura_elapsed then
local id = self._ura_anim.to_ura and self._id_ura_to_ura or self._id_ura_to_oni
if id then
local cell = math.min(URA_CELLS - 1, math.floor(ura_elapsed / URA_ANIM_MS * URA_CELLS))
tex.draw_texture(id, { frame = cell, x = DS_SLOT_X[DIFF_ONI] - 170, y = DS_BOARD_Y - 210, fade = fade })
end
end
for i = 0, 3 do
local diff = (i == DIFF_ONI and oni_shown) and DIFF_URA or i
local info = box:course_info(diff)
if info.has_course then
local lbl = self:course_label(diff)
if lbl then draw_title(lbl, DS_SLOT_X[i], DS_BOARD_Y + DS_COURSE_NAME_DY, fade) end
end
end
-- title / subtitle inside the backboard header
draw_title(self:header_title(box), 960, 178, fade, DS_TEXT_BOX_W, 0)
draw_title(self:header_subtitle(box), 960, 242, fade, DS_TEXT_BOX_W, 0)
end
function SongSelect:song_subtitle(box)
local str = box.text_subtitle
if not str or str == "" then return nil end
local gf = self._inside_frame or box:genre_frame()
local key = gf .. "\0" .. str
if self._sub_key ~= key then
self._sub_key = key
self._sub_label = text.create_raw_text(str, 24, { 255, 255, 255, 255 }, board_outline(gf), false, outline(3.5), 2.0)
end
return self._sub_label
end
-- Song board title, arcade-exact: font from skin_config song_box_name, black outline,
-- cached per title (the cache is dropped wholesale past SONG_TITLE_CACHE_MAX).
function SongSelect:song_title(box)
local name = box.text_name
if not name or name == "" then return box:name() end
local gf = self._inside_frame or box:genre_frame()
local key = gf .. "\0" .. name -- the outline follows the genre, so it keys the cache
local t = self._song_titles[key]
if t == nil then
if SONG_TITLE_SIZE == nil then
local cfg = tex.get_skin_config("song_box_name")
SONG_TITLE_SIZE = (cfg and cfg.font_size and cfg.font_size > 0) and cfg.font_size or 42
end
if self._song_titles_n >= SONG_TITLE_CACHE_MAX then
self._song_titles = {}
self._song_titles_n = 0
end
t = text.create_raw_text(name, SONG_TITLE_SIZE, { 255, 255, 255, 255 }, board_outline(gf),
false, outline(SONG_TITLE_OUTLINE), 2.0)
self._song_titles[key] = t or false
self._song_titles_n = self._song_titles_n + 1
end
return t or box:name()
end
function SongSelect:header_title(box)
local name = box.text_name or ""
if self._header_title_key ~= name then
self._header_title_key = name
self._header_title = text.create_raw_text(name, 48, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, outline(7.0), 2.0)
end
return self._header_title
end
function SongSelect:header_subtitle(box)
local sub = box.text_subtitle
if sub == nil then return box:subtitle_large() end -- engine without the binding
if sub == "" then return nil end
if self._header_sub_key ~= sub then
self._header_sub_key = sub
self._header_sub = text.create_raw_text(sub, 30, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, outline(4.0), 2.0)
end
return self._header_sub
end
function SongSelect:course_label(diff)
local lbl = self._course_labels[diff]
if lbl == nil then
local key = "course_name_" .. diff
if tex.get_skin_config(key) then
lbl = text.create_text(key, { 255, 255, 255, 255 }, { 20, 20, 20, 255 }, false, outline(4.5), 1.0)
end
self._course_labels[diff] = lbl or false
end
return lbl or nil
end