338 lines
15 KiB
Lua
338 lines
15 KiB
Lua
-- Entry mode-select board list (arcade reference layout). The C++ BoxManager owns
|
|
-- selection/input; this script only draws. All positions are 1080p pixels.
|
|
|
|
EntryBox = {}
|
|
EntryBox.__index = EntryBox
|
|
|
|
-- Shared list state (one Lua state for all boxes).
|
|
__hss_entry_boxes = __hss_entry_boxes or {}
|
|
__hss_entry_mode = __hss_entry_mode or { sel = 1, t0 = 0 }
|
|
|
|
local CENTER_X, CENTER_Y = 960, 535 -- arcade open-board centre
|
|
local FRAME_MS = 1000 / 60 -- one arcade frame in ms
|
|
local SLIDE_F = 9 -- list_anim_up/down slide length (frames, linear)
|
|
local TITLE_SIZE = 72 -- text_title font size
|
|
local INFO_SIZE = 34 -- text_info font size
|
|
local TITLE_Y_ON, TITLE_Y_OFF = -97, 4
|
|
local INFO_CY, INFO_LINE_H = 59.5, 53
|
|
local TITLE_OUTER_T, TITLE_INNER_T = 8, 4 -- outer black rim / inner mode-colour outline
|
|
local INFO_COLOR = { 255, 255, 255, 255 }
|
|
local INFO_OUTLINE = { 0, 0, 0, 255 }
|
|
local INFO_BORDER = 5.0
|
|
local TITLE_SPACING, INFO_SPACING = 2.0, 1.0
|
|
|
|
-- Board art is baked into the parent's fixed texture "box" as framed textures; the
|
|
-- mode is recognised by matching the engine's title text against skin_config keys.
|
|
local MODES = {
|
|
game = {
|
|
frame = 0, outline = {251, 1, 29, 255},
|
|
title_key = "entry_game",
|
|
comment_keys = { "entry_game_comment_1", "entry_game_comment_2" },
|
|
},
|
|
practice = {
|
|
frame = 2, outline = {34, 138, 117, 255},
|
|
title_key = "entry_practice",
|
|
comment_keys = { "entry_practice_comment_1", "entry_practice_comment_2" },
|
|
},
|
|
costume = {
|
|
frame = 4, outline = {200, 120, 222, 255},
|
|
title_key = "entry_costume",
|
|
comment_keys = { "entry_costume_comment_1", "entry_costume_comment_2" },
|
|
},
|
|
ai = {
|
|
frame = 2, outline = {34, 138, 117, 255},
|
|
title_key = "entry_ai_battle",
|
|
comment_keys = { "entry_ai_battle_comment_1", "entry_ai_battle_comment_2" },
|
|
},
|
|
dan = {
|
|
frame = 6, outline = {76, 60, 151, 255},
|
|
title_key = "mode_dan",
|
|
comment_keys = { "mode_dan_comment_1", "mode_dan_comment_2", "mode_dan_comment_3" },
|
|
},
|
|
settings = {
|
|
frame = 9, outline = {0, 132, 212, 255},
|
|
title_key = "entry_settings",
|
|
comment_keys = { "entry_settings_comment_1", "entry_settings_comment_2" },
|
|
},
|
|
}
|
|
|
|
-- ja/zh titles can be the same string, so prefer the engine's reported language,
|
|
-- then fall back to a scan over every language.
|
|
local LANGS = { "ja", "en", "zh", "ko" }
|
|
local function find_mode(text_str)
|
|
local ok, cur = pcall(function() return tex.language() end)
|
|
if ok and type(cur) == "string" then
|
|
for key, m in pairs(MODES) do
|
|
if tex.skin_text(m.title_key, cur) == text_str then return m, cur end
|
|
end
|
|
end
|
|
for key, m in pairs(MODES) do
|
|
for _, lang in ipairs(LANGS) do
|
|
if tex.skin_text(m.title_key, lang) == text_str then return m, lang end
|
|
end
|
|
end
|
|
return MODES.settings, "ja", true
|
|
end
|
|
|
|
local function ease_out(t)
|
|
if t <= 0 then return 0 end
|
|
if t >= 1 then return 1 end
|
|
return 1 - (1 - t) * (1 - t)
|
|
end
|
|
|
|
local function clamp01(v)
|
|
if v <= 0 then return 0 end
|
|
if v >= 1 then return 1 end
|
|
return v
|
|
end
|
|
|
|
-- Board openness is a crossfade between the baked "off"/"on" plates, driven by
|
|
-- timelines sampled from anim/mode_board and anim/mode_list.
|
|
local sampler_ok, box_anim = pcall(require, "anim/sampler")
|
|
if not sampler_ok then
|
|
box_anim = { safe = function()
|
|
local n = {}
|
|
n.at = function() return nil end; n.get = function() return nil end
|
|
n.label = function() return nil end; n.has = function() return false end
|
|
n.first = function() return 0 end; n.last = function() return 0 end
|
|
return n
|
|
end }
|
|
end
|
|
local mode_board = box_anim.safe("anim/mode_board")
|
|
local CURSOR_GLOW = box_anim.safe("anim/cursor_glow")
|
|
local CURSOR_GLOW_LEN = math.max(1, CURSOR_GLOW:last() - CURSOR_GLOW:first() + 1) -- 60 frames = 1 s
|
|
local mode_list = box_anim.safe("anim/mode_list")
|
|
local LIST_WAIT = mode_list:label("wait") or 13
|
|
local LIST_IN = mode_list:label("in") or 87
|
|
local BOARD_SELECT_ON = mode_board:label("select_on") or 27
|
|
local BOARD_ON = mode_board:label("on") or 42
|
|
local BOARD_SELECT_OFF = mode_board:label("select_off") or 52
|
|
local BOARD_IN = mode_board:label("in") or 67
|
|
local BOARD_CHOOSE = mode_board:label("choose") or 117
|
|
-- centre band scaleY at the two rest states -> the 0..1 openness the rest of the file speaks
|
|
local BOARD_SY_CLOSED, BOARD_SY_OPEN = 0.1857, 1.0
|
|
|
|
-- Clamp u so a caller that passes a huge value stays on the segment's last frame.
|
|
local function board_seg(base, u, limit)
|
|
if u < 0 then u = 0 elseif u > limit then u = limit end
|
|
return base + u
|
|
end
|
|
local function board_open(f)
|
|
local sy = mode_board:get("board_bg_center_instance", f, "sy") or BOARD_SY_CLOSED
|
|
return clamp01((sy - BOARD_SY_CLOSED) / (BOARD_SY_OPEN - BOARD_SY_CLOSED))
|
|
end
|
|
|
|
-- select_on (label 27): board grows, title rises, comment fades in.
|
|
local ON_LIMIT = BOARD_SELECT_OFF - BOARD_SELECT_ON - 1
|
|
local OFF_LIMIT = BOARD_IN - BOARD_SELECT_OFF - 1
|
|
local IN_LIMIT = BOARD_CHOOSE - BOARD_IN - 1
|
|
local function on_open(u) return board_open(board_seg(BOARD_SELECT_ON, u, ON_LIMIT)) end
|
|
local function on_info(u) return mode_board:get("text_info_instance", board_seg(BOARD_SELECT_ON, u, ON_LIMIT), "a") or 0 end
|
|
-- select_off (label 52): comment fades out, board closes.
|
|
local function off_open(u) return board_open(board_seg(BOARD_SELECT_OFF, u, OFF_LIMIT)) end
|
|
local function off_info(u) return mode_board:get("text_info_instance", board_seg(BOARD_SELECT_OFF, u, OFF_LIMIT), "a") or 0 end
|
|
-- in (label 67): four staggered ramps (fade, open, contents, cursor glow).
|
|
local function in_board_fade(t) return mode_board:get("board_bg_center_instance", board_seg(BOARD_IN, t, IN_LIMIT), "a") or 0 end
|
|
local function in_open(t) return board_open(board_seg(BOARD_IN, t, IN_LIMIT)) end
|
|
local function in_contents(t) return mode_board:get("text_info_instance", board_seg(BOARD_IN, t, IN_LIMIT), "a") or 0 end
|
|
local function in_cursor(t) return mode_board:get("cursor_center", board_seg(BOARD_IN, t, IN_LIMIT), "a") or 0 end
|
|
local IN_TOTAL_F = IN_LIMIT
|
|
-- The list flies in: closed boards start three slots out and ease onto their slot.
|
|
local LIST_IN_DELAY_F, LIST_IN_F = 10, 12
|
|
local LIST_IN_DX, LIST_IN_DY = 170, 878
|
|
do
|
|
local tx, ty = mode_list:at("kanban_3", LIST_IN)
|
|
if tx then LIST_IN_DX, LIST_IN_DY = tx, ty end
|
|
end
|
|
-- choose (label 117): white board-shaped flash overlay.
|
|
local CHOOSE_LIMIT = mode_board:last() - BOARD_CHOOSE
|
|
local function choose_flash(c)
|
|
if c < 0 then return 0 end
|
|
if c > CHOOSE_LIMIT then return 0 end
|
|
return mode_board:get("#22@6", BOARD_CHOOSE + c, "a") or 0
|
|
end
|
|
|
|
-- Slot offsets are read from the exported timeline; the fallback series matches it.
|
|
local function kanban_of(rel)
|
|
if rel == 0 then return "kanban_1" end
|
|
local n = math.abs(rel)
|
|
return "kanban_" .. (2 * n + (rel < 0 and 0 or 1))
|
|
end
|
|
local function slot_offset(rel)
|
|
if rel == 0 then return 0, 0 end
|
|
local tx, ty = mode_list:at(kanban_of(rel), LIST_WAIT)
|
|
if not tx then -- more slots than the arcade list carries
|
|
local n, s = math.abs(rel), (rel < 0 and -1 or 1)
|
|
return s * (50 + (n - 1) * 40), s * (305 + (n - 1) * 191)
|
|
end
|
|
return tx, ty
|
|
end
|
|
|
|
function EntryBox.new(text_str, font_size)
|
|
local self = setmetatable({}, EntryBox)
|
|
|
|
if __hss_entry_reset then
|
|
__hss_entry_boxes = {}
|
|
__hss_entry_mode = { sel = 1, t0 = 0, in_t0 = nil }
|
|
__hss_entry_reset = false
|
|
end
|
|
__hss_entry_boxes[#__hss_entry_boxes + 1] = self
|
|
self.index = #__hss_entry_boxes
|
|
|
|
local mode, lang, unknown = find_mode(text_str)
|
|
self.mode = mode
|
|
self.title_outer = text.create_raw_text(text_str, TITLE_SIZE, { 0, 0, 0, 255 }, { 0, 0, 0, 255 }, false, TITLE_OUTER_T, TITLE_SPACING)
|
|
-- a second, thinner black pass fills the gaps the first leaves so the rim reads smooth
|
|
self.title_outer2 = text.create_raw_text(text_str, TITLE_SIZE, { 0, 0, 0, 255 }, { 0, 0, 0, 255 }, false, TITLE_OUTER_T - 3, TITLE_SPACING)
|
|
self.title = text.create_raw_text(text_str, TITLE_SIZE, { 255, 255, 255, 255 }, mode.outline, false, TITLE_INNER_T, TITLE_SPACING)
|
|
self.info = {}
|
|
if not unknown then
|
|
local lines = {}
|
|
for i, key in ipairs(mode.comment_keys) do lines[i] = tex.skin_text(key, lang) end
|
|
for i, line in ipairs(lines) do
|
|
self.info[i] = text.create_raw_text(line, INFO_SIZE, INFO_COLOR, INFO_OUTLINE, false,
|
|
INFO_BORDER / tex.get_screen_scale(), INFO_SPACING, 1.0, "FOT-Seurat_Pro_B.otf")
|
|
end
|
|
end
|
|
|
|
self.board = tex.get_texture("entry/mode_select/box") -- framed: see MAPPING.md
|
|
self.cursor = tex.get_texture("entry/mode_select/box_highlight_center") -- yellow glow ring
|
|
self.frame_flash = 8 -- white silhouette of the "on" board
|
|
self.frame_on, self.frame_off = mode.frame, mode.frame + 1
|
|
|
|
-- animated state (screen px relative to the list centre)
|
|
self.cur_x, self.cur_y = nil, nil
|
|
self.from_x, self.from_y = 0, 0
|
|
self.tgt_x, self.tgt_y = 0, 0
|
|
self.t0 = 0
|
|
self.open_t0, self.opening, self.was_selected = nil, false, false
|
|
self.choose_t0 = nil
|
|
|
|
return self
|
|
end
|
|
|
|
-- List slide: every board moves to its next slot linearly over SLIDE_F frames.
|
|
function EntryBox:_retarget(now, tx, ty, tvis)
|
|
if self.cur_x == nil then
|
|
self.cur_x, self.cur_y, self.cur_vis = tx, ty, tvis
|
|
self.tgt_x, self.tgt_y, self.tgt_vis = tx, ty, tvis
|
|
self.from_x, self.from_y, self.from_vis = tx, ty, tvis
|
|
self.t0 = now - SLIDE_F * FRAME_MS
|
|
return
|
|
end
|
|
if tx ~= self.tgt_x or ty ~= self.tgt_y or tvis ~= self.tgt_vis then
|
|
self.from_x, self.from_y, self.from_vis = self.cur_x, self.cur_y, self.cur_vis
|
|
self.tgt_x, self.tgt_y, self.tgt_vis = tx, ty, tvis
|
|
self.t0 = now
|
|
end
|
|
local p = clamp01((now - self.t0) / (SLIDE_F * FRAME_MS))
|
|
self.cur_x = self.from_x + (self.tgt_x - self.from_x) * p
|
|
self.cur_y = self.from_y + (self.tgt_y - self.from_y) * p
|
|
self.cur_vis = self.from_vis + (self.tgt_vis - self.from_vis) * p
|
|
end
|
|
|
|
-- Box::draw(x, left_x, right_x, is_selected, move_finished, open_attr, fade, y_pos).
|
|
-- `fade < 1` is the engine's decide signal (its entry animation), so it drives `choose`.
|
|
function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr, fade, y_pos)
|
|
local now = tex.get_current_ms()
|
|
local shared = __hss_entry_mode
|
|
if shared.in_t0 == nil then shared.in_t0 = now end -- first frame the list is drawn
|
|
__hss_entry_guide = { ms = now, phase = "mode", count = #__hss_entry_boxes }
|
|
if is_selected and shared.sel ~= self.index then
|
|
shared.sel = self.index
|
|
shared.t0 = now
|
|
end
|
|
|
|
-- The open runs after the slide, so delay select_on by the slide length.
|
|
if is_selected ~= self.was_selected then
|
|
self.was_selected = is_selected
|
|
self.open_t0 = now + (is_selected and SLIDE_F * FRAME_MS or 0)
|
|
self.opening = is_selected
|
|
end
|
|
|
|
local t_in = (now - shared.in_t0) / FRAME_MS -- frames since the list appeared
|
|
local in_running = t_in <= IN_TOTAL_F
|
|
|
|
local rel = self.index - shared.sel
|
|
local ox, oy = slot_offset(rel)
|
|
-- boards further than one slot away fade out
|
|
self:_retarget(now, ox, oy, (math.abs(rel) <= 1) and 1 or 0)
|
|
|
|
local dx, dy = self.cur_x, self.cur_y
|
|
-- list fly-in: the closed boards start three slots out and ease onto their slot
|
|
if in_running and rel ~= 0 then
|
|
local q = ease_out(clamp01((t_in - LIST_IN_DELAY_F) / LIST_IN_F))
|
|
local s = (rel < 0) and -1 or 1
|
|
dx = dx + (1 - q) * (s * LIST_IN_DX - ox)
|
|
dy = dy + (1 - q) * (s * LIST_IN_DY - oy)
|
|
end
|
|
|
|
-- openness (0 = "off" board, 1 = "on"); the `in` timeline owns it while running
|
|
local o, info_a, cursor_a
|
|
if in_running then
|
|
o = is_selected and in_open(t_in) or 0
|
|
info_a = is_selected and in_contents(t_in) or 0
|
|
cursor_a = is_selected and in_cursor(t_in) or 0
|
|
fade = (fade or 1.0) * in_board_fade(t_in)
|
|
else
|
|
local u = self.open_t0 and ((now - self.open_t0) / FRAME_MS) or 999
|
|
if self.opening then
|
|
o, info_a = on_open(u), on_info(u)
|
|
else
|
|
o, info_a = off_open(u), off_info(u)
|
|
end
|
|
cursor_a = o
|
|
fade = fade or 1.0
|
|
end
|
|
local vis_fade = fade * self.cur_vis
|
|
if vis_fade <= 0.001 then return end
|
|
|
|
-- the cursor glow sits under the board, so characters leaning out stand in front of it
|
|
if cursor_a > 0.001 then
|
|
local pulse = CURSOR_GLOW:get("#12@0", (now * 0.06) % CURSOR_GLOW_LEN, "a") or 1.0
|
|
tex.draw_texture(self.cursor, { x = dx, y = dy, fade = vis_fade * cursor_a * pulse })
|
|
end
|
|
if o < 0.999 then
|
|
tex.draw_texture(self.board, { frame = self.frame_off, x = dx, y = dy, fade = vis_fade * (1 - o) })
|
|
end
|
|
if o > 0.001 then
|
|
tex.draw_texture(self.board, { frame = self.frame_on, x = dx, y = dy, fade = vis_fade * o })
|
|
end
|
|
if info_a > 0.001 and #self.info > 0 then
|
|
local n = #self.info
|
|
for i, t in ipairs(self.info) do
|
|
local cy = CENTER_Y + dy + INFO_CY + (i - 1 - (n - 1) / 2) * INFO_LINE_H
|
|
t:draw({ x = CENTER_X + dx - (t.width - INFO_SPACING) / 2, y = cy - t.height / 2, fade = vis_fade * info_a })
|
|
end
|
|
end
|
|
|
|
-- title rides openness and, during `in`, fades in with the comment
|
|
local title_a = vis_fade
|
|
if in_running and is_selected then title_a = vis_fade * in_contents(t_in) end
|
|
local ty = TITLE_Y_OFF + (TITLE_Y_ON - TITLE_Y_OFF) * o
|
|
self.title_outer:draw({
|
|
x = CENTER_X + dx - (self.title_outer.width - TITLE_SPACING) / 2,
|
|
y = CENTER_Y + dy + ty - self.title_outer.height / 2,
|
|
fade = title_a,
|
|
})
|
|
self.title_outer2:draw({
|
|
x = CENTER_X + dx - (self.title_outer2.width - TITLE_SPACING) / 2,
|
|
y = CENTER_Y + dy + ty - self.title_outer2.height / 2,
|
|
fade = title_a,
|
|
})
|
|
self.title:draw({
|
|
x = CENTER_X + dx - (self.title.width - TITLE_SPACING) / 2,
|
|
y = CENTER_Y + dy + ty - self.title.height / 2,
|
|
fade = title_a,
|
|
})
|
|
|
|
-- `choose`: the picked board flashes white
|
|
if fade < 0.999 and is_selected then
|
|
if self.choose_t0 == nil then self.choose_t0 = now end
|
|
local fl = choose_flash((now - self.choose_t0) / FRAME_MS)
|
|
if fl > 0.001 then
|
|
tex.draw_texture(self.board, { frame = self.frame_flash, x = dx, y = dy, fade = fl })
|
|
end
|
|
end
|
|
end
|