-- SongBox rendering (song board closed/open/diff-select), split out of song_select.lua. -- Mirrors src/objects/song_select/file_navigator/box_song.cpp. local base = require("song_select/file_navigator/box_base") local T = base.T local utf8_char_count = base.utf8_char_count local draw_shadows = base.draw_shadows local draw_yellow_box = base.draw_yellow_box local draw_base_closed = base.draw_base_closed local vertical_name = base.vertical_name -- Difficulty enum (must match enums.h). local DIFF_EASY, DIFF_NORMAL, DIFF_HARD, DIFF_ONI, DIFF_URA = 0, 1, 2, 3, 4 local BLACK = { 0, 0, 0, 255 } local WHITE = { 255, 255, 255, 255 } local BLANK = { 0, 0, 0, 0 } local function is_2p_screen() return (tex.current_screen and tex.current_screen() or "") == "SONG_SELECT_2P" end -- Build a set of vertical OutlinedText labels sized to fit one shared box (mirrors -- build_vertical_label_set in box_song.cpp). local function build_vertical_labels(keys, box_cfg, v_advance, font_bonus, fills, outlines, thickness) local labels = {} local max_chars = 1 for i, key in ipairs(keys) do labels[i] = tex.skin_text(key) if labels[i] == "" then labels[i] = "?" end max_chars = math.max(max_chars, utf8_char_count(labels[i])) end local width_cap = box_cfg.width - 4.0 local height_cap = (box_cfg.height - 4.0) / (1.0 + (max_chars - 1) * v_advance) local font_size = math.floor(math.min(width_cap, height_cap) + font_bonus) local out = {} for i = 1, #keys do out[i] = text.create_raw_text(labels[i], font_size, fills[i], outlines[i], true, thickness, 2.0, v_advance) end return out end -- diff_tower_label (5 diffs) — static cache in C++, cached on self here. local function diff_tower_labels(self) if self._diff_tower_labels then return self._diff_tower_labels end local cfg = tex.get_skin_config("diff_tower_label_box") or { x = 475, y = 158, width = 53, height = 152 } local keys = { "diff_tower_easy", "diff_tower_normal", "diff_tower_hard", "diff_tower_oni", "diff_tower_ura" } local fills = { BLACK, BLACK, BLACK, BLACK, WHITE } local outlines = { BLANK, BLANK, BLANK, BLANK, { 0, 73, 90, 255 } } self._diff_tower_labels = build_vertical_labels(keys, cfg, 0.9, 5.0, fills, outlines, 3.0) return self._diff_tower_labels end -- difficulty_bar_label (4 diffs). local function difficulty_bar_labels(self) if self._difficulty_bar_labels then return self._difficulty_bar_labels end local cfg = tex.get_skin_config("difficulty_bar_label_box") or { x = 481, y = 256, width = 26, height = 96 } local keys = { "diff_tower_easy", "diff_tower_normal", "diff_tower_hard", "diff_tower_oni" } local colors = { BLACK, BLACK, BLACK, BLACK } self._difficulty_bar_labels = build_vertical_labels(keys, cfg, 0.9, 7.0, colors, colors, 0.0) return self._difficulty_bar_labels end -- The three song text objects (name, name_black, subtitle) from BaseBox::load_text + -- SongBox::load_text, cached per box on self. local function song_texts(self, box) self._song_texts = self._song_texts or {} local name = box.text_name or "" local fc = box:fore_color() or { r = 0, g = 0, b = 0, a = 255 } local key = box:path() .. "\0" .. tostring(fc.r or 0) .. "." .. tostring(fc.g or 0) .. "." .. tostring(fc.b or 0) local t = self._song_texts[key] if t == nil then local sub = box.text_subtitle or "" local fore = { fc.r or 0, fc.g or 0, fc.b or 0, fc.a or 255 } local name_cfg = tex.get_skin_config("song_box_name") or { font_size = 40 } local base_name_font = name_cfg.font_size or 40 local name_font = base_name_font local name_outline = 5.0 if utf8_char_count(name) >= 30 then name_font = base_name_font - 10 * tex.get_screen_scale() name_outline = 5.0 * (name_font / base_name_font) end local name_text = text.create_raw_text(name, name_font, WHITE, fore, true, 5.0, 2.0, 1.0) local name_black = text.create_raw_text(name, name_font, WHITE, BLACK, true, name_outline, 2.0, 1.0) local sub_cfg = tex.get_skin_config("yb_subtitle") or { font_size = 30 } local base_sub_font = sub_cfg.font_size or 30 local sub_font = base_sub_font local sub_outline = 5.0 if utf8_char_count(sub) >= 30 then sub_font = base_sub_font - 10 * tex.get_screen_scale() sub_outline = 5.0 * (sub_font / base_sub_font) end local subtitle = text.create_raw_text(sub, sub_font, WHITE, BLACK, true, sub_outline, 2.0, 1.0) t = { name = name_text, name_black = name_black, subtitle = subtitle } self._song_texts[key] = t end return t end -- draw_diff_outline (small crown outline; doubled in 2P). local function draw_diff_outline(x, y, fade) if is_2p_screen() then local half = tex.get_texture_info("yellow_box", "s_crown_dfc") half = (half and half.width or 0) * 0.35 tex.draw_texture(T("yellow_box", "s_crown_outline"), { x = x - half, y = y, fade = fade }) tex.draw_texture(T("yellow_box", "s_crown_outline"), { x = x + half, y = y, fade = fade }) else tex.draw_texture(T("yellow_box", "s_crown_outline"), { x = x, y = y, fade = fade }) end end -- draw_diff_crown (small crown per diff; uses p1/p2 scores in 2P). local function draw_diff_crown(box, diff, x, y, fade) local function draw_one(info, px) if not info.has_score then return end if info.crown == 3 then tex.draw_texture(T("yellow_box", "s_crown_dfc"), { x = px, y = y, fade = fade }) elseif info.crown == 2 then tex.draw_texture(T("yellow_box", "s_crown_fc"), { x = px, y = y, fade = fade }) elseif info.crown >= 1 then tex.draw_texture(T("yellow_box", "s_crown_clear"), { x = px, y = y, fade = fade }) end end if is_2p_screen() then local half = tex.get_texture_info("yellow_box", "s_crown_dfc") half = (half and half.width or 0) * 0.35 draw_one(box:course_info(diff), x - half) draw_one(box:course_info_p2(diff), x + half) else draw_one(box:course_info(diff), x) end end -- draw_box_crown (the closed-state best crown; highest-difficulty score wins). local function draw_box_crown(box, x, y, fade) local function highest(info_fn) local best_key = -1 local best_crown = 0 for d = 0, 4 do local info = info_fn(d) if info.has_course and info.has_score then if d > best_key then best_key = d; best_crown = info.crown end end end if best_key < 0 then return nil, nil end return best_crown, math.min(DIFF_URA, best_key) end local crown_1p, frame_1p = highest(function(d) return box:course_info(d) end) local crown_2p, frame_2p if is_2p_screen() then crown_2p, frame_2p = highest(function(d) return box:course_info_p2(d) end) end if crown_2p then local info = tex.get_texture_info("yellow_box", "crown_dfc") local half = (info and info.width or 0) * 0.35 if crown_1p then base.draw_crown(crown_1p, frame_1p, x - half, y, fade) end base.draw_crown(crown_2p, frame_2p, x + half, y, fade) elseif crown_1p then base.draw_crown(crown_1p, frame_1p, x, y, fade) end end -- draw_text (the open-state name/subtitle, tracking the yellow box's expansion). local function draw_text(box, texts) local right_out = tex.get_animation(10) local right_out_2 = tex.get_animation(14) local top_y_out = tex.get_animation(16) local x = box:box_x() + (right_out.attribute * 0.90 - right_out.start_position * 0.90) + right_out_2.attribute - right_out_2.start_position local sub_cfg = tex.get_skin_config("yb_subtitle") or { x = -15, y = 545, height = 410 } local h = math.min(texts.subtitle.height, sub_cfg.height) texts.subtitle:draw({ x = x + sub_cfg.x, y = sub_cfg.y - h + top_y_out.attribute - top_y_out.start_position, y2 = h - texts.subtitle.height, fade = box.open_fade.attribute }) local name_cfg = tex.get_skin_config("song_box_name") or { x = 47, y = 129, height = 417 } local name_h = math.min(texts.name.height, name_cfg.height) - texts.name.height local name_x = x + name_cfg.x - math.floor(texts.name.width / 2) local name_y = name_cfg.y + top_y_out.attribute - top_y_out.start_position texts.name_black:draw({ x = name_x, y = name_y, y2 = name_h, fade = box.open_fade.attribute }) texts.name:draw({ x = name_x, y = name_y, y2 = name_h, fade = 1 - box.open_fade.attribute }) end function SongSelect:draw_song_closed(box) draw_base_closed(box) local bx = box:box_x() local by = box:box_y() local fade = box.fade.attribute local name = vertical_name(self, box) local name_cfg = tex.get_skin_config("song_box_name") or { x = 47, y = 129, height = 417 } local name_x = bx + name_cfg.x - math.floor(name.width / 2) local name_y = name_cfg.y + by local name_h = math.min(name.height, name_cfg.height) name:draw({ x = name_x, y = name_y, y2 = name_h - name.height, fade = fade }) if box:has_preimage() then tex.draw_texture(T("yellow_box", "preimage_bg"), { x = bx, y = by, fade = fade }) box:draw_preimage(bx, by, fade) elseif box:ex_data_flag() == 3 then -- limited_time tex.draw_texture(T("yellow_box", "ex_data_limited_time_balloon"), { x = bx, y = by, fade = fade }) elseif box.is_new then tex.draw_texture(T("yellow_box", "ex_data_new_song_balloon"), { x = bx, y = by, fade = fade }) end draw_box_crown(box, bx, by, fade) end function SongSelect:draw_song_open(box) local bx = box:box_x() local by = box:box_y() local open_fade = box.open_fade.attribute draw_shadows(bx, by, open_fade, 1) draw_yellow_box(false, 1.0, 0.0) local offset = (tex.get_skin_config("yb_diff_offset") or { x = 60 }).x local texts = song_texts(self, box) -- diff outlines + crowns (diff < URA) for d = 0, 3 do local info = box:course_info(d) if info.has_course then draw_diff_outline(d * offset, 0.0, math.min(open_fade, 0.25)) draw_diff_crown(box, d, d * offset, 0.0, open_fade) end end -- ex_data / new icon local exd = box:ex_data_flag() if exd == 1 then tex.draw_texture(T("yellow_box", "ex_data_new_audio"), { fade = open_fade }) elseif exd == 2 then tex.draw_texture(T("yellow_box", "ex_data_old_audio"), { fade = open_fade }) elseif exd == 3 then tex.draw_texture(T("yellow_box", "ex_data_limited_time"), { fade = open_fade }) elseif box.is_new then tex.draw_texture(T("yellow_box", "ex_data_new_song"), { fade = open_fade }) end if tex.display_bpm() then local bpm = box:bpm_text() local cfg = tex.get_skin_config("song_box_bpm") or { x = 628, y = 120 } if bpm then bpm:draw({ x = cfg.x, y = cfg.y, fade = open_fade }) end end if box.is_favorite then tex.draw_texture(T("yellow_box", "favorite_" .. tex.player_num() .. "p"), { fade = open_fade }) end -- difficulty bars (4) for i = 0, 3 do tex.draw_texture(T("yellow_box", "difficulty_bar"), { frame = i, x = i * offset, fade = open_fade }) if not box:course_info(i).has_course then tex.draw_texture(T("yellow_box", "difficulty_bar_shadow"), { frame = i, x = i * offset, fade = math.min(open_fade, 0.25) }) end end -- difficulty bar labels local labels = difficulty_bar_labels(self) local box_cfg = tex.get_skin_config("difficulty_bar_label_box") or { x = 481, y = 256, width = 26, height = 96 } for i = 0, 3 do local label = labels[i + 1] label:draw({ x = box_cfg.x + i * offset + box_cfg.width / 2 - label.width / 2, y = box_cfg.y, y2 = math.min(label.height, box_cfg.height) - label.height, fade = open_fade }) end -- stars + branch indicator (diff < URA) local offset_y = (tex.get_skin_config("yb_diff_offset") or { y = -17 }).y for d = 0, 3 do local info = box:course_info(d) if info.has_course then for j = 0, info.level - 1 do tex.draw_texture(T("yellow_box", "star"), { x = d * offset, y = j * offset_y, fade = open_fade }) end if info.is_branching and (math.floor(tex.get_current_ms() / 1000) % 2 == 0) then tex.draw_texture(T("yellow_box", "branch_indicator"), { x = d * offset, fade = open_fade }) end end end draw_text(box, texts) end function SongSelect:draw_song_diff_select(box) draw_yellow_box(true, 1.0, 0.0) local diff_fade = box.diff_fade_in.attribute tex.draw_texture(T("diff_select", "back"), { fade = diff_fade }) tex.draw_texture(T("diff_select", "option"), { fade = diff_fade }) tex.draw_texture(T("diff_select", "neiro"), { fade = diff_fade }) local offset_x = (tex.get_skin_config("yb_diff_offset_diff_select") or { x = 115 }).x local offset_y = (tex.get_skin_config("yb_diff_offset_diff_select") or { y = -120 }).y local crown_offset = (tex.get_skin_config("yb_diff_offset_crown") or { x = 8 }).x -- diff crowns (diff < URA) for d = 0, 3 do local info = box:course_info(d) if info.has_course then local cx = d * offset_x + crown_offset draw_diff_outline(cx, offset_y, math.min(diff_fade, 0.25)) draw_diff_crown(box, d, cx, offset_y, diff_fade) end end -- diff towers + labels local tower_labels = diff_tower_labels(self) local box_cfg = tex.get_skin_config("diff_tower_label_box") or { x = 475, y = 158, width = 53, height = 152 } for i = 0, 3 do local label_index = i if i == DIFF_ONI and box.is_ura then tex.draw_texture(T("diff_select", "diff_tower"), { frame = 4, x = i * offset_x, fade = diff_fade }) tex.draw_texture(T("diff_select", "ura_oni_plate"), { fade = diff_fade }) label_index = 4 else tex.draw_texture(T("diff_select", "diff_tower"), { frame = i, x = i * offset_x, fade = diff_fade }) end if not box:course_info(i).has_course then tex.draw_texture(T("diff_select", "diff_tower_shadow"), { frame = i, x = i * offset_x, fade = math.min(diff_fade, 0.25) }) end local label = tower_labels[label_index + 1] label:draw({ x = box_cfg.x + i * offset_x + (box_cfg.width - label.width) / 2, y = box_cfg.y, fade = diff_fade }) end -- ura stars + branch indicator local star_offset_y = (tex.get_skin_config("yb_diff_offset_crown") or { y = -20 }).y for d = 0, 4 do local info = box:course_info(d) if info.has_course then if (d == DIFF_URA and not box.is_ura) or (d == DIFF_ONI and box.is_ura) then -- skip else for j = 0, info.level - 1 do tex.draw_texture(T("yellow_box", "star_ura"), { x = math.min(d, DIFF_ONI) * offset_x, y = j * star_offset_y, fade = diff_fade }) end if info.is_branching and (math.floor(tex.get_current_ms() / 1000) % 2 == 0) then local bt = (d == DIFF_URA) and T("yellow_box", "branch_indicator_ura") or T("yellow_box", "branch_indicator_diff") tex.draw_texture(bt, { x = math.min(d, DIFF_ONI) * offset_x, fade = diff_fade }) end end end end draw_text(box, song_texts(self, box)) end