318 lines
17 KiB
Lua
318 lines
17 KiB
Lua
-- YataiDON-HSS override of PyTaikoGreen/entry/costume_menu.lua
|
|
-- Arcade (reference cabinet) "きせかえをえらぼう!" layout, ported from
|
|
-- script_lua/entry/costume_select_one.lua (+ setting_base.lua for the red/teal panel and
|
|
-- common/ControlGuide.lua ControlGuideS for the えらぶ/決定 drum hint).
|
|
-- Texture NAMES are fixed by the engine (generated from PyTaikoGreen), so arcade art is stored
|
|
-- under parent names: box_center_1p/2p = panel, box_top_1p = pill, box_bottom_1p = drum hint.
|
|
-- entry/setting_base settingbase_1p_instance/board_instance -> box_center_1p / box_center_2p
|
|
-- (drawn by Scripts/entry/player.lua BEFORE the Don, so the Don stands
|
|
-- inside the panel like the arcade don1p at (498,480); this script signals
|
|
-- the open menu through __hss_costume_panel[side] = now_ms)
|
|
-- entry/costume costume_1p_instance (498,540): board_instance/select_instance at
|
|
-- (0,220): 5 icon_instance_N at x -200..+200 (pitch 100), arrows +-277,
|
|
-- text_title box 798x126 at (-398,-398) font 54 left-aligned (vertically
|
|
-- centred in the box -> y 205), guide_instance at (0,337).
|
|
-- costume_2p_instance is the same at (1422,540) => +924.
|
|
-- The engine's (x,y) offsets are NOT used for drawing: the arcade positions are absolute
|
|
-- per side (see skin_config_entry.json which sets entry_costume_menu_1p/2p to (0,0)/(924,0)
|
|
-- so the C++ costume_select_mode icon row lines up too). All numbers are 1080p pixels.
|
|
--
|
|
-- ROUND 11 — the ROW MODEL is now the arcade's (costume_select_one.lua UpdateIcon):
|
|
-- * the five icon slots are FIXED and the yellow cursor ring MOVES between them; the row
|
|
-- only scrolls (`select_offset_`) once the cursor is already at an end. Before this round
|
|
-- the ring was nailed to the centre slot and the whole list rotated under it.
|
|
-- * the ◀ / ▶ arrows are visibility flags, not decoration: `arrow_l` is hidden exactly when
|
|
-- the highlighted item is the first of the list, `arrow_r` when it is the last.
|
|
-- * cursor pulse, arrow drift and the board fade-in are sampled from the cabinet's own
|
|
-- timelines via Scripts/anim (ANIM_PIPELINE.md); all three fail soft.
|
|
-- The arcade's five items ARE our engine's `default / preset_1 / preset_2 / preset_3 /
|
|
-- random_item` (Don.kPreset_Default / Cool / Cute / Joke / Coice); `costume` and `head_body`
|
|
-- are YataiDON-only extras, which is why the row is 7 long and pages.
|
|
-- ROUND 12 — the five preset rows now WORK (C-cos-1/2/3 closed, engine side; see
|
|
-- ENGINE_BINDINGS.md). They dress the Don while highlighted and commit on don, gated on the
|
|
-- skin_config flag `costume_preset_slots`. Still not expressible: the no-wrap clamp (C-cos-5)
|
|
-- and the decide animation (C-cos-4).
|
|
|
|
local anim = require("anim/sampler")
|
|
|
|
CostumeMenu = {}
|
|
CostumeMenu.__index = CostumeMenu
|
|
|
|
local CX, CY = 498, 540 -- costume_1p_instance root placement
|
|
local SIDE_OFF_2P = 924 -- costume_2p_instance - costume_1p_instance
|
|
local PITCH = 100 -- icon_instance pitch
|
|
local PITCH_CPP = 80 -- C++ CostumeMenu::draw uses ITEM_W = 80 in costume_select_mode
|
|
local PILL_Y = CY + 220 -- 760
|
|
local ARROW_DX = 277
|
|
local SLOTS = 5 -- icon_instance_1..5
|
|
-- arcade text_title box (100..898, 142..268), font 54, left aligned, vertically centred
|
|
local TITLE_X, TITLE_Y, TITLE_SIZE = 100, 205, 54
|
|
local GUIDE_X, GUIDE_Y = CX, CY + 337 -- (498,877)
|
|
-- control_guide_s s_menfuchi_kp: select pill at -133, decide pill at +40, text +78 right of each pill
|
|
local GUIDE_SEL_DX, GUIDE_DEC_DX, GUIDE_TEXT_DY, GUIDE_SIZE = -133 + 78, 40 + 78, 0, 24
|
|
|
|
-- Cabinet timelines (entry/costume.nulm), exported by scratchpad/anim_export.py:
|
|
-- costume_cursor sprite 19 `cursor_anim_instance` loop 0..59 (1 s), decide 60..100
|
|
-- #13@0 = outer ring (alpha 1 -> 0 @f29 -> 1 @f58)
|
|
-- #15@1 = inner ring (alpha 1 -> 0.398 @f29 -> 1 @f58)
|
|
-- costume_arrow sprite 40 `arrow_*_instance` arrow_on 5..19, tx 0 -> 2.35 outward, looping
|
|
-- costume_board sprite 51 `costume_1p_instance` fade_in 5..15 (alpha 0 -> 1, scale 0.9 -> 1)
|
|
local CURSOR_ANIM = anim.safe("anim/costume_cursor")
|
|
local ARROW_ANIM = anim.safe("anim/costume_arrow")
|
|
local BOARD_ANIM = anim.safe("anim/costume_board")
|
|
local CURSOR_LOOP_F = 60 -- `decide` starts at 60; the loop is 0..59
|
|
local ARROW_F0, ARROW_LOOP_F = 5, 15
|
|
local BOARD_F0, BOARD_F1 = 5, 15
|
|
|
|
__hss_costume_panel = __hss_costume_panel or {}
|
|
-- ROUND 11 — the board fade-in also has to reach the red/teal panel, which player.lua draws
|
|
-- (before the Don, so the Don stands inside it). Alpha only: scaling the panel would need the
|
|
-- Don scaled and re-centred with it, and the arcade's 0.9 -> 1.0 board scale is one frame of
|
|
-- polish against that risk. Consumers must treat a missing entry as "fully opaque".
|
|
__hss_costume_fade = __hss_costume_fade or {}
|
|
-- ROUND 6 (item 36) — costume-change smoke. `costume_select_one.lua` StartSmoke() plays
|
|
-- `smoke_instance` "smoke_in" every time the Don's costume is swapped; in YataiDON the swap
|
|
-- happens in CostumeMenu::handle_input (EntryPlayer::handle_input rebuilds the Chara3D as soon
|
|
-- as `costume_icon_index` / `pick_stage` changes), and every one of those moves plays a hit
|
|
-- sound. Lua has no input hook, but `audio.is_sound_playing` is bound, so a rising edge on
|
|
-- don/kat while `costume_select_mode` is true is exactly the frame the costume changed.
|
|
-- Scripts/entry/player.lua draws the smoke over the Don from this timestamp.
|
|
__hss_costume_smoke = __hss_costume_smoke or {}
|
|
|
|
-- Engine order (CostumeMenu::ITEMS, costume_menu.h) — do NOT permute: `selected_index` moves
|
|
-- +-1 through THIS order, so any re-ordering of the row would make the cursor jump around it.
|
|
local ITEM_KEYS = {
|
|
"costume", "default", "head_body",
|
|
"preset_1", "preset_2", "preset_3", "random_item",
|
|
}
|
|
|
|
local function clamp(v, lo, hi)
|
|
if v < lo then return lo end
|
|
if v > hi then return hi end
|
|
return v
|
|
end
|
|
|
|
function CostumeMenu.new(is_2p, text_str, title_x, title_y)
|
|
local self = setmetatable({}, CostumeMenu)
|
|
self.is_2p = is_2p
|
|
self.side = is_2p and 1 or 0
|
|
self.off = is_2p and SIDE_OFF_2P or 0
|
|
local suffix = is_2p and "2p" or "1p"
|
|
|
|
self.panel = tex.get_texture("entry/costume_select/box_center_" .. suffix) -- drawn by player.lua
|
|
self.pill = tex.get_texture("entry/costume_select/box_top_1p") -- icon-row pill
|
|
self.guide = tex.get_texture("entry/costume_select/box_bottom_1p") -- えらぶ/決定 drum hint
|
|
self.item_box = tex.get_texture("entry/costume_select/item_box_" .. suffix)
|
|
self.item_box_hl = tex.get_texture("entry/costume_select/item_box_highlighted")
|
|
self.blue_arrow = tex.get_texture("entry/costume_select/blue_arrow")
|
|
self.th_info = tex.get_texture_info("costume_select", "text_highlight_" .. suffix)
|
|
|
|
self.items = {}
|
|
for _, key in ipairs(ITEM_KEYS) do
|
|
local id = tex.get_texture("entry/costume_select/" .. key)
|
|
local info = tex.get_texture_info("costume_select", key)
|
|
self.items[#self.items + 1] = {
|
|
id = id,
|
|
width = info and info.x2[1] or 84,
|
|
height = info and info.y2[1] or 84,
|
|
}
|
|
end
|
|
|
|
self.ib_info = tex.get_texture_info("costume_select", "item_box_" .. suffix)
|
|
self.hl_info = tex.get_texture_info("costume_select", "item_box_highlighted")
|
|
self.arr_info = tex.get_texture_info("costume_select", "blue_arrow")
|
|
|
|
-- arcade: font 54 white, outline 6 (renderer default = black); thickness here is x1.5 on screen
|
|
self.title_text = text.create_raw_text(text_str, TITLE_SIZE, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2.0)
|
|
-- guide words from skin_config (guide_select_label / guide_decide_label)
|
|
self.guide_sel = text.create_raw_text(tex.skin_text("guide_select_label"), GUIDE_SIZE, { 30, 30, 30, 255 }, { 30, 30, 30, 255 }, false, 0, 1.0)
|
|
self.guide_dec = text.create_raw_text(tex.skin_text("guide_decide_label"), GUIDE_SIZE, { 30, 30, 30, 255 }, { 30, 30, 30, 255 }, false, 0, 1.0)
|
|
|
|
self.costume_select_mode = false
|
|
-- arcade cursor_idx_ (1..5, here 0-based) and select_offset_
|
|
self.cursor = 0
|
|
self.offset = 0
|
|
self.prev_index = nil
|
|
self.now_ms = tex.get_current_ms()
|
|
self.open_ms = self.now_ms
|
|
__hss_costume_panel[self.side] = self.now_ms
|
|
__hss_costume_fade[self.side] = 0
|
|
return self
|
|
end
|
|
|
|
function CostumeMenu:update(current_ms)
|
|
self.now_ms = current_ms
|
|
-- keep the panel flag alive while this menu exists (player.lua draws the panel under the Don)
|
|
__hss_costume_panel[self.side] = current_ms
|
|
|
|
-- costume-change smoke (see the __hss_costume_smoke note above)
|
|
local hit = audio.is_sound_playing("don") or audio.is_sound_playing("kat")
|
|
if self.costume_select_mode then
|
|
if not self.was_select_mode then
|
|
-- entering the icon picker rebuilds the Chara3D straight away (chara_index = -1)
|
|
__hss_costume_smoke[self.side] = current_ms
|
|
elseif hit and not self.hit_prev then
|
|
__hss_costume_smoke[self.side] = current_ms
|
|
end
|
|
end
|
|
self.was_select_mode = self.costume_select_mode
|
|
self.hit_prev = hit
|
|
end
|
|
|
|
-- board fade-in: `costume_1p_instance` label fade_in (f5..f15), alpha only (see __hss_costume_fade)
|
|
function CostumeMenu:board_fade()
|
|
local f = BOARD_F0 + (self.now_ms - self.open_ms) * 0.06
|
|
if f >= BOARD_F1 then return 1.0 end
|
|
local _, a = BOARD_ANIM:at("board_instance", f)
|
|
if a == nil then return 1.0 end
|
|
return a
|
|
end
|
|
|
|
-- arcade UpdateIcon: the icons sit still and the cursor moves; the window only scrolls when
|
|
-- the cursor is already parked at an end. `selected_index` is the engine's absolute item index
|
|
-- and it WRAPS (`(i +- 1 + n) % n`), which the arcade never does — a wrap therefore snaps the
|
|
-- window to the corresponding end instead of stepping.
|
|
function CostumeMenu:track_cursor(selected_index, n)
|
|
local max_offset = math.max(0, n - SLOTS)
|
|
local prev = self.prev_index
|
|
if prev == nil then
|
|
self.offset = clamp(selected_index - 2, 0, max_offset)
|
|
elseif selected_index == prev then
|
|
-- nothing moved
|
|
elseif selected_index == (prev + 1) % n then
|
|
if prev == n - 1 then -- wrapped last -> first
|
|
self.offset = 0
|
|
elseif selected_index > self.offset + SLOTS - 1 then
|
|
self.offset = self.offset + 1
|
|
end
|
|
elseif selected_index == (prev - 1 + n) % n then
|
|
if prev == 0 then -- wrapped first -> last
|
|
self.offset = max_offset
|
|
elseif selected_index < self.offset then
|
|
self.offset = self.offset - 1
|
|
end
|
|
else -- unexpected jump: re-centre
|
|
self.offset = clamp(selected_index - 2, 0, max_offset)
|
|
end
|
|
self.offset = clamp(self.offset, 0, max_offset)
|
|
self.cursor = clamp(selected_index - self.offset, 0, SLOTS - 1)
|
|
self.offset = selected_index - self.cursor -- keep offset+cursor == selected_index
|
|
self.prev_index = selected_index
|
|
end
|
|
|
|
-- centre of slot i (0..4) in screen px
|
|
function CostumeMenu:slot_center(i)
|
|
if self.costume_select_mode then
|
|
-- match the C++ icon row: base = item_box x + engine x, ITEM_W = 80
|
|
local base = self.ib_info.x[1] + self.engine_x
|
|
return base + i * PITCH_CPP + PITCH_CPP / 2, self.ib_info.y[1] + self.engine_y + PITCH_CPP / 2
|
|
end
|
|
return CX + self.off + (i - 2) * PITCH, PILL_Y
|
|
end
|
|
|
|
-- ROUND 12 — `preset_seq` is the engine's 5th draw_bg argument (CostumeMenu::draw), added
|
|
-- with the working preset/random slots. It bumps every time a preset row dressed the Don:
|
|
-- landing on `default/preset_1..3` (a one-off change, the set's draw is sticky for the visit),
|
|
-- landing on `random_item` (a FRESH uniform draw over the whole wardrobe every single time,
|
|
-- so the Don visibly changes outfit on each landing), and leaving the row for `costume` /
|
|
-- `head_body` (the preview is dropped and the Don goes back to his saved outfit).
|
|
-- The arcade fires `smoke_instance` "smoke_in" on exactly those `UpdateDon()` calls, so the
|
|
-- seq edge drives the same smoke the sub-list already uses. Skins that predate the argument
|
|
-- just get `nil` here, which is why every use is guarded.
|
|
function CostumeMenu:draw_bg(x, y, selected_index, costume_select_mode, preset_seq)
|
|
if preset_seq and preset_seq ~= self.preset_seq then
|
|
if self.preset_seq ~= nil then
|
|
__hss_costume_smoke[self.side] = tex.get_current_ms()
|
|
end
|
|
self.preset_seq = preset_seq
|
|
end
|
|
self.costume_select_mode = costume_select_mode
|
|
self.engine_x, self.engine_y = x, y
|
|
self.now_ms = tex.get_current_ms()
|
|
__hss_costume_panel[self.side] = self.now_ms
|
|
|
|
local fade = self:board_fade()
|
|
__hss_costume_fade[self.side] = fade
|
|
|
|
local n = #self.items
|
|
if costume_select_mode then
|
|
-- the engine's own costume list: C++ draws a wrapping 5-wide window CENTRED on
|
|
-- costume_icon_index, so the ring belongs on the middle slot there.
|
|
self.cursor, self.offset, self.prev_index = 2, 0, nil
|
|
else
|
|
self:track_cursor(selected_index, n)
|
|
end
|
|
|
|
-- the panel itself is drawn by player.lua (under the Don); see header
|
|
tex.draw_texture(self.pill, { x = self.off, fade = fade })
|
|
|
|
local hw = self.ib_info.x2[1] / 2
|
|
local hlw = self.hl_info.x2[1] / 2
|
|
-- cursor_anim `loop`: free-running 60-frame pulse, not restarted by a cursor move.
|
|
-- Our ring is ONE baked texture (outer #13 + inner #15 unioned), so it follows the inner
|
|
-- ring's curve — the one that never reaches 0 — and the opaque hexagon base is drawn
|
|
-- underneath it so only the yellow rim breathes (LUA_CAPABILITIES C-cos-6).
|
|
local pulse = CURSOR_ANIM:at("#15@1", (self.now_ms * 0.06) % CURSOR_LOOP_F) or 1.0
|
|
for i = 0, SLOTS - 1 do
|
|
local cx, cy = self:slot_center(i)
|
|
tex.draw_texture(self.item_box, { x = cx - hw - self.ib_info.x[1], y = cy - hw - self.ib_info.y[1], fade = fade })
|
|
if i == self.cursor then
|
|
tex.draw_texture(self.item_box_hl, {
|
|
x = cx - hlw - self.hl_info.x[1], y = cy - hlw - self.hl_info.y[1],
|
|
fade = fade * pulse,
|
|
})
|
|
end
|
|
end
|
|
|
|
if not costume_select_mode then
|
|
for i = 0, SLOTS - 1 do
|
|
local item = self.items[self.offset + i + 1]
|
|
if item and item.id then
|
|
local cx, cy = self:slot_center(i)
|
|
tex.draw_texture(item.id, { x = cx - item.width / 2, y = cy - item.height / 2, fade = fade })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function CostumeMenu:draw_fg(x, y, name)
|
|
local fade = __hss_costume_fade[self.side] or 1.0
|
|
-- arrow_on: a looping 15-frame outward drift of 2.35 px, alpha 1 (NOT a fade pulse).
|
|
local drift = ARROW_ANIM:at("#38@0", ARROW_F0 + (self.now_ms * 0.06) % ARROW_LOOP_F) or 0.0
|
|
-- arcade UpdateIcon: each arrow is hidden exactly when there is nothing further that way.
|
|
local index = self.offset + self.cursor
|
|
local show_l = self.costume_select_mode or index > 0
|
|
local show_r = self.costume_select_mode or index < #self.items - 1
|
|
-- texture.json already puts the arrow at the right end (CX+277); x below is the delta
|
|
if show_r then
|
|
tex.draw_texture(self.blue_arrow, { x = self.off + drift, fade = fade })
|
|
end
|
|
if show_l then
|
|
tex.draw_texture(self.blue_arrow, { mirror = "horizontal", x = self.off - 2 * ARROW_DX - drift, fade = fade })
|
|
end
|
|
|
|
-- title, top-left inside the panel (arcade text box 100..898 x 142..268, left aligned)
|
|
self.title_text:draw({ x = TITLE_X + self.off, y = TITLE_Y - self.title_text.height / 2, fade = fade })
|
|
|
|
-- えらぶ / 決定 drum hint under the panel
|
|
tex.draw_texture(self.guide, { x = self.off, fade = fade })
|
|
local gx, gy = GUIDE_X + self.off, GUIDE_Y + GUIDE_TEXT_DY
|
|
self.guide_sel:draw({ x = gx + GUIDE_SEL_DX - self.guide_sel.width / 2, y = gy - self.guide_sel.height / 2, fade = fade })
|
|
self.guide_dec:draw({ x = gx + GUIDE_DEC_DX - self.guide_dec.width / 2, y = gy - self.guide_dec.height / 2, fade = fade })
|
|
|
|
-- name banner for the icon under the cursor (costume/head/body pick only) — text only,
|
|
-- text_highlight's own graphic was never drawn here (C++ used it purely as a position anchor)
|
|
if self.th_info then
|
|
local banner_cx, banner_cy = self.th_info.x[1] + x, self.th_info.y[1] + y
|
|
if name then
|
|
if self.name_str ~= name then
|
|
self.name_str = name
|
|
self.name_text = text.create_raw_text(name, 32, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false)
|
|
end
|
|
self.name_text:draw({ x = banner_cx - self.name_text.width / 2, y = banner_cy - self.name_text.height / 2, fade = fade })
|
|
else
|
|
self.name_str, self.name_text = nil, nil
|
|
end
|
|
end
|
|
end
|