427 lines
19 KiB
Lua
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
|