forked from GanYiZhong/YataiDON14
744 lines
35 KiB
Lua
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
|