Files
2026-10-05 10:05:24 +08:00

744 lines
35 KiB
Lua

local STATE_BROWSING = 0
local STATE_SONG_SELECTED = 1
local DIFF_BACK = -3
local DIFF_MODIFIER = -2 -- engine Difficulty::MODIFIER (演奏オプション)
local DIFF_NEIRO = -1 -- engine Difficulty::NEIRO (音色; skin option option_neiro_row = false)
local URA_FLIP_MS = 350 -- oni <-> ura switch: column flash + icon pop length
local MINI_BAR_SHIFT = 16 -- Back / Options / Hit sound bars: px left of the evenly spaced slots
local T14_CHROME = true
local T14_BOX_GAP_TOP, T14_BOX_GAP_BOTTOM = 16, 10 -- closed boxes: gap under the strip, a bit shorter
-- Folder open, as PyTaikoGreen: Navigator::begin_inline_load moves every box right of the opened
-- folder to screen_width + 150 over 600 ms (cubic ease-in) -- but only for skins without draw_box, so it is
-- redone here from the OPEN_BEGIN wheel event. The rest (list swap, set_positions moves, box fades,
-- close) is the engine's own animation, which this script already follows through box_x / fade.
local WEV_OPEN_BEGIN, WEV_OPEN_SWAP = 3, 4
local FOLDER_OUT_MS, FOLDER_OUT_X = 600, 960 + 150
SongSelect = {}
SongSelect.__index = SongSelect
-- columns.gd / song_select.tscn
local COL_Y = 112 -- MarginContainer anchor_top .089*720 - .08 = 64, + margin_top 48
local BOX_W = 96 -- box_width
local BOX_H = 472 -- bsize.y
local PAD = 24 -- padding
local STYLE_GROW = 23 -- draw_style_box(box, Rect2(0, bsize + 23*ONE))
local OPEN_SONG = 480 -- box_open_size (song)
local OPEN_FOLDER = 256 -- box_open_size (box.def folder)
local OPEN_DIFF = 768 -- box_diff_select_size
local CX = 480 -- viewport width / 2
-- Box colours. juu-yon tints bar / index tab / background with the box.def #BACKCOLOR
-- (TJAMeta.box_back_color, default WHITE). The engine does not hand back_color to Lua, so the
-- genre's colour comes from the engine's own default table (color_utils.cpp DEFAULT_COLORS, the
-- same values the library's box.def files carry); genres without one stay WHITE like juu-yon.
local GENRE_BACK = {
[1] = { 32, 160, 186 }, -- JPOP
[2] = { 255, 152, 0 }, -- ANIME
[4] = { 255, 82, 134 }, -- CHILDREN
[5] = { 142, 212, 30 }, -- VARIETY
[6] = { 209, 162, 19 }, -- CLASSICAL
[7] = { 156, 117, 189 }, -- GAME
[8] = { 255, 90, 19 }, -- NAMCO
[13] = { 35, 102, 170 }, -- DAN
[14] = { 255, 85, 95 }, -- DIFFICULTY
}
local WHITE = { 255, 255, 255 }
local CHOCOLATE = { 210, 105, 30 } -- columns.gd: back.box_back_color = Color.CHOCOLATE
local BLACK = { 0, 0, 0 }
local DIFF_COL_COLOR = { 255, 162, 25 } -- box_difficulty.tres bg_color (1, .635, .098)
local STAR_DOT = { 231, 118, 39 } -- "#E77627"
local CRIMSON = { 220, 20, 60 } -- Color.CRIMSON (10th+ star)
local MAX_STARS = { [0] = 5, [1] = 7, [2] = 8, [3] = 10, [4] = 10 }
local COURSE_IDS = { [0] = "course_easy", [1] = "course_normal", [2] = "course_hard",
[3] = "course_oni", [4] = "course_edit" }
-- TaikoText outline sizes are Godot outline widths (radius = size / 2); OutlinedText multiplies
-- the thickness it is given by screen_scale, so it is handed radius / screen_scale.
local SS = tex.get_screen_scale()
-- juu-yon's outline widths look too heavy at this skin's 960 px (user, 2026-09-27): every text
-- outline is drawn at OUTLINE_K of the juu-yon value.
local OUTLINE_K = 0.6
local function rad(outline_size) return (outline_size / 2) / SS end
local function box_back_color(box)
local kind = box:lua_kind()
if kind == "back" then return CHOCOLATE end
return GENRE_BACK[box:genre_index()] or WHITE
end
--------------------------------------------------------------------------------
-- drawing helpers
--------------------------------------------------------------------------------
local ids = {}
local function id(name) return ids[name] end
local rgba_cache = setmetatable({}, { __mode = "k" })
local function rgba(c)
local t = rgba_cache[c]
if t == nil then
t = { c[1], c[2], c[3], 255 }
rgba_cache[c] = t
elseif t[1] ~= c[1] or t[2] ~= c[2] or t[3] ~= c[3] then
t[1], t[2], t[3] = c[1], c[2], c[3]
end
return t
end
-- StyleBoxTexture (axis stretch) drawn into (x, y, w, h): 9 pieces with src rects.
local NINE_SRC = {}
local NINE_P = { src = NINE_SRC }
local function nine(tid, tw, th, l, t, r, b, x, y, w, h, color, fade)
if not tid or w <= 0 or h <= 0 then return end
local cw, ch = tw - l - r, th - t - b
local dw, dh = math.max(0, w - l - r), math.max(0, h - t - b)
local p, src = NINE_P, NINE_SRC
p.color, p.fade = rgba(color or WHITE), fade
for ci = 1, 3 do
-- column: source x / width, destination x / width
local sx, sw, dx, dwc
if ci == 1 then sx, sw, dx, dwc = 0, l, x, l
elseif ci == 2 then sx, sw, dx, dwc = l, cw, x + l, dw
else sx, sw, dx, dwc = tw - r, r, x + l + dw, r end
if dwc > 0 then
for ri = 1, 3 do
local sy, sh, dy, dhr
if ri == 1 then sy, sh, dy, dhr = 0, t, y, t
elseif ri == 2 then sy, sh, dy, dhr = t, ch, y + t, dh
else sy, sh, dy, dhr = th - b, b, y + t + dh, b end
if dhr > 0 then
src.x, src.y, src.width, src.height = sx, sy, sw, sh
p.x, p.y, p.x2, p.y2 = dx, dy, dwc - tw, dhr - th
tex.draw_texture(tid, p)
end
end
end
end
end
local function rect(x, y, w, h, color, fade)
if w <= 0 or h <= 0 then return end
tex.draw_texture(id("white"), { x = x, y = y, x2 = w - 1, y2 = h - 1, color = rgba(color), fade = fade })
end
-- StyleBoxFlat with corner radius r (all corners, or the top two only)
local function rrect(r, x, y, w, h, color, fade, top_only)
local d = id("disc" .. r)
local function q(sx, sy, dx, dy)
tex.draw_texture(d, { src = { x = sx, y = sy, width = r, height = r }, x = dx, y = dy,
x2 = -r, y2 = -r, color = rgba(color), fade = fade })
end
q(0, 0, x, y); q(r, 0, x + w - r, y)
rect(x + r, y, w - 2 * r, r, color, fade)
if top_only then
rect(x, y + r, w, h - r, color, fade)
else
rect(x, y + r, w, h - 2 * r, color, fade)
q(0, r, x, y + h - r); q(r, r, x + w - r, y + h - r)
rect(x + r, y + h - r, w - 2 * r, r, color, fade)
end
end
-- text cache (OutlinedText objects are rasterised once per string/style)
-- keyed str -> colours -> size / outline radius / vertical (no string building per lookup)
local text_cache, text_count = {}, 0
local function raw_text(str, size, fill, outline, radius_px, vertical)
if not str or str == "" then return nil end
local k2 = ((fill[1] * 256 + fill[2]) * 256 + fill[3]) * 16777216
+ (outline[1] * 256 + outline[2]) * 256 + outline[3]
local k3 = (size * 4096 + radius_px * 16) * 2 + (vertical and 1 or 0)
local by_str = text_cache[str]
local by_col = by_str and by_str[k2]
local t = by_col and by_col[k3]
if t == nil then
if text_count > 256 then text_cache, text_count, by_str, by_col = {}, 0, nil, nil end
t = text.create_raw_text(str, size, rgba(fill), rgba(outline), vertical, radius_px * OUTLINE_K / SS, 2.0) or false
if not by_str then by_str = {}; text_cache[str] = by_str end
if not by_col then by_col = {}; by_str[k2] = by_col end
by_col[k3] = t
text_count = text_count + 1
end
return t or nil
end
-- TaikoText: second outline (outer rim), first outline, fill -- drawn as two OutlinedTexts.
local function taiko_text(str, size, first_size, first_col, second_size, second_col)
local outer = raw_text(str, size, second_col, second_col, second_size / 2, false)
local inner = raw_text(str, size, WHITE, first_col, first_size / 2, false)
return outer, inner
end
local function draw_pair(outer, inner, x, y, fade)
-- both are laid out from the same glyph origin; the outer one is (r2 - r1) px bigger per side
if outer then
outer:draw({ x = math.floor(x - outer.width / 2 + 0.5), y = math.floor(y - outer.height / 2 + 0.5), fade = fade })
end
if inner then
inner:draw({ x = math.floor(x - inner.width / 2 + 0.5), y = math.floor(y - inner.height / 2 + 0.5), fade = fade })
end
end
--------------------------------------------------------------------------------
function SongSelect.new()
local self = setmetatable({}, SongSelect)
tex.load_folder("song_select", "global")
tex.load_folder("song_select", "jy_select")
for _, n in ipairs({ "back", "bar", "bar_selected", "bar_highlight", "bar_small", "star", "cursor_p1", "cursor_p2",
"course_easy", "course_normal", "course_hard", "course_oni", "course_edit",
"taiko", "white", "disc24", "disc8", "t14_don", "t14_don_b", "t14_don_2p", "t14_don_b_2p", "t14_guide" }) do
ids[n] = tex.get_texture("song_select/jy_select/" .. n)
end
self._id_footer = tex.get_texture("song_select/global/footer")
self.sel_w = BOX_W -- width of the selected box (last frame)
self.next_w = BOX_W
self.diff_t = 0.0 -- diff-select progress of the selected box (last frame)
self.next_diff = 0.0
self.bg_color = { 255, 255, 255 }
self.target_bg = WHITE
self.header_col = { 143, 143, 143 }
self.genre_name = nil
self.last_ms = nil
self.sel_box = nil -- geometry of the selected song box for draw_selector
return self
end
function SongSelect:update(current_ms) end
function SongSelect:restart_text_fade() end
--------------------------------------------------------------------------------
-- background: song_select.tscn Parallax2D (repeat 1284, autoscroll -48 px/s) with Back
-- (songselect/back.png) modulated towards the selected box colour (columns.gd lerp delta*6)
--------------------------------------------------------------------------------
function SongSelect:draw_background(nav)
local now = tex.get_current_ms()
local dt = self.last_ms and math.max(0, (now - self.last_ms) / 1000.0) or 0
self.last_ms = now
-- per-frame hand-over of the selected box geometry (draw_box of the previous frame)
self.sel_w, self.next_w = self.next_w, BOX_W
self.diff_t, self.next_diff = self.next_diff, 0.0
local k = math.min(1.0, dt * 6)
for i = 1, 3 do
self.bg_color[i] = self.bg_color[i] + (self.target_bg[i] - self.bg_color[i]) * k
end
local folder = nav:current_folder()
if T14_CHROME then
self:learn_ring(folder)
local ok, seq = pcall(function() return nav.wheel_event_seq end)
if ok and seq and seq ~= self.wev_seq then
local ok2, ev = pcall(function() return nav.wheel_event end)
if self.wev_seq ~= nil and ok2 then self.wev, self.wev_ms = ev, now end
self.wev_seq = seq
end
end
self.folder_color = folder and box_back_color(folder) or nil
self.folder_path = folder and folder:path() or nil
self.folder_fore = folder and folder:fore_color() or nil
self.folder_genre = folder and folder:genre_index() or nil
self.folder_name = folder and folder.text_name or nil
local w = 1284
local off = (now * 0.048) % w
local c = { math.floor(self.bg_color[1] + 0.5), math.floor(self.bg_color[2] + 0.5), math.floor(self.bg_color[3] + 0.5) }
for i = 0, 1 do
tex.draw_texture(id("back"), { x = i * w - off, color = rgba(c) })
end
end
--------------------------------------------------------------------------------
-- list geometry
--------------------------------------------------------------------------------
-- engine position -> list offset (Navigator::set_positions, non-vertical branch)
local function u_of(pos)
local left1 = (594 - 150 - 100) * SS -- offset -1
local centre = 594 * SS -- offset 0
local right1 = (594 - 150 + 100 + 300) * SS -- offset +1
if pos <= left1 then return (pos - (594 - 150) * SS) / (100 * SS) - 0.0 end
if pos >= right1 then return (pos - (594 - 150 + 300) * SS) / (100 * SS) end
if pos <= centre then return (pos - centre) / (centre - left1) end
return (pos - centre) / (right1 - centre)
end
-- list offset -> left edge in juu-yon's layout, for a selected box of width sw
local function x_of(u, sw)
local sel_x = CX - sw / 2
if u <= -1 then return sel_x - (BOX_W + PAD) + (BOX_W + PAD) * (u + 1) end
if u >= 1 then return sel_x + sw + PAD + (BOX_W + PAD) * (u - 1) end
if u <= 0 then return sel_x + (BOX_W + PAD) * u end
return sel_x + (sw + PAD) * u
end
--------------------------------------------------------------------------------
-- one column (columns.gd _draw loop body)
--------------------------------------------------------------------------------
function SongSelect:title_text(box, outline)
local name = box.text_name
if box:lua_kind() == "back" then
local s = tex.skin_text("jy_back")
if s ~= "" then name = s end
end
-- VerticalText2D font_size 32, outline_size 20
return raw_text(name, 32, WHITE, outline, 10, true)
end
function SongSelect:subtitle_text(box, outline)
local s = box.text_subtitle
if not s or s == "" then return nil end
s = s:gsub("^%-%-", ""):gsub("^%+%+", "")
if s == "" then return nil end
return raw_text(s, 24, WHITE, outline, 8, true) -- font_size 24, outline_size 16
end
local SCALED_P = {}
local function draw_scaled_v(t, cx, y, max_h, fade)
if not t then return 0 end
local h, tw = t.height, t.width
local s = math.min(1.0, max_h / h) -- tja_meta.gd update_text_scale
local p = SCALED_P
p.x, p.y, p.y2, p.fade = math.floor(cx - tw / 2 + 0.5), math.floor(y + 0.5), math.floor(h * s + 0.5) - h, fade
t:draw(p)
return tw
end
-- the box's animations: fields before the engine's 2026-09 bindings, methods (box:fade()) after
local function box_anim(box, name)
local a = box[name]
if type(a) == "function" then a = a(box) end
return a
end
function SongSelect:draw_box(box)
local kind = box:lua_kind()
local state = box:draw_state()
local fade = box_anim(box, "fade").attribute
local u = u_of(box:box_x())
local color = box_back_color(box)
-- only the root list feeds learn_ring (it ignores frames inside a folder)
if T14_CHROME and kind ~= "back" and self.frame_boxes and not self.frame_folder then
self.frame_boxes[#self.frame_boxes + 1] = { u = u, path = box:path(), color = color }
end
-- selected-box width: song 480, box.def folder / back 256, diff select 768 (columns.gd)
local is_sel = state ~= "closed"
local w, h, top = BOX_W, BOX_H, COL_Y
local trans, dtrans = 0.0, 0.0
local target = (kind == "song") and OPEN_SONG or OPEN_FOLDER
if is_sel then
trans = box_anim(box, "open_fade").attribute
if state == "diff_select" and kind == "song" then
dtrans = box.diff_fade_in.attribute
w = OPEN_SONG + (OPEN_DIFF - OPEN_SONG) * dtrans
h = BOX_H + 32 * dtrans -- bsize.y 472 -> 504, grows upward
top = COL_Y - 32 * dtrans
trans = 1.0
else
w = BOX_W + (target - BOX_W) * trans
end
self.next_w = w
self.next_diff = dtrans
-- columns.gd: a Back box (or a song) inside a box shows the box's colours
local shown = (kind == "back" and self.folder_color) or color
self.target_bg = shown
self.header_col = shown
if kind == "folder" then self.genre_name = box.text_name; self.genre_idx = box:genre_index(); self.genre_fore = box:fore_color()
elseif kind == "back" and not self.folder_name then self.genre_name = nil; self.genre_idx = nil; self.genre_fore = nil end
elseif math.abs(u) < 0.5 then
-- the cursor's box before it opens (moving / 0.5 s timer)
local shown = (kind == "back" and self.folder_color) or color
self.target_bg = shown
self.header_col = shown
if kind == "folder" then self.genre_name = box.text_name; self.genre_idx = box:genre_index(); self.genre_fore = box:fore_color() end
end
if T14_CHROME then
local g = 1.0 -- every box, selected / open too (user: wider when selected, never taller)
top = top + T14_BOX_GAP_TOP * g
h = h - (T14_BOX_GAP_TOP + T14_BOX_GAP_BOTTOM) * g
end
local sw = self.sel_w
local x
if is_sel then x = CX - w / 2 else x = x_of(u, sw) end
if T14_CHROME and not is_sel and u >= 0.5 then
local p = self:folder_slide()
if p > 0 then x = x + (FOLDER_OUT_X - x) * p end
end
if x > 960 + 40 or x + w + STYLE_GROW < -40 then return end
-- main box: song_box.tres (bar.png 18/17/42/41) modulated with the box colour, at bsize + 23
nine(id("bar"), 95, 456, 18, 17, 42, 41, x, top, w + STYLE_GROW, h + STYLE_GROW, color, fade)
if is_sel then
nine(id("bar_selected"), 72, 432, 18, 17, 18, 17, x, top, w, h, WHITE, fade) -- box_selected.tres
else
nine(id("bar_highlight"), 72, 432, 18, 17, 19, 17, x, top, w, h, WHITE, fade) -- box_highlight.tres
end
-- index tab (box_index.tres StyleBoxFlat, radius 8 top, 30 high, box colour); juu-yon writes the
-- list number in it (font 20) -- the engine does not give Lua the box's list index -> no number.
if kind ~= "back" and not T14_CHROME then
local a = fade * (1.0 - dtrans)
if a > 0 then rrect(8, x, top - 30, w, 30, color, a, true) end
end
-- title: x centre = x + 48 + (bsize.x - 96) (- 16 in diff select, - 28 for an open box.def box)
local outline = BLACK
if not is_sel then
local fc = box:fore_color()
if fc then outline = { fc.r, fc.g, fc.b } end
end
local x_ofs = 48 + (w - BOX_W) - 16 * dtrans
if kind ~= "song" and is_sel then x_ofs = x_ofs - 28 * trans end
local ty = (dtrans > 0) and (COL_Y + 24 * (1 - dtrans)) or (COL_Y + 24)
if T14_CHROME then ty = ty + T14_BOX_GAP_TOP end -- follows the lowered box top
local title = self:title_text(box, outline)
local shrink = T14_CHROME and (T14_BOX_GAP_TOP + T14_BOX_GAP_BOTTOM) or 0
local tw = draw_scaled_v(title, x + x_ofs, ty, 416 - shrink, fade)
if kind == "song" and is_sel then
-- subtitle: x_ofs -= title width + 16, y += 48
local sub = self:subtitle_text(box, outline)
if sub then
draw_scaled_v(sub, x + x_ofs - tw - 16, ty + 48, 376 - shrink, fade * math.min(1.0, trans))
end
self:draw_courses(box, x, top, w, trans, dtrans, fade)
end
end
-- difficulty columns (columns.gd "# Difficulties")
-- Green's folder-open move of the boxes right of the folder (0 = in place, 1 = off screen), held
-- until the engine swaps the list in
function SongSelect:folder_slide()
if self.wev ~= WEV_OPEN_BEGIN or not self.wev_ms then return 0 end
local p = math.min(1, math.max(0, (tex.get_current_ms() - self.wev_ms) / FOLDER_OUT_MS))
return p * p * p -- MoveAnimation ease_in Cubic
end
-- oni / ura weights: YataiDONNijiiro's 3 s song-board loop (oni ~1 s, change, ura ~1 s, change),
-- here as one simultaneous smoothstep cross-fade of 0.5 s each way (user: the staggered linear
-- 0.25 s ramps looked jerky)
local function ura_crossfade(ms)
local t = (ms % 3000) / 1000
local function ss(t0)
local u = math.max(0, math.min(1, (t - t0) / 0.5))
return u * u * (3 - 2 * u)
end
local ura = ss(1.0) - ss(2.5)
return 1 - ura, ura
end
function SongSelect:draw_courses(box, x, top, w, trans, dtrans, fade)
local list = {}
for d = 0, 3 do
local ci = box:course_info(d)
if ci.has_course then list[#list + 1] = { d = d, level = ci.level } end
end
if box.is_ura and #list > 0 and list[#list].d == 3 then
local ci = box:course_info(4)
if ci.has_course then list[#list] = { d = 4, level = ci.level } end
end
local n = #list
if n == 0 then return end
-- oni <-> ura switch (the engine's UraSwitchAnimation panel is hidden in
-- Graphics/song_select/diff_select): the last column flashes white and its icon pops.
-- Only a flip seen while the difficulty select is fully open counts (entering a song resets it).
local now = tex.get_current_ms()
local ura = box.is_ura and true or false
if self.ura_seen ~= nil and ura ~= self.ura_seen and dtrans >= 0.999 then self.ura_flip_ms = now end
self.ura_seen = ura
local flip_t = self.ura_flip_ms and (now - self.ura_flip_ms) / URA_FLIP_MS or 1
if flip_t >= 1 then flip_t = nil end
local base_padding = 24 + (64 - 24) * dtrans
-- the four-course layout for every song (user): the columns use its spacing and, with fewer
-- courses, fill its slots from the right (juu-yon spreads n columns over the width instead)
local diff_width = (24 * 2 + base_padding) * math.max(n, 4)
local diff_padding = (288 / diff_width) * base_padding
local diff_x = x + diff_padding * 2
if T14_CHROME and dtrans <= 0 then diff_x = CX - OPEN_SONG / 2 + diff_padding * 2 end -- in place
if dtrans > 0 then diff_x = diff_x + (w - OPEN_SONG) / 2 end
local slot0 = diff_x -- left edge of the first of 4 slots
diff_x = diff_x + math.max(0, 4 - n) * (diff_padding + 24 * 2)
local diff_h = 372 + 32 * dtrans
if T14_CHROME then diff_h = diff_h - (T14_BOX_GAP_TOP + T14_BOX_GAP_BOTTOM) end
local a = (dtrans > 0) and 1.0 or math.max(0.0, math.min(1.0, trans - 0.5))
if T14_CHROME then a = (dtrans > 0) and 1.0 or math.max(0.0, math.min(1.0, trans * 2)) end
a = a * fade
-- column origin: offset_x(diff_x) -> (diff_x, right.y); right.y = 472 - bsize.y in diff select
local oy = top
local geo = { x = x, top = top, cols = {}, a = a, slot0 = slot0 }
for k, c in ipairs(list) do
rrect(24, diff_x, oy + 64, 48, diff_h, DIFF_COL_COLOR, a)
local flipping = flip_t and k == n and c.d >= 3
-- opts: nodots = skip the level dots, star_from = first star drawn, noicon = skip icon + name
local function content(c, ca, opts)
opts = opts or {}
local icon = (not opts.noicon) and id(COURSE_IDS[c.d]) or nil
if icon then
local info = tex.get_texture_info("jy_select", COURSE_IDS[c.d])
local iw, ih = info and info.width or 48, info and info.height or 36
local s = flipping and (1 + 0.35 * (1 - flip_t) * (1 - flip_t)) or 1
local sw, sh = iw * s, ih * s
tex.draw_texture(icon, { x = math.floor(diff_x + 24 - sw / 2), y = math.floor(oy + 46 - sh / 2),
x2 = sw - iw, y2 = sh - ih, fade = ca })
end
local maxs = MAX_STARS[c.d] or 10
local star_y = diff_h + 64 - 24 - 9
for j = 0, math.max(maxs, c.level) - 1 do
if j <= maxs and not opts.nodots then
tex.draw_texture(id("disc8"), { x = diff_x + 24 - 4, y = oy + star_y + 3 - 4, x2 = 8 - 16, y2 = 8 - 16,
color = rgba(STAR_DOT), fade = ca })
end
if j < c.level and j >= (opts.star_from or 0) then
tex.draw_texture(id("star"), { x = diff_x + 15, y = oy + star_y - 8,
color = rgba(j > 9 and CRIMSON or WHITE), fade = ca })
end
if j % 9 == 0 and j > 0 then star_y = diff_h + 64 - 24 - 9 else star_y = star_y - 20 end
end
-- course name: vertical, font 24, black, from (24, 78)
local nm = raw_text(tex.skin_text("jy_course_" .. c.d), 24, BLACK, BLACK, 0.5, true)
if nm and not opts.noicon then nm:draw({ x = math.floor(diff_x + 24 - nm.width / 2 + 0.5), y = oy + 78 - 12, fade = ca }) end
end
local ura_level = nil
if T14_CHROME and dtrans <= 0 and k == n and c.d == 3 and box:has_ura() then
local ui = box:course_info(4)
if ui.has_course then ura_level = ui.level end
end
if ura_level then
-- the dots and the stars both courses have stay put at full alpha; only the icon, the
-- name and the extra stars cross-fade
local oa, ua = ura_crossfade(tex.get_current_ms())
local common = math.min(c.level, ura_level)
content({ d = c.d, level = common }, a, { noicon = true })
if oa > 0 then content(c, a * oa, { nodots = true, star_from = common }) end
if ua > 0 then content({ d = 4, level = ura_level }, a * ua, { nodots = true, star_from = common }) end
else
content(c, a)
end
if flipping then rrect(24, diff_x, oy + 64, 48, diff_h, WHITE, a * 0.85 * (1 - flip_t)) end
geo.cols[k] = { x = diff_x, d = c.d }
diff_x = diff_x + diff_padding + 24 * 2
end
-- the Back mini bar (song_box_small.tres: bar_small.png 10/10/10/10, modulate (1.69,1.077,.54)),
-- then the engine's two extra difficulty-cursor stops, same style: Options (Difficulty::MODIFIER)
-- and Hit sound (Difficulty::NEIRO), in cursor order Back -> Options -> Hit sound -> courses,
-- spaced evenly between the Back bar and the first course column.
if dtrans > 0 then
local bx = x + 32
local shift = MINI_BAR_SHIFT -- the three mini bars sit a little left of juu-yon's Back bar
local a2 = dtrans * fade
local function mini_bar(left, key)
-- the modulate is baked into jy_select/bar_small.png (it exceeds 1.0 on red)
nine(id("bar_small"), 36, 216, 10, 10, 10, 10, left, top + 64, 48, 240, WHITE, a2)
local t = raw_text(tex.skin_text(key), 24, BLACK, BLACK, 0.5, true)
-- caption 22 px below the bar top (juu-yon 16 sat too close to the edge)
if t then t:draw({ x = math.floor(left + 24 - t.width / 2 + 0.5), y = top + 86, fade = a2 }) end
end
mini_bar(bx + 24 - shift, "jy_back")
geo.back_x = bx - shift
local first = geo.slot0 -- the four-course layout's first column, whatever this song has
local gap = (first - (bx + 72) - 48 * 2) / 3
geo.mod_x = bx + 72 + gap - shift
geo.neiro_x = geo.mod_x + 48 + gap
mini_bar(geo.mod_x, "jy_options")
mini_bar(geo.neiro_x, "jy_neiro")
end
geo.dtrans = dtrans
self.sel_box = geo
end
function SongSelect:draw_box_bg(box) end
-- p1_cursor.png over the chosen column (columns.gd: Vector2(24, 20 + ofs) - size/2 on a column,
-- Vector2(48 - w/2, 24 + ofs) on the Back bar)
function SongSelect:draw_selector(player, is_half, fade_in, pass)
-- [YataiDON14 build patch] publish the course for Nantoka's transition iris
-- (Scripts/global/transition.lua reads NK_SELECTED_DIFF)
do local nd = player:selected_difficulty()
if nd >= 0 and nd <= 4 then NK_SELECTED_DIFF = nd end end
if pass ~= 1 then return end
local g = self.sel_box
if not g or not g.dtrans or g.dtrans <= 0 then return end
local d = player:selected_difficulty()
-- the play HUD (YataiDON14 / Nantoka nk_hud.lua) reads the chosen course for its norma line
if d >= 0 and d <= 4 then
-- NK_SELECTED_DIFF (one number) is the Nantoka transition's; this one is per player
NK_SELECTED_DIFF = d
if type(NK_COURSE_BY_PLAYER) ~= "table" then NK_COURSE_BY_PLAYER = {} end
NK_COURSE_BY_PLAYER[(player.player_num and player:player_num() == 2) and 2 or 1] = d
end
local a = math.min(1.0, g.dtrans)
-- juu-yon is single-player (p1_cursor only); 2P gets jy_select/cursor_p2 (same art, 2P teal)
local cur = id((player.player_num and player:player_num() == 2) and "cursor_p2" or "cursor_p1") or id("cursor_p1")
if d == DIFF_BACK then
if g.back_x then tex.draw_texture(cur, { x = g.back_x + 48 - 20, y = g.top + 24, fade = a }) end
return
end
if d == DIFF_MODIFIER or d == DIFF_NEIRO then
local bx = (d == DIFF_MODIFIER) and g.mod_x or g.neiro_x
if bx then tex.draw_texture(cur, { x = bx + 24 - 20, y = g.top + 24, fade = a }) end
return
end
for _, c in ipairs(g.cols) do
if c.d == d or (d == 4 and c.d == 3) or (d == 3 and c.d == 4) then
tex.draw_texture(cur, { x = c.x + 24 - 20, y = g.top + 20 - 20, fade = a })
return
end
end
end
local T14_DON_BOTTOM, T14_FACE_MS = 655, 100
function SongSelect:draw_t14_footer()
local now = tex.get_current_ms()
if audio.play_count then
local n = audio.play_count("kat")
if self.kat_count and n > self.kat_count then self.face_ms = now end
self.kat_count = n
else
local kat = audio.is_sound_playing("kat")
if kat and not self.kat_prev then self.face_ms = now end
self.kat_prev = kat
end
local alt = self.face_ms ~= nil and now - self.face_ms < T14_FACE_MS
local two = tex.current_screen and tex.current_screen() == "SONG_SELECT_2P"
local solo2 = not two and tex.player_num and tex.player_num() == 2
local function chara(base, flash, right)
local name = alt and flash or base
local d = id(name) or id(base)
if not d then return end
local inf = tex.get_texture_info("jy_select", name) or tex.get_texture_info("jy_select", base)
tex.draw_texture(d, { x = right and (960 - 6 - inf.width) or 6, y = T14_DON_BOTTOM - inf.height })
end
if not solo2 then chara("t14_don", "t14_don_b", false) end
if two or solo2 then chara("t14_don_2p", "t14_don_b_2p", true) end
if self.ov_state == STATE_SONG_SELECTED then return end
local bw, bh, cx, cy = 480, 46, 474, 625
local GUIDE_NAME, GUIDE_FRAMES, GUIDE_FPS, GUIDE_W, GUIDE_H = "t14_guide", 60, 60, 490, 50
local gi = math.floor((tex.get_current_ms() or 0) * GUIDE_FPS / 1000) % GUIDE_FRAMES
local g = tex.get_texture(string.format("song_select/jy_select/%s_%02d", GUIDE_NAME, gi))
if g then
tex.draw_texture(g, { x = math.floor(cx - GUIDE_W / 2), y = math.floor(cy - GUIDE_H / 2) })
end
end
function SongSelect:draw_footer()
if T14_CHROME then return self:draw_t14_footer() end
if self._id_footer then tex.draw_texture(self._id_footer) end
-- select_taiko.tscn inside ControlBanner/Player1: TaikoP1 at (224, 606.656 - 38.656) 192x192
local tx, ty = 224, 568
tex.draw_texture(id("taiko"), { x = tx, y = ty })
-- ControlLabels: Decide (0,32)-(192,104) red, Right (-40,48)-(24,112) "左", Left (168,48)-(232,112) "右"
local red1, red2 = { 240, 22, 8 }, { 67, 8, 1 }
local blue1, blue2 = { 65, 125, 173 }, { 16, 21, 59 }
local o, i = taiko_text(tex.skin_text("jy_ui_decide"), 36, 24, red1, 36, red2)
draw_pair(o, i, tx + 96, ty + 68)
o, i = taiko_text(tex.skin_text("jy_ui_left"), 36, 24, blue1, 36, blue2)
draw_pair(o, i, tx - 8, ty + 80)
o, i = taiko_text(tex.skin_text("jy_ui_right"), 36, 24, blue1, 36, blue2)
draw_pair(o, i, tx + 200, ty + 80)
-- ControlLabelP2: font 32, outline_size 18, centred in Player2 (484.16..960, 606.656..720)
local p2 = raw_text(tex.skin_text("jy_taiko_start_p2"), 32, WHITE, BLACK, 9, false)
if p2 then p2:draw({ x = math.floor((484.16 + 960) / 2 - p2.width / 2 + 0.5),
y = math.floor((606.656 + 720) / 2 - p2.height / 2 + 0.5) }) end
end
local T14_BAND_TOP, T14_BAND_BOTTOM = 50, 670
function SongSelect:draw_overlays(state)
self.ov_state = state -- the footer (drawn before the overlays) reads it next frame
local dark = { 27, 27, 27 }
local hc = self.header_col or WHITE
if T14_CHROME then
rect(0, 0, 960, T14_BAND_TOP, BLACK)
rect(0, T14_BAND_BOTTOM, 960, 720 - T14_BAND_BOTTOM, BLACK)
end
local o, i
if T14_CHROME then
o, i = taiko_text(tex.skin_text("jy_song_select"), 42, 12, BLACK, 20, dark)
else
o, i = taiko_text(tex.skin_text("jy_song_select"), 42, 28, hc, 42, dark)
end
if i then draw_pair(o, i, 32 + i.width / 2, 44) end
local gname = self.folder_name or self.genre_name
if T14_CHROME then
-- the open song box (difficulty select) reaches over the header: no genre name / strip then
if state == STATE_SONG_SELECTED then return end
self:draw_genre_strip()
if gname then
-- outline = the genre's title outline colour (box fore_color, like the box titles), so a
-- WHITE genre (Search / Vocaloid ...) does not get white text on a white outline
local fc = (self.folder_name and self.folder_fore) or (not self.folder_name and self.genre_fore) or nil
local oc = fc and { fc.r, fc.g, fc.b } or hc
if not fc and oc[1] + oc[2] + oc[3] > 600 then oc = { 80, 80, 80 } end -- light, no fore colour
local lab = raw_text(gname, 32, WHITE, oc, 9, false)
local sh = raw_text(gname, 32, BLACK, BLACK, 9, false)
if lab then
local x, y = math.floor(468 - lab.width / 2 + 0.5), math.floor(75 - lab.height / 2 + 0.5)
if sh then sh:draw({ x = x + 3, y = y + 3, fade = 0.3 }) end
lab:draw({ x = x, y = y })
end
end
return
end
if gname then
o, i = taiko_text(gname, 32, 24, hc, 36, dark)
if i then draw_pair(o, i, 928 - i.width / 2, 44) end
end
end
local STRIP_SEG, STRIP_CUR, STRIP_X, STRIP_Y, STRIP_H, STRIP_M = 100, 300, 468, 101, 10, 24
RING_NEXT, RING_PREV, RING_COL = RING_NEXT or {}, RING_PREV or {}, RING_COL or {}
function SongSelect:learn_ring(folder)
local fb = self.frame_boxes
if fb and not self.frame_folder and #fb > 0 then
table.sort(fb, function(a, b) return a.u < b.u end)
local best = nil
for k, b in ipairs(fb) do
RING_COL[b.path] = b.color
if k > 1 and b.u - fb[k - 1].u < 1.5 then
RING_NEXT[fb[k - 1].path] = b.path
RING_PREV[b.path] = fb[k - 1].path
end
if not best or math.abs(b.u) < math.abs(best.u) then best = b end
end
self.root_cur = best and best.path or nil
end
self.frame_boxes = {}
self.frame_folder = folder and folder:path() or nil
end
function SongSelect:draw_genre_strip()
local cur = self.folder_path or self.root_cur
if not cur then return end
local l, r = STRIP_X - STRIP_CUR / 2, STRIP_X + STRIP_CUR / 2
local function seg(x, w, c)
local a, b = math.max(x, STRIP_M), math.min(x + w, 960 - STRIP_M)
if b > a then rect(a, STRIP_Y, b - a, STRIP_H, c) end
end
seg(l, STRIP_CUR, RING_COL[cur] or self.folder_color or WHITE)
local p, guard = cur, 0
while l > STRIP_M and guard < 20 do -- left: previous boxes of the root list
p = RING_PREV[p]; guard = guard + 1
if not p or p == cur then break end
l = l - STRIP_SEG
seg(l, STRIP_SEG, RING_COL[p] or WHITE)
end
p, guard = cur, 0
while r < 960 - STRIP_M and guard < 20 do -- right: next boxes
p = RING_NEXT[p]; guard = guard + 1
if not p or p == cur then break end
seg(r, STRIP_SEG, RING_COL[p] or WHITE)
r = r + STRIP_SEG
end
end
do
local T14_HC = require("global.t14_herecome")
local draw_top = SongSelect.draw_top
function SongSelect:draw_top(dan_progress)
if draw_top then draw_top(self, dan_progress) end
T14_HC.song_select(self)
end
end