From 01e2d72afe6d24110faa8ad5b277cb6c0da06337 Mon Sep 17 00:00:00 2001 From: TaikoZhong <108016253+GanYiZhong@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:58:32 +0800 Subject: [PATCH 1/2] dan select: carry the parent's pre-2026-10-15 dan select as this skin's base Upstream removed the exam grid, value centring, rank plate and total-notes options from PyTaikoGreen's dan_select.lua and moved its numbers into the script, so this skin fell back to the parent's plain exam rows with the parent's offsets (its own SKIN table never reached the parent's local one). dan_select_base.lua is the parent's script as of 4d325135 with those options fixed to this skin's values and the dropped keys given this skin's own numbers from before the migration; dan_select.lua loads it instead of the parent's. Co-Authored-By: Claude Opus 5.5 --- Scripts/dan_select/dan_select.lua | 13 +- Scripts/dan_select/dan_select_base.lua | 618 +++++++++++++++++++++++++ 2 files changed, 628 insertions(+), 3 deletions(-) create mode 100644 Scripts/dan_select/dan_select_base.lua diff --git a/Scripts/dan_select/dan_select.lua b/Scripts/dan_select/dan_select.lua index d5dcafd..5cd2309 100644 --- a/Scripts/dan_select/dan_select.lua +++ b/Scripts/dan_select/dan_select.lua @@ -1,9 +1,10 @@ local SKIN = { - dan_yellow_box_offset = { x = 0, y = 110 }, + dan_yellow_box_offset = { x = 0, y = 110 }, -- the song rows' step (also in dan_select_base) } -- DAN_SELECT paint surface: selection cursor breathing loop and arrow chips, -- sampled from the cabinet's own clips. The dan boxes themselves are drawn by the --- parent skin's dan_select.lua, which is loaded first and extended here. +-- dan_select_base.lua (this skin's copy of the parent's script from before upstream dropped +-- the exam grid and its options), which is loaded first and extended here. -- the parent skin's copy of this script: the second match on package.path (the first is this file) local function load_parent(name) @@ -16,7 +17,13 @@ local function load_parent(name) end end end -local parent = load_parent("dan_select/dan_select") +local function load_own(name) + for tmpl in package.path:gmatch("[^;]+") do + local chunk = loadfile((tmpl:gsub("%?", name))) + if chunk then return chunk end + end +end +local parent = load_own("dan_select/dan_select_base") or load_parent("dan_select/dan_select") if parent then parent() end if not DanSelect then DanSelect = {} diff --git a/Scripts/dan_select/dan_select_base.lua b/Scripts/dan_select/dan_select_base.lua new file mode 100644 index 0000000..c8b4c16 --- /dev/null +++ b/Scripts/dan_select/dan_select_base.lua @@ -0,0 +1,618 @@ +-- Nijiiro's dan select base: PyTaikoGreen's dan_select.lua as of 4d325135, before upstream +-- (2026-10-15) dropped the exam grid, value centring, rank plate and total-notes options and +-- moved their numbers into its own script. Nijiiro still uses all of them, so it carries this +-- copy (loaded by dan_select.lua in place of the parent's). The options are fixed to Nijiiro's +-- values; keys upstream dropped have Nijiiro's own values from before the migration. +-- Generated by make_nj_dan_select_base.py; edit that, not this. +local LEGACY = { + dan_chip_name = { ["x"] = 0, ["y"] = 89, ["height"] = 88, ["font_size"] = 42 }, + dan_exam_gauge_box = { ["x"] = 436, ["y"] = 610 }, + dan_exam_info = { ["x"] = 40, ["y"] = 140, ["width"] = 984 }, + dan_exam_label_pill = { ["x"] = 28, ["y"] = -1 }, + dan_exam_score_box = { ["x"] = 769, ["y"] = 610, ["height"] = 132 }, + dan_exam_seg = { ["x"] = 11, ["y"] = 47, ["width"] = 327 }, + dan_exam_value = { ["x"] = 185, ["y"] = 40 }, + dan_exam_value_center = { ["x"] = 633.0, ["width"] = -6.0 }, -- PyTaikoGreen's x 1.5 + dan_hori_name = { ["x"] = 257, ["y"] = 380, ["width"] = 240 }, + dan_level_counter_margin = { ["x"] = 34 }, + dan_select_exam_border_text = { ["x"] = 309, ["y"] = 55, ["font_size"] = 25, ["outline"] = 2.6 }, + dan_select_gauge_border_text = { ["x"] = 291, ["y"] = 67, ["font_size"] = 32, ["outline"] = 3.1 }, + dan_select_total_notes = { ["x"] = 0 }, + dan_subtitle = { ["x"] = 632, ["y"] = 262, ["height"] = 40, ["font_size"] = 22 }, + dan_title = { ["x"] = 632, ["y"] = 262, ["height"] = 60, ["font_size"] = 36, ["outline"] = 2.6 }, + dan_yellow_box_offset = { ["x"] = 0, ["y"] = 110 }, + exam_counter_margin = { ["x"] = 44 }, + exam_gauge_offset = { ["x"] = 0 }, + song_box_name = { ["x"] = 0, ["y"] = 0, ["height"] = 120, ["font_size"] = 42 }, + total_notes_counter_margin = { ["x"] = 44 }, + yellow_box_bottom = { ["y"] = 974 }, +} +local RENAMED = { dan_title = "dan_select_title", dan_subtitle = "dan_select_subtitle", dan_chip_name = "dan_select_chip_name", song_box_name = "song_select_box_name", dan_exam_info = "dan_select_exam_info" } +local OPTS = { dan_confirm_yes_left = false, dan_exam_grid = true, dan_exam_value_center = true, dan_select_rank_plate = true } +DanSelect = {} +DanSelect.__index = DanSelect + +local _tex = {} +local function T(subset, name) + local key = subset .. "/" .. name + local id = _tex[key] + if id == nil then + id = tex.get_texture("dan_select/" .. subset .. "/" .. name) + _tex[key] = id + end + return id +end + +local function cfg(name) + return (RENAMED[name] and tex.get_skin_config(RENAMED[name])) or tex.get_skin_config(name) or LEGACY[name] +end + +local function opt(name) + return OPTS[name] +end + +-- Texture draw-offset at index 0 (TextureObject::x[0] / y[0] / x2[0]). +local function tex_x0(subset, name) + local info = tex.get_texture_info(subset, name) + if info and info.x and info.x[1] then return info.x[1] end + return 0 +end +local function tex_y0(subset, name) + local info = tex.get_texture_info(subset, name) + if info and info.y and info.y[1] then return info.y[1] end + return 0 +end +local function tex_x2(subset, name) + local info = tex.get_texture_info(subset, name) + if info and info.x2 and info.x2[1] then return info.x2[1] end + return 0 +end + +-- exam.type -> icon texture name (exam_icon_id in box_dan.cpp). "renda" resolves to +-- exam_roll, falling back to exam_drumroll in older skins. Returns the resolved name. +local EXAM_ICON = { + gauge = "exam_gauge", combo = "exam_combo", + hit = "exam_hit", judgebad = "exam_judgebad", + judgegood = "exam_judgegood", judgeperfect = "exam_judgeperfect", + score = "exam_score", renda = "exam_roll", +} +local function exam_icon_name(type) + local name = EXAM_ICON[type] + if not name then return nil end + if T("yellow_box", name) then return name end + if T("yellow_box", "exam_drumroll") then return "exam_drumroll" end + return nil +end + +local yb_box = { path = nil, state = "closed" } + +function DanSelect.new() + local self = setmetatable({ now = 0 }, DanSelect) + yb_box.path, yb_box.state = nil, "closed" + return self +end + +local YB_DIFF_ANIMS = { 13, 14, 15, 16, 17, 18 } + +local function draw_dan_yellow_box(is_diff, fade) + local right_x = tex.get_animation(is_diff and 14 or 10).attribute + local left_x = tex.get_animation(is_diff and 13 or 9).attribute + local center_width = tex.get_animation(is_diff and 15 or 11).attribute + local top_y = tex.get_animation(16).attribute + local center_height = tex.get_animation(17).attribute + local bottom_y = (cfg("yellow_box_bottom") or { y = 0 }).y + local info = tex.get_texture_info("yellow_box", "yellow_box_bottom_right") + local eh = (info and info.height) or 0 + + tex.draw_texture(T("yellow_box", "yellow_box_bottom_right"), { x = right_x, y = bottom_y, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_bottom_left"), { x = left_x, y = bottom_y, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_top_right"), { x = right_x, y = top_y, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_top_left"), { x = left_x, y = top_y, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_bottom"), { x = left_x + eh, y = bottom_y, x2 = center_width, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_right"), { x = right_x, y = top_y + eh, y2 = center_height, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_left"), { x = left_x, y = top_y + eh, y2 = center_height, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_top"), { x = left_x + eh, y = top_y, x2 = center_width, fade = fade }) + tex.draw_texture(T("yellow_box", "yellow_box_center"), { x = left_x + eh, y = top_y + eh, x2 = center_width, y2 = center_height, fade = fade }) +end + +-- Track the open box and start the diff-select expansion on the closed -> diff_select +-- transition (the dan box never passes through the "open" draw_state). +local function track_dan_yellow_box(box) + local state = box:draw_state() + local path = box:path() + if state == "closed" then + if yb_box.path == path then + yb_box.path, yb_box.state = nil, "closed" + end + return + end + if yb_box.path ~= path then + yb_box.path, yb_box.state = path, state + for _, id in ipairs(YB_DIFF_ANIMS) do tex.get_animation(id):reset() end + for _, id in ipairs(YB_DIFF_ANIMS) do tex.get_animation(id):start() end + end +end + +local function update_dan_yellow_box(ms) + for _, id in ipairs(YB_DIFF_ANIMS) do tex.get_animation(id):update(ms) end +end + +function DanSelect:update(ms) + self.now = ms + update_dan_yellow_box(ms) +end + +local function draw_tex(subset, name, params) + local id = T(subset, name) + if id then tex.draw_texture(id, params) end +end + +function DanSelect:draw_back() + draw_tex("global", "bg", {}) + draw_tex("global", "bg_header", {}) + draw_tex("global", "bg_footer", {}) + draw_tex("global", "footer", {}) +end + +function DanSelect:draw_cursor(pos, now, since_move) + if since_move < 0 or since_move >= 967 then return true end + local p = since_move / 967 + local drift = 30 * p + local fade = 0.984 * (1 - p) + draw_tex("global", "arrow", { index = 0, x = -drift, fade = fade }) + draw_tex("global", "arrow", { index = 1, x = drift, fade = fade, mirror = "horizontal" }) + return true +end + +function DanSelect:draw_title() + draw_tex("global", "dan_select", {}) +end + +local CONFIRM_OPTION, CONFIRM_YES = 0, 1 + +-- fade: confirm_fade attribute; index: 0 option / 1 yes / 2 no +function DanSelect:draw_confirm(f, index) + if f <= 0 then return end + tex.draw_rect(0, 0, tex.get_screen_width(), tex.get_screen_height(), 0, 0, 0, math.min(0.5, f) * 255) + draw_tex("confirm_box", "bg", { fade = f }) + draw_tex("confirm_box", "confirmation_text", { fade = f }) + for i = 0, 1 do + draw_tex("confirm_box", "selection_box", { fade = f, index = i }) + end + if index ~= CONFIRM_OPTION then + -- selection_box slots are screen-left / screen-right; yes sits right of no unless swapped + local yes_info = tex.get_texture_info("confirm_box", "yes") + local no_info = tex.get_texture_info("confirm_box", "no") + local yes_right = yes_info and no_info and yes_info.x[1] > no_info.x[1] + local side = ((index == CONFIRM_YES) == (yes_right and true or false)) and 1 or 0 + draw_tex("confirm_box", "selection_box_highlight", { fade = f, index = side }) + draw_tex("confirm_box", "selection_box_outline", { fade = f, index = side }) + end + local swap_dx = 0 + if opt("dan_confirm_yes_left") then + local info = tex.get_texture_info("confirm_box", "selection_box") + if info and info.x and #info.x >= 2 then swap_dx = info.x[2] - info.x[1] end + end + draw_tex("confirm_box", "yes", { x = -swap_dx, fade = f }) + draw_tex("confirm_box", "no", { x = swap_dx, fade = f }) + if T("confirm_box", "option") then + if index == CONFIRM_OPTION then draw_tex("confirm_box", "option_highlight", { fade = f }) end + draw_tex("confirm_box", "option", { fade = f }) + end +end + +-- Draw a texture at an absolute position, ignoring its own x[0]/y[0] anchor (draw_abs). +local function draw_abs(subset, name, X, Y, fade, frame) + local id = T(subset, name) + if not id then return end + tex.draw_texture(id, { + frame = frame or 0, + x = X - tex_x0(subset, name), + y = Y - tex_y0(subset, name), + fade = fade, + }) +end + +local BACK_CX, BACK_BOTTOM, BACK_HALF0, BACK_HALF1 = 642, 539, 44.4, 191 +local BACK_TOP0, BACK_TOP1, BACK_GROW_MS = 338 - 228, 338 - 231, 150 + +function DanSelect:draw_back_box(box) + local bx = box:box_x() + local fade = box.fade.attribute + tex.draw_texture(T("back", "folder"), { x = bx, fade = fade }) + if box:draw_state() == "closed" then + self.back_open_at = nil + tex.draw_texture(T("back", "chip"), { x = bx, fade = fade }) + return + end + self.back_open_at = self.back_open_at or self.now + local ms = self.now - self.back_open_at + local u = math.min(math.max(ms / BACK_GROW_MS, 0), 1) + local half = BACK_HALF0 + (BACK_HALF1 - BACK_HALF0) * u + local top = BACK_TOP0 + (BACK_TOP1 - BACK_TOP0) * u + local l, r = BACK_CX - half, BACK_CX + half - 32 + local w, h = r - l - 32, BACK_BOTTOM - top - 32 + local function piece(name, x, y, x2, y2) + tex.draw_texture(T("back", name), { x = x, y = y, x2 = x2 and x2 - 32 or 0, y2 = y2 and y2 - 32 or 0 }) + end + piece("board_tl", l, top); piece("board_t", l + 32, top, w) + piece("board_tr", r, top); piece("board_r", r, top + 32, nil, h) + piece("board_br", r, BACK_BOTTOM); piece("board_b", l + 32, BACK_BOTTOM, w) + piece("board_bl", l, BACK_BOTTOM); piece("board_l", l, top + 32, nil, h) + piece("board_c", l + 32, top + 32, w, h) + local f = math.min(math.max((ms - 33) / 117, 0), 1) + for _, name in ipairs({ "graphic", "text", "pill" }) do tex.draw_texture(T("back", name), { fade = f }) end +end + +function DanSelect:draw_box(box) + if box.is_back then return self:draw_back_box(box) end + track_dan_yellow_box(box) + local state = box:draw_state() + if state == "closed" then + self:draw_dan_closed(box) + else + self:draw_dan_open(box) + end +end + +-- --------------------------------------------------------------------------- +-- draw_chip (closed): folder board + vertical chip name. +-- --------------------------------------------------------------------------- +function DanSelect:draw_dan_closed(box) + local bx = box:box_x() + local fade = box.fade.attribute + + tex.draw_texture(T("box", "folder"), { frame = box.dan_color, x = bx, fade = fade }) + + if box.clear_mark > 0 and T("box", "clear_mark") then + tex.draw_texture(T("box", "clear_mark"), { frame = box.clear_mark - 1, x = bx, fade = fade }) + end + + local name = box:chip_name() + if name then + local nb = cfg("dan_chip_name") or cfg("song_box_name") or { x = 0, y = 0, height = 0 } + name:draw({ + x = bx + nb.x - name.width / 2, + y = nb.y, + y2 = math.min(name.height, nb.height or name.height) - name.height, + fade = fade, + }) + end +end + +-- --------------------------------------------------------------------------- +-- draw_open: chip + song ribbon + total notes + rank + exam box. +-- --------------------------------------------------------------------------- +function DanSelect:draw_dan_open(box) + self:draw_dan_closed(box) + local f = box.open_fade.attribute + + -- The yellow box board the course details sit on (opaque; its growth is driven by + -- the diff-select animations, matching the song-select wheel). + draw_dan_yellow_box(true, 1.0) + + local off = cfg("dan_yellow_box_offset") or { x = 0, y = 0 } + local offset_x = off.x or 0 + local offset_y = off.y or 0 + + local n = box:song_count() + for i = 0, n - 1 do + local x = i * offset_x + local y = i * offset_y + + tex.draw_texture(T("yellow_box", "genre_banner"), { frame = box:song_genre(i), x = x, y = y, fade = f }) + + local song_label = T("yellow_box", "song_label") + if song_label then + tex.draw_texture(song_label, { frame = math.min(i, 2), x = x, y = y, fade = f }) + end + + tex.draw_texture(T("yellow_box", "difficulty"), { frame = box:song_difficulty(i), x = x, y = y, fade = f }) + if box:song_difficulty(i) == 4 and T("yellow_box", "difficulty_ura") then + tex.draw_texture(T("yellow_box", "difficulty_ura"), { x = x, y = y, fade = f }) + end + tex.draw_texture(T("yellow_box", "difficulty_x"), { x = x, y = y, fade = f }) + tex.draw_texture(T("yellow_box", "difficulty_star"), { x = x, y = y, fade = f }) + + -- Level counter + local lvl = tostring(box:song_level(i)) + local margin = (cfg("dan_level_counter_margin") or { x = 0 }).x + local total_w = #lvl * margin + for j = 1, #lvl do + local digit = string.byte(lvl, j) - 48 + tex.draw_texture(T("yellow_box", "difficulty_num"), { + frame = digit, x = x - total_w / 2 + (j - 1) * margin, y = y, fade = f }) + end + + -- Song title + subtitle + local title_text = box:song_title_text(i) + local sub_text = box:song_subtitle_text(i) + local td = cfg("dan_title") or { x = 0, y = 0, height = 0 } + local sd = cfg("dan_subtitle") or { x = 0, y = 0, height = 0 } + if box:song_masked(i) and T("yellow_box", "hidden_title") then + tex.draw_texture(T("yellow_box", "hidden_title"), { x = x, y = y, fade = f }) + elseif title_text then + title_text:draw({ + x = td.x + x, y = td.y + y, + y2 = math.min(title_text.height, td.height or title_text.height) - title_text.height, + fade = f }) + end + if sub_text then + sub_text:draw({ + x = sd.x + x, + y = sd.y + y - math.min(sub_text.height, sd.height or sub_text.height), + y2 = math.min(sub_text.height, sd.height or sub_text.height) - sub_text.height, + fade = f }) + end + end + + if box.clear_mark > 0 and T("yellow_box", "clear_mark") then + tex.draw_texture(T("yellow_box", "clear_mark"), { frame = box.clear_mark - 1, fade = f }) + end + + -- Total notes + tex.draw_texture(T("yellow_box", "total_notes_bg"), { fade = f }) + local tn_slot = cfg("dan_select_total_notes") + if not tn_slot or tn_slot.x >= 1.0 then + tex.draw_texture(T("yellow_box", "total_notes"), { fade = f }) + local tn = tostring(box.total_notes) + local tn_margin = (cfg("total_notes_counter_margin") or { x = 0 }).x + for i = 1, #tn do + local digit = string.byte(tn, i) - 48 + -- Lumen centres the counter on its 4-digit width + tex.draw_texture(T("yellow_box", "total_notes_counter"), { frame = digit, x = (i - 1 + (4 - #tn) / 2) * tn_margin, fade = f }) + end + end + + -- Rank plate / frame + if box.dan_rank >= 0 and opt("dan_select_rank_plate") then + tex.draw_texture(T("yellow_box", "rank_plate"), { frame = box.dan_rank, fade = f }) + else + tex.draw_texture(T("yellow_box", "frame"), { frame = box.dan_color, fade = f }) + local hori = box:hori_name() + if hori then + local hn = cfg("dan_hori_name") or { x = 0, y = 0, width = 0 } + hori:draw({ + x = hn.x - hori.width / 2, + y = hn.y, + x2 = math.min(hori.width, hn.width or hori.width) - hori.width, + fade = f }) + end + end + + self:draw_exam_box(box, f) +end + +-- --------------------------------------------------------------------------- +-- Exam board (the default "condition rows" layout). +-- --------------------------------------------------------------------------- +function DanSelect:draw_exam_box(box, f) + if opt("dan_exam_grid") then + self:draw_exam_grid(box, f) + return + end + + local offset_y = (cfg("dan_exam_info") or { y = 0 }).y + local margin = (cfg("exam_counter_margin") or { x = 0 }).x + + -- Four or more conditions do not fit the board: drop the soul-gauge row. + local exam_count = box:exam_count() + local shown = {} + for i = 0, exam_count - 1 do + if not (exam_count >= 4 and box:exam_type(i) == "gauge") then + shown[#shown + 1] = i + end + end + if #shown > 3 then + while #shown > 3 do shown[#shown] = nil end + end + + local L = ({ { dt = 38, row0 = 90, pitch = 0 }, { dt = 18, row0 = 37, pitch = 106 } })[#shown] + or { dt = 0, row0 = 0, pitch = offset_y } + local dt = L.dt + local function part(name, p) + p.fade = f + tex.draw_texture(T("yellow_box", name), p) + end + part("exam_box_bottom_right", { y = -dt }) + part("exam_box_bottom_left", { y = -dt }) + part("exam_box_top_right", { y = dt }) + part("exam_box_top_left", { y = dt }) + part("exam_box_bottom", { y = -dt }) + part("exam_box_right", { y = dt, y2 = -2 * dt }) + part("exam_box_left", { y = dt, y2 = -2 * dt }) + part("exam_box_top", { y = dt }) + part("exam_box_center", { y = dt, y2 = -2 * dt }) + part("exam_header", { y = dt }) + + for idx = 1, #shown do + local i = shown[idx] + local y = L.row0 + (idx - 1) * L.pitch + local etype = box:exam_type(i) + local range = box:exam_range(i) + + tex.draw_texture(T("yellow_box", "judge_box"), { y = y, fade = f }) + + local icon_name = exam_icon_name(etype) + if icon_name then tex.draw_texture(T("yellow_box", icon_name), { y = y, fade = f }) end + + if opt("dan_exam_value_center") then + -- Centre the whole value (digits, %, suffix) on dan_exam_value_center.x. + local vc = cfg("dan_exam_value_center") or { x = 0, width = 0 } + local digits = tostring(box:exam_red(i)) + local pct = (etype == "gauge") and T("yellow_box", "exam_percent") or nil + local suf = (range == "more") and T("yellow_box", "exam_more") + or (range == "less") and T("yellow_box", "exam_less") or nil + + local digits_w = (#digits - 1) * margin + tex_x2("yellow_box", "judge_num") + local pct_w = pct and tex_x2("yellow_box", "exam_percent") or 0 + local suf_w = suf and tex_x2("yellow_box", (range == "more") and "exam_more" or "exam_less") or 0 + local gap = vc.width or 0 + local parts = 1 + (pct and 1 or 0) + (suf and 1 or 0) + local total = digits_w + pct_w + suf_w + gap * (parts - 1) + local cx = (vc.x or 0) - total / 2 + + for j = 1, #digits do + local digit = string.byte(digits, j) - 48 + tex.draw_texture(T("yellow_box", "judge_num"), + { frame = digit, x = cx + (j - 1) * margin - tex_x0("yellow_box", "judge_num"), y = y, fade = f }) + end + cx = cx + digits_w + gap + if pct then + tex.draw_texture(pct, { x = cx - tex_x0("yellow_box", "exam_percent"), y = y, fade = f }) + cx = cx + pct_w + gap + end + if suf then + local suf_name = (range == "more") and "exam_more" or "exam_less" + tex.draw_texture(suf, { x = cx - tex_x0("yellow_box", suf_name), y = y, fade = f }) + end + else + local x_offset = 0 + if etype == "gauge" then + tex.draw_texture(T("yellow_box", "exam_percent"), { y = y, fade = f }) + x_offset = (cfg("exam_gauge_offset") or { x = 0 }).x + end + + local counter = tostring(box:exam_red(i)) + for j = 1, #counter do + local digit = string.byte(counter, j) - 48 + local x = x_offset - (#counter - j + 1) * margin + tex.draw_texture(T("yellow_box", "judge_num"), { frame = digit, x = x, y = y, fade = f }) + end + + if range == "more" then + tex.draw_texture(T("yellow_box", "exam_more"), { x = -x_offset * 1.7, y = y, fade = f }) + elseif range == "less" then + tex.draw_texture(T("yellow_box", "exam_less"), { x = -x_offset * 1.7, y = y, fade = f }) + end + end + end +end + +-- --------------------------------------------------------------------------- +-- Exam board grid mode (dan_exam_grid option): a two-column gauge/score layout. +-- --------------------------------------------------------------------------- +local function caption(self, str, font_size, outline) + self._captions = self._captions or {} + local key = str .. "\0" .. tostring(font_size) .. "\0" .. tostring(outline) + local cap = self._captions[key] + if cap == nil then + cap = text.create_raw_text(str, font_size, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, outline) + self._captions[key] = cap + end + return cap +end + +local function draw_value_text(self, cap_str, bt, right_x, top_y, f) + if not bt then return false end + local ol = (bt.outline and bt.outline >= 0) and bt.outline or (3.0 / tex.get_screen_scale()) + local size = (bt.font_size and bt.font_size > 0) and bt.font_size or 28 + local cap = caption(self, cap_str, size, ol) + if not cap then return false end + local pad = math.floor(ol * tex.get_screen_scale()) + 2 + cap:draw({ x = right_x - cap.width + pad, y = top_y - pad, fade = f }) + return true +end + +-- draw_value (grid mode): centred digits + optional percent/suffix, absolute anchors. +local function draw_value(self, red, etype, range, centre_x, top_y, f) + local digits = tostring(red) + local margin = (cfg("exam_counter_margin") or { x = 0 }).x + + local pct_w = 0 + if etype == "gauge" and T("yellow_box", "exam_percent") then + pct_w = tex_x2("yellow_box", "exam_percent") + end + local suffix = (range == "less") and "exam_less" or (range == "more") and "exam_more" or nil + local suf_w = 0 + if suffix and T("yellow_box", suffix) then + suf_w = tex_x2("yellow_box", suffix) + end + + local total = #digits * margin + pct_w + suf_w + local cx = centre_x - total * 0.5 + for j = 1, #digits do + local digit = string.byte(digits, j) - 48 + draw_abs("yellow_box", "judge_num", cx, top_y, f, digit) + cx = cx + margin + end + if pct_w > 0 then + draw_abs("yellow_box", "exam_percent", cx, top_y + 8, f) + cx = cx + pct_w + end + if suf_w > 0 then + draw_abs("yellow_box", suffix, cx, top_y + 6, f) + end +end + +function DanSelect:draw_exam_grid(box, f) + local exam_frame = T("yellow_box", "exam_frame") + if exam_frame then tex.draw_texture(exam_frame, { fade = f }) end + tex.draw_texture(T("yellow_box", "exam_header"), { fade = f }) + + local gb = cfg("dan_exam_gauge_box") + local sb = cfg("dan_exam_score_box") + local gx = (gb and gb.x) or 436.0 + local gy = (gb and gb.y) or 615.0 + local sx = (sb and sb.x) or 769.0 + local sy = (sb and sb.y) or 615.0 + local pitch = (sb and sb.height and sb.height > 0) and sb.height or 132.0 + + local sg = cfg("dan_exam_seg") + local seg_x = (sg and sg.x) or 14.0 + local seg_y = (sg and sg.y) or 48.0 + local seg_dx = (sg and sg.width and sg.width > 0) and sg.width or 327.0 + local vp = cfg("dan_exam_value") + local val_x = (vp and vp.x) or 185.0 + local val_y = (vp and vp.y) or 40.0 + local pp = cfg("dan_exam_label_pill") + local pill_x = (pp and pp.x) or 26.0 + local pill_y = (pp and pp.y) or 2.0 + + local seg_bt = cfg("dan_select_exam_border_text") + local gauge_bt = cfg("dan_select_gauge_border_text") + + local right_row = 0 + local gauge_used = false + local exam_count = box:exam_count() + for i = 0, exam_count - 1 do + local etype = box:exam_type(i) + local range = box:exam_range(i) + local left = (etype == "gauge" and not gauge_used) + if left then gauge_used = true end + -- The cabinet has three right-side slots; skip (don't stop) further right rows. + if left or right_row < 3 then + local BX = left and gx or sx + local BY = left and gy or (sy + pitch * right_row) + + draw_abs("yellow_box", left and "exam_gauge_box" or "judge_box", + BX - (left and 0 or 3), BY - (left and 0 or 2), f) + + local icon_name = exam_icon_name(etype) + if icon_name then draw_abs("yellow_box", icon_name, BX + pill_x, BY + pill_y, f) end + + if left then + if not draw_value_text(self, box:exam_caption(i), gauge_bt, + BX + ((gauge_bt and gauge_bt.x) or 0), + BY + ((gauge_bt and gauge_bt.y) or 0), f) then + draw_value(self, box:exam_red(i), etype, range, BX + 181.0, BY + 45.0, f) + end + else + local gothrough = box:exam_gothrough(i) + local segs = gothrough and 1 or math.min(box:song_count(), 3) + for k = 0, segs - 1 do + local frame = gothrough and 0 or (k + 1) + draw_abs("yellow_box", "exam_seg_label", BX + seg_x + seg_dx * k, BY + seg_y, f, frame) + + local red = gothrough and box:exam_red(i) or box:exam_song_red(i, k) + if not draw_value_text(self, box:exam_song_caption(i, k), seg_bt, + BX + ((seg_bt and seg_bt.x) or 0) + seg_dx * k, + BY + ((seg_bt and seg_bt.y) or 0), f) then + draw_value(self, red, etype, range, BX + val_x + seg_dx * k, BY + val_y, f) + end + end + end + if not left then right_row = right_row + 1 end + end + end +end -- 2.54.0 From f9b8ddb2bf16529f47c972ef905c8665e02efea9 Mon Sep 17 00:00:00 2001 From: TaikoZhong <108016253+GanYiZhong@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:02:28 +0800 Subject: [PATCH 2/2] dan result: give dan_result_song_name its outline default The table that replaced the skin_config entry has no outline, so DanResultDraw.new compared nil and the dan result screen never built. Co-Authored-By: Claude Opus 5.5 --- Scripts/result/dan_result_draw.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/result/dan_result_draw.lua b/Scripts/result/dan_result_draw.lua index c2ac2c3..65997f1 100644 --- a/Scripts/result/dan_result_draw.lua +++ b/Scripts/result/dan_result_draw.lua @@ -1,7 +1,7 @@ local SKIN = { score_info_counter_margin = { x = 28, width = 21 }, dan_result_info_height = { y = 276 }, - dan_result_song_name = { x = 632, y = 192.5, width = 1126, font_size = 40 }, + dan_result_song_name = { x = 632, y = 192.5, width = 1126, font_size = 40, outline = -1 }, dan_result_diff_num_margin = { x = 16 }, dan_result_hori_name = { x = 204, y = 317, width = 240 }, } -- 2.54.0