-- 演奏オプション / 音色 board — replaces ModifierSelector::draw() / NeiroSelector::draw(). -- Same texture.json bases and skin_config keys the C++ board used, so geometry is exact. -- Split out of song_select.lua. local base = require("song_select/file_navigator/box_base") local SKIN = { modifier_offset = { y = 61 }, modifier_offset_2 = { x = 44, y = 140 }, modifier_text = { x = 300, y = 1178, font_size = 26 }, option_p2 = { x = 1490 }, } local get_any = base.get_any local outline = base.outline local OPT_CURSOR_MS = 1000 -- cursor loop = 60 frames @ 60 fps local OPT_CURSOR_MIN = 51 / 256 -- its alpha floor local OPT_GREY = { 166, 168, 171, 255 } local OPT_CHANGED = { 255, 255, 0, 255 } local OPT_SCRIM = { 0, 0, 0, 128 } local OPT_ARROW_NUDGE = 5 function SongSelect:_opt_ids() if self._opt_ids_t then return self._opt_ids_t end -- Must load these subsets here: script_manager.tex is its own TextureWrapper, so a -- subset the engine loaded elsewhere is not resident in the Lua one. tex.load_folder("song_select", "modifier") tex.load_folder("song_select", "neiro") local t = {} t.top = get_any("modifier", "top") t.bottom = get_any("modifier", "bottom") t.bg = get_any("modifier", "background") t.row = get_any("modifier", "mod_bg") t.row_hl = get_any("modifier", "mod_bg_highlight") t.box = get_any("modifier", "mod_box") t.arrow = get_any("modifier", "blue_arrow") t.p = { get_any("modifier", "1p"), get_any("modifier", "2p") } t.icon = { auto = get_any("modifier", "mod_auto"), speed = get_any("modifier", "mod_baisaku"), sanbai = get_any("modifier", "mod_sanbai"), yonbai = get_any("modifier", "mod_yonbai"), speed_by_value = { [11] = get_any("modifier", "mod_speed_x1_1"), [12] = get_any("modifier", "mod_speed_x1_2"), [13] = get_any("modifier", "mod_speed_x1_3"), [14] = get_any("modifier", "mod_speed_x1_4"), [15] = get_any("modifier", "mod_speed_x1_5"), [16] = get_any("modifier", "mod_speed_x1_6"), [17] = get_any("modifier", "mod_speed_x1_7"), [18] = get_any("modifier", "mod_speed_x1_8"), [19] = get_any("modifier", "mod_speed_x1_9"), [20] = get_any("modifier", "mod_speed_x2"), [25] = get_any("modifier", "mod_speed_x2_5"), [30] = get_any("modifier", "mod_speed_x3"), [35] = get_any("modifier", "mod_speed_x3_5"), [40] = get_any("modifier", "mod_speed_x4"), }, display = get_any("modifier", "mod_doron"), inverse = get_any("modifier", "mod_abekobe"), kimagure = get_any("modifier", "mod_kimagure"), detarame = get_any("modifier", "mod_detarame"), } self._opt_ids_t = t return t end -- Row label / value strings as OutlinedText, cached by content. function SongSelect:_opt_text(s) if s == nil or s == "" then return nil end if not self._opt_texts then self._opt_texts = {} local cfg = SKIN.modifier_text self._opt_font = (cfg and cfg.font_size ~= 0 and cfg.font_size) or 26 end local t = self._opt_texts[s] if t == nil then t = text.create_raw_text(s, self._opt_font, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, outline(3.0), 2.0) or false self._opt_texts[s] = t end return t or nil end -- Arcade cursor pulse, sampled from anim/option_cursor; hand triangle is the fallback. local option_cursor_ok, option_cursor_anim = pcall(require, "anim/sampler") local OPT_CURSOR_CLIP = (option_cursor_ok and option_cursor_anim.safe("anim/option_cursor")) or { get = function() return nil end, first = function() return 0 end, last = function() return 0 end } local OPT_CURSOR_LEN = math.max(1, OPT_CURSOR_CLIP:last() - OPT_CURSOR_CLIP:first() + 1) local function opt_cursor_alpha(ms) local f = (ms * 0.06) % OPT_CURSOR_LEN local a = OPT_CURSOR_CLIP:get("#3@0", f, "a") if a then return a end local u = (ms % OPT_CURSOR_MS) / OPT_CURSOR_MS local tri = (u < 0.5) and (u * 2) or ((1 - u) * 2) return 1.0 - (1.0 - OPT_CURSOR_MIN) * tri end -- One board. `rows` = {label, value, changed, greyed, name}; `cursor` = 1-based row (nil = none). function SongSelect:draw_option_board(player, offset, rows, cursor, change) local ids = self:_opt_ids() if not ids.top or not ids.row or not ids.box then return end local p = player:player_num() local p2cfg = SKIN.option_p2 local x = (p - 1) * ((p2cfg and p2cfg.x) or 1490) local pitch = SKIN.modifier_offset pitch = (pitch and pitch.y ~= 0 and pitch.y) or 61 local tcfg = SKIN.modifier_text local ocfg = SKIN.modifier_offset_2 local text_x, text_y = (tcfg and tcfg.x) or 300, (tcfg and tcfg.y) or 1178 local name_x = (ocfg and ocfg.x) or 44 local arrow_span = (ocfg and ocfg.y) or 140 local n = #rows local ms = tex.get_current_ms() tex.draw_texture(ids.top, { x = x, y = offset }) if ids.p[p] then tex.draw_texture(ids.p[p], { x = x, y = offset }) end if ids.bottom then tex.draw_texture(ids.bottom, { x = x, y = offset + n * pitch }) end for i = 1, n do local row = rows[i] local row_y = offset + (i - 1) * pitch local is_cur = (cursor == i) if ids.bg then tex.draw_texture(ids.bg, { x = x, y = row_y }) end tex.draw_texture(ids.row, { x = x, y = row_y }) if row.greyed then -- arcade "grayout": flat grey laid on top of the row plate if ids.row_hl then tex.draw_texture(ids.row_hl, { x = x, y = row_y, color = OPT_GREY }) end elseif is_cur and ids.row_hl then tex.draw_texture(ids.row_hl, { x = x, y = row_y, fade = opt_cursor_alpha(ms) }) end tex.draw_texture(ids.box, { x = x, y = row_y, color = row.changed and OPT_CHANGED or nil }) local label = self:_opt_text(row.label) if label then label:draw({ x = name_x + x, y = text_y + row_y }) end local value = self:_opt_text(row.value) if value then value:draw({ x = math.floor(text_x - value.width / 2 + 0.5) + x, y = text_y + row_y }) end -- engine row icon; state is the raw value so it never depends on the localised string local icon local st = row.state or 0 if row.name == "speed" then if st > 10 then local best for v, id in pairs(ids.icon.speed_by_value or {}) do if id and v <= st and (not best or v > best) then best = v end end icon = best and ids.icon.speed_by_value[best] or nil end if not icon then if st >= 40 then icon = ids.icon.yonbai elseif st >= 30 then icon = ids.icon.sanbai elseif st > 10 then icon = ids.icon.speed end end elseif row.name == "random" then if st == 2 then icon = ids.icon.detarame elseif st == 1 then icon = ids.icon.kimagure end elseif row.changed then icon = ids.icon[row.name] end if icon then tex.draw_texture(icon, { x = x, y = row_y }) end if is_cur and ids.arrow then local nl, nr = 0, 0 if change and change.active then local amp = OPT_ARROW_NUDGE * math.max(0, math.min(1, change.fade)) if change.dir > 0 then nl = amp else nr = amp end end tex.draw_texture(ids.arrow, { x = x - nl, y = row_y }) tex.draw_texture(ids.arrow, { mirror = "horizontal", x = x + arrow_span + nr, y = row_y }) end if row.greyed then tex.draw_texture(ids.row, { x = x, y = row_y, color = OPT_SCRIM }) end end end -- Engine entry point. kind: 1 = 演奏オプション, 2 = the standalone 音色 panel. function SongSelect:draw_option_panel(player, kind) if kind == 1 then local rows = player.modifier_rows and player:modifier_rows() local off = player.modifier_offset and player:modifier_offset() if not rows or not off then return end local cursor = player.modifier_index and player:modifier_index() or nil if cursor and (cursor < 1 or cursor > #rows) then cursor = nil end if player.modifier_confirmed and player:modifier_confirmed() then cursor = nil end self._mod_off[player:player_num()] = off self:draw_option_board(player, off, rows, cursor, player.modifier_change and player:modifier_change()) else local names = player.neiro_names and player:neiro_names() local off = player.neiro_offset and player:neiro_offset() if not names or not off then return end local idx = (player.neiro_index and player:neiro_index()) or 1 local rows = { { name = "neiro", state = idx - 1, value = names[idx] or "", changed = (idx ~= 1), greyed = false } } self:draw_option_board(player, off, rows, 1, nil) end end