dan select: course panel closer to the cabinet #64

Merged
GanYiZhong merged 1 commits from GanYiZhong/YataiDONNijiiro:fix/dan-select-exam-panel into main 2026-10-14 15:37:51 +02:00
11 changed files with 165 additions and 34 deletions

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -83,12 +83,12 @@
"y": 186
},
"exam_header": {
"x": 420,
"y": 568
"x": 421,
"y": 563
},
"exam_frame": {
"x": 418,
"y": 601
"y": 596
},
"exam_box_top_left": {
"x": 420,
+14 -13
View File
@@ -628,7 +628,8 @@
"x": 632,
"y": 262,
"height": 60,
"font_size": 36
"font_size": 36,
"outline": 2.6
},
"dan_subtitle": {
"x": 632,
@@ -837,7 +838,7 @@
"dan_back_label": {
"x": 0,
"y": 89,
"width": 60,
"width": 52,
"height": 88,
"font_size": 42,
"text": {
@@ -952,11 +953,11 @@
},
"dan_exam_gauge_box": {
"x": 436,
"y": 615
"y": 610
},
"dan_exam_score_box": {
"x": 769,
"y": 615,
"y": 610,
"height": 132
},
"dan_exam_seg": {
@@ -965,16 +966,16 @@
"width": 327
},
"dan_select_exam_border_text": {
"x": 319,
"y": 54,
"font_size": 28,
"outline": 2.0
"x": 309,
"y": 55,
"font_size": 25,
"outline": 2.6
},
"dan_select_gauge_border_text": {
"x": 300,
"y": 55,
"font_size": 36,
"outline": 2.6666666666666665
"x": 291,
"y": 67,
"font_size": 32,
"outline": 3.1
},
"dan_exam_value": {
"x": 185,
@@ -982,7 +983,7 @@
},
"dan_exam_label_pill": {
"x": 28,
"y": 2
"y": -1
},
"dan_exam_value_left": {
"x": 528
+132 -2
View File
@@ -84,6 +84,123 @@ local function draw_level(level, x, y, f)
tex.draw_texture(id, { frame = math.min(level, 11) - 1, x = x, y = y, fade = f })
end
-- The course rows are set as the cabinet's: the song title alone (no subtitle line), and the
-- exam values with narrow digits beside full-width 以上/未満 and a little room between them.
-- The skin has one font, so each text is cut into runs of ASCII and of everything else, each
-- run made on its own and scaled as it is drawn, the runs butted together. The parent is handed
-- the box through a view whose song_subtitle_text gives nothing and whose song_title_text gives
-- such a title, and the captions it makes for the exam values are made the same way. Only CJK
-- languages are rescaled; the skin's font for the others is narrow already.
local function cjk_language()
local l = tex.language() or ""
return l:find("^ja") or l:find("^zh") or l:find("^ko") or l == "tw" or l == "cn"
end
local TITLE_CJK = { latin = 1, cjk = 1.02, sy = 1, dy = 0, max_w = 1100 }
local VALUE_CJK = { latin = 0.85, cjk = 1.0, sy = 1.0, dy = 0, gap = 8 }
local TITLE_PLAIN = { latin = 1, cjk = 1, sy = 1, dy = 0, max_w = 1100 }
local VALUE_PLAIN = { latin = 1, cjk = 1, sy = 1, dy = 0 }
local function runs_of(str)
local runs, cur, ascii = {}, {}, nil
for ch in str:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
local a = ch:byte() < 128
if ascii ~= nil and a ~= ascii then
runs[#runs + 1] = { text = table.concat(cur), ascii = ascii }
cur = {}
end
cur[#cur + 1] = ch
ascii = a
end
if #cur > 0 then runs[#runs + 1] = { text = table.concat(cur), ascii = ascii } end
return runs
end
-- create: text.create_raw_text as it was before any wrapping. The parts' sizes are read when
-- they are used, since a text's texture (and size) may still be building when it is made.
local function mixed_text(create, str, size, color, outline_color, outline, spacing, style)
local pad = math.floor((outline or 0) * tex.get_screen_scale()) + 2
local parts = {}
for _, r in ipairs(runs_of(str)) do
parts[#parts + 1] = { t = create(r.text, size, color, outline_color, false, outline, spacing),
sx = r.ascii and style.latin or style.cjk }
end
local function layout()
local function span(k)
local x = 0
for n, q in ipairs(parts) do
q.x = x
local next_sx = parts[n + 1] and parts[n + 1].sx * k or 0
x = x + (q.t.width - pad) * q.sx * k - pad * next_sx
if parts[n + 1] then x = x + (style.gap or 0) * k end
end
local last = parts[#parts]
return last and (last.x + last.t.width * last.sx * k) or 0
end
local k, w = 1, span(1)
if style.max_w and w > style.max_w then
k = style.max_w / w
w = span(k)
end
local h = 0
for _, q in ipairs(parts) do h = math.max(h, q.t.height) end
return k, w, h * style.sy
end
return setmetatable({
text = str,
draw = function(_, p)
local k = layout()
for _, q in ipairs(parts) do
local sx = q.sx * k
q.t:draw({ x = p.x + q.x, y = (p.y or 0) + style.dy, fade = p.fade,
x2 = q.t.width * (sx - 1), y2 = q.t.height * (style.sy - 1) })
end
end,
}, {
__index = function(_, key)
if key == "width" then local _, w = layout(); return w end
if key == "height" then local _, _, h = layout(); return h end
end,
})
end
local title_cache = {} -- by string, size and outline: texts are built once, not per frame
local raw_create = text.create_raw_text -- the engine's, before draw_dan_open wraps it
local function course_view(box)
local titles = {}
return setmetatable({}, {
__index = function(_, k)
if k == "song_subtitle_text" then return function() return nil end end
if k == "song_title_text" then
return function(_, i)
if titles[i] == nil then
local t = box:song_title_text(i)
local ok, str = pcall(function() return t and t.text end)
local td = tex.get_skin_config("dan_title")
if t and ok and type(str) == "string" and str ~= "" and td then
local ol = td.outline >= 0 and td.outline or 5
local key = str .. "\0" .. td.font_size .. "\0" .. ol
title_cache[key] = title_cache[key]
or mixed_text(raw_create, str, td.font_size,
{ 255, 255, 255, 255 }, { 0, 0, 0, 255 }, ol, 2,
cjk_language() and TITLE_CJK or TITLE_PLAIN)
titles[i] = title_cache[key]
else
titles[i] = t or false -- an engine without OutlinedText.text
end
end
return titles[i] or nil
end
end
local v = box[k]
if type(v) == "function" then
return function(_, ...) return v(box, ...) end
end
return v
end,
})
end
-- The course's clear stamp (yellow_box/clear_mark) sits over the dan plate and Don, as the
-- cabinet's stamp_detail does (it is above don_1p). The parent draws it with the box, under
-- both, so it is held back there and drawn with the title, which the engine draws after
@@ -109,7 +226,16 @@ if parent_open then
end
return draw(id, p)
end
local ok, err = pcall(parent_open, self, box)
local create = text.create_raw_text
text.create_raw_text = function(str, size, color, outline_color, vertical, outline, spacing, ...)
if vertical or type(str) ~= "string" then -- only the exam value captions
return raw_create(str, size, color, outline_color, vertical, outline, spacing, ...)
end
return mixed_text(raw_create, str, size, color, outline_color, outline, spacing,
cjk_language() and VALUE_CJK or VALUE_PLAIN)
end
local ok, err = pcall(parent_open, self, course_view(box))
text.create_raw_text = create
tex.draw_texture = draw
if not ok then error(err, 0) end
local f = box.open_fade.attribute
@@ -139,16 +265,20 @@ end
-- across, squeezed to the chip, for other languages.
local CJK = { ja = true, zh = true, ko = true, tw = true, cn = true }
local back_label
local BACK_ACROSS_SIZE = 0.7 -- size of a label set across, against dan_back_label.font_size
local function get_back_label()
if back_label == nil then
local cfg = tex.get_skin_config("dan_back_label")
if not cfg then back_label = false; return false end
local lang = tex.language()
local vertical = CJK[lang] or CJK[lang:sub(1, 2)] or false
-- a word set across has only the chip's width, so it takes a smaller size than the
-- vertical labels rather than being squeezed hard
local size = vertical and cfg.font_size or math.floor(cfg.font_size * BACK_ACROSS_SIZE + 0.5)
back_label = {
cfg = cfg,
vertical = vertical,
main = text.create_raw_text(tex.skin_text("dan_back_label"), cfg.font_size,
main = raw_create(tex.skin_text("dan_back_label"), size,
{ 0, 0, 0, 255 }, { 0, 0, 0, 255 }, vertical, 0, 2),
}
end