Files
PyTaikoGreen/Scripts/entry/box.lua
T
2026-10-11 23:12:37 -04:00

262 lines
12 KiB
Lua

EntryBox = {}
EntryBox.__index = EntryBox
local DESC_COLOR = { 0, 0, 0, 255 }
local DESC_NO_LINE = { 0, 0, 0, 0 }
local function build_description(mode_key)
if not mode_key or mode_key == "" then return nil end
local raw = tex.skin_text(mode_key .. "_desc")
if raw == "" then return nil end
local cfg = tex.get_skin_config("entry_box_description") -- width = column pitch
local latin = tex.get_skin_config("entry_box_description_latin")
local is_cjk = text.language_is_cjk(tex.language())
local size = is_cjk and cfg.font_size or latin.font_size
local lines = {}
for line in raw:gmatch("[^\n]+") do
local content = line
if not is_cjk then
content = text.word_wrap(line, size, 2.0, latin.width)
end
lines[#lines + 1] = text.create_raw_text(content, size, DESC_COLOR, DESC_NO_LINE, is_cjk, 0, 2.0, 1.0)
end
return { lines = lines, is_cjk = is_cjk, cfg = cfg }
end
local MODE_BADGES = {
entry_practice = { "one_player" },
entry_costume = { "new" },
}
-- Greyed-out board
local GREY_FILL, GREY_OUTLINE = { 156, 155, 156, 255 }, { 76, 76, 76, 255 }
function EntryBox.new(text_str, font_size, mode_key)
local self = setmetatable({}, EntryBox)
self.text = text.create_raw_text(text_str, font_size, { 255, 255, 255, 255 }, { 109, 68, 24, 255 }, true, 5)
self.text_selected = text.create_raw_text(text_str, font_size, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, true, 5)
self.box = tex.get_texture("entry/mode_select/box")
self.hl_center = tex.get_texture("entry/mode_select/box_highlight_center")
self.hl_left = tex.get_texture("entry/mode_select/box_highlight_left")
self.hl_right = tex.get_texture("entry/mode_select/box_highlight_right")
self.arrow = tex.get_texture("entry/mode_select/arrow")
self.flash = tex.get_texture("entry/mode_select/flash")
self.star = tex.get_texture("entry/mode_select/star")
self.desc = build_description(mode_key)
self.text_grey = text.create_raw_text(text_str, font_size, GREY_FILL, GREY_OUTLINE, true, 5)
self.box_grey = tex.get_texture("entry/mode_select/box_grey")
self.hl_grey_center = tex.get_texture("entry/mode_select/box_highlight_grey_center")
self.hl_grey_left = tex.get_texture("entry/mode_select/box_highlight_grey_left")
self.hl_grey_right = tex.get_texture("entry/mode_select/box_highlight_grey_right")
self.badges = {}
for _, name in ipairs(MODE_BADGES[mode_key] or {}) do
self.badges[#self.badges + 1] = {
name = name,
tex = tex.get_texture("entry/mode_select/badge_" .. name),
info = tex.get_texture_info("mode_select", "badge_" .. name),
}
-- the tag is baked into the title art in Lumen, so it has a grey twin for disabled boards
if name == "one_player" then
self.badges[#self.badges].tex_grey = tex.get_texture("entry/mode_select/badge_one_player_grey")
end
end
local box_info = tex.get_texture_info("mode_select", "box")
local hl_left_info = tex.get_texture_info("mode_select", "box_highlight_left")
local hl_offset = tex.get_skin_config("entry_box_highlight_offset")
local text_offset = tex.get_skin_config("entry_box_text_offset")
self.y = box_info.y[1]
self.box_width = box_info.width
self.box_height = box_info.height
self.hl_left_width = hl_left_info.width
self.hl_right_width = tex.get_texture_info("mode_select", "box_highlight_right").width
self.arrow_info = tex.get_texture_info("mode_select", "arrow")
self.hl_offset_x = hl_offset.x
self.text_y_offset = text_offset.y
return self
end
local ARROW_MS, ARROW_TRAVEL = 1000, 25
local ARROW_LEFT_GAP, ARROW_RIGHT_GAP = 7, 8 -- arrow centre past the highlight's edge
local HOVER_FLASH_MS = 400
local DECIDE_FLASH = {
{ 83, 1.0, 0.0 }, { 33, 0.0, 1.0 }, { 67, 0.75, 0.0 }, { 33, 0.0, 1.0 }, { 67, 0.75, 0.0 },
}
local DECIDE_FLASH_MS, DECIDE_FADE_MS = 283, 160 -- animation.json id 9 lasts their sum
local function decide_flash(t)
for _, seg in ipairs(DECIDE_FLASH) do
if t < seg[1] then return seg[2] + (seg[3] - seg[2]) * (t / seg[1]) end
t = t - seg[1]
end
return 0
end
local FLASH_W, FLASH_H, FLASH_CAP = 224, 412, 6
function EntryBox:draw_flash(left_x, right_x, y, alpha)
if alpha <= 0 then return end
local width = right_x + self.hl_right_width - left_x
local function piece(src_x, src_w, x, w)
tex.draw_texture(self.flash, {
src = { x = src_x, y = 0, width = src_w, height = FLASH_H },
x = x, y = y, x2 = w - FLASH_W, fade = alpha,
})
end
piece(0, FLASH_CAP, left_x, FLASH_CAP)
piece(FLASH_CAP, FLASH_W - 2 * FLASH_CAP, left_x + FLASH_CAP, width - 2 * FLASH_CAP)
piece(FLASH_W - FLASH_CAP, FLASH_CAP, left_x + width - FLASH_CAP, FLASH_CAP)
end
function EntryBox:draw_arrows(left_x, right_x, fade, has_prev, has_next)
local p = (tex.get_current_ms() % ARROW_MS) / ARROW_MS
local alpha = (1 - p) * fade
local info = self.arrow_info
local top = self.y + self.box_height / 2 - info.height / 2
if has_prev then
local cx = left_x - ARROW_LEFT_GAP - ARROW_TRAVEL * p
tex.draw_texture(self.arrow, { x = cx - info.width / 2, y = top, fade = alpha })
end
if has_next then
local cx = right_x + self.hl_right_width + ARROW_RIGHT_GAP + ARROW_TRAVEL * p
tex.draw_texture(self.arrow, { x = cx - info.width / 2, y = top, fade = alpha, mirror = "horizontal" })
end
end
function EntryBox:draw_description(x, open_attr, fade)
local d = self.desc
if not d then return end
local cfg = d.cfg
local alpha = fade * math.min(1.0, open_attr / 54)
local centre = x + self.box_width / 2
if d.is_cjk then
for i, t in ipairs(d.lines) do
local cx = centre + cfg.x - cfg.width * (i - 1)
t:draw({ x = cx - t.width / 2, y = self.y + cfg.y, fade = alpha })
end
else
local total = 0
for _, t in ipairs(d.lines) do total = total + t.height end
local scale = total > cfg.height and (cfg.height / total) or 1.0
local y = self.y + cfg.y
for _, t in ipairs(d.lines) do
local h = t.height * scale
t:draw({ x = centre + cfg.x - cfg.width - t.width / 2, y = y, y2 = h - t.height, fade = alpha })
y = y + h
end
end
end
local TAG_Y = 357 -- one_player tag, centred under the title
local NEW_X, NEW_Y = 6, -65 -- new bubble over the idle board
local TWO_PLAYER_GAP, TWO_PLAYER_Y = 202, -25 -- two_player bubble: left of the opened highlight
-- Sparkles around the two_player bubble. Each colour loops its own keyframes (frames at 60 fps):
-- {from, to, scale0, scale1, alpha0, alpha1}; gaps between segments are hidden.
local STAR_SEQ = {
[0] = { period = 73, { 0, 30, 1, 1.1, 1, 0 }, { 49, 68, 0.2, 1, 1, 1 }, { 68, 73, 1, 1, 1, 1 } },
[1] = { period = 70, { 0, 17, 0.2, 1, 1, 1 }, { 17, 23, 1, 1, 1, 1 }, { 23, 51, 1.004, 1.1, 0.965, 0 } },
[2] = { period = 50, { 0, 26, 1, 0.2, 1, 1 }, { 26, 50, 0.233, 1, 1, 1 } },
[3] = { period = 70, { 18, 42, 0.2, 1, 1, 1 }, { 42, 48, 1, 1, 1, 1 }, { 48, 70, 1.005, 1.1, 0.953, 0 } },
[4] = { period = 60, { 0, 15, 0.614, 1, 1, 1 }, { 15, 46, 1, 0.2, 1, 1 }, { 46, 60, 0.228, 0.586, 1, 1 } },
}
-- {colour, centre x, centre y relative to the bubble's top-left, base scale}
local STARS = {
{ 0, 45.4, 335.3, 1.0 }, { 1, 174.4, 62.4, 1.0 }, { 2, 153.5, 45.5, 0.6 }, { 3, 76.4, 357.3, 0.7 },
{ 4, 49.4, 307.3, 0.7 }, { 4, 185.8, 91.0, 0.7 }, { 3, 185.4, 119.4, 0.7 }, { 2, 37.4, 283.5, 0.6 },
}
local STAR_SIZE = 40
local function star_state(seq, now)
local f = (now * 0.06) % seq.period
for _, k in ipairs(seq) do
if f >= k[1] and f < k[2] then
local p = (f - k[1]) / (k[2] - k[1])
return k[3] + (k[4] - k[3]) * p, k[5] + (k[6] - k[5]) * p
end
end
end
function EntryBox:draw_badge(x, left_x, y, title_cx, highlighted, open_attr, fade, disabled)
for _, badge in ipairs(self.badges) do
if badge.name == "one_player" then
tex.draw_texture(disabled and badge.tex_grey or badge.tex, { x = title_cx - badge.info.width / 2, y = y + TAG_Y, fade = fade })
elseif badge.name == "new" and not highlighted then
tex.draw_texture(badge.tex, { x = x + NEW_X, y = y + NEW_Y, fade = fade })
elseif badge.name == "two_player" and highlighted then
local bx, by = left_x - TWO_PLAYER_GAP, y + TWO_PLAYER_Y
local bubble_fade = fade * math.min(1.0, open_attr / 54)
tex.draw_texture(badge.tex, { x = bx, y = by, fade = bubble_fade })
local now = tex.get_current_ms()
for _, star in ipairs(STARS) do
local scale, alpha = star_state(STAR_SEQ[star[1]], now)
if scale then
tex.draw_texture(self.star, {
x = bx + star[2] - STAR_SIZE / 2, y = by + star[3] - STAR_SIZE / 2,
frame = star[1], scale = scale * star[4], center = true, fade = alpha * bubble_fade,
})
end
end
end
end
end
function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr, fade, _y_pos, has_prev, has_next,
disabled)
local y = self.y
local now = tex.get_current_ms()
-- animation 9 runs once a board is chosen: blink the chosen one, then fade everything
local decided_flash = nil
if fade < 1 then
local t = (1 - fade) * (DECIDE_FLASH_MS + DECIDE_FADE_MS)
decided_flash = decide_flash(t)
fade = t < DECIDE_FLASH_MS and 1.0 or (DECIDE_FLASH_MS + DECIDE_FADE_MS - t) / DECIDE_FADE_MS
end
local highlighted = is_selected and move_finished
if highlighted and not self.hl_since then self.hl_since = now end
if not highlighted then self.hl_since = nil end
if highlighted then
tex.draw_texture(disabled and self.hl_grey_center or self.hl_center, {
x = left_x + self.hl_left_width,
y = y,
x2 = right_x - left_x + self.hl_offset_x,
fade = fade,
})
tex.draw_texture(disabled and self.hl_grey_left or self.hl_left, { x = left_x, y = y, fade = fade })
tex.draw_texture(disabled and self.hl_grey_right or self.hl_right, { x = right_x, y = y, fade = fade })
if decided_flash then
self:draw_flash(left_x, right_x, y, decided_flash * fade)
else
self:draw_flash(left_x, right_x, y, (1 - (now - self.hl_since) / HOVER_FLASH_MS) * fade)
end
self:draw_arrows(left_x, right_x, fade, has_prev, has_next)
self:draw_description(x, open_attr, fade)
else
if disabled then
-- the grey art has no y of its own in texture.json: place it at the board's row
tex.draw_texture(self.box_grey, { x = x, y = y, fade = fade })
else
tex.draw_texture(self.box, { x = x, fade = fade })
end
end
local label = (is_selected and move_finished) and self.text_selected or self.text
if disabled then label = self.text_grey end
local text_x = x + self.box_width / 2 - label.width / 2
if is_selected then
text_x = text_x + open_attr
end
label:draw({ x = text_x, y = y + self.text_y_offset, fade = fade })
self:draw_badge(x, left_x, y, text_x + label.width / 2, highlighted, open_attr, fade, disabled)
end