Animations: follow the engine's merged move animations #39

Merged
Yonokid merged 2 commits from GanYiZhong/YataiDONNijiiro:fix/merged-move-animations into main 2026-09-30 21:54:19 +02:00
72 changed files with 752 additions and 4666 deletions
Showing only changes of commit afa111e474 - Show all commits

No files matched your search

+2 -3
View File
@@ -41,12 +41,11 @@
"dan_select_rank_plate": true,
"entry_credit_arcade": true,
"dan_confirm_yes_left": false,
"songselect_2p_join": true,
"costume_preset_slots": true,
"dan_title_horizontal": true,
"combo_color_tiers": true,
"gauge_cell_fade_in": true,
"dan_exam_grid": true
"dan_exam_grid": true,
"disable_chara_3d": false
}
},
"score_counter_pos": {
+7 -49
View File
@@ -1,24 +1,4 @@
-- Scripts/anim/sampler.lua — read the tables scratchpad/anim_export.py emits.
--
-- ROUND 7. Every other file in this directory is GENERATED: one row per kept
-- arcade frame, per named MovieClip, straight out of the cabinet's own Lumen
-- timeline (see ANIM_PIPELINE.md). The exporter guarantees that between two
-- kept rows every field is LINEAR, so sampling is a plain lerp and nothing here
-- has to know what curve the animator drew.
--
-- local anim = require("anim/sampler")
-- local board = anim.load("anim/folder_board")
-- local tx, ty, sy, a = board:at("instance_board_center", f) -- f in arcade frames
-- local f0 = board:label("select_off") -- label -> frame
--
-- `f` may be fractional (the engine's clock is ms, not frames): pass
-- `anim.frames(ms)` or do `ms * 0.06` yourself. Frames before the first row
-- clamp to it and frames after the last row clamp to that, which is exactly what
-- a Lumen timeline parked on its last frame does.
--
-- `:at` returns the fields in the table's own `fields` order. A track that does
-- not exist returns nothing, so a caller can `if not tx then return end` instead
-- of crashing a screen over a renamed clip.
-- Sampler for the exported clip tables: lerp between kept frames, clamp to the ends.
local M = {}
@@ -33,11 +13,7 @@ local FPS_MS = 60.0 / 1000.0 -- ms -> arcade frames
function M.frames(ms) return ms * FPS_MS end
function M.ms(frames) return frames / FPS_MS end
-- ROUND 53 (r53-anim-lumen-sweep): the module-level M.frames/M.ms assume 60 fps, which is
-- true of every clip exported so far -- but each generated table carries its OWN `fps`
-- field from the .nulm container, and R49's audit flagged that this file ignored it. A
-- clip whose container says anything other than 60 must be sampled through these
-- per-clip converters instead of M.frames/M.ms.
-- per-clip converters honour the table's own fps field
function Clip:frames(ms) return ms * (self.fps or 60) / 1000.0 end
function Clip:msof(frames) return frames * 1000.0 / (self.fps or 60) end
@@ -48,18 +24,13 @@ local function seek(rows, f)
if f >= rows[hi][1] then return hi end
while lo < hi do
local mid = math.floor((lo + hi + 1) / 2)
-- `hi = mid - 1`, NOT `hi = mid`: with hi == lo + 1 the upper-biased midpoint IS
-- hi, so `hi = mid` never shrinks the window and the loop spins forever (it hung
-- the check harness before this line was right; on the cabinet it would have
-- frozen the screen).
-- upper-biased midpoint: must decrement hi or the loop never shrinks
if rows[mid][1] <= f then lo = mid else hi = mid - 1 end
end
return lo
end
-- Lua 5.1 spells it `unpack`, 5.2+ `table.unpack`; the engine's sol2 build is 5.4 but the
-- fallback costs nothing and an `and/or` chain here would be wrong (a nil first field
-- would silently take the other branch).
-- unpack is `table.unpack` in 5.2+; fall back to the global otherwise
local tunpack = table.unpack
if tunpack == nil then tunpack = unpack end
@@ -97,21 +68,8 @@ function Clip:has(name) return self.tracks[name] ~= nil end
function Clip:first() return self.range[1] end
function Clip:last() return self.range[2] end
-- ROUND 8 — FAIL SOFT. A generated table is the churn-prone part of this pipeline: it is
-- rewritten by a script, it can be renamed, and a screen must never go down because one of
-- them is missing or malformed. `M.safe` therefore ALWAYS returns something with the Clip
-- interface: the real clip, or this NULL clip, whose readers return nil and whose range is
-- empty. Every call site in the skin already ends in `or <the previous constant>` (or
-- tests the first return for nil), so a null clip degrades to the pre-pipeline behaviour
-- rather than raising.
--
-- ALSO A TRAP WORTH KEEPING: the engine indexes skin scripts FLAT, by file STEM
-- (`src/libs/script.cpp` `index_scripts`), across every Scripts/ subfolder, first one wins,
-- and `anim/` sorts before every screen folder. A table named `anim/result_crown.lua`
-- therefore SHADOWED `result/result_crown.lua`, so `preload("ResultCrown", "result_crown")`
-- loaded the data table, the `ResultCrown` global was never defined, and every frame of the
-- result screen threw. A file in this directory MUST NOT share its stem with any screen
-- script in this skin or its parent -- that one is now `anim/crown_mc.lua`.
-- M.safe returns the real clip or this NULL clip (readers return nil / empty range).
-- Trap: files here must not share a stem with any screen script (flat indexing, first wins).
local NULL = {}
NULL.__index = NULL
function NULL:at() return nil end
@@ -120,7 +78,7 @@ function NULL:label() return nil end
function NULL:has() return false end
function NULL:first() return 0 end
function NULL:last() return 0 end
function NULL:frames(ms) return ms * FPS_MS end -- ROUND 53: keep NULL's interface complete
function NULL:frames(ms) return ms * FPS_MS end -- keep NULL's interface complete
function NULL:msof(frames) return frames / FPS_MS end
M.NULL = setmetatable({}, NULL)
+1 -13
View File
@@ -1,16 +1,4 @@
-- Scripts/anim/title_idle_map.lua -- HAND-WRITTEN companion to the generated
-- anim/title_idle table (ROUND 54, r54-anim-engine-referrals).
--
-- Read by the ENGINE (src/libs/sample_table.cpp get_clip_map ->
-- src/objects/title/title_idle.cpp): the tracks of attract/title.nulm sprite 84
-- to draw each frame, in display-list depth order, with the baked shape texture
-- (Graphics/title/camera/idle_c<char>.png, lumen_shape_dump -s <rec> -l <style>)
-- and the shape-space bbox origin of that bake. Entry = {track, texture, ox, oy}.
--
-- Named-instance simplification (documented divergence): logo_lang/_light1/_light2
-- and copyright_instance free-run their 20-frame language sprites in the dump, so
-- they are drawn as the PARENT track transform x the fixed jpn shape (c58/c67).
-- The lights are the same logo shape at animated scale/alpha = the cabinet glow.
-- Track-to-texture draw order for the title idle animation (read by the engine).
return {
table = "title_idle",
intro = { 0, 59 }, -- played once from scene start
+2 -157
View File
@@ -56,39 +56,9 @@ function Background.new(player_num, bpm, scene_preset)
self.arcade_gauge = ArcadeGauge.new()
-- child-only: 演奏スキップ countdown card / dim / banner.
-- Fed by Background:handle_skip, which GameScreen::push_skip_state calls
-- every frame once the feature is armed.
--
-- ROUND 19 (r19-skip2): the arcade own post-info draw list
-- (datatable/enso_post.bin, target 0/2/3/4/5/6/7 -- every single-player
-- enso target) places `skip.nulstb` (part 30) right after `song_info.nulstb`
-- (part 16) and ahead of only `papamama.nulstb`, i.e. AFTER the title/genre
-- plate and the soul gauge, not before them. So the skip panel (card,
-- numeral, dim, banner -- one Lumen subtree, drawn as one unit at that one
-- point in the list) has to sit on top of the whole HUD for the whole time
-- it is alive, not just draw_fore. `allnet_indicator.lua`
-- (global/allnet_indicator.lua) is the engine last Lua paint hook on GAME
-- (game.cpp draw_overlay: it runs after song_info.draw(), draw_skip(), the
-- transitions), so it is the only place this skin can put something that
-- actually lands on top of song_info and the gauge. `self.enso_skip` is
-- published on the shared Lua globals table for that hook to pick up and
-- draw (all skin scripts run in one sol::state -- ScriptManager::init /
-- LuaScript::load -- so a plain global is visible everywhere); it is
-- cleared in Background:destroy so a stale reference cannot survive into
-- the next screen.
self.enso_skip = EnsoSkip.new()
_G.g_active_enso_skip = self.enso_skip
-- ROUND 52 (r52-lua-divergence-fixes, user bug report 「兩個人的狀態下,
-- collab會出現graphic bug」): a SCENEPRESET song in 2P used to build the
-- collab rig anyway, but every collab background is a 1P LAYOUT -- one
-- full-height stage, dancers at 1P-stage y, a single top band -- with no
-- knowledge of game_2p_offset or the dual donbg bands, so the idol sprite
-- drew clipped behind the 2P lane and the collab's top band landed
-- corrupted over the 2P chrome. The skin's 2P background design is the
-- dual-band donbg rig and nothing else (ROUND 18/49 audit: "2P branch: no
-- bg objects by design"), so 2P now always routes there; a real 2P collab
-- stage rig is future work (needs per-collab 2P geometry).
local entry = (not self.is_2p) and scene_preset and COLLABS[scene_preset] or nil
if entry then
local ctor, path, max_dancers = entry[1], entry[2], entry[3]
@@ -119,11 +89,6 @@ function Background.new(player_num, bpm, scene_preset)
self.footer = Footer.new("background", math.random(0, 2))
end
-- ROUND 58 (r58-2p-background-polish): the cabinet's 2P enso_post set
-- (the reference build target 1) carries TWO renda_effect instances of the SAME
-- clip -- playerNo 0 at y=0 and playerNo 1 at y=804 (the 2P donbg
-- band; = game_2p_offset in skin space). Each seat gets its own
-- 25-slot pool, and a P2 roll rains into the P2 band.
local renda_index = math.random(0, 2)
self.renda = RendaController.new("background", renda_index)
if self.is_2p then
@@ -132,9 +97,6 @@ function Background.new(player_num, bpm, scene_preset)
self.chibi = ChibiController.new(bpm, "background")
self.is_clear = false
self.is_rainbow = false
-- ROUND 18 (r18-misc): the troupe size is an absolute function of the
-- gauge (see handle_gauge), not a milestone counter, so there is no
-- `last_milestone` to keep in step any more.
self.clear_progress = nil
end
@@ -191,8 +153,6 @@ function Background:handle_drumroll(player_num)
if self.collab then
self.collab:handle_drumroll(player_num)
else
-- ROUND 58: seat-routed in 2P (arcade target 1 has one renda_effect
-- instance per playerNo; P2's lives in the bottom band).
if player_num == 2 and self.renda_2 then
self.renda_2:add_renda()
else
@@ -201,57 +161,22 @@ function Background:handle_drumroll(player_num)
end
end
-- ROUND 52 (r52-lua-divergence-fixes): balloons and kusudama no longer spawn
-- renda effects. the reference build `EnsoGraphicRendaEffect::Process` (0x14012BDA0) arms
-- its per-frame effect start ONLY while an active note of kind 6 or 9 is in
-- flight, and the the reference build note-kind table (tlb research enso_options.md:258) is
-- explicit: "6/9 drumroll, 0xA balloon, 0xC kusudama" -- so the effect family
-- belongs to DRUMROLLS alone. The engine's handle_balloon covers both the
-- balloon and the kusudama (player.cpp check_balloon/check_kusudama), neither
-- of which the cabinet decorates. Kept as a delegate for collabs that define
-- their own handler.
function Background:handle_balloon(player_num)
if self.collab then
self.collab:handle_balloon(player_num)
end
end
-- ROUND 14 (r14-dan). `Background` is a WRAPPER that delegates to self.collab, and
-- the engine binds its hooks off THIS object (Background.cpp reads
-- lua_object["handle_dan"]). A handle_dan defined only on DanBackground is
-- therefore invisible to the engine -- `wants_dan()` returns false and the
-- binding never fires at all. This delegate is what makes it reachable; it is
-- additive and no-ops for every collab that does not define the method.
-- See ENGINE_BINDINGS.md "ROUND 14" and background/bg_objects/dan_info.lua.
function Background:handle_dan(player_num, state)
if self.collab and self.collab.handle_dan then
self.collab:handle_dan(player_num, state)
end
end
-- `clear_progress` (ROUND 18) is the 5th argument: clear_start/gauge_max for this
-- chart's difficulty. It is optional -- an engine that does not send it leaves it
-- nil and handle_dancer_count falls back (see there).
-- `flash` (ROUND 19, r19-gauge) is the 6th argument: the engine gauge-up
-- flash's current fade value (1 -> 0), 0 when none is active. Forwarded so
-- arcade_gauge.lua can redraw the flash on ITS OWN 21px segment grid instead
-- of the engine's 12px one -- see arcade_gauge.lua for why the stock flash
-- (still drawn by gauge.cpp, harmlessly hidden under this overlay's redraw)
-- cannot just be left alone.
function Background:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress, flash)
-- child-only
-- ROUND 39 (r39-ghostgauge-secondsong): this forwarding call still only
-- passed 5 args after ROUND 37 (r37-duplicate-gauge-clear) added the 6th
-- formal parameter to arcade_gauge.lua's own handle_gauge -- so `flash`
-- (the real 6th arg here) was landing in arcade_gauge's `clear_progress`
-- slot, and arcade_gauge's own `flash` was silently nil every frame,
-- permanently disabling the gauge-up flash redraw. Fixed to forward all
-- six.
self.arcade_gauge:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress, flash)
if self.collab then
-- ROUND 52: forward clear_progress (5th arg) so the collab dancer
-- model can divide by the CLEAR point like reference dance_b_* does
-- (collabs that ignore it are unaffected).
self.collab:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
elseif self.is_2p then
-- Update each player's donbg clear state independently
@@ -266,10 +191,6 @@ function Background:handle_gauge(player_num, progress, is_clear, is_rainbow, cle
self.is_rainbow = is_rainbow
else
self.donbg:set_clear(is_clear)
-- ROUND 52 (r52-lua-divergence-fixes): BOTH edges, matching the reference build
-- BgFever/FeverEffect/DonBg::Process (each caches its flag and plays a
-- clip on every change -- `normal_fever` rising, `fever_normal`
-- falling, re-triggerable per gogo/clear excursion).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear then self.bg_fever:stop() end
if is_rainbow and not self.is_rainbow then self.fever:start() end
@@ -281,33 +202,6 @@ function Background:handle_gauge(player_num, progress, is_clear, is_rainbow, cle
end
end
-- ROUND 18 (r18-misc) -- the cabinet's dancer count.
--
-- `App::EnsoGraphicDancer::Process` (reference source `EnsoGraphicDancer.obj.c:2723-2752`)
-- is evaluated EVERY FRAME and is a pure function of the gauge:
--
-- r = current_gauge / CLEAR_POINT -- +327256 / +327164
-- n = clamp(floor(2r + 3), 3, 5) -- min3 troupes (all six normal ones)
-- CheckDancerNum(this, n)
--
-- so: **3 dancers from the first frame of the song** (r = 0), 4 from HALF the
-- clear point, 5 once the gauge reaches the clear point. It is symmetric --
-- the count falls again when the gauge falls -- and 5 is a hard maximum
-- (`do {...} while (v3 < 5)`, `:2828-2895`).
--
-- What was wrong before: `math.floor(progress * 5)` starting from
-- `last_milestone = 1` gave ONE dancer at song start and reached three only at
-- 60 % of the whole bar. That is the user's 「進歌曲其實預設是3隻」.
--
-- The divisor is the CLEAR point, not the full bar: `AiEnsoGameManager.obj.c:1513`
-- tests `gauge >= (float)*(int *)(v16 + 327164)` for the clear, which pins
-- +327164 as the norma line. Our engine's equivalent is
-- `clear_start = {52, 60, 69, 69} / 87` (`src/objects/game/gauge.cpp:20`), passed
-- in as `clear_progress`. FAIL-SOFT: on an engine that does not pass it (the
-- parent, or a pre-round-18 build) we fall back to the HARD/ONI line, and then
-- correct ourselves the moment `is_clear` flips -- the same "learn the
-- difficulty at the flip" trick `arcade_gauge.lua` already uses, because
-- difficulty itself is not bound to Lua.
local GAUGE_MAX = 87.0
local CLEAR_STARTS = { 52, 60, 69 } -- easy / normal / hard+oni
local CLEAR_DEFAULT = 69 / GAUGE_MAX -- 0.79310
@@ -346,9 +240,6 @@ function Background:draw_back()
self.collab:draw_back()
else
if not self.is_2p then
-- ROUND 52: draw the fever backdrop while it is entering, steady
-- OR exiting (the falling-edge fade), with bg_normal beneath
-- whenever it is not fully opaque.
if self.bg_fever:is_visible() then
if not self.bg_fever.transitioned then
self.bg_normal:draw()
@@ -370,18 +261,7 @@ function Background:draw_back()
if self.renda_2 then self.renda_2:draw() end
if not self.is_2p then
self.dancer:draw()
-- ROUND 18 (r18-play): the 魂-max crowd goes UNDER the `dai` band.
-- The cabinet's crowd (enso/fever/fever_effect1) is a 1920x540 stage
-- whose art runs all the way to stage row 539 = screen 1079, i.e. it
-- fully overlaps the dai band's screen rows 1014..1080; drawn on top
-- it wipes the 紅白幕 out in both bottom corners, which is what the
-- user reported. The crowd rises from BEHIND the stand, so `footer`
-- (= bg_dai) is painted last. `dancer` was already behind it and stays
-- there -- the dancers' feet stop at ~y 1010, above the band, so the
-- band does not clip them.
-- ROUND 52: gate on the object's own visibility (covers the exit
-- ramp), not the raw is_rainbow flag.
-- The original paints the band (bg_dai) first and the crowd over it.
-- footer (dai band) paints after the crowd so it rises from behind it.
self.footer:draw()
if self.fever:is_visible() then self.fever:draw() end
end
@@ -429,32 +309,9 @@ function Background:handle_score(player_num, score)
b:set_score(score, tex.get_current_ms())
end
-- ROUND 80 (r80-gauge-layering-recheck): child-only. The arcade gauge fill
-- USED TO be painted from draw_fore() below, i.e. after the whole GAME HUD --
-- which put the skin's half of the soul gauge on top of every element the
-- cabinet draws OVER the gauge. `datatable/enso_post.bin` target 0 (1P normal
-- enso) lists the graphics in the order EnsoGraphicFactory::CreateEnsoGraphics
-- appends them, and `tamashiigage/tamashii_gage` is row 12 of 33: above
-- donbg / bg_nomal / bg_fever / renda_effect / dancer / bg_dai / gogo_effect /
-- fever_effect / lane / lane_hit / onp_replace, and BELOW don_enso, lane_left,
-- taiko, onp_jump, hit_effect, course, option, score, combo_number,
-- action_result, branch_effect, don_fukidashi, renda_number, name_plate,
-- action_fusen, action_kusudama, song_info, skip and papamama.
--
-- The engine now calls this hook from Player::draw(), immediately after that
-- lane's own Gauge::draw(), so the gauge is ONE layer again. Measured defect
-- it fixes: the note arc / 魂 badge burst (the cabinet's `onp_jump`, row 17)
-- was being clipped dead at the gauge's left edge, screen x 738 -- see
-- MAPPING_hud ROUND 80.
--
-- `player_num` is the lane whose engine gauge just drew; only that record is
-- repainted, so in 2P each lane lands in its own slot.
-- child-only: repaint the arcade gauge for the lane whose engine gauge just drew.
function Background:draw_gauge(player_num)
self.arcade_gauge:draw_one(player_num)
-- ROUND 90 (r90-danplate-brown-and-place): the collab gets this slot too.
-- GAME_DAN's 魂ゲージ condition plate is `gauge_border`, a CHILD of `gauge_mc`
-- inside the cabinet's tamashii_gauge.nulm, so it belongs in the gauge's own
-- layer, not at the end of the frame -- see bg_collabs/dan.lua:draw_gauge.
if self.collab and self.collab.draw_gauge then
self.collab:draw_gauge(player_num)
end
@@ -468,18 +325,6 @@ function Background:draw_fore()
local now = tex.get_current_ms()
for _, b in pairs(self.rank_badges) do b:draw(now) end
end
-- ROUND 80: the arcade gauge fill moved OUT of here, into draw_gauge()
-- above. Do NOT put it back: this hook runs after the whole GAME HUD.
-- ROUND 19 (r19-skip2): the 演奏スキップ panel used to draw here too, but
-- draw_fore runs BEFORE song_info.draw() and the gauge HUD (game.cpp
-- draw() -> draw_players() -> background->draw_fore() -> draw_overlay()),
-- so its full-screen dim could never reach the title/genre plate or the
-- gauge -- exactly the defect the cabinet capture showed. It now draws
-- from global/allnet_indicator.lua's GAME branch instead, the engine's
-- last Lua paint hook on GAME, which lands after song_info AND the gauge,
-- matching datatable/enso_post.bin's own draw order (skip.nulstb right
-- after song_info.nulstb). See the comment at self.enso_skip's
-- construction above for the full citation.
end
function Background:destroy()
+1 -12
View File
@@ -62,8 +62,6 @@ function A3BGNormal:destroy()
end
-- BGFever: extends BGFever4 pattern with petals tiling
-- Uses anims 16-17 for slide-in (same as BGFever3/Python BGFever4),
-- then a new looping vertical_move (duration 1300 = anim 15 duration) for petals.
local A3BGFever = {}
A3BGFever.__index = A3BGFever
@@ -86,8 +84,6 @@ function A3BGFever:start()
self.bg_texture_move:start()
end
-- ROUND 52: falling-edge re-arm -- the next start() replays the slide-in
-- (anim:start() rewinds; BaseAnimation::start -> restart).
function A3BGFever:reset()
self.transitioned = false
end
@@ -156,10 +152,7 @@ function A3Background.new(player_num, bpm)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 0)
self.is_clear = false
self.is_rainbow = false
-- ROUND 52 (r52-lua-divergence-fixes): reference dance_b_* count model (see
-- collab_dancer_model.lua). The old pre-R18 milestone counter and the
-- 3-dancer pre-seed are gone: the troupe is an absolute function of the
-- gauge every frame, starting at 1 (EnsoGraphicDancer.obj.c:2741).
-- reference dance_b_* count model
self.dancer_model = DancerModel.new(MAX_DANCERS)
return self
end
@@ -181,19 +174,15 @@ function A3Background:handle_bad(player_num) self.chibi:add_chibi(player_num, tr
function A3Background:handle_drumroll(player_num) self.renda:add_renda() end
-- ROUND 52: no renda on balloons/kusudama -- the reference build RendaEffect::Process fires
-- for note kinds 6/9 (drumrolls) only; see background.lua's note.
function A3Background:handle_balloon(player_num) end
function A3Background:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
self.donbg:set_clear(is_clear)
-- ROUND 52: both edges, re-triggerable (the reference build BgFever::Process).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear and self.bg_fever.reset then self.bg_fever:reset() end
self.is_clear = is_clear
self.is_rainbow = is_rainbow
-- ROUND 52: reference dance_b_* model, symmetric every frame.
self.dancer_model:apply(self.dancer, progress, is_clear, clear_progress)
end
+3 -13
View File
@@ -1,7 +1,4 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_collabs/animal.lua
-- (ROUND 52, r52-lua-divergence-fixes): byte-copy of the parent plus the reference
-- dance_b_* dancer-count model, the fever falling edge, and the renda
-- drumroll-only rule -- see collab_dancer_model.lua / background.lua.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_collabs/animal.lua.
local DonBG4 = require("background/bg_objects/donbg/4")
local BGNormal = require("background/bg_objects/bg_normal")
local Fever = require("background/bg_objects/fever")
@@ -46,16 +43,14 @@ function AnimalBackground.new(player_num, bpm)
self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH)
self.bg_fever = AnimalBGFever.new()
self.footer = Footer.new(COLLAB_PATH, 0)
-- ROUND 60 (r60-collab-fix-all): class Fever3, folder fever_0 -- see
-- oshiri.lua (Fever.create(3, ...) loaded the nonexistent fever_3).
-- class Fever3, folder fever_0
self.fever = Fever.Fever3.new(0, bpm, COLLAB_PATH)
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH)
self.renda = RendaController.new(COLLAB_PATH, 0)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 0)
self.is_clear = false
self.is_rainbow = false
-- ROUND 52 (r52-lua-divergence-fixes): reference dance_b_* count model
-- (collab_dancer_model.lua) replaces the pre-R18 milestone counter.
-- reference dance_b_* count model
self.dancer_model = DancerModel.new(MAX_DANCERS)
return self
end
@@ -77,13 +72,10 @@ function AnimalBackground:handle_bad(player_num) self.chibi:add_chibi(player_num
function AnimalBackground:handle_drumroll(player_num) self.renda:add_renda() end
-- ROUND 52: no renda on balloons/kusudama -- the reference build RendaEffect fires for
-- note kinds 6/9 (drumrolls) only; see background.lua.
function AnimalBackground:handle_balloon(player_num) end
function AnimalBackground:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
self.donbg:set_clear(is_clear)
-- ROUND 52: both edges, re-triggerable (the reference build BgFever/FeverEffect::Process).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear and self.bg_fever.reset then self.bg_fever:reset() end
if is_rainbow and not self.is_rainbow then self.fever:start() end
@@ -91,7 +83,6 @@ function AnimalBackground:handle_gauge(player_num, progress, is_clear, is_rainbo
self.is_clear = is_clear
self.is_rainbow = is_rainbow
-- ROUND 52: reference dance_b_* model, symmetric every frame.
self.dancer_model:apply(self.dancer, progress, is_clear, clear_progress)
end
@@ -110,7 +101,6 @@ function AnimalBackground:draw_back()
self.renda:draw()
self.dancer:draw()
self.footer:draw()
-- ROUND 52: covers the exit ramp, not just the raw flag.
if self.fever.is_visible and self.fever:is_visible() then self.fever:draw() end
self.chibi:draw()
end
+3 -13
View File
@@ -1,7 +1,4 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_collabs/buttoburst.lua
-- (ROUND 52, r52-lua-divergence-fixes): byte-copy of the parent plus the reference
-- dance_b_* dancer-count model, the fever falling edge, and the renda
-- drumroll-only rule -- see collab_dancer_model.lua / background.lua.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_collabs/buttoburst.lua.
local DonBG4 = require("background/bg_objects/donbg/4")
local BGNormal = require("background/bg_objects/bg_normal")
local Fever = require("background/bg_objects/fever")
@@ -48,16 +45,14 @@ function ButtoBackground.new(player_num, bpm)
self.donbg = DonBG4.new(5, player_num, "background")
self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH)
self.bg_fever = ButtoBGFever.new()
-- ROUND 60 (r60-collab-fix-all): class Fever3, folder fever_0 -- see
-- oshiri.lua (Fever.create(3, ...) loaded the nonexistent fever_3).
-- class Fever3, folder fever_0
self.fever = Fever.Fever3.new(0, bpm, COLLAB_PATH)
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH)
self.renda = RendaController.new(COLLAB_PATH, 0)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 0)
self.is_clear = false
self.is_rainbow = false
-- ROUND 52 (r52-lua-divergence-fixes): reference dance_b_* count model
-- (collab_dancer_model.lua) replaces the pre-R18 milestone counter.
-- reference dance_b_* count model
self.dancer_model = DancerModel.new(MAX_DANCERS)
return self
end
@@ -79,13 +74,10 @@ function ButtoBackground:handle_bad(player_num) self.chibi:add_chibi(player_num,
function ButtoBackground:handle_drumroll(player_num) self.renda:add_renda() end
-- ROUND 52: no renda on balloons/kusudama -- the reference build RendaEffect fires for
-- note kinds 6/9 (drumrolls) only; see background.lua.
function ButtoBackground:handle_balloon(player_num) end
function ButtoBackground:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
self.donbg:set_clear(is_clear)
-- ROUND 52: both edges, re-triggerable (the reference build BgFever/FeverEffect::Process).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear and self.bg_fever.reset then self.bg_fever:reset() end
if is_rainbow and not self.is_rainbow then self.fever:start() end
@@ -93,7 +85,6 @@ function ButtoBackground:handle_gauge(player_num, progress, is_clear, is_rainbow
self.is_clear = is_clear
self.is_rainbow = is_rainbow
-- ROUND 52: reference dance_b_* model, symmetric every frame.
self.dancer_model:apply(self.dancer, progress, is_clear, clear_progress)
end
@@ -111,7 +102,6 @@ function ButtoBackground:draw_back()
self.donbg:draw()
self.renda:draw()
self.dancer:draw()
-- ROUND 52: covers the exit ramp, not just the raw flag.
if self.fever.is_visible and self.fever:is_visible() then self.fever:draw() end
self.chibi:draw()
end
+9 -104
View File
@@ -35,50 +35,7 @@ function DanDonBG.new(player_num)
self._id_layer_d = tex.get_texture(self.path .. "/" .. self.name .. "/layer_d")
self._id_petal = tex.get_texture(self.path .. "/" .. self.name .. "/petal")
-- ROUND 26 (r26-danbg-tiling): DonBGBase:draw() never calls draw_textures
-- below (that hook does not exist on the base class and is dead code --
-- true here and in every other bg_collabs/*.lua override, grep shows only
-- definitions, zero call sites). What it actually renders is
-- self.states.normal, which DonBGBase.new() just populated from
-- background/bg_data/donbg.lua's entry for name "0_1" -- generated by
-- r13-bg's export script from the ARCADE's *plain gameplay* donbg tile
-- (period=496, vx=-93.29 px/s, an 8 px gradient strip meant to scroll and
-- wrap every 496 px). Round 19b swapped this folder's `background.png` for
-- the unrelated 1920x280 donbg_dojo_1p flatten but never touched this
-- table, so draw_layers was tiling the new full-width art at n=ceil(1920/
-- 496)+1=5 copies 496 px apart -- exactly the "4-5 vertical seams / sakura
-- clusters cutting off at boundaries" the user's screenshot showed.
-- donbg_dojo_1p has no measured scroll (full.py sampled all 10 keyframes
-- of MC 29, every shape's matrix is identical across frames -- this movie
-- is not animated in the .nulm at all), so the correct list is exactly
-- what the dead draw_textures override above already intended: the band
-- and bg_overlay_3 (round 7's 段位道場 title) each drawn once, period=0.
-- Bypassing the shared generated table here (instead of hand-editing it)
-- keeps every OTHER DonBGBase instance -- the plain non-dan gameplay donbg
-- pool, which legitimately owns and needs that "0_1" period=496 entry --
-- untouched.
--
-- ROUND 76 (r76-gamedan-band-and-autoplay): the "no measured scroll"
-- sentence above is WRONG, and so is ROUND 35 point 4 which re-derived it.
-- Both sampled the movie's ROOT node (`#28@0`, character 29) -- which has
-- only 10 frames and IS constant (tx=960 ty=138 m=[1,0,0,1] on every one of
-- frames 0..9) -- and never descended into its children. Dumped in full
-- (`lumen_anim_dump --all --leaves --range 0,1759`, 1760 frames, 47 nodes)
-- the CHILDREN all scroll, each on its own period:
-- #6@1 -> -1.500000 px/frame = -90.0000 px/s, wrap 1920 px, loop 1281 f
-- #10@2 -> -1.200750 px/frame = -72.0450 px/s, wrap 1920 px, loop 1600 f
-- #16@3 -> -1.091529 px/frame = -65.4918 px/s, wrap 1920 px, loop 1760 f
-- #27@6 -> -0.750000 px/frame = -45.0000 px/s, wrap 960 px, loop 1281 f
-- #19@4/#19@5 -> two sakura-petal drifters, 320-frame loop, see below
-- The picture already changes on FRAME 1 (105,247 px differ from frame 0).
-- The band is therefore drawn as the real five-layer parallax it is: an
-- opaque navy seigaiha plate, two cloud silhouette bands, a seigaiha-filled
-- cloud band, the falling petals, and a sakura-blossom row.
--
-- Fail-soft: any layer whose PNG/texture.json entry is missing is simply
-- left out; with none of them present this degrades to the bare opaque
-- plate (background.png is fully opaque, alpha 255/255 everywhere), never
-- to gaps or to the parent's art.
-- hand-built five-layer parallax band; missing layers are simply skipped.
local back, front = {}, {}
local function add(list, id, period, vx)
if id == nil then return end
@@ -91,36 +48,19 @@ function DanDonBG.new(player_num)
add(back, self._id_layer_a, 1920, -90.0000) -- #6@1 dark cloud silhouettes
add(back, self._id_layer_b, 1920, -72.0450) -- #10@2 outlined cloud bands
add(back, self._id_layer_c, 1920, -65.4918) -- #16@3 seigaiha-filled clouds
-- petals (#19@4/#19@5) are drawn between these two lists -- they sit at
-- root depths 4 and 5, i.e. under #27@6.
-- petals sit between the back and front lists
add(front, self._id_layer_d, 960, -45.0000) -- #27@6 sakura blossom row
-- ROUND 92 -- user asked for the 段位道場 title not to appear on the play
-- background (「段位道場這四個字不要出現」). ROUND 7's `bg_overlay_3` draw is
-- removed rather than the art deleted, so restoring it is one line.
-- 段位道場 title (bg_overlay_3) intentionally not drawn
self._layers_back = back
self._layers_front = front
self.states = { normal = {} }
for _, l in ipairs(back) do table.insert(self.states.normal, l) end
for _, l in ipairs(front) do table.insert(self.states.normal, l) end
-- ROUND 60 (r60-collab-fix-all): DonBGBase now routes collab paths through
-- the draw_textures contract; dan is the one collab whose correct list is
-- the hand-built states above, so it opts back into the layer path.
-- use the hand-built layer list above
self.use_layers = true
return self
end
-- ROUND 76: the two petal drifters. Root depths 4 and 5 place the SAME shape
-- record (480x240, nine sakura petals) 960 px apart and walk both of them from
-- (2073,-218) to (675,525) over 320 frames, then snap back -- measured off the
-- anim dump, and confirmed by pasting shape record 5 centre-registered at the
-- dumped (tx,ty) on top of the plate+tile reconstruction: mean abs error
-- against the cabinet's own render drops from 2.08 to 0.10 at frame 120 and
-- from 2.61 to 0.37 at frame 1759.
--
-- The cabinet clips them to its 1920x276 stage; we have no stage to clip to, so
-- the bottom half of a petal cluster would otherwise fall out of the band and
-- over the play area for ~1.7 s of every 5.33 s cycle. `src`+`y2` crops the
-- draw to the band instead, which is what the stage boundary does.
local PETAL_W, PETAL_H = 480, 240
local PETAL_LOOP_FRAMES = 320
local PETAL_X0, PETAL_Y0 = 2073.0, -218.0
@@ -149,10 +89,7 @@ function DanDonBG:_draw_petals(y)
end
end
-- ROUND 76: the petals sit between the scrolling cloud bands and the sakura
-- row, so the draw is split rather than left to DonBGBase:draw(). Dan has no
-- fever state (states.fever is nil), so none of the base's crossfade paths
-- applied here anyway.
-- petals sit between the back and front layers
function DanDonBG:draw(y)
y = y or 0
self:draw_layers(self._layers_back or {}, 1.0, y)
@@ -166,24 +103,7 @@ function DanDonBG:update(current_time_ms)
self.overlay_move_2:update(current_time_ms)
end
-- ROUND 19 (r19-dan): `background.png` is now the arcade's own
-- `enso_dani/enso/donbg/donbg_dojo_1p.nulm` band (measured/flattened,
-- scratchpad/r19dn/bake -- 1920x280, navy seigaiha + sakura petals), not the
-- inherited PyTaikoGreen 8px gradient strip. The old tiled overlay draws below
-- (bg_overlay/bg_overlay_2/overlay_1/overlay_2, all still PyTaikoGreen's
-- orange-diagonal art since no arcade source for THOSE specific layers was
-- found) are dropped so they don't paint the wrong pattern back on top; the
-- new band is one static image, not a scrolling composite, so nothing here
-- depends on `self.move` / `self.overlay_move*` any more. `bg_overlay_3`
-- (round 7's 段位道場 title placement) is unchanged and still drawn.
--
-- ROUND 26 (r26-danbg-tiling): DEAD CODE -- DonBGBase:draw() (base.lua) only
-- ever calls draw_layers(self.states...), it has no call to draw_textures on
-- any DonBGBase subclass anywhere in this repo. This method's intent (band +
-- bg_overlay_3, each once) is now what DanDonBG.new() builds directly into
-- self.states above, which IS on the real draw path. Left in place only so a
-- future reader sees what round 19's author meant to happen; do not rely on
-- this function running.
-- dead code: DonBGBase:draw() uses self.states, not this hook.
function DanDonBG:draw_textures(fade, y, frame)
tex.draw_texture(self._id_bg, { y = y })
tex.draw_texture(self._id_bg_overlay_3)
@@ -223,9 +143,7 @@ function DanBackground:handle_gauge(player_num, progress, is_clear, is_rainbow)
self.donbg:set_clear(is_clear)
end
-- ROUND 14 engine binding (ENGINE_BINDINGS.md): DanGameScreen::update pushes the
-- live 段位道場 exam state here every frame. See background/bg_objects/dan_info.lua
-- for what is drawn with it and why the rest of the panel is still the engine's.
-- engine pushes the live 段位道場 exam state here every frame.
function DanBackground:handle_dan(player_num, state)
self.dan_info:handle(state)
end
@@ -237,12 +155,7 @@ function DanBackground:draw_back()
self.chibi:draw()
end
-- ROUND 7: the dan soul gauge's separator overlay is drawn by gauge.cpp at a hardcoded
-- fade 0.15; the arcade composites the same 6 px black bands at 0.30 (MAPPING_hud ROUND 5c,
-- shape 33 colour-transform alpha 77/256). GameDanScreen::draw() runs draw_fore() after the
-- gauge, so one more pass at 0.15 composites to 1-(1-0.15)^2 = 0.2775. The overlay is static
-- (it does not depend on the fill), so this needs no gauge state - which is just as well,
-- because Player::update takes the dan_gauge branch and never calls Background:handle_gauge.
-- dan soul gauge separator overlay (static, drawn at a hardcoded fade).
function DanBackground:draw_fore()
if not self._gauge_overlay then
self._gauge_overlay = tex.get_texture("game/gauge_dan/overlay") or 0
@@ -252,15 +165,7 @@ function DanBackground:draw_fore()
end
end
-- ROUND 90 (r90-danplate-brown-and-place): the 魂ゲージ condition plate MOVED out
-- of draw_fore() into the gauge's own draw slot. On the cabinet it is
-- `gauge_border`, a child of `gauge_mc` inside tamashii_gauge.nulm, and the reference build's
-- datatable/enso_post.bin target 2 puts that whole movie at draw-order row 10 of
-- 29 -- below the notes and below every HUD readout. draw_fore() runs after
-- Player::draw()'s draw_notes(), so from there the plate painted OVER the notes
-- (user report 「魂guage那個白色的景,不應該把音符蓋過去」).
-- DanGameScreen::draw() calls Background::draw_gauge(P1) right after
-- dan_gauge.draw(); background.lua forwards it here.
-- dan condition plate, drawn in the gauge's own layer.
function DanBackground:draw_gauge(player_num)
self.dan_info:draw()
end
+2 -19
View File
@@ -33,7 +33,6 @@ function FunassyiBGFever:start()
self.bg_texture_move:start()
end
-- ROUND 52: falling-edge re-arm -- the next start() replays the slide-in.
function FunassyiBGFever:reset()
self.transitioned = false
end
@@ -70,17 +69,7 @@ function FunassyiBackground.new(player_num, bpm)
self.donbg = DonBG4.new(1, player_num, COLLAB_PATH)
self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH)
self.bg_fever = FunassyiBGFever.new()
-- ROUND 60 (r60-collab-fix-all): funassyi ships NO footer, fever or renda
-- art (the parent constructed all three anyway -- a parent bug). The
-- texture name->id map is GLOBAL and name-keyed, so after the folder load
-- fails, tex.get_id("fever_0", "overlay_l") / ("renda", "renda_0") still
-- return ids that belong to a DIFFERENT owner of those names (verified
-- live: l=811 r=812 / id=1128 on a fresh boot) -- and drawing them paints
-- unrelated sheets as a black/white striped arc over the top band (the
-- same artifact class as oshiri's texture-id-0 fever draw). These three
-- subsystems are therefore inert for funassyi: footer index -1 is the
-- Footer class's own documented "no footer" mode, fever/renda get inert
-- same-API stubs.
-- funassyi ships no footer/fever/renda art; use inert stubs.
self.footer = Footer.new(COLLAB_PATH, -1)
self.fever = {
start = function() end, stop = function() end,
@@ -96,8 +85,7 @@ function FunassyiBackground.new(player_num, bpm)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 0)
self.is_clear = false
self.is_rainbow = false
-- ROUND 52 (r52-lua-divergence-fixes): reference dance_b_* count model
-- (collab_dancer_model.lua) replaces the pre-R18 milestone counter.
-- reference dance_b_* count model
self.dancer_model = DancerModel.new(MAX_DANCERS)
return self
end
@@ -119,13 +107,10 @@ function FunassyiBackground:handle_bad(player_num) self.chibi:add_chibi(player_n
function FunassyiBackground:handle_drumroll(player_num) self.renda:add_renda() end
-- ROUND 52: no renda on balloons/kusudama -- the reference build RendaEffect fires for
-- note kinds 6/9 (drumrolls) only; see background.lua.
function FunassyiBackground:handle_balloon(player_num) end
function FunassyiBackground:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
self.donbg:set_clear(is_clear)
-- ROUND 52: both edges, re-triggerable (the reference build BgFever/FeverEffect::Process).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear and self.bg_fever.reset then self.bg_fever:reset() end
if is_rainbow and not self.is_rainbow then self.fever:start() end
@@ -133,7 +118,6 @@ function FunassyiBackground:handle_gauge(player_num, progress, is_clear, is_rain
self.is_clear = is_clear
self.is_rainbow = is_rainbow
-- ROUND 52: reference dance_b_* model, symmetric every frame.
self.dancer_model:apply(self.dancer, progress, is_clear, clear_progress)
end
@@ -152,7 +136,6 @@ function FunassyiBackground:draw_back()
self.renda:draw()
self.dancer:draw()
self.footer:draw()
-- ROUND 52: covers the exit ramp, not just the raw flag.
if self.fever.is_visible and self.fever:is_visible() then self.fever:draw() end
self.chibi:draw()
end
+2 -20
View File
@@ -1,7 +1,4 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_collabs/imas.lua
-- (ROUND 52, r52-lua-divergence-fixes): byte-copy of the parent plus the reference
-- dance_b_* dancer-count model, the fever falling edge, and the renda
-- drumroll-only rule -- see collab_dancer_model.lua / background.lua.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_collabs/imas.lua.
local DonBGBase = require("background/bg_objects/donbg/base")
local Fever = require("background/bg_objects/fever")
local Footer = require("background/bg_objects/footer")
@@ -116,12 +113,6 @@ function ImasBGFever:destroy()
end
-- Fever: BaseFever + 2-frame texture_change overlay (2-beat cycle)
-- ROUND 58 (r58-2p-background-polish): the class now CHAINS to Fever.Base
-- (metatable __index) instead of only copying a base instance's fields --
-- the ROUND 52 per-method stop/destroy/is_visible/alpha shims are gone, and
-- any method Fever.Base gains in the future is inherited automatically
-- (this was the "copied-fields-without-metatable" latent pattern that made
-- destroy unreachable before ROUND 52).
local ImasFever = setmetatable({}, { __index = Fever.Base })
ImasFever.__index = ImasFever
@@ -137,9 +128,6 @@ function ImasFever.new(bpm, path)
return self
end
-- start/stop/destroy/is_visible/alpha: inherited from Fever.Base via the
-- ROUND 58 metatable chain above.
function ImasFever:update(current_time_ms, bpm)
Fever.Base.update(self, current_time_ms, bpm)
self.texture_change:update(current_time_ms)
@@ -170,8 +158,7 @@ function ImasBackground.new(player_num, bpm, path, max_dancers)
self.max_dancers = max_dancers
self.is_clear = false
self.is_rainbow = false
-- ROUND 52 (r52-lua-divergence-fixes): reference dance_b_* count model
-- (collab_dancer_model.lua) replaces the pre-R18 milestone counter.
-- reference dance_b_* count model
self.dancer_model = DancerModel.new(max_dancers)
return self
end
@@ -194,13 +181,10 @@ function ImasBackground:handle_bad(player_num) self.chibi:add_chibi(player_num,
function ImasBackground:handle_drumroll(player_num) self.renda:add_renda() end
-- ROUND 52: no renda on balloons/kusudama -- the reference build RendaEffect fires for
-- note kinds 6/9 (drumrolls) only; see background.lua.
function ImasBackground:handle_balloon(player_num) end
function ImasBackground:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
self.donbg:set_clear(is_clear)
-- ROUND 52: both edges, re-triggerable (the reference build BgFever/FeverEffect::Process).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear and self.bg_fever.reset then self.bg_fever:reset() end
if is_rainbow and not self.is_rainbow then self.fever:start() end
@@ -208,7 +192,6 @@ function ImasBackground:handle_gauge(player_num, progress, is_clear, is_rainbow,
self.is_clear = is_clear
self.is_rainbow = is_rainbow
-- ROUND 52: reference dance_b_* model, symmetric every frame.
self.dancer_model:apply(self.dancer, progress, is_clear, clear_progress)
end
@@ -227,7 +210,6 @@ function ImasBackground:draw_back()
self.renda:draw()
self.dancer:draw()
self.footer:draw()
-- ROUND 52: covers the exit ramp, not just the raw flag.
if self.fever.is_visible and self.fever:is_visible() then self.fever:draw() end
self.chibi:draw()
end
+1 -13
View File
@@ -105,9 +105,6 @@ function ImasSidemBGFever:destroy()
end
-- Fever: same 2-frame overlay as imas collab (2-beat cycle)
-- ROUND 58 (r58-2p-background-polish): chained to Fever.Base (metatable
-- __index) instead of only copying a base instance's fields -- the ROUND 52
-- per-method shims are gone and future Fever.Base methods inherit for free.
local ImasSidemFever = setmetatable({}, { __index = Fever.Base })
ImasSidemFever.__index = ImasSidemFever
@@ -123,9 +120,6 @@ function ImasSidemFever.new(bpm)
return self
end
-- start/stop/destroy/is_visible/alpha: inherited from Fever.Base via the
-- ROUND 58 metatable chain above.
function ImasSidemFever:update(current_time_ms, bpm)
Fever.Base.update(self, current_time_ms, bpm)
self.texture_change:update(current_time_ms)
@@ -155,8 +149,7 @@ function ImasSidemBackground.new(player_num, bpm)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 0)
self.is_clear = false
self.is_rainbow = false
-- ROUND 52 (r52-lua-divergence-fixes): reference dance_b_* count model
-- (collab_dancer_model.lua) replaces the pre-R18 milestone counter.
-- reference dance_b_* count model
self.dancer_model = DancerModel.new(MAX_DANCERS)
return self
end
@@ -179,13 +172,10 @@ function ImasSidemBackground:handle_bad(player_num) self.chibi:add_chibi(player_
function ImasSidemBackground:handle_drumroll(player_num) self.renda:add_renda() end
-- ROUND 52: no renda on balloons/kusudama -- the reference build RendaEffect fires for
-- note kinds 6/9 (drumrolls) only; see background.lua.
function ImasSidemBackground:handle_balloon(player_num) end
function ImasSidemBackground:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
self.donbg:set_clear(is_clear)
-- ROUND 52: both edges, re-triggerable (the reference build BgFever/FeverEffect::Process).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear and self.bg_fever.reset then self.bg_fever:reset() end
if is_rainbow and not self.is_rainbow then self.fever:start() end
@@ -193,7 +183,6 @@ function ImasSidemBackground:handle_gauge(player_num, progress, is_clear, is_rai
self.is_clear = is_clear
self.is_rainbow = is_rainbow
-- ROUND 52: reference dance_b_* model, symmetric every frame.
self.dancer_model:apply(self.dancer, progress, is_clear, clear_progress)
end
@@ -212,7 +201,6 @@ function ImasSidemBackground:draw_back()
self.renda:draw()
self.dancer:draw()
self.footer:draw()
-- ROUND 52: covers the exit ramp, not just the raw flag.
if self.fever.is_visible and self.fever:is_visible() then self.fever:draw() end
self.chibi:draw()
end
+2 -13
View File
@@ -125,20 +125,14 @@ function OshiriBackground.new(player_num, bpm)
self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH)
self.bg_fever = OshiriBGFever.new()
self.footer = Footer.new(COLLAB_PATH, 0)
-- ROUND 60 (r60-collab-fix-all): Fever.create(3, ...) used index 3 both
-- as the class selector AND the folder suffix, loading fever/fever_3 --
-- which no collab ships (the art is fever/fever_0/overlay.png). The load
-- failed ("No textures loaded"), _id_overlay stayed 0, and at 魂-max the
-- draw painted texture id 0 (whatever sheet owns that id) bouncing over
-- the band. Class Fever3 (single-overlay draw), folder fever_0.
-- class Fever3, folder fever_0
self.fever = Fever.Fever3.new(0, bpm, COLLAB_PATH)
self.dancer = OshiriDancerGroup.new(bpm)
self.renda = RendaController.new(COLLAB_PATH, 0)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 0)
self.is_clear = false
self.is_rainbow = false
-- ROUND 52 (r52-lua-divergence-fixes): reference dance_b_* count model
-- (collab_dancer_model.lua) replaces the pre-R18 milestone counter.
-- reference dance_b_* count model
self.dancer_model = DancerModel.new(MAX_DANCERS)
return self
end
@@ -160,13 +154,10 @@ function OshiriBackground:handle_bad(player_num) self.chibi:add_chibi(player_num
function OshiriBackground:handle_drumroll(player_num) self.renda:add_renda() end
-- ROUND 52: no renda on balloons/kusudama -- the reference build RendaEffect fires for
-- note kinds 6/9 (drumrolls) only; see background.lua.
function OshiriBackground:handle_balloon(player_num) end
function OshiriBackground:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
self.donbg:set_clear(is_clear)
-- ROUND 52: both edges, re-triggerable (the reference build BgFever/FeverEffect::Process).
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear and self.bg_fever.reset then self.bg_fever:reset() end
if is_rainbow and not self.is_rainbow then self.fever:start() end
@@ -174,7 +165,6 @@ function OshiriBackground:handle_gauge(player_num, progress, is_clear, is_rainbo
self.is_clear = is_clear
self.is_rainbow = is_rainbow
-- ROUND 52: reference dance_b_* model, symmetric every frame.
self.dancer_model:apply(self.dancer, progress, is_clear, clear_progress)
end
@@ -193,7 +183,6 @@ function OshiriBackground:draw_back()
self.renda:draw()
self.dancer:draw()
self.footer:draw()
-- ROUND 52: covers the exit ramp, not just the raw flag.
if self.fever.is_visible and self.fever:is_visible() then self.fever:draw() end
self.chibi:draw()
end
+4 -76
View File
@@ -1,53 +1,6 @@
-- YataiDON-HSS child-skin addition (no parent counterpart).
--
-- ============================================================================
-- ROUND 96 (r96-soulgauge-cellgrid): this object NO LONGER DRAWS ANYTHING.
--
-- From ROUND 6 to ROUND 80 this file redrew the ordinary soul gauge's fill on
-- top of the engine's own, because the engine drew on an 87-cell / 12 px grid
-- while the cabinet's grid is 50 cells x 21 px, and because gauge.cpp hardcoded
-- the separator overlay at fade 0.15 where the cabinet uses Amul 77/256.
-- Redrawing on top meant this file also had to ERASE the engine's overshoot,
-- guess the difficulty from the is_clear flip so it knew which plate to erase
-- with, re-composite the separator strength in two passes, and put the
-- クリア caption back on top afterwards.
--
-- Every one of those was a workaround for the engine's grid, and the last of
-- them was also the defect the user kept reporting
-- (「soul gauge還是有錯,他的新增格數應該是一格一格,他看起來有gap」):
-- `erase()` repainted only 24 px past the arcade fill edge, but the engine's
-- own gauge-up flash `<pn>p_bar_fade` (a 24 px SOLID slab based at texture.json
-- x 732) landed at `732 + 12*floor(87*progress)`, i.e. up to
-- 18 + 12*floor(87p) - 21*floor(50p) = +33 px
-- past the true cell boundary. What survived erase() was a detached,
-- wrong-height orange fragment one cell ahead of the fill, with unfilled groove
-- in between -- the "gap". Measured live at soul 945 / 2788 / 5996 as
-- +30 / +33 / +33 px (scratchpad/r96/scan.py, crop
-- scratchpad/r96/crops/live_a_58_edge.png). Worse, past screen x 1335 with the
-- difficulty still unknown, `erase()`'s PLATE_SHARED_END clamp made its width
-- <= 0 and it returned WITHOUT ERASING AT ALL, so the engine's raw fill
-- overshoot showed too.
--
-- CORRECTION TO ROUND 19: the comment that used to stand in this file --
-- "The engine's own copy is otherwise harmless (this file's opaque redraw +
-- erase() covers it)" -- was wrong, and had been wrong since ROUND 19. It is
-- the direct cause of the report ROUND 96 fixed. ROUND 34's and ROUND 37's
-- findings about this file's OWN flash placement and about
-- Background:handle_gauge's argument count were correct and are preserved in
-- MAPPING_hud.md; they simply no longer have any code to apply to.
--
-- ROUND 96 moved the grid into the engine instead: `Gauge::draw()`'s NORMAL
-- branch now draws the fill, the transition cell, the clear-zone tiles and the
-- flash on the cabinet's own 50-cell / 21 px grid (X0 = 738, clear cell
-- 30/35/40 by clear-zone art tier), and draws the separator overlay once at the
-- cabinet's real Amul 77/256 = 0.30078. It knows the true difficulty, so the
-- plate-guessing goes away as well. There is nothing left for a skin-side
-- overlay to correct, and drawing one again can only re-introduce seams.
--
-- What is kept: the `handle_gauge` / `draw_one` hook shape (background.lua and
-- Player::draw both call them, and a future round may want the hook), and
-- ROUND 43's texture-id-contamination unload -- see below.
-- ============================================================================
-- Soul-gauge skin hook. No longer draws (the engine owns the cabinet grid now);
-- keeps the handle_gauge / draw_one hook shape and unloads the gauge folders so
-- other scripts' gauge draws stay honest.
local ArcadeGauge = {}
ArcadeGauge.__index = ArcadeGauge
@@ -56,33 +9,13 @@ function ArcadeGauge.new()
local self = setmetatable({}, ArcadeGauge)
self.p = {}
-- ROUND 43 (r43-gauge-contamination): the engine's script-side texture id
-- map is keyed on "<subset-leaf>/<name>" WITHOUT the screen name
-- (texture.cpp load_folder: `map_key = subset_key + "/" + tex_name`), so
-- "game/gauge/1p_unfilled_hard" and "result/gauge/1p_unfilled_hard" are the
-- SAME id -- and whichever folder loaded most recently owns the
-- TextureObject, including its texture.json PLACEMENT bases. This file no
-- longer draws gauge textures through the script wrapper, so it can no
-- longer BE the victim, but the unload is kept: it costs one lookup at
-- construction and it keeps result_player.lua's own gauge draw honest if
-- any other script starts loading "game/gauge" later. (The C++ screens draw
-- through a separate TextureWrapper instance and are unaffected either way.)
tex.unload_folder("result", "gauge")
tex.unload_folder("game", "gauge")
return self
end
-- ROUND 37 (r37-duplicate-gauge-clear): `Background::handle_gauge`
-- (background.cpp:111) passes SIX arguments -- (player_num, progress, is_clear,
-- is_rainbow, clear_progress, flash). The formal list must keep matching it:
-- Lua binds positionally and silently drops extras, and an earlier mismatch
-- here made `flash` receive `clear_progress` for many rounds.
--
-- Nothing here draws any more, so this only records state for anyone who wants
-- to read it (and keeps the hook alive so a future round has somewhere to put
-- skin-side gauge behaviour -- e.g. the cabinet's per-cell additive `glow_N`,
-- still unimplemented; see MAPPING_hud.md ROUND 80 "Known gap").
-- Record gauge state for readers; nothing draws.
function ArcadeGauge:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress, flash)
if player_num ~= 1 and player_num ~= 2 then return end
local g = self.p[player_num]
@@ -94,11 +27,6 @@ function ArcadeGauge:handle_gauge(player_num, progress, is_clear, is_rainbow, cl
g.flash = flash or 0
end
-- ROUND 80 (r80-gauge-layering-recheck) put this call in Player::draw(), right
-- after that lane's Gauge::draw(), so a skin-side gauge layer lands in its own
-- draw slot instead of on top of the whole HUD from draw_fore(). The hook is
-- kept (and still called) even though it now draws nothing, so that z-order
-- placement does not have to be rediscovered.
function ArcadeGauge:draw_one(player_num) end
function ArcadeGauge:draw() end
+2 -10
View File
@@ -1,13 +1,5 @@
-- Generic replay of an exported cabinet backdrop movie (lower half of the screen).
--
-- Data: Scripts/background/bg_data/bg_anim/<name>.lua (generated by
-- scratchpad/lyric/movie_export.py): `shapes[c] = {w, h, ox, oy}` and `normal`, a
-- list of shape instances in depth order, each with the period of its own loop and
-- channels tx/ty/sx/sy/rot/a (one number when constant, else one value per frame;
-- a = 0 means not placed) plus an optional presence mask `on`/`on_period`.
-- `blend = 8` marks a leaf the cabinet draws with the add blend mode (lantern glows).
-- Textures: Graphics/background/bga_<name>/s<c>.png. The stage is 1920x540 and is
-- drawn at screen y 540; the same schema as the donbg band player.
-- Replays an exported cabinet backdrop movie (lower half of the screen).
-- Data: background/bg_data/bg_anim/<name>.lua; textures: bga_<name>/s<c>.png.
local BGAnim = {}
BGAnim.__index = BGAnim
+11 -46
View File
@@ -1,47 +1,17 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/bg_fever.lua
--
-- ROUND 12. The cabinet's gogo/clear backdrop is
-- enso/background/bg_fever_a_{01,02,03}.nulm: a 1920x540 stage (the lower half
-- of the screen, same anchor as bg_nomal) made of one static plate plus one to
-- three named `scroll_*_mc` strips, each sliding at a constant px/frame with a
-- fixed repeat period. There is no flipbook anywhere in it, so the whole rig
-- ports as: plate + per-strip {tile, period, px/s}, replayed here.
--
-- The measured numbers live in Scripts/background/bg_data/bg_fever.lua,
-- generated by scratchpad/r12bg/bgfever.py. If that table is missing this
-- falls back to drawing the static plate only -- never to the parent's art,
-- because Graphics/background/bg_fever/* is fully ours (every texture name the
-- generated id map declares is either a shipped layer or an 8x8 transparent
-- stub).
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/bg_fever.lua.
-- The gogo/clear backdrop is a static plate plus scrolling strips, wiped in/out
-- over a fixed-length fade; falls back to the plate only when data is missing.
local ok, DATA = pcall(require, "background/bg_data/bg_fever")
if not ok or type(DATA) ~= "table" then DATA = {} end
-- per-element replay of the cabinet movie (koi, fans, tops... each on its own
-- loop) when bg_data/bg_anim/bg_fever_N.lua has been exported; the layer lists
-- above stay as the fallback. The rise/fall wipe is still this file's fade.
-- per-element replay of the cabinet movie when bg_data/bg_anim/bg_fever_N.lua
-- has been exported; the layer lists above stay as the fallback.
local ok_anim, BGAnim = pcall(require, "background/bg_objects/bg_anim")
if not ok_anim or type(BGAnim) ~= "table" then BGAnim = nil end
local ArcadeBGFever = {}
ArcadeBGFever.__index = ArcadeBGFever
-- The cabinet wipes normal->fever over `normal_fever` (root frames 1..29 at
-- 60 fps). We cannot reproduce the wipe with one texture, so it is a fade of
-- the same length, and `transitioned` flips when it ends -- that is the flag
-- background.lua uses to stop drawing bg_normal underneath.
--
-- ROUND 52 (r52-lua-divergence-fixes): the wipe now runs on BOTH edges and is
-- re-triggerable, matching the reference build `EnsoGraphicBgFever::Process`
-- (EnsoGraphicBgFever.obj.c:297-308): the cached flag at +88 is compared
-- against `IsNormaClear` EVERY frame, and each change of state GotoAndPlay's a
-- clip -- `normal_fever` on the rising edge, `fever_normal` on the falling
-- edge. The old `started`/`transitioned` one-way latch meant only the FIRST
-- clear ever wiped and a gauge dropping below the norma hard-cut the fever
-- backdrop in a single frame. `stop()` is the falling-edge clip (a fade of
-- the same 29-frame length, from wherever the current fade sits, so an edge
-- mid-wipe reverses smoothly), and `start()` re-arms after it.
-- `is_visible()` is the new flag background.lua uses to keep drawing this
-- object while the exit fade is still alive.
local TRANSITION_MS = 29000 / 60
local TRANSITION_MS = 29000 / 60 -- normal<->fever wipe length
function ArcadeBGFever.new(index, path)
local self = setmetatable({}, ArcadeBGFever)
@@ -83,8 +53,7 @@ function ArcadeBGFever.new(index, path)
end
end
if #self.layers == 0 then
-- fail soft: the generated table is missing or named textures this
-- folder does not have. Draw the one name every slot ships.
-- fail soft: draw the one name every slot ships
local id = tex.get_texture(self.path .. "/" .. self.name .. "/background")
if id then table.insert(self.layers, {id = id, period = 0, vx = 0, n = 0}) end
end
@@ -96,7 +65,7 @@ function ArcadeBGFever:start()
if self.started then return end
self.started = true
self.transitioned = false
-- `normal_fever`: wipe in from wherever the (possibly mid-exit) fade sits.
-- `normal_fever`: wipe in from wherever the (possibly mid-exit) fade sits
local from = self.fade and self.fade.attribute or 0.0
self.fade = anim.fade(TRANSITION_MS, {
initial_opacity = from,
@@ -106,7 +75,7 @@ function ArcadeBGFever:start()
self.fade:start()
end
-- ROUND 52: falling edge (`fever_normal`). Re-arms start() for the next clear.
-- Falling edge (`fever_normal`); re-arms start() for the next clear.
function ArcadeBGFever:stop()
if not self.started then return end
self.started = false
@@ -120,9 +89,7 @@ function ArcadeBGFever:stop()
self.fade:start()
end
-- ROUND 52: true while anything of this backdrop is on screen -- entering,
-- steady, or exiting. background.lua draws bg_normal beneath whenever the
-- fever plate is not yet (or no longer) fully opaque.
-- True while anything of this backdrop is on screen (entering, steady, or exiting).
function ArcadeBGFever:is_visible()
if self.started then return true end
return (self.fade.attribute or 0) > 0.001 and not self.fade.is_finished
@@ -149,9 +116,7 @@ function ArcadeBGFever:draw()
return
end
-- back to front, in the cabinet's own depth order: the static art is NOT
-- all behind the strips (bg_fever_a_02's spinning tops sit in front of an
-- opaque checker strip), so the order in the table is load-bearing.
-- back to front, in the cabinet's own depth order (load-bearing)
for _, l in ipairs(self.layers) do
if l.period > 0 then
local off = (self.elapsed / 1000.0) * l.vx
+2 -20
View File
@@ -168,13 +168,6 @@ end
local BGNormal4 = setmetatable({}, {__index = BGNormalBase})
BGNormal4.__index = BGNormal4
-- ROUND 58 (r58-2p-background-polish): the glow PERIOD is cabinet-true now.
-- The shared animation.json id 14 breathes over 166.7 ms halves; the cabinet's
-- bg_nomal_a_05 glow (sprite 15) is a 50-frame cycle at 60 fps -- 25 frames
-- down, 25 up = 416.7 ms halves (the number the old in-file C-list note
-- carried). Rather than fork the parent's shared animation file, this class
-- keeps its own linear triangle phase and no longer reads anim 14 at all;
-- the 0.5 .. 0.164 endpoints below are the same measured pair as before.
local GLOW_HALF_MS = 25000 / 60 -- 25 cabinet frames
function BGNormal4.new(index, path)
@@ -201,15 +194,8 @@ function BGNormal4:update(current_time_ms)
self.glow_last_ms = current_time_ms
end
-- ROUND 18, item 3. The cabinet's ten lantern glows (bg_nomal_a_05 shape 13, char 15)
-- are NOT on a regular grid: each has its own scale and five of them are mirrored.
-- Measured with `lumen_anim_dump.exe enso_normal/enso/background/bg_nomal_a_05.nulm
-- --all --leaves`; the registration is the shape's TOP-LEFT (verified against a
-- `lumen_shape_dump -m` composite: all ten halo centres land on tx + 113*s, or
-- tx - 113*s for the mirrored ones). texture.json carries the top-left, this table
-- carries the scale and the mirror. The cabinet composites them ADDITIVELY.
-- {scale, mirrored} index 0..9 == texture.json light_overlay entry 0..9
local BG4_GLOW = {
-- {scale, mirrored} index 0..9 == texture.json light_overlay entry 0..9
{0.95, true}, {1.25, true}, {1.00, true}, {1.15, true}, {0.90, true},
{0.90, false}, {1.10, false}, {1.05, false}, {1.25, false}, {1.00, false},
}
@@ -219,10 +205,6 @@ function BGNormal4:draw()
for i = 0, 9 do
tex.draw_texture(self._id_paper_lamp, {frame = 9 - i, index = i})
end
-- The cabinet's glow alpha breathes over 0.5 .. 0.164 (sprite 15, 50
-- frames). ROUND 58: driven by this class's own 416.7 ms-half triangle
-- (see GLOW_HALF_MS above) -- the old shared-anim-14 remap kept the
-- arcade RANGE but ran at the parent's 166.7 ms period (was C-list).
local p = self.glow_phase_ms / GLOW_HALF_MS -- 0..2
if p > 1 then p = 2 - p end -- triangle 0..1..0
local ga = 0.5 - p * (0.5 - 0.164) -- 0.5 dim to 0.164, back
@@ -247,7 +229,7 @@ local BGNormal = {}
BGNormal.Base = BGNormalBase
BGNormal.BGNormal2 = BGNormal2
-- Exact replay of the cabinet movie (bg_data/bg_anim/bg_N.lua) when exported;
-- Exact replay of the cabinet movie when bg_data/bg_anim/bg_N.lua is exported;
-- the hand-built classes below stay as the fallback.
local ok_anim, BGAnim = pcall(require, "background/bg_objects/bg_anim")
if not ok_anim or type(BGAnim) ~= "table" then BGAnim = nil end
+3 -24
View File
@@ -1,27 +1,6 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/chibi.lua
--
-- ROUND 58 (r58-2p-background-polish): the chibi subsystem is OFF in this skin,
-- structurally. LUA_CHN_AUDIT.md's chibi row asked for the Japanese-naming
-- re-search before deciding revive-vs-remove (R12's "cabinet has no chibi" came
-- from an English filename search, the method that wrongly killed `dai`).
-- Done this round against the reference dump: the full per-directory movie
-- inventory of the reference sprite dump `lumen_sprites/000_default/enso_normal/enso`
-- (background / dancer / fever / information / lane / don / donbg / onpu /
-- renda_effect / tamashiigage) contains NO chibi-class element under ANY
-- naming -- the only candidate the English search could have missed,
-- `information/papamama`, resolves to a SetDynamicText notice WINDOW
-- (`enso_papamama_window_title/_text`, EnsoGraphicPapamama::Preparing), not a
-- per-hit character. the reference build's enso_post.bin part list (ROUND 41/58) agrees: no
-- part maps to a per-良 spawn. The subsystem is a PyTaiko invention.
--
-- The skin has carried 113 transparent 71-byte PNG stubs (every chibi texture)
-- since the child skin was cut, so every 良/不可 spawned an invisible object:
-- allocation + per-frame update/draw churn for nothing drawn. This override
-- keeps the parent's exact ChibiController API (background.lua and every
-- bg_collab construct/update/draw/destroy it unconditionally) but loads no
-- folders and spawns nothing. Reviving later = delete this file (the parent
-- module and the child's chibi/2.lua fix are still in place) and ship real art.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/chibi.lua:
-- the chibi subsystem is OFF in this skin — no art ships, so keep the parent's
-- ChibiController API but load no folders and spawn nothing.
local ChibiController = {}
ChibiController.__index = ChibiController
+1 -4
View File
@@ -25,10 +25,7 @@ function Chibi2.new(index, bpm, is_2p, path)
end
function Chibi2:update(current_time_ms, bpm)
-- ROUND 58 (r58-2p-background-polish): LUA_CHN_AUDIT chibi row item (3) --
-- the `bpm ~= self.bpm` test ran AFTER BaseChibi.update had already stored
-- the new bpm into self.bpm, so it was always false and the rotation never
-- retimed on #BPMCHANGE. Compare against the PRE-update value instead.
-- retime the rotation against the pre-update bpm
local prev_bpm = self.bpm
BaseChibi.update(self, current_time_ms, bpm)
self.rotate:update(current_time_ms)
@@ -1,24 +1,5 @@
-- YataiDON-HSS child-only helper (ROUND 52, r52-lua-divergence-fixes).
--
-- The COLLAB dancer-count model, ported from the reference build
-- `EnsoGraphicDancer::Process` (EnsoGraphicDancer.obj.c:2723-2752). The
-- function is evaluated EVERY frame and is a pure function of the gauge; the
-- byte at +108 selects between the two branches:
--
-- r = current_gauge / CLEAR_POINT -- +327256 / +327164
-- normal troupes (+108 set): n = clamp(floor(2r + 3), 3, 5) -- ROUND 18
-- dance_b_* collabs (+108 0): n = clamp(floor(4r + 1), 1, 5) -- THIS FILE
--
-- i.e. a collab troupe starts at ONE dancer, gains one for every quarter of
-- the CLEAR point (not the full bar), and is symmetric -- the count falls
-- again when the gauge falls. The skin's collabs used the pre-R18 model
-- (floor(progress * MAX) over the whole bar); ROUND 52 replaces it with this.
-- The upper clamp is the collab's own MAX_DANCERS (per the user's directive
-- the per-collab maxima stay as shipped; reference's own clamp is 5).
--
-- The divisor handling (clear_progress plumbed as handle_gauge's 5th arg,
-- with the "learn the difficulty at the is_clear flip" fallback) is the same
-- fail-soft scheme background.lua:handle_dancer_count has used since R18.
-- Collab dancer-count model: troupe size = clamp(floor(4r + 1), 1, max), where
-- r = gauge / clear_point, grown/shrunk against a BaseDancerGroup-shaped object.
local CollabDancerModel = {}
CollabDancerModel.__index = CollabDancerModel
@@ -48,17 +29,14 @@ function CollabDancerModel:target(progress, is_clear, clear_progress)
local cp = self.clear_progress or CLEAR_DEFAULT
local r = (progress or 0) / cp
local n = math.floor(4 * r + 1)
-- the engine flips is_clear one gauge unit early; honour it so the full
-- troupe and the クリア state arrive on the same frame (same nudge R18
-- applies to the normal troupes).
-- honour the engine's one-gauge-unit-early is_clear flip so the full troupe
-- and the クリア state arrive on the same frame
if is_clear then n = self.max end
if n < 1 then n = 1 elseif n > self.max then n = self.max end
return n
end
-- Drives a BaseDancerGroup-shaped object (add_dancer/remove_dancer/
-- active_count) to the target count. Skips quietly until the group has
-- spawned its initial dancer (active_count 0 = first update not yet run).
-- Drives a BaseDancerGroup-shaped object to the target count.
function CollabDancerModel:apply(group, progress, is_clear, clear_progress)
local cur = group.active_count or 0
if cur <= 0 then return end
+14 -214
View File
@@ -1,168 +1,29 @@
-- YataiDON-HSS — the 段位道場 exam-panel extras that only Lua can draw.
--
-- ROUND 14. `Background:handle_dan(player, state)` is new this round (see
-- ENGINE_BINDINGS.md): DanGameScreen::update pushes the live exam state into the
-- Background script every frame, which is what MAPPING_dan_info's C-4 and
-- LUA_CAPABILITIES item 56 asked for. `Background:draw_fore()` already ran after
-- the whole GAME_DAN HUD -- it just had nothing to draw with.
--
-- The state table:
-- { dan_color, dan_title, song_index, song_count, total_notes,
-- remaining_notes, gauge, gauge_max,
-- exams = { { type, range, red, gold, value, progress, bar, failed }, ... } }
-- `value` is already normalised the way the engine's own panel prints it (for
-- range="less" it is the REMAINING allowance), `progress` is 0..1.
--
-- WHAT THIS DRAWS, and why each thing is here rather than in the engine's
-- draw_dan_info():
--
-- 1. The 「N %以上」 soul-gauge callout. The cabinet marks the dan's pass
-- threshold on the soul gauge itself (dani_enso: txt_num_border_gauge,
-- MS 明朝 36 #ffffff, right-aligned). `GaugeMode::DAN` has no label slot
-- and `Gauge` exposes no value, so this was unreachable from any skin
-- (MAPPING_dan_info "NOT REACHABLE" 6). The threshold is the `gauge` exam's
-- `red`, which handle_dan now carries, and the gauge bar's own geometry is
-- read out of game/gauge_dan/texture.json rather than written down here.
--
-- Everything else on the panel (row plates, bars, labels, thresholds, live
-- counts, 達成失敗, 残りノーツ数) is still the engine's own draw_dan_info() --
-- this module deliberately does NOT re-implement it. Repainting it from Lua
-- would mean neutralising fifteen engine draws with "y2": 0 and re-baking the
-- 3-sliced bar art; the row pitch, which was the actual arcade mismatch, is
-- fixed properly instead by the new per-screen `dan_exam_info_game` key.
--
-- FAIL SOFT: every read is guarded and every draw is skipped when its input is
-- missing, so a GAME_DAN run on an engine that never calls handle_dan (or with a
-- course that has no `gauge` exam) is exactly what it was before this file.
-- 段位道場 exam-panel extras only Lua can draw: the soul-gauge pass-threshold
-- callout plate + captions, anchored on the gauge's own 50-cell grid.
local M = {}
M.__index = M
-- The dan soul gauge, from Graphics/game/gauge_dan/texture.json: `1p_bar` sits at
-- (726, 248) and the bar art is 1080x48.
--
-- CORRECTED BY ROUND 100 -- the old line here read "i.e. 100 % spans x 726..1806"
-- and the threshold was placed with `GAUGE_X + GAUGE_W * pct/100`. That is the
-- ART's json base and PNG width, NOT the gauge's geometry. `Gauge::draw`'s DAN
-- branch (src/objects/game/gauge.cpp:652-665, ROUND 19) draws the fill on
-- kDanSegs = 50, kDanPitch = 21.0, kDanX0 = 738
-- so the gauge is 50 cells of 21 px running 738..1788 -- pitch 21.0, not
-- 1080/50 = 21.6, and starting 12 px right of the art base. Measured live on
-- scratchpad/r100/shots/before/gd_100.png: the olive groove runs x 738..1781.
-- The `overlay.png` separator art agrees independently: 49 bars, pitch EXACTLY
-- 21.000, texture columns 33+21k..38+21k -> screen 738+21k..743+21k (k = 1..49).
--
-- Consequence of the old formula: at 100 % it put the marker at 1806, i.e. 18 px
-- PAST the end of the gauge -- the red arrow visibly pointed at empty space to
-- the right of the bar (before_right_end_x3.png). The error was 0 near the
-- middle and grew to ~29 px at the ends, because the pitch was 2.9 % too wide.
local GAUGE_Y = 248
local GAUGE_X0 = 738 -- gauge.cpp kDanX0: cell 1 starts here
local CELL_PITCH = 21 -- gauge.cpp kDanPitch
local CELL_COUNT = 50 -- gauge.cpp kDanSegs
local GAUGE_X0 = 738 -- cell 1 starts here
local CELL_PITCH = 21
local CELL_COUNT = 50
local LABEL_SIZE = 30 -- arcade MS 明朝 36; ours reads heavier
-- `enso_dani/dani_select/dani_select` id 27 `txt_num_border_gauge`: size 36,
-- border 4.0, align right. ROUND 16 (r16-outline): a Lumen `border` is already
-- in 1080p stage px and OutlinedText multiplies the thickness by screen_scale
-- (1.5), so it is divided out here; this used to pass 3 and draw 4.5 px.
local WHITE = { 255, 255, 255, 255 }
local BLACK = { 0, 0, 0, 255 }
-- ── ROUND 64b (r64-danselect-fidelity, GAME_DAN half) ───────────────────────
-- The report: 「右上方的"魂Gauge 100%以上" 繪製錯誤」. The callout above the dan
-- soul gauge is not bare text on the cabinet -- it is a PLATE.
--
-- Source: the reference build `enso_dani/enso/tamashiigauge/tamashii_gauge.{nulm,nutexb}`,
-- root timeline (`lumen_anim_dump --list`: `tx_border` char 141, `tx_tamashii`
-- char 143; `lumen_shape_dump -m -O 960 540 -f 24 -c -v`):
--
-- shape 137 @ (-445,-39) a 586x60 light-grey rounded plate carrying a cream
-- rounded caption pill in its left half AND the red
-- down-arrow threshold marker baked at its right end
-- (the arrow tracks the plate in every frame of the
-- movie, which is how it was identified as one shape)
-- id 141 @ (-432,-79.5) size 36, align 1 (CENTRE), rgba ffffff -- "100%以上"
-- id 142 @ (-718,-74.5) size 26, align 2, rgba ffffff -- "魂ゲージ"
-- shape 139 @ (-176, 16) a 28x61 white/black-outlined cell that replaces the
-- gauge's own cell at the threshold. DECODED, NOT
-- PORTED: our dan gauge art has its own cell pitch,
-- so dropping the cabinet's cell onto it would land
-- between cells. Extracted for a later round as
-- game/dan_info/gauge_border_cell.png.
--
-- The plate's RIGHT end (where the arrow is) is the threshold position, so the
-- whole plate hangs to the LEFT of it with its bottom on the gauge's top edge.
-- Art extracted verbatim by scratchpad/r64/extract_gauge.py.
local PLATE_W, PLATE_H = 586, 60
--
-- ROUND 90 (r90-danplate-brown-and-place) -- ALL FOUR NUMBERS BELOW RE-MEASURED.
--
-- ROUND 64b read its placements off the ROOT timeline at frame 24, which is
-- inside the movie's `dani_result` region, and expressed them as raw stage
-- coordinates. This round reads sprite 145 (`gauge_border`) at ITS OWN frame 0,
-- the `dani_enso` label -- the state this screen is actually in -- rendered with
-- lumen_shape_dump ... -m -r 145 -f 0 -c -v -O 960 540
-- so every number is SPRITE-LOCAL with the threshold cell (shape 139, placed at
-- exactly (0,0)) as the origin. The relative geometry agrees with ROUND 64b to
-- the pixel; what changes is where that geometry is ANCHORED.
--
-- plate body (#EAEAEA) sprite-local x -562 .. +24, y -85 .. -26 (586 x 60)
-- gauge bar (sprite 146, gauge_mc) bar top = 146-local y +3, 33 px tall
-- the cell origin is 146-local (x, +16), so plate top = bar_top - 72
-- plate btm = bar_top - 12
--
-- Two consequences, and they are the user's items 1 and 3:
--
-- * X: the plate's right end is NOT the threshold -- it overhangs it by 24 px.
-- The red arrow baked into the plate is centred at plate-local 561.5, i.e.
-- ON the threshold, and 24 px of rounded plate end continues past it. Ours
-- put the plate's right EDGE on the threshold, so the arrow pointed 24 px
-- left of the cell it marks.
-- * Y: the plate does not sit ON the gauge, it floats 12 px above it. Ours had
-- `py = GAUGE_Y - PLATE_H`, i.e. flush. That is the reported 「該稍微上移」.
local PLATE_GAP = 12 -- px between the plate's bottom and the bar top
local PLATE_OVERHANG = 24 -- px the plate extends PAST the threshold
--
-- The two captions, from the same render (ink bounding boxes of the real glyphs,
-- measured against a no-font render of the same frame so only glyph pixels are
-- counted; the JP-path problem is solved with a directory junction, see
-- MAPPING_dan_info ROUND 90):
--
-- tx_tamashii (id 142, align 2 = CENTRE) box centre plate-local x 147,
-- glyph ink -463..-361 x -78..-53 -> ink centre plate-local (150, 19.5)
-- tx_border (id 141, align 1 = RIGHT) box right edge plate-local 532,
-- glyph ink -188..-26 x -80..-46 -> ink centre y plate-local 22
--
-- CORRECTION TO ROUND 64b, which recorded id 141 as "align 1 (CENTRE)": it is
-- not centred. Its 228 px box spans plate-local 304..532 (centre 418) while the
-- 「100%以上」 ink measures 374..536 -- flush with the box's RIGHT edge, 44 px
-- right of the box centre. align 1 is RIGHT. (ROUND 64b was still right that the
-- old code's bug was right-aligning to the THRESHOLD, 54 px further right again.)
local PILL_CX, PILL_CY = 150, 20 -- 魂ゲージ ink centre, plate-local
local VALUE_RIGHT = 532 -- 100%以上 is RIGHT-aligned to this
local VALUE_CY = 22 -- its ink centre y, plate-local
local NAME_SIZE = 26 -- arcade id 142
local VALUE_SIZE = 36 -- arcade id 141
--
-- The outline colour. See MAPPING_dan_info ROUND 90 §1 for the full derivation
-- and for the correction it makes to ROUND 78 §5.
--
-- Short version: the record itself carries no border colour (`+50 == 0`), so
-- `AppLumenRenderer::RenderText` takes the renderer default `this[1102]`, which
-- `SetTextColor` sets to pure black for a non-black fill -- ROUND 78, still true.
-- But RenderText then runs that colour through the PlaceObject colour transform
-- (`out = colour * Cmul + Cadd`, AppLumenRenderer.obj.c:1265-1282), and the
-- caption's wrapper sprite `tx_tamashii` (char 143) carries
-- cadd = [80, 60, 45, 0] (= tamashii_gauge.nulm styles[63], #503C2D)
-- while `tx_border` next to it carries cadd [0,0,0,0]. So:
-- outline = BLACK * 1 + (80,60,45) = #503C2D BROWN
-- fill = WHITE * 1 + (80,60,45) = saturates back to #FFFFFF
-- which is exactly why the add is invisible except on the outline, and exactly
-- why the user reported the pill caption as black-when-it-should-be-brown while
-- saying nothing about the 「100%以上」 next to it.
local CAP_FILL = { 255, 255, 255, 255 }
local CAP_OUTLINE = { 80, 60, 45, 255 } -- #503C2D, the colour ADD, measured
local VALUE_OUTLINE = { 0, 0, 0, 255 } -- cadd [0,0,0,0] -> stays pure black
local CAP_OUTLINE = { 80, 60, 45, 255 } -- #503C2D
local VALUE_OUTLINE = { 0, 0, 0, 255 }
function M.new()
return setmetatable({ state = nil, label = nil, label_for = nil,
@@ -197,53 +58,7 @@ function M:draw()
local ex = gauge_exam(st)
if not ex or not ex.red or ex.red <= 0 then return end
-- ── ROUND 100 (r100-danplate-anchor) ────────────────────────────────────
-- The threshold, on the gauge's own 50-cell grid.
--
-- The cabinet does NOT compute a pixel position at all. Its owner,
-- `App::DojoEnsoGraphicTamashiiGage`, contains ZERO position/transform API
-- calls -- the complete list of `lumen::` symbols in the whole 60-function
-- module is CalcHash / BaseMovieInterface(::inner_type) /
-- DisplayList::search_descendant / DynamicText(Interface)(::set_text) /
-- MovieClipInterface::{goto_label,play,search_child,search_descendant,stop} /
-- Player::root_mc / Ptr / String / WeakPtr. All it does is
-- goto_label(gauge_mc, "border_" .. N); stop(gauge_mc);
-- (DojoEnsoGraphicTamashiiGage.obj.c:1081-1097), so the movie's own frame is
-- the entire answer.
--
-- And the movie MOVES THE WHOLE PLATE. `enso_dani/enso/tamashiigauge/
-- tamashii_gauge.nulm` MC 146 (`gauge_mc`) has 51 frames labelled
-- border_0..border_50 -- the gauge is in 2 % units -- and the instance
-- `gauge_border` (the plate, both captions and the baked red arrow, all one
-- child) is placed at
-- tx = -575 + 21*(N-1) N >= 1 (border_0 aliases to border_1)
-- i.e. -575, -575, -554, -533 ... -71 (50 %) ... 454 (100 %). Exactly the
-- 21 px pitch of the sibling `glow_1..glow_50` gauge cells, which sit at
-- -574 + 21*(N-1). Walked in scratchpad/r100/border_track2.py; cross-checked
-- in pixels against r90's own s146_f0/f25 renders (plate bodies 504 px
-- apart, JSON says -71 - -575 = 504 -- exact).
--
-- SO THRESHOLD-TRACKING IS THE CABINET'S RULE, NOT OUR INVENTION. A 50 %以上
-- course really does render this plate mid-gauge on a reference cabinet.
-- Do not "fix" it to the right; see MAPPING_dan_info ROUND 100.
--
-- What IS ported here that was not before:
-- * the 2 % QUANTISATION. The cabinet has 51 discrete frames and nothing in
-- between, so N = threshold/2. This matters: 13 of the 25 shipped courses
-- have an ODD threshold (91/93/95/97/99).
-- UNVERIFIED: whether the cabinet floors or rounds. Floor is used because
-- the one place the reference source still shows the arithmetic is an int cast,
-- `(int)(<field> * 0.5)` at DojoEnsoGraphicTamashiiGage.obj.c:1078, which
-- truncates -- but IDA lost the actual argument handed to `to_string<int>`
-- at :1082, so this is an INFERENCE, not a read.
-- * the 21 px pitch and the 738 origin (see the header block).
--
-- `x` is the plate's ANCHOR = `gauge_border`'s origin, which the movie puts
-- at the CENTRE of the marker cell: sprite 145 places shape 139 (the 28x61
-- replacement cell) at exactly (0,0) and that shape's local box is
-- -16..+16, so its centre IS the origin. The cabinet's marker tile lands at
-- MC146-local -591..-559 for N=1 while the glow cell's tile is -590..-558 --
-- the same slot, nudged 1 px left, which is the `- 1` below.
-- the threshold, on the gauge's own 50-cell grid (2 % quantised)
local pct = ex.red
local n = math.floor(pct / 2)
if n < 1 then n = 1 end -- border_0 == border_1 on the cabinet
@@ -263,26 +78,16 @@ function M:draw()
self.label_for = ex.red
end
-- ROUND 64b / re-anchored ROUND 90: the plate. Clamped so a very low
-- threshold cannot push the plate off-screen left.
local pid = self:plate_id()
-- ROUND 90: the plate overhangs the threshold by 24 px (the arrow, baked at
-- plate-local 561.5, is what lands ON it) and floats 12 px above the bar.
-- ROUND 100: `px` is a pure function of `ex.red`, and `ex.red`'s ONLY writer
-- is dan_select.cpp:949 (`sd.selected_dan_exam = current->exams`) on leaving
-- DAN_SELECT -- game_dan.cpp only ever reads it. So the plate cannot move
-- inside a course. Measured, not just argued: 432,240 draw calls across a
-- whole 3-song course, both song transitions included, produced exactly ONE
-- distinct (px, py). See MAPPING_dan_info ROUND 100 §B.
-- the plate overhangs the threshold by 24 px (the baked arrow lands ON it)
-- and floats PLATE_GAP above the bar
local px = math.max(0, math.floor(x - PLATE_W + PLATE_OVERHANG + 0.5))
local py = GAUGE_Y - PLATE_GAP - PLATE_H
if pid then
tex.draw_texture(pid, { x = px, y = py })
end
-- FAIL SOFT: with no plate art the two captions still draw, at the same
-- places, so this degrades to (a better-placed) version of what it replaced
-- rather than to nothing.
-- fail soft: with no plate art the two captions still draw, at the same places
if self.name_label then
self.name_label:draw({
x = math.floor(px + PILL_CX - self.name_label.width / 2 + 0.5),
@@ -290,12 +95,7 @@ function M:draw()
})
end
if self.label then
-- ROUND 90: `tx_border` is align = 1 = RIGHT, and its box's right edge is
-- plate-local 532 -- 30 px INSIDE the threshold, not at it. ROUND 64b
-- read it as centred; the measured glyph ink says otherwise (see the
-- header). The pre-ROUND-64b bug was right-aligning to the threshold
-- ITSELF (plate-local 586), 54 px further right again, so this is not a
-- revert of that fix.
-- tx_border is RIGHT-aligned; its box's right edge is plate-local 532
self.label:draw({
x = math.floor(px + VALUE_RIGHT - self.label.width + 0.5),
y = math.floor(py + VALUE_CY - self.label.height / 2 + 0.5),
+15 -94
View File
@@ -1,5 +1,4 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/dancer.lua: Dancer0_4 bounce 20*sc; dancer spacing from info.x2[1]
-- round 11: the normal-gameplay dancers are now the reference cabinet rig (bottom of file).
-- BaseDancer: single dancer that starts off-screen and bounces in, then loops
local BaseDancer = {}
BaseDancer.__index = BaseDancer
@@ -381,55 +380,8 @@ function DancerGroupPoof2.new(index, bpm, max_dancers, path)
return BaseDancerGroup.new(index, bpm, max_dancers, path, DancerPoof2)
end
-- ============================================================================
-- ROUND 11 -- reference cabinet dancers (enso_normal/enso/dancer/dancer_*.nulm)
-- ----------------------------------------------------------------------------
-- Everything above this line is the PyTaikoGreen rig, kept verbatim because the
-- collab backdrops (a3.lua, funassyi.lua) still build their dancers out of it.
-- The NORMAL gameplay backdrop now uses the classes below instead, which replay
-- the cabinet's own dancer timelines: every arcade dancer frame is a single
-- shape placement with an affine transform (no skeletal deformation), so the
-- rig ports exactly -- the shapes become the frames of `<v>_loop` and the
-- per-frame {slot,tx,ty,sx,sy,rot,alpha} rows live in
-- Scripts/background/dancer_data/dancer_<g>.lua (generated).
--
-- Timing: the clips are authored at 60 fps with every pose boundary on a
-- multiple of 20 frames and every loop an exact multiple of 80 frames, i.e. a
-- beat is 40 frames (90 BPM). We replay them at BEAT_FRAMES per beat so the
-- dancers stay locked to the song.
--
-- ROUND 15 (r15-dancer) -- "dancer的舞步應該要同步". The round-11 rig gave
-- every slot its OWN `fpos` accumulator, started at whatever wall-clock moment
-- `add_dancer()` happened to fire (a soul-gauge milestone), so the five dancers
-- ran at five unrelated phases: slot 1 could be mid-hop while slot 2 was
-- landing. That is not what the cabinet does, and the cabinet PHYSICALLY
-- CANNOT do it. Measured, from the arcade movies themselves
-- (`lumen_anim_dump.exe dancer_*.nulm --sprite <root> --all`):
--
-- * the root timeline places all five slots `pos0..pos4` on frame 0 and has
-- a bare stop() there, so the root never advances and never re-places
-- anything -- root frames 0..4 are identical display lists;
-- * every slot is an instance of the SAME one-frame "slot" sprite, which
-- itself carries ALL of that movie's `DancerN` variant clips at once
-- (tetsuo: 5 slots x 5 variants = 25 dancer sprite instances live at all
-- times), and the game picks what you see with visibility, not the timeline;
-- * dumped densely, all 25 instances report the SAME frame cursor on every
-- single stage frame ("f":0,1,2,... in lockstep).
--
-- So the arcade rig has exactly ONE playhead for the whole troupe. A per-slot
-- phase does not exist in the data, which is why the steps are always together.
-- This port now models that directly: the GROUP owns `gframe` and every slot
-- reads it. Sync is structural, not maintained -- there is no second clock to
-- drift against.
--
-- Entering / leaving. Each variant clip is `in` [0,80) -> `dance` [80,out) ->
-- `out` [out,n). `in` is 80 frames in all six movies, and 80 frames is a pose
-- boundary at every multiple inside every dance loop (measured: run starts land
-- on 0, 80, 160, ... for all six), so 80 is the phrase quantum. A dancer that
-- joins therefore begins its hop so that it LANDS on a phrase boundary of the
-- shared loop, and a dancer that leaves peels off at the next phrase boundary.
-- Worst-case wait is one phrase (2 beats), and the joiner is on the group's own
-- frame number from its first danced frame -- never near it, exactly it.
-- Reference cabinet dancers: a single playhead per troupe, replayed at
-- BEAT_FRAMES per beat.
local BEAT_FRAMES = 40
local ARCADE_GROUPS = 6
local PHRASE = 80
@@ -462,9 +414,7 @@ end
function ArcadeDancer:begin_leave(g)
if self.state == "idle" or self.state == "out" then return end
if self.state == "in" and g < self.join then
-- earned and un-earned again before it ever landed: it simply never
-- arrives. Without this it would fall into the `out` branch below and
-- be drawn from the dance loop it has not joined yet.
-- earned and un-earned again before it ever landed: it simply never arrives
self.state = "idle"
return
end
@@ -472,8 +422,8 @@ function ArcadeDancer:begin_leave(g)
self.state = "out"
end
-- The one place a frame number is produced. Everything is a pure function of
-- the GROUP's clock `g`, so two slots in the same state are on the same frame.
-- The one place a frame number is produced; everything is a pure function of the
-- group's clock `g`, so two slots in the same state are on the same frame.
function ArcadeDancer:frame_at(g)
local vd = self.vd
local st = self.state
@@ -505,14 +455,12 @@ function ArcadeDancer:draw(px, py, g)
if not f then return end
local vd = self.vd
local row = find_key(vd.keys, math.floor(f))[2]
-- Child-skin textures are loaded at scale 1.0 (texture.cpp:525), so every
-- coordinate here is in the child's own 1920x1080 space -- do NOT multiply
-- by tex.get_screen_scale(), which is the 1280x720 PARENT-space factor.
-- child-skin textures are loaded at scale 1.0, so every coordinate here is in
-- the child's own 1920x1080 space (do NOT multiply by tex.get_screen_scale())
for i = 1, #row do
local d = row[i]
local sx, sy = d[4], d[5]
-- every frame is padded to the same size (the engine draws them all at
-- frame 0's) but keeps its OWN origin, which `origin` supplies per draw.
-- every frame keeps its own origin, which `origin` supplies per draw
local o = vd.o[d[1] + 1]
tex.draw_texture(self.tex, {
frame = d[1],
@@ -538,27 +486,11 @@ function ArcadeDancerGroup.new(index, bpm, max_dancers, path)
self.data = require("background/dancer_data/dancer_" .. g)
tex.load_folder(self.path, "dancer/" .. self.name)
-- pos0..pos4 on the cabinet are centre, left, right, far-left, far-right --
-- the same spawn order the parent rig used, so slot i is simply pos(i-1).
-- pos0..pos4 on the cabinet are centre, left, right, far-left, far-right
self.max_dancers = max_dancers or 5
-- ROUND 18 (r18-misc). The cabinet's troupe size is NOT "start at one and
-- earn the rest": `App::EnsoGraphicDancer::Process` (reference source
-- `EnsoGraphicDancer.obj.c:2723-2752`) computes, every frame,
-- r = gauge / clear_point
-- n = clamp(floor(2r + 3), 3, 5) -- when kParamTable's min3 flag is set
-- and hands `n` to `CheckDancerNum`. All SIX normal-enso troupes
-- (dancer_donko / _inu / _kame / _tetsuo / _omen / _unagi -- exactly the six
-- this class serves) carry that flag, so the floor really is THREE and the
-- very first Process call (gauge 0 => n = 3) makes slots 0,1,2 hop in
-- together. Only the collab `dance_b_*` troupes use the other branch,
-- floor(4r + 1) with a floor of one, and those do not come through here --
-- `Dancer.create` routes them to the parent PyTaikoGreen classes.
-- The count is what changed; the PHASE model (round 15) did not: a slot
-- added at gframe 0 goes through the same `begin_enter`, so the three
-- openers land on the same phrase boundary as any later joiner.
self.min_dancers = 3
self.now = nil
self.gframe = 0 -- THE playhead: one per troupe, exactly as in the movie
self.gframe = 0 -- the troupe's single playhead
self.active = 0
self.slots = {}
@@ -585,19 +517,12 @@ function ArcadeDancerGroup:add_dancer()
end
function ArcadeDancerGroup:remove_dancer()
-- `CheckDancerNum` shrinks down to the value `Process` asked for, and for a
-- min3 troupe that value can never be below 3 -- so 3 is the floor here too.
-- (`min_dancers or 1` keeps the old behaviour if this class is ever reused
-- for a `dance_b_*` collab troupe, whose branch really does bottom out at 1.)
if self.active <= (self.min_dancers or 1) then return end
self.slots[self.active]:begin_leave(self.gframe)
self.active = self.active - 1
end
-- The cabinet's `CheckDancerNum(this, n)` (EnsoGraphicDancer.obj.c:2901-3008):
-- grow slot-by-slot in index order, shrink slot-by-slot in reverse. Expressing
-- the count ABSOLUTELY -- rather than as a milestone delta the caller has to
-- keep in sync -- is what stops the two from ever drifting apart.
-- Set the absolute troupe count; grows/shrinks slot by slot.
function ArcadeDancerGroup:set_dancer_count(n)
local lo = self.min_dancers or 1
if n < lo then n = lo elseif n > self.max_dancers then n = self.max_dancers end
@@ -611,15 +536,13 @@ function ArcadeDancerGroup:update(current_time_ms, bpm)
if dt < 0 then dt = 0 end
self.now = current_time_ms
-- one advance, one clock. BPM changes retime the whole troupe at once, so
-- a #BPMCHANGE cannot open a phase gap between slots either.
-- one advance, one clock; BPM changes retime the whole troupe at once
local frame_ms = (60000 / ((bpm and bpm > 0) and bpm or 120)) / BEAT_FRAMES
self.gframe = self.gframe + dt / frame_ms
if self.needs_initial then
self.needs_initial = false
-- gframe is still 0, so all three get enter = 0 / join = dance and hop in
-- together as the movie starts -- the cabinet's first `CheckDancerNum(0 -> 3)`.
-- gframe is still 0, so all three get enter = 0 / join = dance and hop in together
self:set_dancer_count(self.min_dancers or 1)
end
@@ -638,8 +561,7 @@ function ArcadeDancerGroup:draw()
local pos = self.data.pos
local py = self.data.posy
local g = self.gframe
-- far dancers first so the centre one ends up in front, matching the root
-- display-list depth order (pos4 deepest .. pos0 shallowest).
-- far dancers first so the centre one ends up in front
for i = self.max_dancers, 1, -1 do
local d = self.slots[i]
if d and d.state ~= "idle" then d:draw(pos[i] or 960, py, g) end
@@ -658,8 +580,7 @@ Dancer.ArcadeDancerGroup = ArcadeDancerGroup
function Dancer.create(index, bpm, path, max_dancers)
max_dancers = max_dancers or 5
-- The normal gameplay backdrop uses the ported arcade rig; the collab
-- backdrops still ship the parent's art and keep the parent's classes.
-- the normal gameplay backdrop uses the arcade rig; collab backdrops keep the parent's classes
if (path or "background") == "background" then
return ArcadeDancerGroup.new(index, bpm, max_dancers, path)
end
+1 -4
View File
@@ -1,5 +1,2 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/1.lua
-- ROUND 12: the cabinet's band is a single static 1920x276 plate (see base.lua),
-- so the parent's per-index tile/scroll/overlay rig has nothing left to do and
-- every index is the base class.
-- Cabinet band is a single static plate (see base.lua); every index is the base class.
return require("background/bg_objects/donbg/base")
+1 -4
View File
@@ -1,5 +1,2 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/2.lua
-- ROUND 12: the cabinet's band is a single static 1920x276 plate (see base.lua),
-- so the parent's per-index tile/scroll/overlay rig has nothing left to do and
-- every index is the base class.
-- Cabinet band is a single static plate (see base.lua); every index is the base class.
return require("background/bg_objects/donbg/base")
+1 -4
View File
@@ -1,5 +1,2 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/3.lua
-- ROUND 12: the cabinet's band is a single static 1920x276 plate (see base.lua),
-- so the parent's per-index tile/scroll/overlay rig has nothing left to do and
-- every index is the base class.
-- Cabinet band is a single static plate (see base.lua); every index is the base class.
return require("background/bg_objects/donbg/base")
+5 -18
View File
@@ -1,15 +1,6 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/4.lua
-- ROUND 12: the cabinet's band is a single static 1920x276 plate (see base.lua),
-- so for the DEFAULT donbg pack every index is the base class (draw_layers).
--
-- ROUND 60 (r60-collab-fix-all): but DonBG4 is ALSO the class funassyi's
-- COLLAB band uses (DonBG4.new(1, player_num, "background/collab/funassyi")),
-- and the plain `return base` alias silently dropped the parent DonBG4's
-- overlay row (the funassyi flag banners, donbg/0_1/overlay, anim 2 bob) on
-- top of the collab path's tile scroll. This override restores the parent's
-- DonBG4 shape on top of the ROUND 60 base contract: for the default pack the
-- base's draw_layers path still runs (animal/buttoburst, name "4_1", DATA
-- table), for a collab path draw_textures below adds the overlay row.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/4.lua:
-- restores the parent DonBG4 overlay row (funassyi's flag banners) on top of the
-- base contract.
local DonBGBase = require("background/bg_objects/donbg/base")
DonBG4 = setmetatable({}, { __index = DonBGBase })
@@ -21,7 +12,7 @@ function DonBG4.new(index, player_num, path)
for k, v in pairs(base) do self[k] = v end
self.overlay_move = tex.get_animation(2)
self.overlay_texture = tex.get_texture_info(self.name, "overlay")
self._id_overlay = tex.get_texture(self.path .. "/" .. self.name .. "/overlay") -- nil = missing (ROUND 60: id 0 is valid)
self._id_overlay = tex.get_texture(self.path .. "/" .. self.name .. "/overlay") -- nil = missing
return self
end
@@ -44,11 +35,7 @@ function DonBG4:draw_textures(fade, y, frame)
y = y
})
end
-- parent DonBG4 geometry: banner row scrolls with the tiles and bobs
-- on anim 2. (The ghost trails this draw showed mid-round were the
-- id-0 bug -- see base.lua: the tile row was being skipped, so the
-- band was never repainted and every moving sprite smeared. With the
-- tiles painting again, the moving overlay is covered every frame.)
-- banner row scrolls with the tiles and bobs on anim 2
if self._id_overlay and self.overlay_texture then
tex.draw_texture(self._id_overlay, {
frame = frame,
+1 -4
View File
@@ -1,5 +1,2 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/5.lua
-- ROUND 12: the cabinet's band is a single static 1920x276 plate (see base.lua),
-- so the parent's per-index tile/scroll/overlay rig has nothing left to do and
-- every index is the base class.
-- Cabinet band is a single static plate (see base.lua); every index is the base class.
return require("background/bg_objects/donbg/base")
+3 -47
View File
@@ -1,46 +1,5 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/6.lua
-- ROUND 12: the cabinet's band is a single static 1920x276 plate (see base.lua),
-- so the parent's per-index tile/scroll/overlay rig has nothing left to do and
-- every index is the base class.
--
-- ROUND 102 (r102-bg-sweep). That is still true of index 6's two SCROLLING
-- layers -- bg_data/donbg.lua's "5_x" carries them and this round re-measured
-- them as correct (-91.92 px/s wrapping 490 px, and -91.98 px/s wrapping 980 px).
-- What ROUND 13 missed is a THIRD thing in `enso/donbg/donbg_a_06_{1,2}p`: three
-- sakura-petal drifters, `#13@0/#6@{1,2,3}/#4@0` (fever: `#27@1/#21@{1,2,3}/#19@0`),
-- sitting at root depths 1..3 -- i.e. BETWEEN the two scrolling layers (#3@0 is
-- depth 0, #12@4 is depth 4). They were never sampled because their own parent
-- `#6@k` is constant (tx 2110, ty -178, every frame); only the grandchild moves.
-- This is the same "sampled the wrong level of the display list" mistake ROUND 26,
-- ROUND 35 and ROUND 76 each hit on the dan band.
--
-- Dumped with `lumen_anim_dump --sprite <root> --all --leaves --auto-stop
-- --range 0,700` and read frame by frame, one drifter walks
-- (2308, -106) -> (1540, 382) over 160 frames, then snaps back
-- i.e. -4.830189 px/frame = -289.81 px/s and +3.069182 px/frame = +184.15 px/s at
-- the stage's own 60 fps ("fps":60 in the dump; NOT Common.FPS = 120, which is the
-- Lua tick). The three instances are in phase and 980 px apart. Both players'
-- movies measure identically.
--
-- The art is `overlay_2` (frame 0 normal, frame 1 fever): a 391x208 nine-petal
-- cluster lifted straight out of the cabinet with
-- lumen_shape_dump -m -r 6 -O 700 60 (normal)
-- lumen_shape_dump -m -r 21 -O 700 60 (fever)
-- and cropped to the exact alpha>0 bounding box, which is x 703..1093, y 28..235 --
-- verified explicitly rather than trimmed by a heuristic (ROUND 88). No colour
-- transform is involved (`alpha: 1/1 placement(s) visible, mean Amul = 256.0/256`),
-- so it is a plain source-over composite, never ImageChops.screen (ROUND 94).
-- Its registration point is (-195, -104) from the placement, so the art's top-left
-- is (world_x - 195, world_y - 104).
--
-- `overlay_2` was an 8x8 placeholder that nothing drew, so this introduces no new
-- texture name -- a child skin cannot invent one (ROUND 11) and does not need to.
--
-- The cabinet clips the drifters to its 1920x276 stage; we have no stage, so the
-- draw is cropped to the band with `src` + `y2` exactly the way ROUND 76's
-- bg_collabs/dan.lua crops its petals. Without that the bottom of a cluster would
-- fall over the play area, and on 2P (band drawn at y = 804) the top of one would
-- paint over 1P's lane.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/6.lua:
-- adds the cabinet's three sakura-petal drifters (between the two scrolling layers).
local DonBGBase = require("background/bg_objects/donbg/base")
DonBG6 = setmetatable({}, { __index = DonBGBase })
@@ -61,10 +20,8 @@ function DonBG6.new(index, player_num, path)
local self = setmetatable({}, DonBG6)
local base = DonBGBase.new(index, player_num, path)
for k, v in pairs(base) do self[k] = v end
-- ROUND 60: id 0 is a valid texture id, so nil (not 0) means missing.
self._id_petal = tex.get_texture(self.path .. "/" .. self.name .. "/overlay_2")
-- Fail soft: only the DEFAULT pack ("5_x", path "background") owns this rig.
-- A collab routed through DonBG6 keeps the plain base behaviour.
-- fail soft: only the DEFAULT pack ("5_x", path "background") owns this rig
self._petals_on = self.use_layers and (self._id_petal ~= nil)
return self
end
@@ -117,7 +74,6 @@ function DonBG6:draw(y)
if self.is_clear then
fade = self.clear_fade.attribute
elseif self.exit_fade then
-- ROUND 52: falling-edge reverse fade (`fever_normal`).
fade = self.exit_fade.attribute
end
if fade and fever then
@@ -1,21 +1,9 @@
-- Cabinet-faithful donbg band player.
--
-- Data: Scripts/background/bg_data/donbg_anim/a_NN_Mp.lua (generated by
-- scratchpad/lyric/donbg_export2.py from the cabinet's enso/donbg movie): for each
-- state ("normal" / "fever") the list of shape instances in depth order. Each one
-- carries the period of its own loop and the channels tx/ty/sx/sy/rot/a, a channel
-- being either one number (constant) or one value per frame of the loop (a = 0 means
-- not placed). A flipbook frame additionally has a presence mask `on` (string of
-- '0'/'1') with its own `on_period`, so the fish's arc (parent motion) and its frame
-- swap need not share a period. Textures: Graphics/background/donbga_NN_Mp/s<c>.png;
-- shapes[c] = {w, h, ox, oy} says where the shape origin sits inside its png.
--
-- Everything runs on one 60 Hz clock, so the squid that peeks up, the fish that
-- jumps, the fans that drift and the tiles that scroll keep the cabinet's timing.
-- Data: background/bg_data/donbg_anim/a_NN_Mp.lua; textures: donbga_NN_Mp/s<c>.png.
local DonBGAnim = {}
DonBGAnim.__index = DonBGAnim
local FRAME_MS = 1000.0 / 60.0 -- the band steps at 60 Hz (checked against the original)
local FRAME_MS = 1000.0 / 60.0 -- the band steps at 60 Hz
local ONE = string.byte("1")
function DonBGAnim.new(index, player_num, path)
+13 -93
View File
@@ -1,50 +1,10 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/base.lua
--
-- ROUND 13. The band behind the player MOVES, and round 12 froze it.
--
-- The cabinet's enso/donbg/donbg_a_XX_Np.nulm is a 1920x276 stage holding one
-- group per state (normal / fever / the unreachable `miss`), and it is that
-- group's CHILDREN that slide: every one of them translates left at a constant
-- ~92 px/s and wraps on its own period (496 px for index 0, 492 for 1 and 2,
-- 490 + 986 for 3, 490 for 4, 490 + 980 for 5). Round 12 probed tx on the
-- group, which is a constant, and shipped one static plate.
--
-- ROUND 19 (r19-donbg). The character row `donbg_obj_a_01` -- the drummer, the
-- mikoshi, the fox mask, the chicks -- that rounds 12/13/18 drew on top of this
-- band is NOT on the cabinet's enso screen. `datatable/enso_post.bin` is the
-- enso placement table (33 records for target 0) and it places exactly ONE
-- donbg movie, `donbg/donbg_a_0N_1p` at (0,0); `donbg_obj_a_01` appears in no
-- enso_post record for any target, in no `enso_rand_part.bin` random pool, in
-- no `music_ensoparts` / `qrcode_info_ensoparts` row, in no lua under
-- `the reference lua tree` (there is no enso lua at all), and as no string in
-- the reference build (byte search: `donbg` 0 hits while `lumen/000_default` gets 27 and
-- `enso_post` 1, so the search is sound). It is the only `*_obj_*` movie in
-- any donbg folder in any of the 27 lumen packs -- leftover art from an older
-- build. The user's own arcade reference says the same (REFERENCE_SCREENS.md,
-- Gameplay): "Top band: pink umbrella pattern with Don (left), song title
-- top-right + genre pill" -- pattern plus the 2D Don at the LEFT, no character
-- row. So the row, and its bob, are gone.
--
-- So this draws, back to front, the layer list that
-- Scripts/background/bg_data/donbg.lua carries for the current state:
-- period > 0 : a tile, repeated across 1920 px and scrolled at vx px/s
-- period == 0: a plate drawn once at its texture.json position
-- Layer ORDER is load-bearing:
-- the cabinet puts static motifs (index 1, 2 and 4's spinning tops, kites and
-- streamers) IN FRONT of the opaque scrolling backdrop.
--
-- frame 0/1 of a name are not "normal/fever" any more -- a name's frames are
-- just its layers, and the table says which frame and which texture.json entry
-- (params.index) each draw uses. The cabinet's third state, `miss`, still has
-- no Lua hook (MAPPING.md's C list).
--
-- Fails soft: with no generated table this draws `background` frame 0 once,
-- never the parent's art -- every texture name these folders declare is ours.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/donbg/base.lua.
-- The cabinet band is a 1920x276 stage whose children slide left and wrap; the
-- default pack is DATA-driven layer lists, collab paths use the parent tiler.
local ok, DATA = pcall(require, "background/bg_data/donbg")
if not ok or type(DATA) ~= "table" then DATA = {} end
-- per-element replay of the cabinet movie (squid / waves / fans each on their own
-- motion) when bg_data/donbg_anim/a_NN_Mp.lua has been exported for this band;
-- otherwise the layer lists above.
-- per-element replay of the cabinet movie when bg_data/donbg_anim/a_NN_Mp.lua is
-- exported for this band; otherwise the layer lists.
local ok_anim, DonBGAnim = pcall(require, "background/bg_objects/donbg/anim_player")
if not ok_anim or type(DonBGAnim) ~= "table" then DonBGAnim = nil end
@@ -59,22 +19,7 @@ function DonBGBase.new(index, player_num, path)
tex.load_folder(path, string.format("donbg/%s", self.name))
self.bg_texture = tex.get_texture_info(self.name, "background")
-- ROUND 60 (r60-collab-fix-all). bg_data/donbg.lua describes the DEFAULT
-- donbg pack (arcade enso/donbg/donbg_a_XX) and NOTHING else, but its keys
-- are bare "<index>_<player>" names -- so a COLLAB DonBG (path
-- "background/collab/*", art from the parent skin) with name "0_1" was
-- picking up donbg_a_01's layer list (period 496, vx -93.29) and tiling
-- the collab's own art with it: oshiri's 168px blue/green tile drawn every
-- 496px (band full of gaps leaking the loading screen), index/frame values
-- indexing texture.json entries the collab art does not have (x[index]
-- reads out of range engine-side -> garbage coordinates). This is the
-- user-reported OSHIRI band corruption. The default table is now gated to
-- the default pack, and collab paths get the PARENT DonBGBase contract
-- back: a 5-copy tile scroll drawn through the `draw_textures(fade, y,
-- frame)` hook (frame 0 normal, frame 1 fever crossfade) that every collab
-- subclass (a3/oshiri/imas_sidem/tutorial) overrides -- those overrides
-- had been dead code since the ROUND 13 rewrite (dan.lua's ROUND 26 note
-- documented exactly this and hand-built its states instead).
-- DATA describes only the DEFAULT pack; collab paths get the parent tiler
self.use_layers = (path == "background")
self.anim = nil
if self.use_layers and DonBGAnim then
@@ -89,21 +34,13 @@ function DonBGBase.new(index, player_num, path)
self.is_clear = false
self.clear_fade = tex.get_animation(1)
-- ROUND 52 (r52-lua-divergence-fixes): the falling edge. the reference build
-- `EnsoGraphicDonBg::Process` (EnsoGraphicDonBg.obj.c:114-119, 291, 330)
-- plays a clip on BOTH edges (`normal_fever` / `fever_normal`); this file
-- used to be the only one that handled the exit at all, but it reverted
-- with an instant clear_fade:reset(). The exit is now the reverse fade of
-- the same 29-frame length the bg_fever wipe uses.
-- exit is a reverse fade of the same length the bg_fever wipe uses
self.exit_fade = nil
self.elapsed = 0
self.last_ms = nil
-- ROUND 60: id 0 is a VALID texture id (the global name->id map hands
-- out 0 to the first registered name -- live-verified: funassyi's
-- "0_1/background" IS id 0), so "or 0" would make a real texture
-- indistinguishable from a missing one. nil = missing, everywhere here.
-- nil = missing (id 0 is a valid texture id)
self._id_bg = tex.get_texture(self.path .. "/" .. self.name .. "/background")
local d = DATA[self.name]
@@ -118,7 +55,6 @@ function DonBGBase.new(index, player_num, path)
table.insert(out, {
id = id, frame = l.frame, index = l.index,
period = period, vx = l.vx or 0,
-- ROUND 102: optional y triangle, cabinet frames at 60 fps
bob = l.bob, bob_frames = l.bob_frames,
-- enough copies to cover 1920 px whatever the phase
n = (period > 0) and (math.ceil(1920 / period) + 1) or 0,
@@ -149,8 +85,7 @@ function DonBGBase:set_clear(is_clear)
end
if not is_clear and self.is_clear then
self.is_clear = false
-- ROUND 52: reverse-play instead of the old instant reset -- fade the
-- fever layers back out from wherever the entry fade currently sits.
-- reverse-play instead of the old instant reset
local from = self.clear_fade.attribute or 1.0
self.exit_fade = anim.fade(EXIT_MS, {
initial_opacity = from,
@@ -163,15 +98,12 @@ function DonBGBase:set_clear(is_clear)
end
function DonBGBase:update(current_time_ms)
-- same wall-clock accumulator bg_fever uses: the scroll is a constant px/s,
-- and the guard drops the load-time gap and any backwards step
-- same wall-clock accumulator bg_fever uses; guard drops the load-time gap
if self.last_ms then
local dt = current_time_ms - self.last_ms
if dt > 0 and dt < 500 then self.elapsed = self.elapsed + dt end
end
self.last_ms = current_time_ms
-- ROUND 60: the collab-contract scroll (and any subclass-replaced move,
-- e.g. tutorial's 6000 ms one) is advanced here, as the parent base did.
if self.move then self.move:update(current_time_ms) end
if self.anim then self.anim:update(current_time_ms) end
self.clear_fade:update(current_time_ms)
@@ -181,12 +113,7 @@ function DonBGBase:update(current_time_ms)
end
end
-- ROUND 102 (r102-bg-sweep): a layer may also carry a y triangle. `donbg_a_02`'s
-- crest motif row is a child of a container whose ty runs 138 -> 148 -> 138 on a
-- 120-frame cycle (measured frame by frame off the anim dump: +0.1667 px per frame
-- for 60 frames, then the mirror). Amplitude and period are cabinet FRAMES at the
-- stage's own 60 fps, converted here against the same wall-clock `elapsed` the
-- scroll uses, so the skin's 120 Hz Lua tick never enters the arithmetic.
-- A layer may also carry a y triangle (bob), amplitude/period in cabinet frames.
local function bob_offset(l, elapsed)
if not l.bob or not l.bob_frames or l.bob_frames <= 0 then return 0 end
local half = l.bob_frames / 2.0
@@ -213,10 +140,7 @@ function DonBGBase:draw_layers(layers, fade, y)
end
end
-- ROUND 60 (r60-collab-fix-all): default parent-contract tiler, used by the
-- collab path when a subclass does not override it (imas/imas_cg/imas_ml use
-- the plain base; funassyi goes through donbg/4.lua). Same geometry as the
-- parent base: 5 copies at bg width, scrolled by self.move.
-- default parent-contract tiler (5 copies at bg width, scrolled by self.move)
function DonBGBase:draw_textures(fade, y, frame)
if not self._id_bg then return end
local w = self.bg_texture and self.bg_texture.x2[1] or 0
@@ -238,15 +162,12 @@ end
function DonBGBase:draw(y)
y = y or 0
-- ROUND 60: collab path = the parent contract (frame 0 + fever frame 1
-- crossfade through the draw_textures hook); default pack = the ROUND 13
-- DATA-driven layer lists.
-- collab path = parent contract; default pack = DATA-driven layer lists
if not self.use_layers then
self:draw_textures(1.0, y, 0)
if self.is_clear then
self:draw_textures(self.clear_fade.attribute, y, 1)
elseif self.exit_fade then
-- ROUND 52: falling-edge reverse fade (`fever_normal`).
self:draw_textures(self.exit_fade.attribute, y, 1)
end
return
@@ -266,7 +187,6 @@ function DonBGBase:draw(y)
if self.is_clear and self.states.fever then
self:draw_layers(self.states.fever, self.clear_fade.attribute, y)
elseif self.exit_fade and self.states.fever then
-- ROUND 52: falling-edge reverse fade (`fever_normal`).
self:draw_layers(self.states.fever, self.exit_fade.attribute, y)
end
end
+4 -13
View File
@@ -1,20 +1,12 @@
-- Ending banner (clear / full combo / donderful / fail), replayed from the cabinet
-- movie data exported by scratchpad/lyric/clip_export.py:
-- Scripts/game/ending_anim/ending_<kind>.lua -- frames[f+1] = {{shape, tx, ty, sx, sy, rot, a[, 8]}, ...}
-- (an 8th value 8 marks a leaf the cabinet draws with the add blend mode)
-- Graphics/game/ending_<kind>_anim/s<c>.png -- one texture per shape
-- The clip plays once from frame 0 and then loops from `loop_at`, at 60 fps, on the
-- 1920x264 stage the original places at the lane (screen y 276 for 1P; the 2P
-- lane is game_p2_y lower). The engine keeps its own ending
-- animation running for the sounds and hands the drawing to this object through
-- Background:handle_ending / draw_ending.
-- Ending banner (clear / full combo / donderful / fail), replayed from exported
-- cabinet movie data: game/ending_anim/ending_<kind>.lua frames + one texture per shape.
local Ending = {}
Ending.__index = Ending
local FRAME_MS = 1000.0 / 60.0
local STAGE_Y = 276
-- the movie root places every ending clip at (1209, 109.5) on its 1920x264 stage;
-- the exported frames are in clip-local coordinates, so add that placement
-- the exported frames are clip-local, so add that placement
local CLIP_X, CLIP_Y = 1209, 109.5
local CLIPS = { donderful = "ending_donderful", full_combo = "ending_fullcombo",
clear = "ending_clear", fail = "ending_fail" }
@@ -40,8 +32,7 @@ function Ending.new(kind, player_num, path)
self.t0 = nil
self.y = STAGE_Y
if player_num == 2 then
-- the 2P lane sits at game_p2_y, the 1P lane at 184 (both x screen scale); game_2p_offset
-- (536) is not that distance and put the 2P banner below the screen
-- the 2P lane sits at game_p2_y, the 1P lane at 184 (both x screen scale)
local p2 = tex.get_skin_config("game_p2_y")
local lane1 = 184 * tex.get_screen_scale()
self.y = STAGE_Y + ((p2 and p2.y) and (p2.y - lane1) or 0)
+11 -55
View File
@@ -1,45 +1,6 @@
-- YataiDON-HSS child-skin addition (no parent counterpart).
--
-- 演奏スキップ — the cabinet's on-screen cue, drawn from Background:draw_fore()
-- so it lands in front of the whole lane/HUD exactly as the arcade's Lumen layer
-- does. The engine owns the RULE (GameScreen::poll_skip: rim-only, alternating,
-- unused drum, ten hits, no timeout) and pushes the live counter here through
-- Background:handle_skip(player, { count, total, remaining, skipped }).
--
-- ARCADE SOURCE — lumen/000_default/enso_normal/enso/information/skip.nulm.
-- Root sprite 60 places mc `skip_end` (sprite 35) at (960, 804); `skip_end`
-- places mc `skip_end_panel` (sprite 34) at (0, 0). Nothing below is guessed:
-- the two timelines ship as generated tables (ANIM_PIPELINE.md) in
-- Scripts/anim/skip_card.lua (sprite 35) and Scripts/anim/skip_panel.lua
-- (sprite 34), and every rectangle is the shape's own local bounds.
--
-- depth 0 shape 1 the card, local (-504,-264)..(504,264) -> 456,540 1008x528
-- depth 1 shapes 12,14,..,30 the numeral, local (-172,-172)..(172,172)
-- depth 2 shape 32 the dim, 8x8 solid #000000 scaled (240.002, 135.001)
-- at ty -264 -> exactly 1920x1080 over (960,540)
-- depth 3 mc 11 `lang_skip_text` jpn=shape3 / en_us=5 / kor=7 / cn_tw=9,
-- settling at tx 0, ty -386 -> (960, 418)
--
-- THE COUNT IS SHOWN AS "HITS REMAINING", NOT AS "HITS SO FAR". The panel's
-- frame labels are count_0..count_10 and the display list they settle on is
-- shape 12 at count_1 ... shape 30 at count_10; shape 12 is the glyph "9" and
-- shape 30 is "0". So the first rim hit puts a 9 on the card and the tenth puts
-- a 0 on it — which is what the cabinet does and what the reference shows.
--
-- Per count label the panel plays 9 frames and stops: frames label+1..label+8
-- bounce the numeral that is ALREADY on the card (scale-y 1 -> 1.1 -> 1, with a
-- matching upward ty), and only on label+9 is it replaced by the new glyph.
-- count_1 has no bounce — its keyframe already holds the 9.
--
-- At count_10 the panel keeps playing instead of stopping: frames 120..123 fade
-- the full-screen dim in to alpha 192/256, and frames 123..135 grow the banner
-- from scale-y 0.05 to 1 while it fades in. The dim sits at depth 2, i.e. OVER
-- the card and the numeral and UNDER the banner — which is why the arcade's 0
-- reads as greyed out once the skip has fired.
--
-- Fail-soft throughout: anim.safe() never raises, a missing table degrades to
-- "no animation" (alpha 1, scale 1), and a missing texture simply does not draw.
-- 演奏スキップ — the cabinet's on-screen "hits remaining" cue, driven by the
-- engine's rule and pushed here through Background:handle_skip. Drawn from
-- draw_fore() so it lands in front of the whole lane/HUD.
local anim = require("anim/sampler")
local EnsoSkip = {}
@@ -90,13 +51,13 @@ function EnsoSkip:handle(state)
if count ~= self.count then
if count > 0 then
-- gotoAndPlay("count_N"): the panel restarts at that label.
-- gotoAndPlay("count_N"): the panel restarts at that label
self.phase = "in"
self.label = self.panel_clip:label("count_" .. count) or 0
self.panel_f = self.label
if self.count == 0 then self.card_f = IN_F end -- skip_end "in"
else
-- back to 0 = the arcade reset (a hit on the playing drum).
-- back to 0 = the arcade reset (a hit on the playing drum)
self.phase = "out"
self.card_f = OUT_F
end
@@ -113,8 +74,8 @@ function EnsoSkip:update(current_ms)
if self.phase == "in" then
self.card_f = math.min(self.card_f + df, ON_HOLD_F)
-- Each count_N segment stops on label+9; count_10 keeps playing through
-- the dim (120..123) and the banner reveal (123..135) and stops there.
-- each count_N segment stops on label+9; count_10 keeps playing through
-- the dim and the banner reveal and stops there
local stop = (self.count >= self.total) and (self.label + 25) or (self.label + 9)
self.panel_f = math.min(self.panel_f + df, stop)
elseif self.phase == "out" then
@@ -134,9 +95,7 @@ function EnsoSkip:draw()
local panel = id(self, "panel")
if panel then tex.draw_texture(panel, { fade = a }) end
-- the numeral: before label+9 the OLD glyph is still on the card. During the
-- "out" fade the count is already back to 0, so the last glyph is held --
-- the arcade fades the whole panel out, numeral included.
-- the numeral: before label+9 the OLD glyph is still on the card
local off = self.panel_f - self.label
local live = (self.count > 0) and self.count or self.held
local shown = live
@@ -144,13 +103,11 @@ function EnsoSkip:draw()
self.held = live
local digit = (shown > 0) and (self.total - shown) or -1 -- count_N shows (10 - N)
if digit >= 0 and digit <= 9 then
-- frame index i of number.png is the digit 9 - i (the strip is baked in
-- shape order 12,14,..,30 == "9".."0").
-- frame index i of number.png is the digit 9 - i (baked "9".."0")
local frame = 9 - digit
local sy, ty = 1, 0
if live > 1 then
-- Every count_N bounce is the same nine rows; #14@1 is the copy that
-- carries it at count_3 (label 40), so sample it at label+off.
-- every count_N bounce is the same nine rows; sample #14@1 at label+off
local bty, bsy = self.panel_clip:get("#14@1", 40 + off, "ty"),
self.panel_clip:get("#14@1", 40 + off, "sy")
if bsy then sy, ty = bsy, bty or 0 end
@@ -183,8 +140,7 @@ function EnsoSkip:draw()
sy = sy or 1
ba = ba or 1
if ba > 0 then
-- texture.json parks the banner at its resting rect; the reveal is a
-- scale-y about the shape origin, so re-anchor on TEXT_CY.
-- texture.json parks the banner at its resting rect; re-anchor on TEXT_CY
local cy = (ty and (CARD_CY + ty)) or TEXT_CY
tex.draw_texture(banner, {
fade = ba,
+5 -22
View File
@@ -1,17 +1,4 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/fever.lua: bounce total_distance 50*screen_scale
--
-- ROUND 52 (r52-lua-divergence-fixes): the 魂-max crowd is now EDGE-DRIVEN on
-- both sides and re-triggerable, matching the reference build `EnsoGraphicFeverEffect::Process`
-- (EnsoGraphicFeverEffect.obj.c:123-129): the cached flag at +88 is compared
-- against `IsTamashiiMax` every frame and each change plays a clip --
-- `normal_fever` rising, `fever_normal` falling. The skin used to pop the
-- crowd in and out in a single frame (draw gated on the raw is_rainbow flag in
-- background.lua) and never replayed the entrance on a re-max. The entry/exit
-- clips themselves are not exported (the cabinet's 40-frame 1920x540 flipbook
-- stays out -- ROUND 12 trade), so both edges are an alpha ramp of the same
-- 29-frame length BgFever's measured wipe uses (a documented stand-in, not a
-- measured FeverEffect number). `is_visible()` replaces background.lua's
-- is_rainbow draw gate so the exit ramp actually gets drawn.
local FEVER_EDGE_MS = 29000 / 60
local BaseFever = {}
@@ -25,9 +12,6 @@ function BaseFever.new(index, bpm, path)
local half_beat = (60000 / bpm) / 2
self.bounce_up = anim.move(half_beat, {total_distance = 50 * tex.get_screen_scale(), ease_out = "quadratic"})
self.bounce_down = anim.move(half_beat, {total_distance = 50 * tex.get_screen_scale(), ease_in = "quadratic", delay = half_beat})
-- ROUND 60: nil = missing. id 0 is a VALID texture id (the global
-- name->id map starts at 0), so "or 0" hid real textures and, worse, made
-- a missing folder draw texture id 0 (an unrelated sheet).
self._id_overlay_l = tex.get_texture(self.path .. "/" .. self.name .. "/overlay_l")
self._id_overlay_r = tex.get_texture(self.path .. "/" .. self.name .. "/overlay_r")
self._id_overlay = tex.get_texture(self.path .. "/" .. self.name .. "/overlay")
@@ -46,7 +30,7 @@ function BaseFever:start()
self.bounce_down:start()
end
-- ROUND 52: falling edge (`fever_normal`); re-arms start().
-- Falling edge (`fever_normal`); re-arms start().
function BaseFever:stop()
if not self.active then return end
self.active = false
@@ -96,7 +80,7 @@ function Fever0:draw()
local a = self:alpha()
if a <= 0.001 then return end
local y = self.bounce_down.attribute - self.bounce_up.attribute
-- ROUND 60 (r60-collab-fix-all): fail soft when the folder shipped no art.
-- fail soft when the folder shipped no art
if self._id_overlay_l then tex.draw_texture(self._id_overlay_l, {y = y, fade = a}) end
if self._id_overlay_r then tex.draw_texture(self._id_overlay_r, {y = y, fade = a}) end
end
@@ -135,7 +119,7 @@ end
function Fever3:draw()
local a = self:alpha()
if a <= 0.001 then return end
-- ROUND 60: fail soft on a missing fever folder (see Fever0:draw).
-- fail soft on a missing fever folder
if not self._id_overlay then return end
local y = self.bounce_down.attribute - self.bounce_up.attribute
tex.draw_texture(self._id_overlay, {y = y, fade = a})
@@ -152,9 +136,8 @@ Fever.Fever2 = Fever2
Fever.Fever3 = Fever3
-- The cabinet's random pool holds two crowds, fever_effect1 and fever_effect2
-- (fever_effect0 exists but is never drawn from: a single figure parked off-screen).
-- When their exported movie data is present that pool is the whole choice;
-- otherwise the four flat overlays.
-- (fever_effect0 exists but is never drawn from). When their exported movie data
-- is present that pool is the whole choice; otherwise the four flat overlays.
local ANIM_POOL = { 1, 2 }
local function anim_ready(path)
+2 -16
View File
@@ -1,22 +1,8 @@
-- 魂-max crowd, replayed from the cabinet's own movie data.
--
-- Data: Scripts/background/bg_data/fever_anim/f_<n>.lua (generated by
-- scratchpad/lyric/fever_export.py from enso/fever/fever_effect<n>.nulm): for every
-- authored root frame, the world transform of every crowd group in display order.
-- Textures: Graphics/background/fevera_<n>/s<shape>.png, one per group.
--
-- The cabinet drives the movie with two labels only -- `normal_fever` when the
-- gauge reaches 魂 max, `fever_normal` when it drops below -- so the state machine is
-- wait -> (start) enter [normal_fever, fever) -> loop [fever, fever_normal)
-- -> (stop) exit [fever_normal, nframes) -> wait
-- stepped at the song's beat rate (40 frames per beat). Every crowd group is its own sprite (bob + squash),
-- and their depth order comes from the data, so the turtle/shiba groups sit exactly
-- where and how the cabinet layers them.
-- 魂-max crowd, replayed from the cabinet's own movie data (f_<n>.lua frames).
local FeverAnim = {}
FeverAnim.__index = FeverAnim
-- The original re-times this movie to the song (EnsoGraphicBase::FrameSync, shared
-- with the dancers): the 40-frame bob is one beat, i.e. 60 fps at 90 BPM.
-- The original re-times this movie to the song: the 40-frame bob is one beat.
local BEAT_FRAMES = 40
local function frame_ms(bpm)
if not bpm or bpm <= 0 then return 1000.0 / 60.0 end
+3 -7
View File
@@ -1,10 +1,6 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/footer.lua
--
-- ROUND 60 (r60-collab-fix-all): the parent's Footer:draw() passes
-- tex.get_id("footer", index) straight to tex.draw_texture; when the collab
-- ships no footer folder (funassyi asks for footer index 2 but has no footer
-- art at all) get_id returns nil and the draw call raises a per-frame Lua
-- error, aborting the rest of that frame's draw_back. Same logic, nil-safe.
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/footer.lua:
-- parent's Footer:draw() passes a possibly-nil texture id straight to draw_texture;
-- same logic, nil-safe.
local Footer = {}
Footer.__index = Footer
+2 -9
View File
@@ -1,13 +1,6 @@
-- Score-rank badge (粋 / 雅 / 極) at the top-left of the player panel, shown as the
-- score crosses each rank line during play.
--
-- Original: enso/information/score.nulm `scorerank_1p` (498x264 panel stage at screen
-- y 276; placed at (152, -118) -> screen (152, 158) for 1P, `scorerank_2p` at
-- (152, 646) -> 922 for 2P). The game only ever plays the label `in`; the clip then
-- runs in (f4..28) -> keep (to f148) -> out (f149..165) on its own. Exported from
-- `in` as game/ending_anim/rank_hud (162 frames, last frame empty) with the seven
-- plate shapes (`scorerank_panel` rank_1..7); the plate is swapped per rank and a new
-- rank line simply restarts the clip, as the original's goto_label("in") does.
-- score crosses each rank line during play; the plate is swapped per rank and a new
-- rank line restarts the clip.
--
-- Rank lines (the engine's own table): 1 white 500 000, 2 bronze 600 000,
-- 3 silver 700 000, 4 gold 800 000, 5 pink 900 000, 6 purple 950 000,
+7 -36
View File
@@ -1,12 +1,6 @@
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/renda.lua: Renda1 origin from x2[1]/y2[1]; frame from frame_count
-- The three roll effects, replayed the way the original places them: a piece is
-- spawned somewhere over the LOWER backdrop (x 6..1100, y 540..1020 -- the region
-- the original's spawn box covers below the lane) and flies for 120 frames at
-- 60 Hz along its movie's own path:
-- renda_0 birds (chidori) +17.3 px/frame right, -10.9 px/frame up
-- renda_1 shuriken (shuriken) +17.3 px/frame right, level, spinning -6 deg/frame
-- renda_2 arrow (hamaya) +13.1 px/frame right, -13.8 px/frame up
-- They are drawn before the dancers (see background.lua), as the original does.
-- The three roll effects (birds / shuriken / arrow), replayed as a fixed
-- round-robin pool of at most POOL_SIZE live pieces.
local FRAME_MS = 1000.0 / 60.0
local LIFE = 120
local SPAWN_X0, SPAWN_X1 = 6, 1100
@@ -28,7 +22,7 @@ function BaseRenda.new(index, vx, vy, spin, path)
self.frame = math.random(0, ((info and info.frame_count) or 1) - 1)
self.w = (info and info.x2 and info.x2[1]) or 0
self.h = (info and info.y2 and info.y2[1]) or 0
self._id = tex.get_texture(self.path .. "/renda/" .. self.name) -- nil = missing (ROUND 60: id 0 is valid)
self._id = tex.get_texture(self.path .. "/renda/" .. self.name) -- nil = missing
return self
end
@@ -71,39 +65,16 @@ local Renda2 = setmetatable({}, {__index = BaseRenda})
Renda2.__index = Renda2
function Renda2.new(index, path) return setmetatable(BaseRenda.new(index, 13.1, -13.8, nil, path), Renda2) end
-- ROUND 52 (r52-lua-divergence-fixes): the cabinet's renda effect is a FIXED
-- round-robin pool, not an unbounded per-hit spawner. the reference build
-- `EnsoGraphicRendaEffect` (EnsoGraphicRendaEffect.obj.c):
-- * the pool is 25 movie clips -- `EffectPlayerBase::Initialize(a1, 25, ...)`
-- (:2379);
-- * `Process` (0x14012BDA0, :2270-2295) arms AT MOST ONE
-- `EffectPlayer::Start` per frame (a single flag at +88, set while an
-- active kind-6/9 roll note is in flight, consumed once);
-- * `EffectPlayer::Start` (0x14012C640, :2848-2860) takes slot
-- `counter % pool_size` and REPLACES whatever is there (a still-playing
-- clip is swapped out via DoSwaping, not skipped), then counter++.
-- The skin used to push one object per hit with no cap (~40 live at
-- 20 hit/s over the 1500 ms crossing), so the shower read "how fast you hit"
-- instead of "a roll is happening". Ported: pool of 25 slots, at most one
-- spawn per rendered frame, round-robin slot replacement. (The engine hook
-- is still per-hit -- there is no per-frame "roll active" Lua signal -- so a
-- slow roll spawns fewer than the cabinet's one-per-frame; the cap and the
-- steady-state density now match.)
-- Observed on the original: about ten pieces are ever live at once -- the oldest
-- is dropped as soon as an eleventh starts, before it leaves the screen.
local POOL_SIZE = 10
local RendaController = {}
RendaController.__index = RendaController
-- ROUND 58 (r58-2p-background-polish): optional y_offset -- the cabinet's 2P
-- enso_post set (target 1) places TWO renda_effect instances, playerNo 0 at
-- y=0 and playerNo 1 at y=804 (the 2P donbg band), pure translate. A second
-- controller with y_offset = game_2p_offset.y is that pn1 instance.
-- optional y_offset: the 2P band's second renda instance sits at y = game_2p_offset.y
function RendaController.new(path, index, y_offset)
local self = setmetatable({}, RendaController)
self.rendas = {} -- slot -> live effect (holes allowed)
self.rr = 0 -- round-robin counter (EffectPlayer +76)
self.rr = 0 -- round-robin counter
self.spawned_this_frame = false
self.path = path or "background"
self.index = (index ~= nil) and index or math.random(0, 2)
@@ -113,14 +84,14 @@ function RendaController.new(path, index, y_offset)
end
function RendaController:add_renda()
-- <=1 Start per frame, exactly like Process's single +88 flag.
-- at most one spawn per frame
if self.spawned_this_frame then return end
self.spawned_this_frame = true
local classes = {Renda0, Renda1, Renda2}
local cls = classes[self.index + 1]
local slot = (self.rr % POOL_SIZE) + 1
self.rr = self.rr + 1
self.rendas[slot] = cls.new(self.index, self.path) -- replace-in-slot (DoSwaping)
self.rendas[slot] = cls.new(self.index, self.path) -- replace in slot
end
function RendaController:update(current_time_ms)
+4 -25
View File
@@ -1,31 +1,10 @@
-- YataiDON-HSS: DAN_SELECT paint surface (ROUND 57, r57-dani-leftovers).
--
-- The ROUND 53/54 referral closed: DAN_SELECT never had a Lua hook, so the
-- selection cursor's 60-frame breathing loop and the two arrow chips lived as
-- inline C++ sample tables (dan_select.cpp ROUND 17). The engine now hands
-- the draw to this class when it loads (DanNavigator::load_paint_surface),
-- keeping WHEN (selected-chip x, ms since the last ribbon step) in C++ and
-- WHAT here, sampled from the cabinet's own clips through anim/sampler:
--
-- anim/dan_cursor : dani_select.nulm #267 (cursor_big), track #259@0 alpha,
-- 60-frame loop -- the dense export (29 rows) replaces the
-- C++ every-5th-frame 12-point table (real floor 0.25 at
-- f29; the coarse table said 0.301 at f30).
-- anim/dan_arrow : dani_select.nulm #263 (arrow_right), track #261@0 --
-- tx 0->10 over f0..60, alpha 1 to f30 then out at f60,
-- labels arrow_on=0 / arrow_off=61. Non-looping: plays on
-- a move and parks invisible. arrrow_left (#266) is the
-- same authored clip mirrored, so one export drives both
-- (outward = +drift right / -drift left).
--
-- FAIL-SOFT both ways: if this file or a table is missing/malformed the
-- engine keeps its inline tables; if only a table is missing, the NULL clip's
-- nils fall through to the same constants the C++ uses.
-- DAN_SELECT paint surface: selection cursor breathing loop and arrow chips,
-- sampled from the cabinet's own clips.
DanSelect = {}
DanSelect.__index = DanSelect
local _ok, anim = pcall(require, "anim/sampler")
if not _ok then anim = { safe = function()
local ok, anim = pcall(require, "anim/sampler")
if not ok then 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
+82 -200
View File
@@ -1,62 +1,28 @@
-- YataiDON-HSS override of PyTaikoGreen/entry/box.lua
-- Arcade (reference cabinet) mode-select board list, ported from
-- script_lua/entry/mode_select.lua + ModeSelectBorad.lua and lumen entry/mode_select.
-- The C++ BoxManager still owns selection/input; this script only draws. It ignores the
-- x / left_x / right_x / y_pos the engine hands it and lays the boards out the way the
-- arcade list does: selected board open ("on" label) at the screen centre, the others
-- closed ("off" label) stacked above/below at the arcade offsets
-- (mode_select_list_instance "wait": +-305, +-496, +-687 in y, +-50/90/130 in x).
-- Everything here is 1080p pixels (child skin, screen_scale-independent).
-- 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). Entry.new() (entry.lua) sets
-- __hss_entry_reset so the next batch of EntryBox.new calls starts a fresh list.
-- 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 (350..720)
-- ROUND 6 — every timing below is a frame count read straight off the the reference build lumens
-- (60 fps, `Common.FPS`); see Graphics/entry/MAPPING.md "ROUND 6" for the decoded tables.
local F = 1000 / 60 -- one arcade frame in ms
local SLIDE_F = 9 -- entry/entry mode_select_list_instance list_anim_up/down: 9 frames, LINEAR
local TITLE_SIZE = 72 -- text_title DefineEditText font size
-- text_info DefineEditText (id 66): font 32, white #ffffff, outline 5, box 478x192, align centre.
-- Nominal 32 reads smaller than the arcade MS P-Gothic atlas ink, so 34 (user's measurement).
local INFO_SIZE = 34
-- text_title_instance sits at y=-106 (on) / 0 (off); inside it text_instance_1/2 at -84.5 hold the
-- 758x126 text box at (-378,2) -> box centre -20.5 (off) / -126.5 (on). Lumen dynamic text is
-- vertically centred in its box (the reference research notes 3a).
-- TITLE_Y_ON: the open board's title sits in the middle of the dark band at the top of the
-- plate (plate rows 64..187 -> centre 104.5 above the board centre). The text object's
-- box is taller than its ink (descender room), so its ink centre lands ~16 px above the
-- nominal y; -97 nominal puts the ink on the band centre (tuned on screen).
local TITLE_Y_ON, TITLE_Y_OFF = -97, 4 -- closed board: user saw the title riding the top edge (+16)
-- text_info_instance at y=57, text_info rect (-2,-2,478,192) at (-238,-92.5) -> box -37.5..156.5,
-- centre 59.5. The cabinet renderer centres the text block in the box and advances lines by the
-- atlas line height (jp_32 fontSize 53 x scale 32/32 = 53 px) -> line centres ~33 / ~86.
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
-- the title is two stacked DefineEditTexts: text_instance_2 (outline 9, renderer default black)
-- under text_instance_1 (outline 3, colour-add = mode colour from text_title_instance's label
-- frame). create_raw_text thickness is multiplied by screen_scale (1.5). In the arcade capture the
-- visible ring is ~6 px mode colour with a thin black edge outside it -> inner 4 (=6 px),
-- outer 5 (=7.5 px).
local TITLE_OUTER_T, TITLE_INNER_T = 8, 4 -- user: thicker black rim, as the arcade 9-under-3
local INFO_COLOR = { 255, 255, 255, 255 } -- text_info colour #ffffff
local INFO_OUTLINE = { 0, 0, 0, 255 } -- outline 5 (renderer default black) -> 3 (=4.5 px)
-- ROUND 16 (r16-outline): the record's border 5.0 is already in 1080p stage px and
-- OutlinedText multiplies the thickness by screen_scale (1.5); dividing it out makes
-- the ring exactly 5.0 px instead of the 4.5 px the rounded-down 3 drew.
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
-- OutlinedText pads both sides by outline+2 (symmetric), but raylib's MeasureTextEx adds the glyph
-- spacing after the LAST glyph too, so the ink sits spacing/2 left of the texture centre.
local TITLE_SPACING, INFO_SPACING = 2.0, 1.0
-- mode table: frame = index of the "on" frame in entry/mode_select/box (frame+1 = "off";
-- the board art is baked into the parent's fixed texture name "box" as a framed texture
-- because the engine's TexID table is generated from PyTaikoGreen); titles = what BoxManager passes
-- (skin_config entry_* text) so we can recognise the mode and the language.
-- 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},
@@ -78,21 +44,11 @@ local MODES = {
title_key = "entry_ai_battle",
comment_keys = { "entry_ai_battle_comment_1", "entry_ai_battle_comment_2" },
},
-- ROUND 83 (r83-dandojo-as-mode) — 段位道場 is a MODE on the cabinet
-- (`mode_select.lua` CreateBoardList l.289-296 appends GameMode.kDani right after
-- kEnso), not a folder in the song wheel. Its board is the arcade `dani` style —
-- frames 6/7, the dark blue the user pointed at — and its title colour is the
-- colour-add on `text_title_instance` depth 1 at label `dani` (frame 5) = (76,60,151).
-- Strings are the cabinet's own: wordlist keys mode_select_dani /
-- mode_select_comment_dani (three lines; every other board here has two).
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" },
},
-- ROUND 83: ゲーム設定 has no cabinet counterpart and was wearing 段位道場's board
-- (dani, frames 6/7) since ROUND 6. Moved onto `aprilfool`, baked as frames 9/10 by
-- bake_entry.py's EXTRA list; its title colour-add at label `aprilfool` is (0,132,212).
settings = {
frame = 9, outline = {0, 132, 212, 255},
title_key = "entry_settings",
@@ -100,13 +56,8 @@ local MODES = {
},
}
-- ROUND 83: 段位道場's ja and zh titles are the SAME string, so a bare scan over
-- `pairs(m.titles)` could resolve either language and hand back the wrong comment lines
-- (pairs() order is unspecified). Ask the engine which language it is actually running
-- (`tex.language()`, script.cpp:693 -> config.general.language) and try that key first;
-- the old scan stays as the fallback for a language the table has no column for.
-- Titles and comments live in skin_config (entry_* / entry_*_comment_N); the engine hands
-- us the title it drew, so the mode is the one whose skin_config title matches.
-- 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)
@@ -135,112 +86,73 @@ local function clamp01(v)
return v
end
-- ---------------------------------------------------------------------------
-- ROUND 6 — arcade board timelines (lumen entry/mode_select, board_main_instance,
-- MC 128: labels init 0 / off 1 / inside 4 / select_on 27 / on 42 / select_off 52 /
-- in 67 / choose 117). All functions take a FRAME count relative to the label.
--
-- The board is a 3-slice in the arcade (top cap ty -26..-140, centre band scaleY
-- 0.186..1.0, bottom cap +26..+140) and this skin's art is two baked frames
-- ("off" 964x157 / "on" 1050x436), so the openness `p` below drives a crossfade
-- instead of the band growing. Every other channel is reproduced exactly.
-- ---------------------------------------------------------------------------
-- ROUND 7 — every one of the eight channels below is now SAMPLED out of the cabinet's
-- own timeline rather than fitted by hand: `Scripts/anim/mode_board.lua` is exported by
-- scratchpad/anim_export.py straight from `entry/mode_select.nulm` sprite 128.
-- See ANIM_PIPELINE.md. Re-checking round 6's hand-derived constants against the export
-- found NO numeric error in this rig — select_on, select_off, `in` and `choose` were all
-- frame-exact — so this change buys provenance and re-exportability, not a visible fix.
-- (The one deviation that remains is deliberate and is NOT taken from the table:
-- TITLE_Y_ON / TITLE_Y_OFF above, which the user asked to sit 8-12 px lower than the
-- arcade's 0 / -106.)
-- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL
-- clip when a generated table is missing or malformed, so every reader below falls
-- through to its `or <constant>` default instead of taking the screen down.
local _ba_ok, box_anim = pcall(require, "anim/sampler")
if not _ba_ok then box_anim = { safe = function()
-- sampler.lua itself is missing: hand back the same NULL clip shape it would
-- 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 MB = box_anim.safe("anim/mode_board")
-- the selected board's cursor glow pulse (shared with song select -- see draw)
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 ML = box_anim.safe("anim/mode_list")
local ML_WAIT = ML:label("wait") or 13
local ML_IN = ML:label("in") or 87
local MB_SELECT_ON = MB:label("select_on") or 27
local MB_ON = MB:label("on") or 42
local MB_SELECT_OFF = MB:label("select_off") or 52
local MB_IN = MB:label("in") or 67
local MB_CHOOSE = MB:label("choose") or 117
-- the centre band's scaleY at the two rest states -> the 0..1 openness the rest of the
-- file speaks (our art is two baked plates, so `o` drives a crossfade, not a 3-slice)
local MB_SY_CLOSED, MB_SY_OPEN = 0.1857, 1.0
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
-- a label's segment ends one frame before the next label starts; clamping there keeps a
-- caller that passes a huge `u` ("this board has been settled for ages") on the segment's
-- own last frame instead of walking into the next animation.
local function mb_seg(base, u, limit)
-- 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 mb_open(f)
local sy = MB:get("board_bg_center_instance", f, "sy") or MB_SY_CLOSED
return clamp01((sy - MB_SY_CLOSED) / (MB_SY_OPEN - MB_SY_CLOSED))
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 f30..f36, cursor alpha with it, title y 0 -> -106,
-- comment alpha f36..f41.
local ON_LIMIT = MB_SELECT_OFF - MB_SELECT_ON - 1
local OFF_LIMIT = MB_IN - MB_SELECT_OFF - 1
local IN_LIMIT = MB_CHOOSE - MB_IN - 1
local function on_p(u) return mb_open(mb_seg(MB_SELECT_ON, u, ON_LIMIT)) end
local function on_info(u) return MB:get("text_info_instance", mb_seg(MB_SELECT_ON, u, ON_LIMIT), "a") or 0 end
-- select_off (label 52): comment fades f52..f57, board closes f55..f61.
local function off_p(u) return mb_open(mb_seg(MB_SELECT_OFF, u, OFF_LIMIT)) end
local function off_info(u) return MB:get("text_info_instance", mb_seg(MB_SELECT_OFF, u, OFF_LIMIT), "a") or 0 end
-- in (label 67): the board fades UP while still closed, then opens, then its contents
-- fade in, then the cursor glow ramps -- four staggered ramps, all read from the table.
local function in_board_fade(t) return MB:get("board_bg_center_instance", mb_seg(MB_IN, t, IN_LIMIT), "a") or 0 end
local function in_p(t) return mb_open(mb_seg(MB_IN, t, IN_LIMIT)) end
local function in_contents(t) return MB:get("text_info_instance", mb_seg(MB_IN, t, IN_LIMIT), "a") or 0 end
local function in_cursor(t) return MB:get("cursor_center", mb_seg(MB_IN, t, IN_LIMIT), "a") or 0 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 itself flies in (mode_select_list_instance label "in" 87): the closed
-- boards start 3 slots out (dy -+878, dx -+170) and settle on their slot with a
-- quadratic ease-out over frames 98..109 (12 frames) after an 11-frame hold.
-- ROUND 49: 11 -> 10. The exported plateau's last held frame is mode_list f97 = t_in 10
-- (label `in` 87 + 10), so the ease starts at t_in 10 and settles at f109, not f110.
-- 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
-- the parked position at label `in` (held f87..f97, then eased onto the slot by f109)
local LIST_IN_DX, LIST_IN_DY = 170, 878
do
local tx, ty = ML:at("kanban_3", ML_IN)
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): the white board-shaped flash is MC 22, placed unnamed at depth 6 --
-- hence the track key `#22@6`. Peaks at +4/+12/+20/+28, troughs between, then out over 6.
local CHOOSE_LIMIT = MB:last() - MB_CHOOSE
-- 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 MB:get("#22@6", MB_CHOOSE + c, "a") or 0
return mode_board:get("#22@6", BOARD_CHOOSE + c, "a") or 0
end
-- ROUND 7 — the slot offsets and the `in` fly-in start are read out of
-- `Scripts/anim/mode_list.lua` (entry/entry.nulm `mode_select_list_instance`, sprite 23,
-- labels init 0 / wait 13 / list_anim_up 27 / list_anim_down 36 / outside 45 / inside 66 /
-- in 87) instead of being the arithmetic series round 6 fitted to them. The export
-- confirmed that series exactly -- +-50/305, +-90/496, +-130/687, and the 9-frame LINEAR
-- slide -- so again no visible change, but the numbers now have a provenance and a
-- re-export path.
-- kanban_1 is the selected slot; the pairs above/below it are kanban_2/3, 4/5, 6/7, 8/-.
-- 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)
@@ -248,7 +160,7 @@ local function kanban_of(rel)
end
local function slot_offset(rel)
if rel == 0 then return 0, 0 end
local tx, ty = ML:at(kanban_of(rel), ML_WAIT)
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)
@@ -270,8 +182,7 @@ function EntryBox.new(text_str, font_size)
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 single wide outline pass leaves star-shaped gaps between the 8 offset copies; a second,
-- thinner black pass fills them so the rim reads smooth.
-- 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 = {}
@@ -286,9 +197,7 @@ function EntryBox.new(text_str, font_size)
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
-- ROUND 6: frame 8 is a white silhouette of the "on" board (its own alpha, RGB 255),
-- baked from box/2.png — the stand-in for the arcade `choose` flash overlay (MC 22).
self.frame_flash = 8
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)
@@ -296,22 +205,19 @@ function EntryBox.new(text_str, font_size)
self.from_x, self.from_y = 0, 0
self.tgt_x, self.tgt_y = 0, 0
self.t0 = 0
-- open/close timeline: `open_t0` = ms of the last selection change for THIS board,
-- `opening` = which of select_on / select_off is running.
self.open_t0, self.opening, self.was_selected = nil, false, false
self.choose_t0 = nil
return self
end
-- List slide: entry/entry `mode_select_list_instance` list_anim_up / list_anim_down move
-- every board to its next slot LINEARLY over 9 frames (150 ms). `vis` (the >1-slot fade)
-- rides the same ramp.
-- 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 * F
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
@@ -319,52 +225,41 @@ function EntryBox:_retarget(now, tx, ty, tvis)
self.tgt_x, self.tgt_y, self.tgt_vis = tx, ty, tvis
self.t0 = now
end
local p = clamp01((now - self.t0) / (SLIDE_F * F))
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` is BoxManager's entry animation 9 (1 -> 0 over 160 ms) and is started only when a
-- board is picked, so `fade < 1` is the one decide signal Lua can see: it drives `choose`.
-- 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
-- ModeSelect: the cabinet's guide shows `all` (rim to choose + face to decide) when the
-- list has more than one board, `decide` alone otherwise (entry_main.lua ChangeMode)
__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
-- per-board select_on / select_off timeline
-- ROUND 49 (r49-lua-chn-crosscheck): the arcade SEQUENCES the open after the slide —
-- mode_select.lua MoveCursor only plays list_anim_up/down + Close(); state MenuMove then
-- waits `list_mc_:IsPlay() == false`, does SetAllBoard() (all Off) and ONLY THEN
-- kanban:Open() -> select_on. Opening concurrently with the 9-frame slide (as before)
-- had the incoming board part-open while still moving. Delaying select_on by the slide
-- (SLIDE_F * F = 150 ms) reproduces the cabinet order; on_p/on_info clamp negative u.
-- 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 * F or 0)
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) / F -- frames since the list appeared
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)
-- The arcade list places kanban_5/7 at dy +496/+687 too, but the user's arcade capture (list
-- = AI + enso) shows only ONE closed board under the open one; a second one (952..1110) would
-- sit on the nameplate / free-play row, so boards further than one slot away fade out.
-- 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 3 slots out and ease-out-quad onto their slot
-- 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
@@ -372,20 +267,19 @@ function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr
dy = dy + (1 - q) * (s * LIST_IN_DY - oy)
end
-- openness (0 = "off" board, 1 = "on"): the `in` timeline owns it while it runs,
-- otherwise select_on / select_off do.
-- 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_p(t_in) or 0
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) / F) or 999
local u = self.open_t0 and ((now - self.open_t0) / FRAME_MS) or 999
if self.opening then
o, info_a = on_p(u), on_info(u)
o, info_a = on_open(u), on_info(u)
else
o, info_a = off_p(u), off_info(u)
o, info_a = off_open(u), off_info(u)
end
cursor_a = o
fade = fade or 1.0
@@ -393,16 +287,8 @@ function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr
local vis_fade = fade * self.cur_vis
if vis_fade <= 0.001 then return end
-- The arcade stacks the cursor glow UNDER the board (mode_select.nulm: cursor_* at
-- depths 3-5, board_bg_* at 7-9, board_chara_l/r at 16-17), so the characters that
-- lean out of the board stand in front of the glow, not behind it.
-- the cursor glow sits under the board, so characters leaning out stand in front of it
if cursor_a > 0.001 then
-- ROUND 8: the cursor pulse is the SAME 60-frame MC the song-select boards use
-- (`anim/cursor_glow`, char 16 of common_song_select_main_song_board): checked
-- against `entry/mode_select.nulm` `cursor_*/#7@0`, which reads 0.6797 at board
-- frame 42, 0.75 at 44 and 0.9297 at 51 -- the table's own values at those frames.
-- Round 6's `0.75 + 0.25*sin(2*pi*t)` was neither the right shape nor the right
-- range: the gradient our plate is a bake of swings all the way to 0.
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
@@ -420,13 +306,10 @@ function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr
end
end
-- title: rides `o` (arcade text_title_instance y 0 -> -106) and, during `in`, fades in
-- with the comment (arcade frames 78..84)
-- 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
-- both passes are the same string/font, so ink centre = (width - spacing)/2 for each;
-- centring each on its own width keeps them on the same x.
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,
@@ -443,11 +326,10 @@ function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr
fade = title_a,
})
-- `choose`: the picked board flashes white. The engine only gives 160 ms (animation 9)
-- before the screen changes, so one blink of the arcade's 8-frame cycle is what fits.
-- `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) / F)
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
+16 -81
View File
@@ -1,37 +1,4 @@
-- 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).
-- "きせかえをえらぼう!" costume picker (5 fixed slots, moving cursor, scrolling window).
local anim = require("anim/sampler")
@@ -51,12 +18,6 @@ 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")
@@ -65,22 +26,12 @@ 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".
-- shared with player.lua (which draws the panel under the Don); missing entry = 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.
-- costume-change smoke timestamp (drawn by player.lua over the Don)
__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.
-- do not reorder: selected_index walks this exact order
local ITEM_KEYS = {
"costume", "default", "head_body",
"preset_1", "preset_2", "preset_3", "random_item",
@@ -122,14 +73,14 @@ function CostumeMenu.new(is_2p, text_str, title_x, title_y)
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
-- font 54 white, outline 6 (thickness 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_
-- 0-based cursor and scroll offset
self.cursor = 0
self.offset = 0
self.prev_index = nil
@@ -159,7 +110,7 @@ function CostumeMenu:update(current_ms)
self.hit_prev = hit
end
-- board fade-in: `costume_1p_instance` label fade_in (f5..f15), alpha only (see __hss_costume_fade)
-- board fade-in (alpha only)
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
@@ -168,10 +119,7 @@ function CostumeMenu:board_fade()
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.
-- icons sit still, cursor moves; the window scrolls only at an end (a wrap snaps to that end)
function CostumeMenu:track_cursor(selected_index, n)
local max_offset = math.max(0, n - SLOTS)
local prev = self.prev_index
@@ -210,15 +158,7 @@ function CostumeMenu:slot_center(i)
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.
-- preset_seq bumps whenever a preset row dresses the Don; its edges fire the change smoke
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
@@ -236,22 +176,18 @@ function CostumeMenu:draw_bg(x, y, selected_index, costume_select_mode, preset_s
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.
-- C++ draws a wrapping window centred on the icon, so the ring sits mid-row
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
-- the panel itself is drawn by player.lua (under the Don)
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).
-- free-running 60-frame pulse; only the yellow rim breathes
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)
@@ -277,9 +213,9 @@ 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).
-- looping outward drift, 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.
-- hide an arrow 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
@@ -291,7 +227,7 @@ function CostumeMenu:draw_fg(x, y, name)
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)
-- title, top-left inside the panel
self.title_text:draw({ x = TITLE_X + self.off, y = TITLE_Y - self.title_text.height / 2, fade = fade })
-- えらぶ / 決定 drum hint under the panel
@@ -300,8 +236,7 @@ function CostumeMenu:draw_fg(x, y, name)
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)
-- name banner for the cursor's icon (costume/head/body pick only)
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
+15 -15
View File
@@ -97,17 +97,17 @@ function Entry:get_side_select_fade()
return self.side_select_fade.attribute
end
local _ea_ok, entry_anim = pcall(require, "anim/sampler")
if not _ea_ok then entry_anim = { safe = function()
local ea_ok, entry_anim = pcall(require, "anim/sampler")
if not ea_ok then entry_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 EBG = entry_anim.safe("anim/entry_bg")
local CR = entry_anim.safe("anim/credit_row")
local CF = entry_anim.safe("anim/credit_fade")
local entry_bg = entry_anim.safe("anim/entry_bg")
local credit_row = entry_anim.safe("anim/credit_row")
local credit_fade = entry_anim.safe("anim/credit_fade")
local CREDIT_CX = 960
local ROW_CY = { 432 + 72, 432 + 248 }
@@ -120,7 +120,7 @@ local CANCEL_CX, CANCEL_CY = 960, 838
local ROW_LOOP_F0, ROW_LOOP_LEN = 5, 120
local DIM_UNSELECTED = 0.72
local DECIDE_FLASH_F = 24
local EBG_LEN = math.max(1, EBG:last() - EBG:first() + 1)
local entry_bg_len = math.max(1, entry_bg:last() - entry_bg:first() + 1)
local BG_LIT_END = 19
local ENTRY_TWINKLES = {
{ 0, 0, "#11@1/#5@0/#3@0" },
@@ -130,14 +130,14 @@ local ENTRY_TWINKLES = {
}
function Entry:draw_background()
local f = ((tex.get_current_ms() - self.bg_t0) * 0.06) % EBG_LEN -- arcade frames @ 60 fps
local f = ((tex.get_current_ms() - self.bg_t0) * 0.06) % entry_bg_len -- arcade frames @ 60 fps
tex.draw_texture(self.bg_bg)
if self.bg_twinkle then
for i = 1, #ENTRY_TWINKLES do
local tw = ENTRY_TWINKLES[i]
local s, a = EBG:at(tw[3], f)
local s, a = entry_bg:at(tw[3], f)
if a and a > 0.002 then
tex.draw_texture(self.bg_twinkle,
{ index = tw[1], frame = tw[2], center = true, scale = s, fade = a })
@@ -146,7 +146,7 @@ function Entry:draw_background()
end
if self.bg_glow then
local ga = EBG:get("#16@3/#14@0", f, "a") or 1.0
local ga = entry_bg:get("#16@3/#14@0", f, "a") or 1.0
for i = 0, 1 do
tex.draw_texture(self.bg_glow, { index = i, frame = i, fade = ga })
end
@@ -155,7 +155,7 @@ function Entry:draw_background()
if self.bg_street_lit then
local lf = (tex.get_current_ms() - self.bg_t0) * 0.06 -- monotonic, NOT wrapped
if lf < BG_LIT_END then
local la = EBG:get("#17@7", lf, "a") or 0.0
local la = entry_bg:get("#17@7", lf, "a") or 0.0
if la > 0.002 then tex.draw_texture(self.bg_street_lit, { fade = la }) end
end
end
@@ -176,8 +176,8 @@ function Entry:draw_credit_row(row, f, a)
end
-- fill alpha and the outline copy colour multiply, both read from the export
local ma = CR:get("text_message_instance", f, "a") or 1
local _, cr = CR:at("text_message_instance_2", f)
local ma = credit_row:get("text_message_instance", f, "a") or 1
local _, cr = credit_row:at("text_message_instance_2", f)
local yellow = (cr and cr / 256.0) or 0.0 -- 0 = black outline, 1 = (256,236,67)
local msg = self.text_credit_msg
if msg and ma > 0.002 then
@@ -220,7 +220,7 @@ function Entry:draw_side_select_buttons(side)
self.cr_decide_t0 = nil
local fi = math.min(15, 5 + (now - self.cr_t0) * 0.06)
local a = CF:get("player1_text_instance", fi, "a") or 1.0
local a = credit_fade:get("player1_text_instance", fi, "a") or 1.0
local sel = (not self.cr_arcade)
and ((side == 0 and 1) or (side == 2 and 2) or 0)
@@ -266,9 +266,9 @@ function Entry:draw_credit_decide()
if self.cr_decide_t0 == nil then return end
local t = (now - self.cr_decide_t0) * 0.06 -- frames since decide
local flash_a = CR:get("#8@4", 125 + math.min(t, DECIDE_FLASH_F), "a") or 0
local flash_a = credit_row:get("#8@4", 125 + math.min(t, DECIDE_FLASH_F), "a") or 0
local fade_f = 16 + math.max(0, t - (DECIDE_FLASH_F + 1)) -- fade_out starts after
local a = CF:get("player1_text_instance", math.min(fade_f, 35), "a") or 0
local a = credit_fade:get("player1_text_instance", math.min(fade_f, 35), "a") or 0
if a <= 0.002 then
self.cr_decide_t0 = nil
return
+8 -42
View File
@@ -1,12 +1,10 @@
-- YataiDON-HSS override of PyTaikoGreen/entry/player.lua: no big drum on the arcade entry
-- screen; Don stands on the street at the arcade spot (1P ~x220, feet ~y900; 2P mirrored).
-- Entry player: Don stands on the street (no big drum), 1P ~x220 / 2P mirrored.
EntryPlayer = {}
EntryPlayer.__index = EntryPlayer
function EntryPlayer.new(side)
local self = setmetatable({}, EntryPlayer)
-- entry.cpp passes the side-select cursor (0 = left/1P, 2 = right/2P); fold it to 0/1 so
-- every table below (DON_CX, COS_CX, __hss_costume_panel) keys by 1P=0 / 2P=1.
-- fold the side-select cursor (0=1P, 2=2P) to 0/1
self.side = (side == 0) and 0 or 1
local drum_offset_cfg = tex.get_skin_config("entry_drum_offset")
@@ -57,58 +55,26 @@ function EntryPlayer:update(current_ms)
self.nameplate_fadein:update(current_ms)
end
-- Arcade placement (1080p px). entry/entry.nulm: don_1p at (220,735) / smoke_instance_1 at (220,794)
-- (2P mirrored at x=1700); the user's arcade capture puts Don's centre at ~(220,775), feet ~900.
-- Chara3D::draw(x,y): the visible centre is ~130 px above the returned y (x centre = x+5); the
-- parent's entry bounce (drum_move_1+2) settles at move_y = -367.5, so on mode select the returned
-- y = centre + 130 + 367.5 (measured: 1255 gave a visible centre at 758). Costume select adds no bounce.
-- Costume select (entry/costume.nulm): costume_1p_instance/board_instance/don1p at (498,480)
-- scale 0.8, the Don stands INSIDE the red panel (~y 450..650). The panel is drawn here, before the
-- Don, when costume_menu.lua reports an open menu via __hss_costume_panel[side] (shared Lua state).
-- Arcade placement (1080p px)
local DON_ANCHOR_DY, DON_ANCHOR_DX = 130, -5
local BOUNCE_END = -367.5 -- animation.json entry 2 (-295) + 3 (+50), x1.5
local DON_CX = { [0] = 220, [1] = 1920 - 220 }
local DON_CY = 780
local COS_CX = { [0] = 498, [1] = 498 + 924 }
local COS_CY = 548
-- arcade draws the Don at 0.8 inside the costume panel (MAPPING item 37).
-- Engine binding: EntryPlayer:draw_drum_back() may return a third element,
-- the Chara3D scale multiplier (see ENGINE_BINDINGS.md).
-- Don scale inside the costume panel
local COS_DON_SCALE = 0.8
__hss_costume_panel = __hss_costume_panel or {}
-- ROUND 11: costume_menu.lua publishes the arcade board fade-in (entry/costume.nulm
-- `costume_1p_instance` label fade_in, alpha 0 -> 1 over f5..f15) here so the panel — which
-- this script draws, because it has to go UNDER the Don — fades in with the rest of the
-- board. Missing entry = fully opaque (the menu may not have drawn its first frame yet).
-- board fade-in shared from costume_menu.lua; missing entry = fully opaque
__hss_costume_fade = __hss_costume_fade or {}
__hss_costume_smoke = __hss_costume_smoke or {}
-- ROUND 6 (item 36) — arcade costume-change smoke, lumen entry/costume `smoke_instance`
-- (MC 10, 33 frames, labels smoke_in 0 / smoke_loop 7 / smoke_out 16), placed inside
-- board_instance at (498,482) with instance scale 1.20; the shape is 400x400.
-- The parent's entry/side_select/cloud IS the same art (its 4 frames are pixel-for-pixel
-- shapes 2/4/6/8 of that MC, baked at 320x320 = 400 x 0.8), so the drawn scale is
-- timeline_scale x 1.20 / 0.8 = timeline_scale x 1.5.
-- Per-frame table straight off the lumen (60 fps):
-- f 0..6 frame 0, scale 0.10 +0.1667/f -> 1.10, alpha 0 -> 1
-- f 7..15 frame 0, scale wobble 1.10 1.13 1.15 1.18 1.21 1.18 1.15 1.13 1.10
-- f16..20 frame 1, scale 1.10, dy 0 -2.5 -5 -7.5 -10
-- f21..26 frame 2, scale 1.10, dy -10
-- f27..32 frame 3, scale 1.10, dy -20 -22 -24 -26 -28 -30, alpha 1 -> 0
local SMOKE_F = 1000 / 60
local SMOKE_LAST_F = 32
local SMOKE_CX = { [0] = 498, [1] = 498 + 924 }
local SMOKE_CY = 482 + (548 - 480) -- arcade smoke y + (our panel Don centre - arcade's)
local SMOKE_WOBBLE = { 1.10, 1.13, 1.15, 1.18, 1.21, 1.18, 1.15, 1.13, 1.10 }
-- ROUND 53 (r53-anim-lumen-sweep): the per-frame table above was hand-transcribed from the
-- lumen dump in ROUND 6; ROUND 7 exported the very same MC to `Scripts/anim/costume_smoke.lua`
-- but nothing ever consumed it (LUA_CHN_AUDIT.md item 68). Sample the export instead.
-- The MC is a 4-leaf flipbook (#2/#4/#6/#8 = cloud texture frames 0..3): each leaf's rows only
-- span the frames it is on the display list, so "which texture frame" = the leaf whose row
-- window contains floor(f), and scale/dy/alpha are that leaf's sx/ty/a channels.
-- ROUND 8 fail-soft: anim.safe hands back a NULL clip (no tracks) on any load problem, and
-- smoke_state() then falls through to the ROUND 6 hand table -- the screen never goes down.
local ok_anim, anim = pcall(require, "anim/sampler")
if not ok_anim then anim = nil end
local SMOKE_CLIP = anim and anim.safe("anim/costume_smoke") or nil
@@ -123,7 +89,7 @@ local function smoke_state(f)
local name, tex_frame = SMOKE_LEAVES[i][1], SMOKE_LEAVES[i][2]
local rows = SMOKE_CLIP.tracks[name]
if rows and #rows > 0 and fi >= rows[1][1] and fi <= rows[#rows][1] then
-- fields = {ty, sx, a} (see the generated header)
-- fields = {ty, sx, a}
local dy, sc, a = SMOKE_CLIP:at(name, f)
if dy ~= nil then return tex_frame, sc, dy, a end
end
@@ -160,9 +126,9 @@ function EntryPlayer:draw_drum_back()
return { cx + DON_ANCHOR_DX, cy + move_y + DON_ANCHOR_DY, don_scale }
end
-- entry smoke (arcade smoke_instance_1/2 at (220,794)): the parent's 4-frame cloud, centred on Don
-- entry smoke: the parent's 4-frame cloud, centred on Don
function EntryPlayer:draw_drum_front()
-- ROUND 6: costume-change smoke wins while it is running (the entry cloud is long finished)
-- costume-change smoke wins while it runs
local t0 = __hss_costume_smoke[self.side]
if t0 and costume_open(self.side) then
local f = (tex.get_current_ms() - t0) / SMOKE_F
+2 -8
View File
@@ -1,10 +1,4 @@
-- Generic replay of an exported cabinet clip (scratchpad/lyric/clip_export.py):
-- data.frames[f+1] = {{shape, tx, ty, sx, sy, rot, a[, 8]}, ...} (8 = the cabinet's add
-- blend mode on that leaf), data.shapes[c] = {w, h, ox, oy},
-- data.nframes, data.loop_at (the frame the clip continues from after its last one).
-- Plays at 60 fps from the moment start() is called. `shape_map` (optional) swaps
-- one shape for another at draw time -- the way the original picks a variant with a
-- label (rank_1 .. rank_7) on a clip whose motion is otherwise identical.
-- Generic 60 fps replay of an exported clip (data.frames / data.shapes tables).
local ClipPlayer = {}
ClipPlayer.__index = ClipPlayer
@@ -76,7 +70,7 @@ function ClipPlayer:draw(now, fade)
origin = { s[3] * sx, s[4] * sy },
rotation = r[6],
fade = r[7] * fade,
blend = (r[8] == 8) and "additive" or nil, -- cabinet add-blended leaf
blend = (r[8] == 8) and "additive" or nil, -- add-blended leaf
})
end
end
+1 -5
View File
@@ -1,8 +1,4 @@
-- Combo announce (100/200/.../1000+) using the per-digit "announce_digit" strip
-- instead of the engine's default hundreds/thousands number+add composite.
-- Ported from src/objects/game/combo_announce.cpp so this skin's digit art
-- (Graphics/game/combo/announce_digit_1p.png, _2p.png) can be optional per skin
-- without the engine eagerly probing for it on every skin.
-- Combo announce (100/200/.../1000+) drawn from the per-digit announce_digit strip.
ComboAnnounce = {}
ComboAnnounce.__index = ComboAnnounce
+1 -3
View File
@@ -1,6 +1,4 @@
-- Ported 1:1 from src/scenes/game_over.cpp (pure visual/audio, no input).
-- The "copyright" tail (animation ids 8/9) and its blackout cue (id 10) are
-- optional per skin, same as the original tex.has_animation/has_texture guard.
-- Game-over scene (ported 1:1 from src/scenes/game_over.cpp); the copyright tail is optional.
GameOver = {}
GameOver.__index = GameOver
+3 -92
View File
@@ -1,29 +1,5 @@
-- YataiDON-HSS override of PyTaikoGreen/global/allnet_indicator.lua
-- ROUND 6 (LUA_CAPABILITIES item 43) — boot-loading chrome.
--
-- `AllNetIcon:draw` is the last Lua call on LOADING (loading.cpp: black fill, progress-bar
-- track + fill, KIDOU::WARNING, white fade, allnet_indicator.draw), so it is the skin's only
-- paint hook on the boot screen.
--
-- Arcade reference: lumen attract/notice, `notice_instance_0` at (959.7,542.4) —
-- d0 8x8 black stretched 240x135 -> 1920x1080 backdrop (engine: DrawRectangle BLACK)
-- d1 NOTICE board 846x886 @ (0,-46) -> top-left (536.7, 53.4) (kidou/warning)
-- d2 8x8 #650000 stretched 115.47x5 -> a 923.7x40 track @ (497.9, 977.1)
-- d3 8x8 #ff0000 stretched 0.50x5 -> the fill, left-anchored at x 494.7, growing to 923.7
-- d4/d5 two 62x49.5 black caps at the ends of the track (invisible on black)
-- d6 8x8 white stretched 240x135, alpha 0 -> the fade-out (engine: Fade(WHITE, …))
-- The engine already copies the arcade's exact colours (101,0,0 / RED) and the white fade;
-- what it cannot match from a skin is the GEOMETRY: `progress_bar_*` in LoadingScreen::
-- on_screen_start is derived from the screen metrics (0.43*W x 50*scale at y = 0.85*H =
-- 825.6x75 @ (547.2, 918)) and `progress` has no Lua binding, so the bar cannot be redrawn
-- here. Its top edge (918) lands 11 px inside the last NOTICE line (ink bottom 929 with the
-- arcade board position restored in kidou/texture.json), so the board is simply re-drawn on
-- top from here — in the arcade the two never overlap (board ends 939.4, track starts 977.1),
-- so the stacking is unobservable there and this restores the readable result.
--
-- The ALL.Net icon itself is skipped on LOADING: the arcade boot notice screen carries no
-- overlay at all, and the engine draws this icon AFTER its white fade-out, so it would
-- otherwise hang in front of the full-white flash into the title screen.
-- ALL.Net icon overlay, plus the 「M曲」 song-max plate on the GAME screen.
AllNetIcon = {}
AllNetIcon.__index = AllNetIcon
@@ -38,22 +14,7 @@ function AllNetIcon:update(current_ms, online)
self.online = online
end
-- ROUND 9 - 「M曲」 on the gameplay plate (LUA_CAPABILITIES items 5 / 17 / 18).
-- `song_info.nulm` ET 9 `text_song_max` is a 110x40 box centred (1814,122),
-- font 24 / outline 3.0 / centre-aligned, cross-fading with the genre plate on
-- the same timeline as 「N曲目」 (root MC 15 `song_number` cmul 12). The engine
-- draws the 「N曲目」 half itself (SongNum at song_num_game) but had no value
-- for the total; `tex.song_limit()` is the new binding that supplies it.
-- allnet_indicator is the last Lua paint hook on GAME (game.cpp draw_overlay),
-- which is why this lives here and not in background.lua (draw_fore runs before
-- song_info.draw). The engine now also raises global_data.in_transition for
-- these frames while the song-loading transition is still on screen, so the
-- plate text does not float over the rainbow.
local SONG_MAX_CX, SONG_MAX_CY = 1814, 122
-- ROUND 16 (r16-outline): the record's border 3.0 is already in 1080p stage px,
-- and OutlinedText multiplies the thickness it is given by screen_scale (1.5),
-- so the value to pass is border / screen_scale. This used to pass 3 and draw a
-- 4.5 px ring. (`tex.get_screen_scale` is read lazily, at first draw.)
local SONG_MAX_FONT, SONG_MAX_BORDER = 24, 3.0
function AllNetIcon:draw_song_max()
@@ -67,76 +28,26 @@ function AllNetIcon:draw_song_max()
end
local t = self._song_max
if not t then return end
-- animation id 3 is the genre <-> song-number cross-fade SongInfo drives;
-- reading the same live object keeps both halves of the plate in step.
-- id 3 = the genre <-> song-number cross-fade; sharing it keeps both plate halves in step.
if not self._plate_fade then self._plate_fade = tex.get_animation(3, false) end
local fade = self._plate_fade and self._plate_fade.attribute or 1.0
if fade <= 0 then return end
-- ROUND 11 (text sharpness): snap to whole pixels. OutlinedText is a rasterised
-- texture drawn 1:1 with BILINEAR filtering, so an odd-width label centred on an
-- integer lands on a .5 and every texel gets blended 50/50 with its neighbour
-- (+34..69 % edge width, measured — see Graphics/song_select/MAPPING.md ROUND 11).
t:draw({ x = math.floor(SONG_MAX_CX - (t.width - 2.0) / 2 + 0.5),
y = math.floor(SONG_MAX_CY - t.height / 2 + 0.5), fade = fade })
end
function AllNetIcon:draw(x, y)
local screen = tex.current_screen and tex.current_screen() or ""
-- ROUND 71 (r71-dan-skip-popup): GAME_DAN joins GAME here.
--
-- The 段位道場 skip fired with no on-screen cue at all, while the ordinary
-- enso showed the whole card/numeral/dim/banner. Nothing was missing from
-- the pipeline -- DanGameScreen::update_skip_dan() already calls the
-- inherited push_skip_state() (game_dan.cpp:1299), which feeds
-- Background:handle_skip -> self.enso_skip, and DanGameScreen builds a real
-- Background ("DAN", game_dan.cpp:108) whose constructor publishes
-- _G.g_active_enso_skip. The ONLY thing dan mode never did was DRAW it:
-- this hook was gated on `screen == "GAME"`, so GAME_DAN fell through to
-- the bare-indicator tail below and the popup was updated every frame and
-- painted never.
--
-- The cabinet does show it, and shows the SAME asset -- this is not an
-- inference from "the trigger is shared". `datatable/enso_post.bin` (the reference build,
-- tlb decode_datatable.py) is the per-mode placement table; its dojo set is
-- `target 2` (the set carrying donbg_dojo_1p, dani_enso*, and ROUND 69's
-- dani_enso_between), and that set contains
-- part 30 lumen/000_default/enso_normal/enso/information/skip.nulstb
-- x = 0, y = 0, playerNo = 0
-- byte-for-byte the same row as the normal-enso set (`target 0`), pointing
-- at the same enso_normal clip -- there is no enso_dani/.../skip.nulm on
-- the cabinet at all, only this one. In target 2 it is the LAST row in the
-- list, i.e. topmost, and in particular it is placed AFTER
-- `dani_enso_between` (part 38), so the popup draws OVER the between-song
-- fusuma exactly as it does here.
if screen == "GAME" or screen == "GAME_DAN" then
-- The song-number plate belongs to GAME's own song_info cross-fade;
-- GAME_DAN's header is ROUND 70's and is deliberately left alone.
if screen == "GAME" and not (tex.in_transition and tex.in_transition()) then
self:draw_song_max()
end
local frame = self.online and 2 or 0
tex.draw_texture(self.indicator, { frame = frame, x = x, y = y })
-- ROUND 19 (r19-skip2): 演奏スキップ's card/dim/banner, published on the
-- shared globals table by background.lua (self.enso_skip). Drawn from
-- here, the last Lua paint hook on GAME (and, via GameScreen::draw_overlay,
-- on GAME_DAN too -- game_dan.cpp:1178 calls the inherited overlay), so it
-- lands after song_info and the gauge and under the outgoing
-- result_transition -- see the citation at Background.new in
-- Scripts/background/background.lua for why this hook and not draw_fore.
if _G.g_active_enso_skip then _G.g_active_enso_skip:draw() end
return
end
if screen == "LOADING" then
-- ROUND 15 (r15-audit-global): this used to re-draw `loading/kidou/warning` here, to lift
-- the NOTICE board back over the engine's progress bar, whose top edge (918) cut 11 px
-- into the last line of text. The bar now sits where the cabinet puts it
-- (`loading_progress_bar` = 924x40 @ (498,977), from `attract/notice` depth 2), so the two
-- no longer overlap -- board ends 939, track starts 977, exactly as on the cabinet.
-- Removing the re-draw also fixes a defect it introduced: `LoadingScreen::draw` runs this
-- hook AFTER its white fade-out, so the re-drawn board stayed at full opacity while the
-- rest of the screen whitened into TITLE.
-- The ALL.Net icon is still deliberately not drawn: the arcade boot notice screen carries
-- no overlay at all.
return
end
local frame = self.online and 2 or 0
+19 -170
View File
@@ -1,59 +1,26 @@
-- YataiDON-HSS override of PyTaikoGreen/global/coin_overlay.lua
-- Arcade (the reference build) global chrome drawn on every non-game screen:
-- * common/credit : 「フリープレイ」 text, centred (960,1048), skin_config free_play font 34 (see draw)
-- * common/qr__check : QR status chip (106x106 @1570,38) — label ng/ok; YataiDON has no QR -> always NG (frame 0)
-- * common/credit_player_side : 2P cloud bubble 「2人プレイ / 太鼓をたたいてスタート!」 (416x280 @1492,638),
-- shown while the 2P side is not entered (player_num == 1). Blinks in the arcade: 3 s loop, 2 s solid (cloud 0.70/text 1.0) + 0.5 s out + 0.5 s in (see draw).
-- Global credit line / QR chip / 2P invite cloud, drawn on every non-game screen.
CoinOverlay = {}
CoinOverlay.__index = CoinOverlay
-- ROUND 8 — the 2P invite cloud's blink is no longer the three hand-written branches
-- round 6 fitted to it: `Scripts/anim/credit_side.lua` is exported straight out of
-- `common/credit_player_side.nulm` sprite 10 (the 180-frame MC the root parks on) by
-- scratchpad/anim_export.py. See ANIM_PIPELINE.md. The export CONFIRMED round 6's
-- model exactly -- cloud `#3@0` 179/256 -> 0 over f0..f30, back over f30..f59, steady
-- f59..f179, and the text `credit_all_instance` the same shape at 256/256 -- so this is
-- provenance and a re-export path, not a visible fix. The one thing that stays a
-- constant is the 8-frame pop-in (root sprite 11 label `main` f9: +32/256 per frame,
-- exactly linear), because a two-row table for a straight line buys nothing.
-- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL
-- clip when a generated table is missing or malformed, so every reader below falls
-- through to its `or <constant>` default instead of taking the screen down.
local _ca_ok, cs_anim = pcall(require, "anim/sampler")
if not _ca_ok then cs_anim = { safe = function()
-- sampler.lua itself is missing: hand back the same NULL clip shape it would
local ca_ok, cs_anim = pcall(require, "anim/sampler")
if not ca_ok then cs_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 CS = cs_anim.safe("anim/credit_side")
local CS_LEN = math.max(1, CS:last() - CS:first() + 1) -- 180 frames = 3 s (1 if the table is gone)
local CS_POP_F = 8 -- verified against sprite 11 f9..f17
local credit_side = cs_anim.safe("anim/credit_side")
local credit_side_len = math.max(1, credit_side:last() - credit_side:first() + 1) -- 180 frames = 3 s
local credit_side_pop_f = 8
function CoinOverlay.new()
local self = setmetatable({}, CoinOverlay)
-- outline 5 = the arcade record's `border` (ET4 +68); ROUND 12.
-- ROUND 15: 5 was the record's `border` passed raw; OutlinedText scales it by 1.5, so it
-- drew 7.5 px. 3 gives 4.5 px, the closest integer to the cabinet's 5.
self.text = text.create_text("free_play", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 3, 5)
self.config = tex.get_skin_config("free_play")
-- texture names are fixed by the engine's generated TexID table, so arcade art is
-- stored under existing names: banapass_osaifu_keitai = QR chip (0 NG / 1 OK),
-- camera = 2P cloud bubble (both frames identical)
self.qr = tex.get_texture("global/overlay/banapass_osaifu_keitai")
self.bubble = tex.get_texture("global/overlay/camera")
-- ROUND 15 (r15-audit-global): both lines are `credit_player_side.nulm` DefineEditText records
-- ET7 (`text_player`) and ET5 (`text_credit`), and both say size **40**, border **6**,
-- `color="#ffffff"` -- white fill with a black outline, not the dark-grey/no-outline pair
-- round 6 guessed. (Border colour is not in the record -- research/lumen_text.md section 2 --
-- but `movie/attractdemo_cn_tw.wmv` frame 1 shows the same white-fill/black-border treatment
-- on the name plate, which is the same renderer default.) See Graphics/global/MAPPING.md
-- ROUND 15 items G3/G4.
-- OutlinedText multiplies the thickness by screen_scale (`text.cpp:200`,
-- `outline_thickness * global_tex.screen_scale`), which is 1.5 in this skin, while the
-- record's `border` is already in 1080p stage px. Pass 6/1.5 = 4 to draw the arcade's 6.
self.qr = tex.get_texture("global/overlay/banapass_osaifu_keitai") -- QR chip (0 NG / 1 OK)
self.bubble = tex.get_texture("global/overlay/camera") -- 2P cloud bubble
self.bubble_player = text.create_text("credit_side_player", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2)
self.bubble_msg = text.create_text("credit_side_msg", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2)
self.bubble_cfg_p = tex.get_skin_config("credit_side_player")
@@ -64,45 +31,12 @@ end
function CoinOverlay:update(current_ms) end
-- common/credit: root places credit_text_instance at (960,1052.25) [absolute, this lumen's root
-- sits at (0,0)]; `text_credit` = ET4, box **(760,991.75)-(1160,1087.75)** = 400x96, size 40,
-- outline 5, align byte 2 (centre), font stylekey '0' -> size bucket "64" -> **jp_64**.
-- ROUND 12: ROUND 4d sized this off the `face="MS 明朝"` string, which `lumen_fonts.md` §4c
-- proves is inert Flash-authoring residue -- the cabinet has no vector font and picks its atlas
-- from the record's style key alone. The real face is the heavy jp_64 display weight, so 34/3 was
-- ~15 % too small. Arcade pen (lumen_text.md 3/3a, the same rule Graphics/result/MAPPING.md
-- ROUND 6 uses): lineHeight = 92 * 40/64 = 57.5, pen.y = 991.75 + (96-57.5)/2 + 5 = 1016.0, and
-- the katakana ink of 「フリープレイ」 then runs **1023.5..1061.6, centre 1042.56**.
-- Our OutlinedText centres its whole image, whose katakana ink sits 3.0 px above that centre at
-- size 40, so the centre that lands the ink on the cabinet's is 1042.56 + 3.0 = 1045.6 -> 1046.
-- (The arcade's fill is also `border` = 5 px RIGHT of the box centre -- x 965, not 960 -- because
-- FontImpl insets the fill by `border` on both axes; that 5 px is deliberately NOT copied here,
-- see Graphics/loading/MAPPING.md ROUND 12 class C.)
-- OutlinedText adds the skin_config x/y as an offset, so both are subtracted here.
local FREE_PLAY_CY = 1046
-- ROUND 15: the two lines inside the 2P invite cloud, in stage px for the 2P seat
-- (`credit_side_instance_2` at (1700,706)); the 1P seat mirrors x only.
local BUBBLE_PLAYER_CY = 749
local BUBBLE_MSG_CY = 802
-- ROUND 16 (r16-select, QA defect #3 -- the CLIPPING half only; the alpha half was measured and
-- dismissed, see the note at the bottom of draw()).
-- The cabinet centres both lines on the seat (1700 / 220) inside EditText boxes that are WIDER
-- than the screen allows on that side: ET7 `text_player` (1472,694)-(1928,790) = 456 wide and
-- ET5 `text_credit` (1278,747)-(2122,843) = 844 wide, both centred on 1700. At that centre only
-- 2*(1920-1700) = 440 px of a centred string can be on screen at all, and the cloud art itself is
-- 416 px wide (`attract_credit_instance__shape3`, local (-208,-108)..(208,172)).
-- The cabinet gets away with it because its own strings are short: ET5 is 「あと〇コイン入れてね!」
-- and ET7 「〇人プレイ」 (CreditSide.lua Setup swaps in `credit_rest_2p` / `credit_play_2p`).
-- Our localised strings ("Hit the taiko to start!", 「太鼓をたたいてスタート!」) are far wider and ran
-- off both screen edges. FIX: clamp the drawn width to the cloud and let OutlinedText squeeze the
-- glyphs horizontally via `x2` -- the same shrink-to-fit `nameplate.lua` uses for the name box
-- (`draw_in_box`), which is what the cabinet's own renderer does inside an EditText rect.
-- DEVIATION, deliberate: the cabinet never squeezes these two lines because its JP strings fit.
-- The squeeze only engages when a string is wider than the bubble, so JP/short strings are
-- byte-for-byte unchanged; it is localisation headroom, not a re-layout.
local BUBBLE_BOX_W = 416 -- cloud art width; also <= the 440 px a centred string can occupy at x=1700/220
local BUBBLE_BOX_W = 416 -- cloud art width
-- Centred, whole-pixel-snapped OutlinedText draw clamped to `box_w` (see nameplate.lua draw_in_box).
-- Centred, whole-pixel-snapped OutlinedText draw clamped to `box_w`.
local function draw_bubble_line(t, cfg, cx, cy, fade)
if not t or t.width == nil then return end
local w = math.floor(math.min(t.width, BUBBLE_BOX_W) + 0.5)
@@ -111,141 +45,56 @@ local function draw_bubble_line(t, cfg, cx, cy, fade)
x2 = w - t.width,
fade = fade })
end
-- ROUND 73 (r73-danselect-condition-chrome), QA defect 1: 「free play在這畫面不展示」.
--
-- The credit line used to be drawn UNCONDITIONALLY at the top of draw(), while
-- the QR chip and the 2P invite cloud below it both carry explicit screen lists.
-- That is ROUND 71's bug in the other direction: a global overlay member with no
-- screen gate at all, so it leaked onto DAN_SELECT, where the cabinet shows none.
--
-- What the cabinet actually is (the reference build, reference-sourced): the credit line is
-- `AppAlwaysDisplayPartsCredit`, slot 5 of the `AppAlwaysDisplay` parts array
-- (the only part carrying SetAttract/SetEntry/SetGame, vtable +48/+56/+64, which
-- write mode 1/0/2 to this+36). Each scene picks its mode, and the per-scene
-- table (`AppAlwaysDisplay::Scene*`, one folded function per preset) is:
-- SceneTitle / SceneAttractMovie 0x140021FC0 -> +48 SetAttract
-- SceneEntry 0x140022360 -> +56 SetEntry
-- SceneResult 0x140022810 -> +56 SetEntry
-- SceneSongSelect (+ AI variants) 0x140022490 -> +64 SetGame
-- SceneDaniResult 0x140022940 -> +64 SetGame
-- SceneEnsoGame 0x1400226F0 -> +24 FinishDisp (hidden)
-- SceneDaniSelect has NO preset of its own: `SceneDaniSelect.obj.c` inlines the
-- same calls and hits slot 5 with +64 (SetGame) then +8 (StartDisp).
--
-- HONEST LIMIT, recorded rather than papered over: that table does NOT explain
-- this defect. DAN_SELECT and SONG_SELECT take the identical branch (SetGame +
-- StartDisp), yet the cabinet shows the caption on song select and not on dan
-- select, so the real discriminator is somewhere below `SetCreditString`'s
-- mode-2 arm (this+36 == 2, which then keys off the credit counter this+20 and
-- IsFreePlay) and is NOT decoded. The list below is therefore the USER's direct
-- side-by-side observation of the cabinet, written as a screen list so it has
-- the same shape as the two gates underneath it and so the next round can
-- extend it from evidence instead of unpicking a negation.
local CREDIT_SCREENS = {
ENTRY = true,
PRACTICE_SELECT = true, AI_SELECT = true,
TITLE = true, RESULT = true,
-- DAN_SELECT: deliberately absent (QA defect 1).
-- SONG_SELECT / SONG_SELECT_2P: absent -- the cabinet's song select shows no
-- COINS / FREE PLAY line (user, side by side with the cabinet); RESULT keeps it.
-- DAN_SELECT and SONG_SELECT intentionally absent (no credit line there).
}
function CoinOverlay:draw(x, y)
local screen = tex.current_screen and tex.current_screen() or ""
-- Fail soft: an unknown/blank screen name keeps the old always-on behaviour
-- rather than silently blanking the credit line everywhere.
-- unknown/blank screen keeps the always-on behaviour
local show_credit = (screen == "") or (CREDIT_SCREENS[screen] == true)
if show_credit then
-- ROUND 11 (text sharpness): whole-pixel snap — an OutlinedText is a rasterised
-- texture drawn 1:1 with BILINEAR, so a .5 destination costs a full 50/50 texel blend.
local text_x = math.floor(tex.get_screen_width() / 2 - self.text.width / 2 - self.config.x + 0.5)
local text_y = math.floor(FREE_PLAY_CY - self.text.height / 2 - self.config.y + 0.5)
self.text:draw({ x = text_x, y = text_y })
end
-- ROUND 5: the engine re-draws this overlay on top of the song-loading (rainbow) transition
-- with tex.in_transition() == true. The arcade loading screen shows ONLY the credit text
-- (no QR chip, no 2P invite cloud), so stop here in that case.
if tex.in_transition and tex.in_transition() then return end
-- Anything a screen script wants drawn ABOVE the credit text (song_select.lua: the course-select
-- drum guide pill, which the arcade layers over the credit line). Consumed and cleared each frame.
-- per-frame hook for a screen to draw above the credit text
if YD_DRAW_ABOVE_CREDIT then
local fn = YD_DRAW_ABOVE_CREDIT
YD_DRAW_ABOVE_CREDIT = nil
fn()
end
-- ROUND 6 (item 46): the arcade title/attract screen carries no status row — the
-- 段位道場 / 1プレイ4曲 / IC Card chips are entry_overlay.lua's and skin_config already
-- parks them off-screen there (entry_overlay_title.x = -4000), but the QR chip is drawn
-- from here, so it needs its own guard.
-- The chip row belongs to the screens where a card can still be tapped: entry and the
-- select screens. Title, the loading transition, result, game over and settings show none.
-- ROUND 73: `screen` is now read once at the top of draw() (the credit gate).
local chip_screens = (screen == "ENTRY" or screen == "SONG_SELECT" or screen == "SONG_SELECT_2P"
or screen == "PRACTICE_SELECT" or screen == "AI_SELECT" or screen == "DAN_SELECT")
if chip_screens and not (tex.in_transition and tex.in_transition()) then
tex.draw_texture(self.qr, { frame = 0 })
end
-- player_num: 1 = 1P alone -> invite on the 2P side (credit_side_instance_2 @1700,706),
-- 2 = 2P alone -> invite on the 1P side (credit_side_instance_1 @220,706), 3 = both -> none
-- The arcade only instantiates credit_side (CreditSide.lua) in entry and song select
-- (entry_main.lua / song_select_all.lua); result, game over, dan, settings never show it.
local scr = (screen ~= "") and screen or "ENTRY"
local on_entry_or_select = (scr == "ENTRY" or scr == "SONG_SELECT" or scr == "SONG_SELECT_2P"
or scr == "PRACTICE_SELECT" or scr == "AI_SELECT")
-- ROUND 6 (2P): `global_data.player_num` is NEVER set to TWO_PLAYER(3) - scenes/entry.cpp
-- forces it back to P1 once the second player joins, so tex.player_num() stays 1 for a
-- 2P session and the invite cloud kept drawing on top of the 2P Don. The two signals the
-- skin does get are the screen name (SONG_SELECT_2P) and entry.lua's draw_footer, which
-- the engine hands p1_joined / p2_joined before it draws this overlay.
local pn = tex.player_num()
local both_joined = (pn == 3) or (scr == "SONG_SELECT_2P")
or (scr == "ENTRY" and YD_ENTRY_BOTH_JOINED == true)
-- ROUND 11 (entry credit screen): entry_main.lua turns `credit_side_[n]` visible only in
-- ModeSelectStartWait -- never during CreditWait -- so the invite cloud must not draw
-- while entry.lua still has the 1人プレイ/2人プレイ pills up (it refreshes
-- YD_ENTRY_CREDIT_MS every frame it draws them, including the decide/fade-out tail).
local credit_up = (scr == "ENTRY") and YD_ENTRY_CREDIT_MS ~= nil
and (tex.get_current_ms() - YD_ENTRY_CREDIT_MS) < 100
-- ROUND 15 (mid-song-select 2P join): `song_select_all.lua` `CreditSideVisible`
-- (l.1622) only turns `credit_side_[n]` visible while `playSongCount < 2`, the
-- same window `Set2PEntryEnable` (l.1806) uses for the join itself. We used to
-- advertise the cloud on every song select, including songs the engine now
-- (correctly) refuses to join on. Entry is unconditional there (entry_main.lua
-- ModeSelectStartWait keys only off is_use_credit), so gate the select screens.
local join_window = (scr == "ENTRY") or not tex.songs_played or (tex.songs_played() < 2)
if on_entry_or_select and join_window and not credit_up and not both_joined and (pn == 1 or pn == 2) then
local side = (pn == 1) and 0 or 1
local cx_p = (pn == 1) and self.bubble_cfg_p.x or (tex.get_screen_width() - self.bubble_cfg_p.x)
local cx_m = (pn == 1) and self.bubble_cfg_m.x or (tex.get_screen_width() - self.bubble_cfg_m.x)
-- ROUND 8: sampled out of `anim/credit_side` (MC 10, 180 frames, no action -> loops
-- forever): the cloud is track `#3@0` (placed at 179/256, so its own row already
-- carries the 0.70) and the text is `credit_all_instance` (256/256). Visible 2 s,
-- blinks away 1 s (fully invisible only on frame 30), 3 s period.
local elapsed = tex.get_current_ms() - self.blink_t0
local f = (elapsed * 0.06) % CS_LEN -- arcade frame 0..179
local pop = math.min(1, elapsed * 0.06 / CS_POP_F) -- 133 ms pop-in on first show
-- ROUND 16 (r16-select): DO NOT "fix" this to 1.0. QA round 16 reported the cloud as
-- "far too transparent, you can read the stalls through it"; measured against the cabinet
-- it is exactly right and opaque would be the regression. `common/credit_player_side.nulm`
-- places shape 3 (the cloud, art fully opaque white) with color_mul [256,256,256,179]
-- => 179/256 = 0.6992, which is the 0.6992 in `anim/credit_side`. `lum.py`'s render of the
-- cabinet's own MC 10 peaks at alpha 178; ours draws 255 * 0.6992 = 178.3. The "ghost"
-- reading is ENTRY-specific: 70 % white over the bright street art is low contrast by
-- design, and the identical draw reads solid over song select's blue backdrop.
local fade = (CS:get("#3@0", f, "a") or 0) * pop
local fade_t = (CS:get("credit_all_instance", f, "a") or 0) * pop
local f = (elapsed * 0.06) % credit_side_len -- arcade frame 0..179
local pop = math.min(1, elapsed * 0.06 / credit_side_pop_f) -- pop-in on first show
local fade = (credit_side:get("#3@0", f, "a") or 0) * pop
local fade_t = (credit_side:get("credit_all_instance", f, "a") or 0) * pop
tex.draw_texture(self.bubble, { index = side, fade = fade })
-- OutlinedText adds the skin_config x/y as an offset to params.x/y, so subtract cfg.x/y again
-- ROUND 11: whole-pixel snap (see draw() above)
-- ROUND 15: y is now centred on the arcade's own ink centre instead of being read as a
-- top-left out of skin_config. ET7 box (1472,694)-(1928,790) and ET5 box (1278,747)-(2122,843)
-- for `credit_side_instance_2` at (1700,706); Lumen text is vertically centred in its rect,
-- and the same lineHeight/border correction the free-play line uses (ROUND 12) puts the ink
-- centres at 745.8 and 798.8, +3.0 for our katakana raster = 749 / 802.
-- ROUND 16: clamped to the cloud so a long localised string cannot run off the screen edge
-- (x=1920 on the 2P seat, x=0 on the 1P seat). No-op for a string narrower than the cloud.
draw_bubble_line(self.bubble_player, self.bubble_cfg_p, cx_p, BUBBLE_PLAYER_CY, fade_t)
draw_bubble_line(self.bubble_msg, self.bubble_cfg_m, cx_m, BUBBLE_MSG_CY, fade_t)
end
+1 -11
View File
@@ -1,14 +1,4 @@
-- YataiDON-HSS override of PyTaikoGreen/global/entry_overlay.lua
-- Arcade entry top-right status row (entry/entry.nulm instances, 1080p px):
-- numberofstage_instance -> common/numberofstage 「1プレイ 4曲」 plate 240x64 @ (1200,45) (= banapass_or)
-- cardicon_instance -> common/banapass__check 「IC Card NG/OK」 chip 106x106 @ (1460,38) (= banapass_card 0/1)
-- qricon_instance -> common/qr__check (drawn by coin_overlay.lua, shared with song select)
-- 「段位道場 NG」 chip (grey, 106x106 @ (1040,38)) -> banapass_no: the arcade row is
-- 段位道場 | 1プレイ4曲 | IC Card | QR (x 1040..1676). No lumen sprite draws this chip in
-- 000_default (searched every lumen_sprites/*/*.json, entry/*.lua, common/*.lua: it is
-- native UI), so it is baked from the IC Card NG chip base + grey label + skin-font text.
-- banapass_osaifu_keitai (QR chip) and camera (2P bubble) are drawn by coin_overlay.lua.
-- Textures are positioned by texture.json; the engine passes (0, entry_overlay_entry.y) -> override y = 0.
-- Entry top-right status row: 段位道場 / 1プレイ4曲 / IC Card chips.
EntryOverlay = {}
EntryOverlay.__index = EntryOverlay
+9 -49
View File
@@ -1,13 +1,5 @@
-- YataiDON-HSS override of PyTaikoGreen/global/indicator.lua
-- Arcade common/control_guide: white top-left panel with a drum; 'main/taiko_small/all' is a 650-frame
-- loop at the lumen's 60 fps (10.83 s): frames 0..209 right stick + up arrow (hits @60,145),
-- 210..419 left stick + down arrow (hits @270,355), 420..649 both sticks + 「決定」 pill (face flash
-- 521..569). Round 4: re-baked every 2nd frame (325 cells = 30 fps, 352x276 cells, y offset 12 in
-- texture.json, origin = screen (0,0) so skin_config *_indicator positions should be {0,0}) into
-- global/indicator/background; the previous bake was every 5th frame (12 fps) which read as sluggish.
-- Wall-clock loop length is the arcade's (elapsed ms * 60 / FRAME_STEP). The other parent textures
-- (text, drum_face/kat/don, blue_arrow) are not drawn. All engine call sites use State.SELECT;
-- the other states fall back to the same loop.
-- Control-guide panel (drum + arrows) shown while a screen asks for input.
Indicator = {}
Indicator.__index = Indicator
@@ -18,7 +10,7 @@ Indicator.State = {
WAIT = 3,
}
local FRAME_STEP = 2 -- arcade frames per baked frame (bake_guide.py STEP)
local FRAME_STEP = 2 -- arcade frames per baked frame
local ARCADE_FPS = 60
function Indicator.new(state)
@@ -32,42 +24,21 @@ end
function Indicator:update(current_ms) end
-- The baked loop is two guides back to back: cells 0..SPLIT-1 (arcade frames 0..419) = rim hit
-- + arrow (song select: "pick with the rim"), cells SPLIT..DECIDE_END-1 (arcade frames 420..649,
-- 230 frames = 3.83 s, the same length as the arcade's taiko_small 'decide' loop) = both sticks +
-- 決定 pill (entry: "decide with the face"). The engine passes the same State everywhere,
-- so which guide a screen wants is carried by its skin_config position: the
-- song-select keys are set to y=1 (see skin_config), entry/others to y=0.
local SPLIT, DECIDE_END = math.floor(420 / FRAME_STEP), math.floor(650 / FRAME_STEP) -- cells 210..324 = both sticks + 決定
__hss_costume_panel = __hss_costume_panel or {}
__hss_entry_guide = __hss_entry_guide or nil -- { ms, phase = "side" | "mode", count } from the entry scripts
-- ROUND 11 drew the 段位道場 shutter doors from HERE, purely for layering: the
-- indicator was the last skin-owned draw on SONG_SELECT and therefore the only slot
-- that landed over the whole HUD (LUA_CAPABILITIES C-dan-3). ROUND 14 added the real
-- slot -- `SongSelect:draw_top(dan_progress)`, called from SongSelectScreen::draw()
-- immediately before dan_transition->draw() -- so the doors now live in
-- song_select.lua where they belong and this file is back to being the indicator.
--
-- The fallback is kept for one reason only: on an engine WITHOUT draw_top, nothing
-- would ever call the rig. `dan_doors.draw()` returns false unless a transition is
-- actually running, and song_select.lua marks the frame it already drew, so on the
-- current engine this costs one table lookup and draws nothing twice.
local _dd_ok, dan_doors = pcall(require, "song_select/dan_doors")
local dd_ok, dan_doors = pcall(require, "song_select/dan_doors")
function Indicator:draw(x, y, fade)
fade = fade or 1.0
if _dd_ok and not dan_doors.drawn_this_frame() and dan_doors.draw() then return end
-- arcade costume select (entry/costume.nulm) hides the top-left control guide: its own
-- title box (100..898 x 142..268) sits where the guide panel is; costume_menu.lua keeps
-- __hss_costume_panel[side] fresh while a costume menu is open (skin_config gives both
-- indicator_entry_* the same (0,0), so the side cannot be told apart here -> hide for any side)
if dd_ok and not dan_doors.drawn_this_frame() and dan_doors.draw() then return end
-- costume menu hides the guide (its title box overlaps this panel)
local now = tex.get_current_ms()
for _, t in pairs(__hss_costume_panel) do
if now - t < 100 then return end
end
-- entry phases (published by entry.lua / entry/box.lua while they draw): TaikoSelect hides
-- the guide, ModeSelect with several boards shows the `all` loop instead of `decide`
-- entry phases (from entry.lua): TaikoSelect hides the guide, multi-board ModeSelect shows "all"
local g = __hss_entry_guide
local show_all = false
if g and now - g.ms < 100 then
@@ -86,18 +57,7 @@ function Indicator:draw(x, y, fade)
tex.draw_texture(self.background, { frame = frame, x = x, y = y, fade = fade })
end
-- ROUND 17 — the top-most slot on DAN_SELECT.
--
-- `DanSelectScreen::draw()` calls this as its very last statement, after the coin
-- overlay, the GLOBAL::DAN_SELECT plate and the allnet chip (Indicator::draw_top,
-- src/objects/global/indicator.{h,cpp}). It is DAN_SELECT's equivalent of
-- `SongSelect:draw_top`, and it exists for exactly one thing: the 段位道場 shutter's
-- SECOND half. The cabinet's doors OPEN on the dan-select screen
-- (`loading_dani_intro` f187..200) — up to round 16 our transition ended with a
-- hard cut on SONG_SELECT and the reveal was never played at all.
--
-- `dan_doors.draw_open()` returns false unless a transition actually handed over,
-- so on every other visit to DAN_SELECT this is one table lookup and no draw.
-- DAN_SELECT's top draw slot: plays the shutter's open half.
function Indicator:draw_top()
if _dd_ok then dan_doors.draw_open() end
if dd_ok then dan_doors.draw_open() end
end
+14 -122
View File
@@ -1,70 +1,13 @@
-- YataiDON-HSS override of PyTaikoGreen/global/nameplate.lua
--
-- ROUND 15 (agent r15-audit-global) -- REBUILT against `common/name_plate.nulm`.
-- Sources for every number below:
-- * `common/NamePlate.lua` (the reference build script) SetPlayerInfoPlayData -- which frame LABEL is used,
-- * `common/name_plate.nulm` MC 280 ("main", 100 frames, 20 labels) -- the display list per label,
-- * its DefineEditText records -- the text boxes, font sizes, fill colours and border widths,
-- * `movie/attractdemo_cn_tw.wmv` frame 1 -- the cabinet's own render of the coin plate, which is
-- what settled the name's colour (see "Name colour" below).
--
-- ## Canvas mapping
-- MC 280 is registered at the plate CENTRE. Our textures live on a 408x96 canvas whose plate art
-- sits at x 32..376 (round 6), so
-- our canvas px = MC-280-local + (204, 48)
-- Check: badge shape139 is 88x88 at local (-168,-44) -> canvas (36,4) == 1p.png's texture.json
-- entry; dan_emblem_bg shape243 is 160x48 at local (-160,-4) -> canvas (44,44) == its entry.
--
-- ## The labels the cabinet actually uses (MC 280 label -> frame)
-- 1p_coin 0 | 1p 5 | 1p_dani 10 | 1p_shougou 15 | 1p_shougou_dani 20 (2p_* = +25, newentry 50, ai 70)
-- SetPlayerInfoPlayData picks Np_coin for `user_type == kCoin` and -- this is the part the port got
-- wrong -- appends `_shougou` / `_dani` ONLY in the non-coin branch. A cabinet plate is therefore
-- NEVER "coin plate + dan chip": that combination does not exist. We drew it, which is the
-- squashed dark chip over the 1P badge the user reported.
--
-- ## Display list per label (MC-280-local, before the +(204,48))
-- d3 shape69 white plate 344x96 @ (-172,-48) every label
-- d4 shape238 title band 336x40 @ (-168,-40) every label EXCEPT *_coin
-- d5 shape243 dan chip plate 160x48 @ (-160, -4) *_dani only
-- d6 shape240 empty-band fill 320x40 @ (-160,-40) Np / newentry only (no title set)
-- d8 shape139/248 player badge 88x88 @ (-168,-44) every label except newentry
-- d11 MC232 `dani` (the disk + kanji) @ (-62,16) *_dani only
-- d10 text_title, d12 text_name see the box table
--
-- ## Text boxes (rect from the ET record, converted to canvas px)
-- label text_name box centre font fill border
-- 1p_coin / 2p_coin (131,23)-(321,83) (226, 53) 30 #ffffff 3
-- 1p / 1p_shougou (131,37.5)-(321,97.5) (226, 67.5) 24 #ffffff 3
-- 1p_dani / *_dani (166,37.5)-(356,97.5) (261, 67.5) 24 #ffffff 3
-- newentry (109,37.5)-(299,97.5) (204, 67.5) 24 #ffffff 3
-- text_title (band) (119,4.5)-(334,64.5) (226.5, 34.5) 22 #000000 0
-- Lumen dynamic text is always vertically centred in its rect (FontImpl::GetAlignOffsetY,
-- research/lumen_text.md 3a) and align byte 2 = centred horizontally, so the box centre IS the
-- ink centre. raylib's MeasureTextEx adds the glyph spacing after the last glyph too, so an
-- OutlinedText's ink centre is (width - spacing)/2 -- applied below.
--
-- ## Name colour -- the one thing the record alone could not settle
-- Every text_name ET is color="#ffffff" with border 3, but the border COLOUR is not in the record
-- (`+50` is zero corpus-wide, so the renderer substitutes its own default -- research/lumen_text.md
-- section 2). Rounds 4-6 guessed the fill was dark and dropped the border.
-- `movie/attractdemo_cn_tw.wmv` frame 1 shows the cabinet's own 1P coin plate with the name
-- rendered as WHITE letters with a BLACK outline on the white pill, which matches the record's
-- fill exactly and pins the border colour to black. So: white fill, black outline 3.
-- text_title is #000000 with border 0 -- black, no outline, unchanged.
-- Player name/title plate (coin / band / dan variants), drawn over the player row.
local ENGINE_DEFAULT_TITLE = "Donder Debut!"
local TEXT_SPACING = 2.0
-- ROUND 62: see the long note in `Nameplate.new`. The dan ladder is 25 ranks
-- (0=初級 .. 24=達人) -- `Graphics/global/nameplate/texture.json`'s 25 `dan_emblem`
-- crops and `src/scenes/dan_select.cpp`'s `dan_index > 24` range check agree.
local DAN_FRAME_COUNT = 25
-- `frame_top` bakes the band + the 5 title backgrounds (texture.json: 5 crops), which
-- is exactly the 5 values `title_bg_1p`/`title_bg_2p` offer in the settings menu.
local TITLE_BG_FRAMES = 5
local TITLE_BG_FRAMES = 5 -- the 5 title backgrounds
-- name boxes, canvas px: {centre_x, centre_y, font}
local NAME_COIN = { 226, 53, 30 } -- 1p_coin / 2p_coin
local NAME_COIN_BARE = { 204, 53, 30 } -- no badge (YataiDON's PLAYER_NUM_ALL; no cabinet label)
local NAME_COIN_BARE = { 204, 53, 30 } -- no badge (PLAYER_NUM_ALL)
local NAME_BAND = { 226, 67.5, 24 } -- 1p / 1p_shougou
local NAME_BAND_DANI = { 261, 67.5, 24 } -- 1p_dani / 1p_shougou_dani
local NAME_BAND_BARE = { 204, 67.5, 24 } -- newentry
@@ -86,8 +29,7 @@ function Nameplate.new(name, title, player_num, dan, is_gold, is_rainbow, title_
self.is_rainbow = is_rainbow
self.title_bg = title_bg
-- "Donder Debut!" is the scores.db default for a fresh player, i.e. "no title set" for us
-- (round 4). The cabinet's own rule is playData.user_data.title ~= "".
-- "Donder Debut!" is the "no title set" default.
self.has_title = (title ~= nil and title ~= "" and title ~= ENGINE_DEFAULT_TITLE)
self.shadow = tex.get_texture("global/nameplate/shadow")
self.ai = tex.get_texture("global/nameplate/ai")
@@ -98,58 +40,19 @@ function Nameplate.new(name, title, player_num, dan, is_gold, is_rainbow, title_
self.dan_emblem = tex.get_texture("global/nameplate/dan_emblem")
self.dan_emblem_gold = tex.get_texture("global/nameplate/dan_emblem_gold")
-- ROUND 15: `dan` must be a real frame index into `dan_emblem`, not merely "not -1".
-- Booting straight into a chart (`YataiDON.exe <tja> <diff>`) hands over a PlayerData that
-- never went through ENTRY, and its `dan` came through as **69** against a 25-frame texture:
-- `draw_texture` then threw `vector::_M_range_check` once per frame, forever. Clamp to the
-- texture's own frame count and treat anything outside it as "no dan", which is also the
-- cabinet's condition (`daniRecordInfo.now_dani > 0` gates the whole `_dani` suffix).
--
-- ROUND 62 (r62-nameplate-fix): the clamp above was correct in intent and WRONG in
-- source. `dan_emblem` is a `crop` SHEET in `Graphics/global/nameplate/texture.json`
-- (25 rectangles, 72x40 each), not a multi-file `FramedTexture`, and
-- `get_texture_info().frame_count` is computed as
-- `dynamic_cast<FramedTexture*>(tex_obj)` ? textures.size() : 1
-- (`src/libs/script.cpp` get_texture_info) -- it does not look at `crop_data` at all,
-- so for this texture it reports **1**. `dan < 1` therefore rejected every rank
-- except 初級 (0): `has_dan` was false for dan 1..24 on EVERY screen, so the dan chip
-- (`dan_emblem_bg` + `dan_emblem`) was never drawn and the name box never shifted from
-- (226,67.5) to the *_dani (261,67.5) box. Measured live this round with a probe on
-- the shipped player row (dan=24): `frames=1 has_dan=false`.
-- This is the exact trap `Scripts/entry/entry.lua:129-134` already documents for
-- `indicator/background` ("the bake is a `crop` list ... `get_texture_info` only
-- reports a real count for `FramedTexture` -- for this one it returns 1, so a
-- `frame_count >= 325` guard silently drew nothing"); ROUND 15 wrote this guard before
-- that note existed and fell into the same hole.
-- The count is a fixed arcade quantity, not a skin variable: the ladder is 25 ranks,
-- 0=初級 .. 24=達人, which is texture.json's 25 crops AND the engine's own course
-- range check (`dan_select.cpp`: `if (dan_index < 0 || dan_index > 24) dan_index = -1`).
-- `frame_count` is still honoured when it is meaningful (>1), so a skin that ever
-- re-bakes the sheet as real frames keeps working.
-- ROUND 62 also fixed the binding itself (`get_texture_info` now maxes in
-- `crop_data->size()`, ENGINE_BINDINGS.md ROUND 62): on a current engine `reported`
-- is 25 and the constant is never reached. Verified live after the rebuild:
-- `dan_emblem.frame_count=25 indicator.background.frame_count=325 has_dan=true`.
-- The constant stays as the belt-and-braces path for a pre-R62 binary.
-- clamp dan to the texture's frame count (fail-soft: a chart boot can hand over dan = 69)
local emblem_info = tex.get_texture_info("nameplate", "dan_emblem")
local reported = (emblem_info and emblem_info.frame_count) or 0
local dan_frames = (reported > 1) and reported or DAN_FRAME_COUNT
-- R15's fail-soft is preserved: booting straight into a chart
-- (`YataiDON.exe <tja> <diff>`) once handed over dan = 69, and an unclamped frame
-- index threw `vector::_M_range_check` once per frame, forever.
self.has_dan = (type(dan) == "number" and dan >= 0 and dan < dan_frames)
-- Np_coin == no title AND no dan. Anything else is the band family, exactly as
-- SetPlayerInfoPlayData builds Np[_shougou][_dani].
-- coin plate = no title and no dan
self.is_coin = not self.has_title and not self.has_dan
self.has_badge = (player_num ~= PLAYER_NUM_ALL and player_num ~= PLAYER_NUM_AI)
self.rainbow_anim = tex.get_animation(12, "global")
local box = self:name_box()
-- white fill + black outline 3 (see the header); the cabinet never shrinks the font, it
-- squeezes the glyphs horizontally inside the box, which is what the x2 override does below.
-- OutlinedText multiplies the thickness by screen_scale (1.5 here) and the record's border
-- is already in 1080p stage px, so 3/1.5 = 2 draws the cabinet's 3.
-- white fill + black outline; the font is squeezed horizontally to fit the box (x2 below)
self.name_text = text.create_raw_text(name or "", box[3], { 255, 255, 255, 255 }, { 0, 0, 0, 255 },
false, 2, TEXT_SPACING)
self.title_text = text.create_raw_text(title or "", TITLE_FONT, { 0, 0, 0, 255 }, { 0, 0, 0, 0 },
@@ -163,7 +66,7 @@ function Nameplate.new(name, title, player_num, dan, is_gold, is_rainbow, title_
return self
end
-- which text_name box this plate uses, per the label SetPlayerInfoPlayData would pick
-- which name box this plate uses
function Nameplate:name_box()
if self.is_coin then
return self.has_badge and NAME_COIN or NAME_COIN_BARE
@@ -173,8 +76,6 @@ function Nameplate:name_box()
end
-- one centred, whole-pixel-snapped OutlinedText draw inside a Lumen EditText box.
-- ROUND 11 (text sharpness): an OutlinedText is a rasterised texture drawn 1:1 with BILINEAR,
-- so a .5 destination blends every texel 50/50 -- snap x, y and the width.
local function draw_in_box(t, x, y, cx, cy, box_w, fade)
if not t or t.width == nil then return end
local w = math.floor(math.min(t.width, box_w) + 0.5)
@@ -195,18 +96,11 @@ function Nameplate:draw(x, y, fade)
return
end
-- d3 shape69: the white plate, present in every label
-- white plate
tex.draw_texture(self.shadow, { x = x, y = y, fade = fade })
if not self.is_coin then
-- d4 shape238 + d7 title_instance: the title band. Our frame_top bakes the band and the
-- four title backgrounds into one 5-frame texture; the cabinet's plain shape240 fill
-- (the band with no title set) is not baked, so a dan-only plate borrows title_bg's colour.
-- ROUND 62: clamp, for the same fail-soft reason the dan index is clamped.
-- `title_bg` arrives straight from the `players.title_bg` column with no engine-side
-- validation (`config_ref.h` `nameplate_1p/title_bg` writes it raw), and an
-- out-of-range frame throws `vector::_M_range_check` once per frame forever
-- (`texture.cpp` draw path re-throws after logging) -- the dan=69 failure mode.
-- title band + title background; clamp title_bg to a valid frame
local frame = (self.player_num == PLAYER_NUM_ALL) and 2 or self.title_bg
if type(frame) ~= "number" or frame < 0 or frame >= TITLE_BG_FRAMES then frame = 0 end
if self.is_rainbow then
@@ -221,26 +115,24 @@ function Nameplate:draw(x, y, fade)
tex.draw_texture(self.outline, { x = x, y = y, fade = fade })
end
-- d5 shape243 + d11 MC232: the dan chip. *_dani labels only, and those only exist in the
-- band family -- drawing it on the coin plate is what squashed the badge (see the header).
-- dan chip (band family only)
if self.has_dan and not self.is_coin then
tex.draw_texture(self.dan_emblem_bg, { x = x, y = y, fade = fade })
local emblem = self.is_gold and self.dan_emblem_gold or self.dan_emblem
tex.draw_texture(emblem, { frame = self.dan, x = x, y = y, fade = fade })
end
-- d8 shape139 / shape248 + d9 plate_player_no
-- player-number badge
if self.has_badge then
tex.draw_texture(self.pnum_tex, { x = x, y = y, fade = fade })
end
-- d10 text_title -- band family only; Np_coin clears the title outright
-- (SetPlayerInfoPlayData: if plate_label == playerSide .. "p_coin" then userTitle = "" end)
-- title text (band family only)
if not self.is_coin and self.has_title then
draw_in_box(self.title_text, x, y, TITLE_CX, TITLE_CY, TITLE_BOX_W, fade)
end
-- d12 text_name
-- name text
local box = self:name_box()
draw_in_box(self.name_text, x, y, box[1], box[2], NAME_BOX_W, fade)
end
+5 -34
View File
@@ -1,26 +1,4 @@
-- YataiDON-HSS — the GAME→RESULT transition, ROUND 13.
--
-- The cabinet does NOT close a shutter here. `000_default/loading/loading_kuro_result.nulm`
-- (root sprite 4, 166 frames, labels `init 0 / in 6 / stop 115 / out 145 / end 165`) is a
-- SINGLE full-stage black shape whose **alpha** ramps linearly 0 → 1 over frames 6…115 —
-- 109 frames = 1816.7 ms — then holds at `stop` until the result screen takes over.
--
-- The engine's built-in rig (`src/objects/game/result_transition.cpp`) is a `move`
-- animation with no alpha at all, so this could not be expressed as skin data; round 13
-- added the scripted hook (see ENGINE_BINDINGS.md) and this file is the consumer.
-- The engine calls `start` / `update` / `draw` / `is_finished`; defining `draw` is what
-- takes the shutter out of the picture, and defining `is_finished` is what stops the scene
-- cutting to RESULT at 983 ms, when the screen would still be only ~54 % black.
--
-- The ramp itself is NOT hand-fitted: it is the exported arcade timeline,
-- `Scripts/anim/kuro_result.lua` (ANIM_PIPELINE.md). `anim_export.py` compressed the 318
-- dense rows to 4 breakpoints, which is itself the proof the ramp is linear between them,
-- and `anim_check.py kuro_result` samples the shipped table through the shipped sampler at
-- every arcade frame: `a max|d| = 0.001938`, inside the 0.004 (1/256) export tolerance.
--
-- The fade-out half of the record (`stop 115` → `end 165`) is deliberately NOT played
-- here: the result screen's own entry already fades its backdrop out over the board
-- (`result` animation 15), so playing both would double the dip.
-- GAME→RESULT fade: a full-stage black plate whose alpha ramps 0 → 1, then holds.
local anim = require("anim/sampler")
@@ -36,14 +14,9 @@ function ResultTransition.new(player_num)
self.player_num = player_num
self.t0 = nil
self.alpha = 0.0
-- Belt and braces: the engine already refuses to forward `update` / `is_finished`
-- before `start()` (it drives this object from the first frame of the song, and
-- `is_finished` is what ends the GAME screen), but a script that can run its clock
-- unstarted would cut the chart short, so refuse here too.
-- refuse to run the clock before start()
self.started = false
-- FAIL SOFT: `anim.safe` never raises — a missing or malformed generated table
-- yields the NULL clip, whose readers return nil, and `update` falls back to the
-- arcade's own documented linear ramp below.
-- missing table -> NULL clip, update falls back to the linear ramp below
self.clip = anim.safe("anim/kuro_result")
self.t_black = tex.get_texture("global/result_transition/fade")
return self
@@ -63,7 +36,7 @@ function ResultTransition:update(current_ms)
local a = self.clip:at(TRACK, f)
if not a then
-- the generated table is gone: the record is linear f6…f115, 0 → 1
-- fallback: linear 0 → 1 across the ramp
a = (f - F_IN) / (F_STOP - F_IN)
end
if a < 0 then a = 0 elseif a > 1 then a = 1 end
@@ -80,9 +53,7 @@ function ResultTransition:draw()
if self.alpha <= 0.004 then return end -- below the engine's own 1/256 quantisation
local w = tex.get_screen_width()
local h = tex.get_screen_height()
-- `fade.png` is an 8×8 black plate; texture.json carries its own 8×8 size as the base
-- destination rect and `x2`/`y2` are ADDED to it (texture.cpp:655-680), so this
-- stretches it to exactly the screen at any resolution.
-- 8×8 plate stretched to the full screen via x2/y2
tex.draw_texture(self.t_black, { x = 0, y = 0, x2 = w - 8, y2 = h - 8, fade = self.alpha })
end
+3 -23
View File
@@ -1,23 +1,4 @@
-- YataiDON-HSS child override of PyTaikoGreen/Scripts/global/timer.lua
-- (ROUND 52, r52-lua-divergence-fixes). Byte-copy of the parent's drawing and
-- tick logic; only the VOICE cue schedule changes, corrected against the
-- arcade's own common/Timer.lua (the reference build script_lua; every call site Setup(9)):
--
-- * timer_30sec at 30 s -- the parent had no 30 s cue at all
-- (new sample: Sounds/global/voice_timer_30.ogg = voice_common_v12a
-- timer_30sec, Timer.lua:91-93).
-- * timer_10sec at 10 s / timer_05sec at 5 s -- kept, but with the arcade's
-- pre-empt: a still-playing earlier voice is stopped first
-- (Timer.lua:87-89 StopSlot on now_count 10 or 5).
-- * NOTHING at 0. The cabinet stops any playing voice and StopCount()s
-- (Timer.lua:106-112); the parent's voice_timer_0 is a Green-ism. The
-- parent's finish gate ("wait until voice_timer_0 stops playing") is
-- therefore replaced by an immediate finish, with any live voice cut.
-- * the per-second red-zone blip (timer_count <= red_limit 9) is unchanged
-- -- the parent's `time < 10` (9..0) already matches.
--
-- audio.stop_sound is a ROUND 52 engine binding; every use is guarded so this
-- file stays load-safe on an engine built before it.
-- Countdown timer; the voice cues match the arcade's schedule (30/10/5 s, nothing at 0).
Timer = {}
Timer.__index = Timer
@@ -58,8 +39,7 @@ function Timer.new(time, current_time_ms, confirm_func, is_frozen)
end
function Timer:update(current_ms)
-- Arcade Timer.lua:106-112 -- reaching 0 stops any playing voice and
-- StopCount()s at once; nothing is spoken and nothing is waited for.
-- reaching 0 stops any playing voice and finishes immediately
if self.time == 0 and not self.is_finished then
self.is_finished = true
stop_all_voices()
@@ -86,7 +66,7 @@ function Timer:update(current_ms)
if self.time == 30 then
audio.play_sound("voice_timer_30", "voice")
elseif self.time == 10 then
-- Timer.lua:87-89: pre-empt a still-running earlier voice.
-- pre-empt a still-running earlier voice
stop_voice("voice_timer_30")
audio.play_sound("voice_timer_10", "voice")
elseif self.time == 5 then
+21 -138
View File
@@ -1,85 +1,6 @@
-- YataiDON-HSS — the song-loading (rainbow) transition, ROUND 11.
--
-- The engine (`src/objects/game/transition.cpp`) offers a scripted backdrop: a
-- skin that defines `SongTransition` gets `update` / `draw_bg` / `draw_info`
-- called instead of `Transition::draw_default()`. We take `update` + `draw_bg`
-- only and leave `draw_info` to the engine, so the song title / subtitle stay
-- the engine's own `OutlinedText` at their `skin_config` positions with the
-- engine's own fades — the band under them is ours, drawn in `draw_bg`, which
-- the engine calls first, so the z-order comes out right for free.
--
-- ROUND 12 (text alignment). Those `skin_config` positions are now derived from the
-- movie's own `DefineEditText` records through the arcade pen rule, not eyeballed:
-- song_title_text ET10 box (0,311)-(1920,439) size 64 border 7 align 2 jp_64
-- song_sub_title_text ET12 box (0,396.99)-(1920,525.01) size 40 border 0 align 2 jp_64
-- pen.y = box.top + (box.h - lineHeight)/2 + border, lineHeight = 92 * size/64
-- -> title cell top 336, subtitle cell top 432.25; the kanji ink inside a jp_64 cell runs
-- 14..72 of 97, so the cabinet's ink centres are **379.0** and **459.1**. Our
-- OutlinedText centres its whole image and its kanji ink sits 47.9/1024 * size above
-- that centre, so `transition_title` y = 379 + 3.0 + 1224 = 1606 and
-- `transition_subtitle` y = 459.1 + 2.5 + 1224 = 1686 (the +1224 is `rainbow_up`,
-- which `Transition::draw_song_info` subtracts). Measured in the framebuffer
-- afterwards: title ink centre 379.0, subtitle 459.5.
-- See Graphics/loading/MAPPING.md ROUND 12.
--
-- WHAT THE ARCADE ACTUALLY DOES (the reference build `loading/loading_song.nulm`, root MC 58,
-- 110 frames @ 60 fps, labels init 0 / in 5 / stop 55 / out 60 / end 109; the
-- numbers are exported by scratchpad/anim_export.py into `anim/loading_song.lua`):
--
-- depth 0/1 char 47 / 49 two 960x1080 CURVED half-rainbows. f5..f34 they
-- slide in from off-screen (tx -240 -> 480 / 2160 -> 1440) while
-- stretching horizontally (sx 0.5 -> 1.0). At f34 they have met
-- but still leave a wedge of the screen behind them uncovered.
-- f79..f109 is the exact reverse — they retract sideways.
-- depth 2 char 51 the flat 7-stripe full-screen rainbow (7x 1920x160
-- slabs placed at (960,1000)). Present f35..f78 ONLY: it replaces
-- the two curtains the frame after they meet, and that is what
-- fills the wedge. Static while present.
-- depth 3 char 53 an 8x160 gradient drawn 1920x512 at y 568..1080,
-- **blend = add**, alpha 0 -> 77/256 over f35..f55, held to f60,
-- back to 0 by f69. A warm glow along the bottom, NOT a dark
-- strip (round 2 read it as one; round 4 deleted it because the
-- arcade capture showed nothing dark — both wrong, it is additive).
-- depth 4 sprite 3 ELEVEN copies of one 128x128 star (char 1) at fixed
-- positions and scales. No motion, no sprite sheet: the whole
-- group fades 0 -> 1 over f45..f55 and 1 -> 0 over f60..f69.
-- depth 5/6 don / katsu 560x560. They fly in along an ARC from off-screen
-- low (f35 (-166,738)/(2086,738) -> f55 (160,786)/(1760,786), the
-- ty rising to 656 at f43), hold, dip 786 -> 806 over f60..f65,
-- then fly straight up to y 294 and fade out by f79.
-- depth 7 char 56 the 1600x256 title band at (960,400). f45..f55 it
-- OPENS: sx 0.25 -> 1 with alpha 0 -> 1. f60..f69 it SQUASHES
-- shut: sx 1 -> 1.5, sy 1 -> 0.1, alpha -> 0.
-- depth 8+ song title / subtitle / ruby / tips: alpha 0 until f55, 1 at
-- `stop`, 0 at `out`. The cabinet parks on f55 for the whole load.
--
-- MAPPING ONTO THIS ENGINE. There is no "park" here: `song_select.cpp` shows
-- the transition for exactly `song_info_fade` (delay 266 + duration 266 = 532 ms,
-- global animation.json ids 0/3) and then `game.cpp` builds a second one
-- (`is_second`) that runs another 532 ms while the chart loads. So:
-- scene 1 (is_second == false): t 0..532 ms -> arcade frame 5 .. 55
-- scene 2 (is_second == true): t 0..532 ms -> arcade frame 60 ..109
-- Both joins are frames that fully cover the screen (f55 and f60 are both inside
-- the full-rainbow window), so neither scene change can flash.
--
-- The title band and the hint block are the exception: they are driven off the
-- engine's own fade curves rather than off the arcade frame, because they have
-- to appear and disappear in lockstep with the engine-drawn title. The arcade's
-- band open is linear in both sx and alpha simultaneously, so re-timing it onto
-- `song_info_fade` keeps its shape exactly and only changes its window.
--
-- TEXTURE SLOTS. `build/generated/texture_ids_generated.h` gives this subset
-- seven names and nothing else loads, so the new layers are packed into the two
-- that `draw_default()` used for 8x8 blanks and that nothing reads once a skin
-- scripts `SongTransition`. See Graphics/loading/MAPPING.md ROUND 11.
-- rainbow_bg the flat rainbow, 1:1 with the screen
-- rainbow_bg_top atlas: curtain L (0,0,960,1080) | curtain R (960,0,960,1080)
-- rainbow_bg_bottom atlas: band (0,0,1600,256) / glow (0,288,1600,512)
-- / sparkle (0,832,128,128)
-- text_bg 8x8 transparent — suppresses the engine's own band draw
local _ok, anim = pcall(require, "anim/sampler")
local CLIP = (_ok and anim and anim.safe) and anim.safe("anim/loading_song") or nil
-- Song-loading (rainbow) transition backdrop, sampled from the arcade timeline.
local ok, anim = pcall(require, "anim/sampler")
local CLIP = (ok and anim and anim.safe) and anim.safe("anim/loading_song") or nil
SongTransition = {}
SongTransition.__index = SongTransition
@@ -97,14 +18,12 @@ local SRC_BAND = { x = 0, y = 0, width = 1600, height = 256 }
local SRC_GLOW = { x = 0, y = 288, width = 1600, height = 512 }
local SRC_STAR = { x = 0, y = 832, width = 128, height = 128 }
-- rest positions written into texture.json; every draw below passes a delta off
-- them. The two tables must agree (scratchpad/r11ld/bake_r11.py writes the json).
-- rest positions from texture.json; draws pass a delta off them
local REST_CHARA_L = { -120, 506 }
local REST_CHARA_R = { 1480, 506 }
local REST_HINT = { 0, 568 }
-- sprite 3's eleven star placements, world coords (the sprite sits at (960,540)
-- with an identity matrix, so its children's tx/ty already ARE screen pixels).
-- eleven star placements, already in screen px
local STARS = {
{ 209.75, 542.55, 0.59998 }, { 92.35, 283.00, 0.47829 },
{ 154.20, 330.75, 0.26784 }, { 292.35, 246.35, 0.44998 },
@@ -116,9 +35,7 @@ local STARS = {
local function clamp(v, a, b) if v < a then return a elseif v > b then return b else return v end end
-- Fallbacks for every value the generated table supplies, so a missing or
-- renamed `anim/loading_song.lua` degrades to the arcade's own end states
-- instead of taking the loading screen down.
-- read a track field, falling back to `default` when the table is gone
local function track(name, f, field, default)
if not CLIP then return default end
local v = CLIP:get(name, f, field)
@@ -126,19 +43,12 @@ local function track(name, f, field, default)
return v
end
-- 演奏スキップON badge. `loading/loading_song.nulm`: mc `enso_skip_on` (sprite 46)
-- sits at stage (350, 106) and holds shape 43, whose local bounds are
-- (-312,-52)..(312,52) -- a 624x104 pill, so its top-left is (38, 54). Its child
-- `text_enso_skip` is a DefineEditText with box (-2,-2)..(598,78) placed at
-- (52, 71), i.e. stage x 50..650 / y 69..149, centre-aligned at font size 30.
-- 演奏スキップON badge (624x104 pill at (38,54))
local SKIP_BADGE_X, SKIP_BADGE_Y = 38, 54
local SKIP_BADGE_CX = 350 -- 38 + 624/2, the EditText's centre
local SKIP_TEXT_CY = 109 -- 69 + 80/2, the EditText box's centre
local SKIP_TEXT_SIZE = 30
-- `put()` adds the texture's OWN texture.json origin to the coordinates it is
-- given, so every caller passes that origin as `rest` to cancel it out (see
-- REST_HINT). badge.png declares x=38,y=54 -- the arcade position -- so its rest
-- is the same pair; passing ORIGIN instead drew it at (76,108).
-- badge.png declares its own (38,54) origin, so pass it back as rest to cancel
local SKIP_REST = { 38, 54 }
function SongTransition.new(title, subtitle, is_second)
@@ -153,9 +63,7 @@ function SongTransition.new(title, subtitle, is_second)
self.chara_l = tex.get_texture("global/rainbow_transition/chara_left")
self.chara_r = tex.get_texture("global/rainbow_transition/chara_right")
self.hint = tex.get_texture("global/rainbow_transition/chara_center")
-- The hint block is baked in Japanese. Any other interface language draws the
-- three lines as text (skin_config transition_hint_title / _line1 / _line2) at the
-- rows measured off the texture, on the same fade.
-- the hint block is baked in Japanese; other languages draw it as text
local lang = tex.language and tex.language() or "ja"
if lang ~= "ja" and text and text.create_text then
local white, black = { 255, 255, 255, 255 }, { 0, 0, 0, 255 }
@@ -163,32 +71,19 @@ function SongTransition.new(title, subtitle, is_second)
self.hint_line1 = text.create_text("transition_hint_line1", white, black, false, 3.0, 2.0)
self.hint_line2 = text.create_text("transition_hint_line2", white, black, false, 3.0, 2.0)
end
-- 演奏スキップON badge (the reference build loading/loading_song.nulm, mc `enso_skip_on`)
-- 演奏スキップON badge
self.skip_badge = tex.get_texture("loading/skip_badge/badge")
-- NOT latched here: GameScreen::on_screen_start builds the transition
-- (game.cpp:69) BEFORE it calls init_skip() (game.cpp:76), so at construction
-- time tex.skip_enabled() is still reporting the PREVIOUS enso. Resolved on
-- first draw instead -- see SongTransition:skip_armed().
-- resolved on first draw (skip state isn't ready at construction)
self.skip_on = nil
if text and text.create_raw_text then
-- The cabinet does NOT bake this label: `text_enso_skip` is a Lumen
-- DefineEditText whose HTML is
-- <p align="center"><font face="MS Pゴシック" size="30"
-- color="#000000">演奏スキップON</font></p>
-- black, size 30, centred, NO outline (its border field is 0.0). It has
-- no language variants, so the cabinet shows the Japanese string in every
-- locale and so do we. Built HERE and not in skip_armed(): the glyph
-- atlas needs a frame to upload, so creating it on the first drawn frame
-- left the first frames showing an empty pill.
-- built here (not on first draw) so the glyph atlas is ready
self.skip_text = text.create_raw_text(tex.skin_text("skip_badge_text"), SKIP_TEXT_SIZE,
{ 0, 0, 0, 255 }, { 0, 0, 0, 0 }, false, 0, 2)
end
return self
end
-- Resolved lazily, once, on the first frame that actually draws: by then
-- init_skip() has run and tex.skip_enabled() is true iff the feature ARMED for
-- this enso (skin row on + a player has it on + a free drum, so false in 2P).
-- resolved lazily on first draw, once init_skip() has run
function SongTransition:skip_armed()
if self.skip_on == nil then
self.skip_on = (tex.skip_enabled ~= nil) and tex.skip_enabled() or false
@@ -204,15 +99,12 @@ function SongTransition:update(current_ms, rainbow_up, song_info_fade)
self.info = clamp(1.0 - t / FADE_OUT_MS, 0, 1)
else
self.f = F_IN + (F_STOP - F_IN) * (t / SCENE_MS)
-- the engine hands us `song_info_fade`; use it so the band can never
-- drift from the title the engine draws on top of it
-- use the engine's fade so the band tracks the title it draws on top
self.info = clamp(song_info_fade or ((t - FADE_DELAY) / FADE_MS), 0, 1)
end
end
-- Draw one texture into an absolute screen rect. `scale = 0` makes the engine's
-- `x2[i] * scale + params.x2` collapse to exactly `params.x2`, which is the only
-- way to get independent width and height out of `draw_texture`.
-- draw a texture into an absolute screen rect (scale=0 -> exact x2/y2)
local function put(id, rest, x, y, w, h, a, src, blend)
if not id then return end
tex.draw_texture(id, {
@@ -240,7 +132,7 @@ function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down)
put(self.atlas_c, ORIGIN, rx - 480 * rs, 0, 960 * rs, 1080, 1, SRC_CURT_R)
end
-- 2. additive glow along the bottom (char 53, 1920x512 at y568, alpha -> 77/256)
-- 2. additive glow along the bottom
local ga = track("#55@3", f, "a", 0)
if ga > 0.002 then
put(self.atlas_b, ORIGIN, 0, 568, 1920, 512, ga, SRC_GLOW, "additive")
@@ -256,7 +148,7 @@ function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down)
end
end
-- 4. don / katsu on their arc (560x560, registration = canvas centre)
-- 4. don / katsu on their arc
local da = track("don", f, "a", 1)
if da > 0.002 then
put(self.chara_l, REST_CHARA_L,
@@ -270,9 +162,7 @@ function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down)
560, 560, ka)
end
-- 5. the title band. Arcade open: sx 0.25 -> 1 and alpha 0 -> 1, both linear
-- and simultaneous, re-timed onto the engine's own title fade. Arcade close:
-- sx 1 -> 1.5, sy 1 -> 0.1, alpha -> 0, on the fade-out.
-- 5. the title band (opens/closes on the engine's title fade)
local info = self.info
if info > 0.002 then
local bw, bh
@@ -284,11 +174,9 @@ function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down)
bh = 256
end
put(self.atlas_b, ORIGIN, 960 - bw * 0.5, 400 - bh * 0.5, bw, bh, info, SRC_BAND)
-- 6. the hint block (「ゲームのヒント」 + 3 lines) on the same curve: the
-- arcade turns every text field on at `stop` and off at `out`.
-- 6. the hint block on the same curve
if self.hint_title then
-- texture rows (alpha > 200, outline included): title 165..202, line 1 228..257,
-- line 2 282..310, all centred on x = 960; the texture sits at y = 568.
-- texture rows, all centred on x = 960
local function centred(t, y)
if t then t:draw({ x = math.floor(960 - t.width / 2 + 0.5), y = y, fade = info }) end
end
@@ -300,12 +188,7 @@ function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down)
end
end
-- 7. 演奏スキップON badge, top-left, on the same fade as the band. The arcade
-- shows it ONLY on the song-loading screens (loading/loading_song,
-- enso_ai/loading/loading_ai and the two collab AI loadings) -- there is no
-- `enso_skip_on` anywhere in the song-select lumens, so nothing is drawn
-- there. It is skipped in 2P for free: tex.skip_enabled() is false whenever
-- the feature did not arm, which is exactly the arcade's greyed-row case.
-- 7. 演奏スキップON badge, top-left, on the same fade
if self:skip_armed() and self.skip_badge then
put(self.skip_badge, SKIP_REST, SKIP_BADGE_X, SKIP_BADGE_Y, 624, 104, info)
if self.skip_text then
+2 -6
View File
@@ -1,9 +1,5 @@
-- Ported 1:1 from src/scenes/dan_result.cpp's draw-side (draw_page1/draw_page2/
-- draw_exam_info/draw_gauge_row/draw_celebration/draw_congrats/draw_best_score/
-- draw_digit_counter/draw_nosave_banner/draw_chara_and_plate). Input handling,
-- sound cues, the page2 timeline builder and score persistence (apply_reward)
-- all stay native in DanResultScreen; this file only ever reads the state it's
-- handed each frame, it never advances it.
-- Dan result draw-side (1:1 port of dan_result.cpp's drawing). Reads state only;
-- the engine owns input, sound, and page2 timing.
local FRAME_MS = 1000.0 / 60.0
local DIGIT_ROLL_MS = 500.0
+7 -53
View File
@@ -1,32 +1,10 @@
-- YataiDON-HSS: arcade best_score_mc bar (score/high_score, text-free background) +
-- live-rendered "New Top Score!" / "ベストスコア更新!" caption + small digit7 counters (pitch 14).
-- ROUND 39 (r39-topscore-banner-text): the caption used to be baked into score/high_score_en.png
-- / _ja.png (proof it was baked-per-locale: two whole 448x64 bitmaps differing ONLY in a ~200x40
-- px text patch). Re-render it live via OutlinedText instead, same as every other localized
-- caption on this screen (text.create_text + skin_config text{} table), so it no longer needs a
-- baked image per language and any future locale just needs a skin_config string, not a re-bake.
-- The background bar art (score/high_score.png) had its baked text patch removed by comparing
-- the two old bakes pixel-for-pixel: every column outside x 52..253 was byte-identical between
-- them (so is definitely real ribbon/pill art, not ink), and the interior was reconstructed with
-- that unaffected art as the only ground truth (scratchpad r39tsb/recon8.py — a coincidence-guarded
-- Laplace fill: naive diff-based "background = pixels equal in both bakes" is wrong here because
-- both languages' dark outline strokes cross the same pixel often enough to look "equal", which
-- silently poisoned every earlier attempt with a dark smear the shape of the old text).
-- Arcade best_score_mc bar with a live-rendered "New Top Score!" caption.
HighScoreIndicator = {}
HighScoreIndicator.__index = HighScoreIndicator
-- ROUND 16 outline convention: `border` is stage px (a Lumen border field is ALREADY in 1080p
-- stage px), so create_text wants border / screen_scale, not the literal border value.
local function ol(border) return border / tex.get_screen_scale() end
-- `border` is already in 1080p stage px, so create_text wants border / screen_scale.
local function outline(border) return border / tex.get_screen_scale() end
-- MAPPING ROUND 5 (Graphics/result/MAPPING.md "2. ...ベストスコア更新!..."): ET169 box screen
-- (64.4,266.6)-(364.4,310.6) = 300x44, size 24, outline 4, align CENTRE; the arcade's own pen
-- formula (box_left + border + (box_w - text_w)/2) puts the ink CENTRE 4 px right/down of the
-- plain box centre -- i.e. at (box_left + border + box_w/2, box_top + border + box_h/2) =
-- (64.4+4+150, 266.6+4+22) = (218.4, 292.6), relative to the bar's own texture.json origin
-- (64,266) that is +154.4/+26.6. Centring an OutlinedText's (padded) width/height on that same
-- target point reproduces the same ink position (the padding is symmetric, so it cancels out of
-- the centring math) without needing the raw ink-only width the bake script used.
local TEXT_CX_REL = 154.4
local TEXT_CY_REL = 26.6
local BAR_BASE_X = { [0] = 64, [1] = 1020 } -- texture.json high_score frame 0 (1P) / 1 (2P)
@@ -35,9 +13,6 @@ local BAR_BASE_Y = 266
function HighScoreIndicator.new(score_diff, is_2p)
local self = setmetatable({}, HighScoreIndicator)
tex.load_animations("result")
-- arcade best_score_mc "start" (result/result.nulm MC 202, label start = f5): the bar drops
-- in, overshoots, bounces and settles at its texture.json position (no permanent upward
-- offset — Green's anim 18 left it 22 px up, under the gauge). ROUND 8: sampled, see below.
self.start_ms = nil
self.y_offset = 24
self.fade_val = 0
@@ -46,31 +21,13 @@ function HighScoreIndicator.new(score_diff, is_2p)
self.t_label = tex.get_texture("result/score/high_score")
self.t_num = tex.get_texture("result/score/high_score_num")
self.sc_margin = 14
-- White fill / dark-brown outline sampled directly off the old bakes' glyph pixels
-- (RGB ~(70,35,10), the same ink colour in both the EN and JA images).
self.text = text.create_text("result_topscore_text", { 255, 255, 255, 255 }, { 70, 35, 10, 255 },
false, ol(4.0), 0)
false, outline(4.0), 0)
return self
end
-- ROUND 8 — `KEYS = {{0,24},{5,-22},{15,2},{25,0}}` and `fade = min(1, f/5)` are gone:
-- `Scripts/anim/best_score.lua` is exported straight out of `result/result.nulm` MC 202 by
-- scratchpad/anim_export.py (track `#196@0` = the bar). See ANIM_PIPELINE.md.
-- What the export changed — the hand-fitted 4-key table was not just imprecise, it was the
-- wrong SHAPE. The cabinet's bar, in frames after label `start`:
-- t 0..5 drop in, y +24 -> -21.95, alpha 0 -> 1 (five frames, eased)
-- t 5..9 rebound, y -21.95 -> 0
-- t 9..13 OVERSHOOT past the rest line, y 0 -> +7
-- t 13..18 settle, y +7 -> 0 (and it is there from t 18, not t 25)
-- Round 6's table lerped straight from -22 to +2 over t 5..15 and so drew NO overshoot at
-- all; it also faded in over 5 frames linearly where the cabinet's alpha is 0.36/0.64/0.84/
-- 0.96/1.0. The `sy` squash-stretch that rides the same track (1 -> 1.2 -> 1 -> 0.8 -> 1)
-- is NOT applied — see LUA_CAPABILITIES.md C-res-4.
-- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL
-- clip when a generated table is missing or malformed, so every reader below falls
-- through to its `or <constant>` default instead of taking the screen down.
local _ha_ok, hs_anim = pcall(require, "anim/sampler")
if not _ha_ok then hs_anim = { safe = function()
local ha_ok, hs_anim = pcall(require, "anim/sampler")
if not ha_ok then hs_anim = { safe = function()
-- sampler.lua itself is missing: hand back the same NULL clip shape it would
local n = {}
n.at = function() return nil end; n.get = function() return nil end
@@ -95,10 +52,7 @@ function HighScoreIndicator:draw()
local y = self.y_offset
local fade = self.fade_val
if self.t_label then tex.draw_texture(self.t_label, { y = y, fade = fade, index = p_idx }) end
-- Live caption, replacing the old baked-per-locale score/high_score_en|ja.png text patch.
-- Centred on the arcade's own ink-centre target (see TEXT_CX_REL/TEXT_CY_REL derivation
-- above); ROUND 11 whole-pixel snap (ink-centring an odd-width label on an integer centre
-- lands it on a .5 and the GPU blends every texel 50/50 with its neighbour).
-- Live caption, centred on the arcade's ink-centre target.
if self.text then
local cx = BAR_BASE_X[p_idx] + TEXT_CX_REL
local cy = BAR_BASE_Y + TEXT_CY_REL + y
+3 -9
View File
@@ -1,11 +1,5 @@
-- YataiDON-HSS: arcade result background (lumen 000_default/result/result_bg: main_mc "1p_l" / "2p").
--
-- ROUND 11: this is no longer one flat plate. `result_bg_layers.lua` draws the arcade's 14 layers
-- (2 sky halves, Mt Fuji, 11 drifting cloud bars) and the header band on top of them, off the
-- exported timeline `Scripts/anim/result_bg.lua`. The success ("clear") variant cross-fades in on
-- the same clock, driven by `__hss_result_bg_clear`, which `result_player.lua` publishes from the
-- same `fade_in_bottom` animation that draws `result/background/footer_1p` (now just the fireworks
-- and sparkles, since the clear sky / Fuji / clouds are live layers).
-- Arcade result background; layers and the clear cross-fade are drawn by
-- result_bg_layers.lua, driven by __hss_result_bg_clear.
ResultBackground = {}
ResultBackground.__index = ResultBackground
@@ -23,7 +17,7 @@ function ResultBackground.new(player_num, width)
if ok then self.layers = obj end
end
if not self.layers then
-- last resort: the sky plate alone, static, exactly as rounds 2-10 drew it
-- fallback: static sky plate alone
self.t_bg = tex.get_texture(player_num == 3 and "result/background/background_2p"
or "result/background/background_1p")
end
+8 -90
View File
@@ -1,21 +1,5 @@
-- YataiDON-HSS / ROUND 11 -- the animated 1P result backdrop.
--
-- The arcade backdrop is NOT a flat plate. `result/result_bg.nulm` main_mc "1p_l" is
-- depth 2 bg_1p_l (MC 88) -> init = sprite 83 (720 f) : 2 sky halves, Mt Fuji, 11 cloud bars
-- -> success = sprite 87 (720 f) : gold sky, cyan pulsing Fuji, 11 clear
-- cloud bars, 3 fireworks + 10 sparkles
-- depth 3 shape 89 : the white header band, ABOVE all of it
-- Every cloud bar drifts leftwards at its own speed on a 720-frame (12 s) loop and fades in at
-- spawn / out at despawn. Up to round 10 we shipped one baked 1920x1080 PNG of frame 0, which is
-- why the screen looked dead.
--
-- Motion comes from the EXPORTED timeline (`Scripts/anim/result_bg.lua`, ANIM_PIPELINE.md), never
-- from hand-fitted constants. If that table is missing the layers still draw, parked on the arcade
-- frame-0 constants in LAYERS below -- a data file must never be able to take the screen down.
--
-- Geometry note: every one of these shapes has local origin (0,0), so the arcade PlaceObject
-- translation IS the sprite's top-left on the 1920x1080 stage. A negative x-scale means the arcade
-- tx is the sprite's RIGHT edge, so we draw at tx - width*|sx| with mirror = "horizontal".
-- Animated 1P result backdrop: sky halves, Mt Fuji and drifting cloud bars, plus
-- a white header band, all driven by the exported result_bg timeline.
local anim = require("anim/sampler")
@@ -23,10 +7,7 @@ local ResultBgLayers = {}
ResultBgLayers.__index = ResultBgLayers
local LOOP = 720 -- frames, sprite 83 / 87
local FUJI_LOOP = 90 -- frames, huji_1p_l_mc (sprite 86), clear state only.
-- ROUND 84: this is a ONE-SHOT length, not a loop -- the clip
-- stops itself on its frame 89 and holds. Kept for reference;
-- fuji_frame() no longer wraps on it.
local FUJI_LOOP = 90 -- frames, huji_1p_l_mc (sprite 86), clear state only
local MS_TO_F = 0.06 -- 60 fps
-- depth, cloud texture index, y, scale (|sx| == sy for every one of them), mirror, source width
@@ -60,10 +41,7 @@ function ResultBgLayers.new(player_num)
self.fuji_t0 = nil
if self.two_player then
-- The arcade 2P backdrop (main_mc "2p" -> bg_2p, MC 10) is two static sky halves with no
-- clouds and no Fuji, so it stays a single baked plate: the red half (shape 3, placed
-- mirrored) on the left and the cyan half (shape 1) on the right. The header band is
-- the same one the 1P layers draw, so it is drawn here too instead of being baked in.
-- 2P: single baked plate (no clouds or Fuji) + header
self.t_bg = tex.get_texture("result/background/background_2p")
self.t_header = tex.get_texture("result/background/bg_header")
return self
@@ -94,59 +72,7 @@ function ResultBgLayers:frame()
return math.fmod((ms - self.t0) * MS_TO_F, LOOP)
end
-- ROUND 17 -- Mt Fuji runs on its OWN clock, zeroed when the clear state fires.
--
-- User report 「result後面, 這個山在錯誤的跳動」. Measured this round:
-- * `result_bg.nulm --sprite 88` (`bg_1p_l`): `crear_1P_l` (sprite 87) is PLACED
-- at frame 5 (the `success` label) with alpha 0 and cross-fades to 1 over
-- frames 5..14 -- and its own cursor starts at 0 on that same frame.
-- * `--sprite 87 --range 0,250` shows the nested `huji_1p_l_mc` cursor running
-- 0,1,...,89,0,1,... -- a plain 90-frame loop, no stop and no label.
-- * `--sprite 86` is that 90-frame timeline: sy 1.000 @f0 -> 0.800 @f22 ->
-- 1.300 @f50 -> 0.900 @f60 -> 1.100 @f74 -> 0.900 @f89, ty compensating so
-- the foot of the mountain stays on y~1064.
-- So the cabinet's mountain starts its squash-and-stretch from REST (sy 1.0) at
-- the exact frame the gold backdrop starts fading in.
--
-- What we did instead: sample `fmod(self:frame(), 90)`, i.e. the SHARED 720-frame
-- cloud clock, which has been free-running since the result screen opened -- but
-- `__hss_result_bg_clear` only leaves 0 tens of seconds later, after the gauge and
-- the count-up. The mountain therefore popped in at a uniformly random point of
-- the bounce (typically mid-stretch at sy 1.2-1.3) and then snapped, which is the
-- wrong jump the user is seeing. The 720 clock is right for the clouds (their
-- tracks are seamless at f719==f0, so phase is invisible); it is wrong for this
-- one-shot-into-bounce channel. Same class of bug as ENTRY's `entry_bg` strobe.
--
-- ROUND 84 (r84-result-fuji-jitter) -- IT IS A ONE-SHOT, NOT A LOOP.
--
-- User report 「result畫面,這個山會一直抽動,但他應該不是一直抽動」. Round 17's
-- "sprite 86 has 90 frames, no stop and no gotoAndPlay" was a TOOL ARTEFACT:
-- `lumen_anim_dump` only honours a clip's own DoAction stop()/goto when it is
-- given `--auto-stop`, and that flag is OFF by default precisely so a clip that
-- stops itself still shows its whole authored timeline. Round 17 dumped without
-- it, saw the dumper's own replay, and read it as an authored loop.
--
-- Re-measured this round, `--sprite 87 --all --leaves --range 0,719`:
-- without --auto-stop : huji_1p_l_mc cursor restarts at parent frames
-- 90, 180, 270, 360, 450, 540, 630 (the artefact)
-- with --auto-stop : cursor 0,1,...,88,89,89,89,... stop flag set at
-- its own frame 89 and never cleared for the remaining
-- 630 frames of the parent.
-- The held pose is ty = 360.89, sy = 0.89997 (foot y = 1080.87) from parent
-- frame 89 to 719 inclusive -- identical at f=90, 120, 200, 300, 400.
-- The right-hand rig is the same design, not a counter-example: char 32 (64
-- frames) stops at its frame 63 on the IDENTICAL (360.89, 0.89997).
--
-- So the cabinet's mountain does its squash-and-stretch EXACTLY ONCE -- 90
-- frames = 1500 ms at the container's 60 fps, starting the frame the gold
-- backdrop begins fading in -- and then parks, permanently, 10% squashed. Our
-- `math.fmod(..., FUJI_LOOP)` restarted that bounce every 1500 ms for as long
-- as the result screen was up, which is the constant twitching the user sees.
--
-- The fix is to let the frame counter run monotonically: `sampler.lua` clamps
-- any frame past the last row to that row ("exactly what a Lumen timeline parked
-- on its last frame does"), and `anim/result_bg_fuji.lua`'s last row IS
-- { 89, 360.89, 0.9 }. No fmod, no clamp of our own, no rebuild.
-- Mt Fuji runs on its own clock, zeroed when the clear state fires.
function ResultBgLayers:fuji_frame(clear)
if clear <= 0 then
self.fuji_t0 = nil -- re-arm: the next clear starts from rest again
@@ -192,11 +118,7 @@ function ResultBgLayers:draw(fade, clear)
tex.draw_texture(self.t_sky_clear, { index = 1, fade = fade * clear, mirror = "horizontal" })
end
-- 2. Mt Fuji. Static in the base state (round 6 recorded a pulse there; the export shows
-- sy == 1.0 for all 720 frames of sprite 83). The pulse is the CLEAR state's
-- huji_1p_l_mc: a 90-frame ONE-SHOT (ROUND 84 -- it stops on its own frame 89),
-- sy 1.000 -> 0.800 @f22 -> 1.300 @f50 -> 0.900 @f60 -> 1.100 @f74 -> 0.900 @f89 and
-- then held forever, with ty compensating so the foot stays on y ~ 1080.9.
-- 2. Mt Fuji, pulsing only in the clear state (a 90-frame one-shot, held at the end).
if self.t_fuji and clear < 1.0 then
tex.draw_texture(self.t_fuji, { x = FUJI_X, y = FUJI_Y, fade = fade * (1.0 - clear) })
end
@@ -209,10 +131,7 @@ function ResultBgLayers:draw(fade, clear)
})
end
-- 3. the eleven cloud bars. The clear rig's tracks are frame-identical to the base rig's for
-- ten of the eleven placements (only depth 10 drifts faster there), so both states are
-- driven off the one exported table -- which also means the cross-fade never makes a cloud
-- jump. See MAPPING.md ROUND 11.
-- 3. the eleven cloud bars, driven off the one exported table for both states.
for i = 1, #LAYERS do
local L = LAYERS[i]
local idx, y, scale, mirror, w, track = L[2], L[3], L[4], L[5], L[6], L[7]
@@ -227,8 +146,7 @@ function ResultBgLayers:draw(fade, clear)
end
end
-- 4. the header band + the "n曲目" plate, last: main_mc depth 3 is above bg_1p_l depth 2, and
-- two of the cloud bars (depths 10 and 11) do reach into the top 176 px.
-- 4. header band, drawn last (above the clouds).
if self.t_header then tex.draw_texture(self.t_header, { fade = fade }) end
end
+10 -82
View File
@@ -1,56 +1,11 @@
-- YataiDON-HSS override of PyTaikoGreen/result/result_crown.lua
-- ROUND 6 (LUA_CAPABILITIES item 33) — the arcade crown fly-in + crown_loop sparkles,
-- keyed frame-for-frame off lumen `result/result` MC 73 `crown_mc`
-- (125 frames, labels init 0 / start 5 / end 120). `t` below is FRAMES since label "start",
-- i.e. since this object is constructed (result_player.lua builds it at the crown beat).
--
-- d3 `crown` MC 45 -> result/crown/crown_clear|fc|dfc (crown_effect__shape39/41/43)
-- f 5..18 scale 3.0 -> 0.8, alpha 0 -> 1. Both channels are driven by u = (t/13)^2
-- (verified: scale = 0.8 + 2.2*(1-u), alpha = u, max error < 1/256 on all 14 frames)
-- f18..22 scale 0.8 -> 1.0 (+0.05/frame) — the settle overshoot, then held at 1.0
-- d2 `crown_effect` the same crown as a white flash -> result/crown/crown_fade
-- f18..22 scale 0.8 -> 1.0 with the crown, alpha 1
-- f22..47 scale 1.0 -> 2.0 (+0.04/frame), alpha 1 -> 0 (25 frames)
-- d0/d1 MC 38 -> crown_mc__shape36, a 288x288 radial burst, two copies
-- f18..22 alpha 161/256 -> 1; scale d0 0.050 -> 0.154, d1 0.100 -> 0.299
-- f22..54 alpha 1 -> 0; scale d0 0.154 -> 0.749, d1 0.299 -> 1.423 (32 frames)
-- d4 MC 50 -> a 5-star cluster (score_rank_kiwami_mc__shape46)
-- f22..59 scale 0.5 -> 1.0, offset (30.2,-2.2) -> (63.2,-17.0)
-- f59..100 scale 1.0 -> 1.2, offset -> (76.5,-23.0)
-- f101..119 alpha 1 -> 0
-- d5 `crown_loop` MC 71/72 — from f120 a 180-frame loop whose only moving part is a
-- 3-step shine sweep: shape65 @ (-28,-30) frames 0..2, shape67 @ (6,8)
-- frames 3..5, shape69 @ (28,42) frames 6..8, then 171 idle frames.
--
-- The loop shine is a solid white polygon on the cabinet (a plain fill, no texture),
-- clipped to the crown; gleam frames 0..2 carry it as white with the clipped alpha.
-- d0/d1, d4 and the loop shine all live in `result/crown/gleam` — ROUND 6 re-baked it from
-- 3 transparent 152x152 frames into 5 x 320x320 frames, each centred on the crown's own
-- registration point (see scratchpad r6_bake_gleam.py and Graphics/result/MAPPING.md):
-- 0/1/2 = the loop shine sweep, 3 = the radial burst, 4 = the star cluster.
-- Arcade crown fly-in and crown_loop shine, sampled from result/result crown_mc.
ResultCrown = {}
ResultCrown.__index = ResultCrown
-- ROUND 7 — the per-channel constants above are no longer typed in by hand: every
-- curve below is SAMPLED out of `Scripts/anim/crown_mc.lua`, exported straight from
-- the cabinet's own `result/result.nulm` sprite 73 by scratchpad/anim_export.py
-- (see ANIM_PIPELINE.md). Track names are the arcade display-list paths; an unnamed
-- placement is `#<character>@<depth>`, which is why the two radial-burst copies read as
-- `#38@0` / `#38@1` and the star cluster's container as `#50@4`.
--
-- Two things the export corrected in the round 6 hand-port:
-- * the radial burst is TWO EQUAL copies (both 0.100 -> 0.307 -> 1.498). Round 6 read
-- the first one as half the size of the second (0.050 -> 0.154 -> 0.749), so our
-- smaller burst was drawing at half the cabinet's diameter for its whole life.
-- * the star cluster's fade-out runs f100..f119 of the MC, i.e. t = 95..114 frames
-- after `start`. Round 6 keyed it on t = 100..119, so it faded 83 ms late.
local F = 1000 / 60 -- one arcade frame in ms
local FRAME_MS = 1000 / 60 -- one arcade frame in ms
-- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL
-- clip when a generated table is missing or malformed, so every reader below falls
-- through to its `or <constant>` default instead of taking the screen down.
local _cr_ok, crown_anim = pcall(require, "anim/sampler")
if not _cr_ok then crown_anim = { safe = function()
local cr_ok, crown_anim = pcall(require, "anim/sampler")
if not cr_ok then crown_anim = { safe = function()
-- sampler.lua itself is missing: hand back the same NULL clip shape it would
local n = {}
n.at = function() return nil end; n.get = function() return nil end
@@ -65,12 +20,6 @@ local CROWN_END_F = CROWN:label("end") or 120
local GL_SHINE = { 0, 1, 2 } -- gleam frames of the crown_loop sweep
local GL_BURST, GL_STARS = 3, 4
-- ROUND 53 (r53-anim-lumen-sweep): the crown_loop shine sweep, sampled. `anim/crown_loop`
-- is the nested MC 71 (`crown_loop`, 180 f) that sprite 73 parks on from the `end` label:
-- the gleam shape steps through three placements — (-28,-30) f0..2, (6,8) f3..5,
-- (28,42) f6..8 — then 171 idle frames. Those offsets are baked into our gleam frames
-- 0/1/2, so the leaf windows drive WHICH frame shows and WHEN; the R7 hand ladder
-- (k<9, floor(k/3)) is kept below as the fail-soft fallback.
local CLOOP = crown_anim.safe("anim/crown_loop")
local CLOOP_LEN = CLOOP:last() - CLOOP:first() + 1 -- 180
local CLOOP_LEAVES = { { "#65@1", 1 }, { "#67@1", 2 }, { "#69@1", 3 } } -- -> GL_SHINE idx
@@ -109,14 +58,6 @@ function ResultCrown.new(crown_type, is_2p, has_2p)
self.t_crown_fade = tex.get_texture("result/crown/crown_fade")
self.t_gleam = tex.get_texture("result/crown/gleam")
-- ROUND 52 (r52-lua-divergence-fixes): the arcade crown SE is
-- colour x seat, not one generic sample. ScoreBase.lua:322-342 builds
-- "crown_" .. silver|gold|rainbow (g_crown 1/2/3 = clear/FC/DFC) ..
-- _l/_r when g_entryType == 2, else _c. YataiDON crown_type is 0/1/2 =
-- clear/fc/dfc, i.e. arcade colour index + 1. Samples shipped ROUND 52
-- (se_result_v12a crown_{silver,gold,rainbow}_{c,l,r}); the old generic
-- "crown" (= crown_gold_c) stays on disk as the fallback for a stale
-- sound folder.
local colours = { [0] = "silver", [1] = "gold", [2] = "rainbow" }
local seat = has_2p and (is_2p and "_r" or "_l") or "_c"
self.sound_name = "crown_" .. (colours[crown_type] or "silver") .. seat
@@ -126,13 +67,7 @@ end
function ResultCrown:update(current_ms)
self.now = current_ms
-- ROUND 15: the cue is the FLY-IN, not the settle. ScoreBase.lua StartCrownDisplay
-- does `crownMc_:GotoAndPlay("start")` and `TSound:Play("se_result_v12a", crown_<colour>_c)`
-- in the same statement block, so the sound lands on t = 0. Round 6 put it at t = 17
-- (the settle) because that was also the beat that popped the message bubble; the
-- bubble is now on its own arcade beat (MoveResultAction), so the two are independent.
if not self.sound_played and (current_ms - self.t0) / F >= 0 then
-- ROUND 52: colour x seat variant (see new()).
if not self.sound_played and (current_ms - self.t0) / FRAME_MS >= 0 then
audio.play_sound(self.sound_name or "crown", "sound")
self.sound_played = true
end
@@ -140,24 +75,22 @@ end
-- result_player.lua gates the 「クリア」 bubble on this: the crown has landed at scale 1.0.
function ResultCrown:is_settled()
return (self.now - self.t0) / F >= 17
return (self.now - self.t0) / FRAME_MS >= 17
end
function ResultCrown:draw()
local t = (self.now - self.t0) / F
local t = (self.now - self.t0) / FRAME_MS
if t < 0 then return end
local idx = self.p_idx
local f = CROWN_START_F + t -- absolute arcade frame on sprite 73
-- ---- d3 crown -------------------------------------------------------------
local c_scale = CROWN:get("crown", f, "sx") or 1.0
local c_alpha = CROWN:get("crown", f, "a") or 1.0
if self.t_crown then
tex.draw_texture(self.t_crown, { scale = c_scale, center = true, index = idx, fade = c_alpha })
end
-- ---- d2 crown_effect (white flash ghost) -----------------------------------
if self.t_crown_fade and t >= 13 and t <= 42 then
local s = CROWN:get("crown_effect", f, "sx") or 1.0
local a = CROWN:get("crown_effect", f, "a") or 0.0
@@ -168,10 +101,7 @@ function ResultCrown:draw()
if not self.t_gleam then return end
-- ---- d0 / d1 radial burst ---------------------------------------------------
-- two EQUAL copies, differing only in a sub-pixel jitter the cabinet's tween leaves
-- behind; drawn twice because the arcade draws it twice and the doubled coverage is
-- what makes the flash read as bright rather than as one thin ring.
-- radial burst: two equal copies, drawn twice for the doubled coverage
if t >= 13 and t <= 49 then
local a = CROWN:get("#38@1", f, "a") or 0.0
local s1 = CROWN:get("#38@1", f, "sx") or 0.1
@@ -183,7 +113,7 @@ function ResultCrown:draw()
end
end
-- ---- d4 drifting star cluster ----------------------------------------------
-- drifting star cluster
if t >= 17 and t <= 119 then
local s = CROWN:get("#50@4", f, "sx") or 0.5
local ox = CROWN:get("#50@4", f, "tx") or 30.2
@@ -193,9 +123,7 @@ function ResultCrown:draw()
{ frame = GL_STARS, scale = s, center = true, index = idx, x = ox, y = oy, fade = clamp01(a) })
end
-- ---- d5 crown_loop shine sweep (180-frame loop from "end") -------------------
-- ROUND 53: which gleam frame shows, and when, now comes from anim/crown_loop's own
-- leaf windows; the R7 hand ladder is the fail-soft fallback.
-- crown_loop shine sweep (180-frame loop from "end")
if f >= CROWN_END_F - 5 then
local k = (f - (CROWN_END_F - 5)) % (CLOOP_LEN > 1 and CLOOP_LEN or 180)
local shine, have_table = crown_loop_shine(k)
+5 -30
View File
@@ -1,33 +1,12 @@
-- YataiDON-HSS: arcade msg_L_mc / msg_R_mc bubbles (perfect/success/near/miss) baked as
-- crown/message frames. 0..3 = left bubble (miss, near, success, perfect), 4..7 = right bubble
-- (tail on the other side) for 2P.
--
-- ROUND 15 — the pop is the CABINET's, not PyTaikoGreen's.
-- Until now this replayed `result/animation.json` ids 21/22/23: scale 0 -> 1.15 over 166.67 ms
-- (ease-out quad), 1.15 -> 1.0 over 66.67 ms, alpha 0 -> 1 over 166.67 ms. The cabinet's
-- `msg_L_mc` (result/result.nulm sprite 283) plays the SAME nine frames for all four states
-- (labels perfect 5 / success 35 / near 65 / miss 95) and they are a different shape:
-- f0..f4 scale 0.500 -> 0.806 -> 1.025 -> 1.156 -> 1.200, alpha 0 -> .438 -> .75 -> .938 -> 1
-- f4..f9 scale 1.200 -> 1.192 -> 1.168 -> 1.128 -> 1.072 -> 1.000, alpha 1
-- i.e. it starts at HALF size (not zero), overshoots to 1.20 (not 1.15) in 66.7 ms (not 166.7),
-- and is fully open at 150 ms instead of 233 ms. The numbers come from
-- `Scripts/anim/result_msg.lua`, exported by scratchpad/anim_export.py (ANIM_PIPELINE.md);
-- nothing here is fitted by eye.
--
-- The bubble PNGs no longer bake the message in -- text is drawn separately with
-- `text.create_text`, sharing the bubble's pop scale/fade so the two still move as one.
-- (The cabinet fades its TEXT in separately, five frames behind the bubble art; we don't
-- replicate that stagger here -- the exported sample table only covers the "success" label
-- range MSG_F0..MSG_F1, so sampling f-4 near f0 would land before that range starts.)
-- Arcade msg_L_mc / msg_R_mc bubbles (perfect/success/near/miss) baked as
-- crown/message frames; 0..3 left bubble, 4..7 right bubble (2P).
ResultCrownMessage = {}
ResultCrownMessage.__index = ResultCrownMessage
local F = 1000 / 60
local FRAME_MS = 1000 / 60
-- FAIL SOFT (ROUND 8 rule): a missing or malformed generated table must degrade to "no pop",
-- never take the result screen down at require time.
local _ok, msg_anim = pcall(require, "anim/sampler")
if not _ok then msg_anim = { safe = function()
local ok, msg_anim = pcall(require, "anim/sampler")
if not ok then msg_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
@@ -48,10 +27,6 @@ local MSG_KEY = {
[3] = "crown_message_perfect",
}
-- Bubble centres, from Graphics/result/crown/texture.json's "message" entries
-- (1P x=410, 2P x=989, both y=626) plus half the bubble art's canvas size (520x340,
-- unchanged from the original baked PNGs) -- the anchor `tex.draw_texture(...,
-- center = true)` scales around.
local MSG_W, MSG_H = 520, 340
local MSG_CENTER = {
{ 410 + MSG_W / 2, 626 + MSG_H / 2 }, -- 1P
+1 -3
View File
@@ -1,6 +1,4 @@
-- YataiDON-HSS: result fade-in = the arcade background drawn on top and faded out (animation 15).
-- ROUND 11: it draws the same live layer stack as ResultBackground (not a static plate), so the
-- clouds do not pop into motion when the wipe finishes.
-- Result fade-in: the arcade background drawn on top and faded out (animation 15).
ResultFadeIn = {}
ResultFadeIn.__index = ResultFadeIn
+31 -240
View File
@@ -1,11 +1,5 @@
-- YataiDON-HSS: Nijiiro arcade result board (lumen 000_default/result/result, script_lua/result/ResultMain.lua + ScoreBase.lua)
-- Layout (1080p px, 1P = left board; 2P = same board shifted +956 px = main.score_base_R vs score_base_L):
-- board frame main__shape313/317 @ (40,180)/(996,180) difficulty_mc tab @ score_base+(-596.6,-187.7)
-- option_icon_bg @ score_base+(-353,-167), option icons 43px pitch total score digits @ score_base+(-318,20) pitch 46
-- judge table: score_board_count_label @ score_base+(-90,93); 4-digit counters @ score_base+(249, -30/32/94/156/218) pitch 31
-- crown_mc @ score_base+(-212,165) best_score_mc @ score_base+(-316,-46) tamashii gauge (0.7 scale) @ main+(-466,-302)
-- donback @ main+(-464,244) msg_L_mc @ main+(-300,246) control_guide_L @ main+(-476,486)
-- Texture names are the parent's (the TexID enum is generated from PyTaikoGreen); see Graphics/result/MAPPING.md.
-- Nijiiro arcade result board (1P/2P). Draws the score board, gauge, crown and
-- message; the engine owns the result state machine.
ResultPlayer = {}
ResultPlayer.__index = ResultPlayer
@@ -14,37 +8,13 @@ local GAUGE_CELLS = 50 -- the cabinet's gauge is 50 cells (kMa
local GAUGE_UNIT = 21 * GAUGE_SCALE -- one 21 px cell of the groove, at the board's 0.7
local OPTION_PITCH = 43
-- ROUND 15 — the whole result sequence is now the cabinet's own state machine, read out of
-- `script_lua/result/ResultMain.lua` (Main.m_State) and `ScoreBase.lua`. Every number below is
-- a frame count from those scripts; nothing here is tuned by eye. 1 frame = 1000/120 ms
-- (ROUND 24, r24-fpsaudit: Common.FPS is the cabinet's 120 fps script-logic clock, not 60 --
-- see ENGINE_BINDINGS.md; this file's ms comments below are corrected to match).
--
-- Fadein g_isFadefinish -> BGM primal_loop
-- WaitUpdateTamashiiGage timer_ > 100 (833.3 ms) then count_up_loop_c starts
-- UpdateTamashiiGage kGageSpeed_ = 7 frames per gauge unit, out of kMaxGage_ = 50
-- WaitUpdateScore timer_ > 100 (833.3 ms) then StartScoreDisplay
-- UpdateScore timer_ > 500 (4166.7 ms)
-- ScoreRank skipped outright when g_scoreRank == 0 -- YataiDON has no rank
-- Crown timer_ > 150 (1250.0 ms) then MoveResultAction
-- MoveResultAction MoveDonResult + MoveBg (StartSuccessEffect) + DisplayMessage
--
-- Two consequences the earlier rounds had wrong:
-- * the clear ("success") backdrop and the message bubble are NOT gauge/crown-settle events.
-- Both fire in MoveResultAction, at the END of the Crown state -- 10.8 s after the count-up
-- starts. We fired the gold backdrop the moment the gauge finished, roughly 12 s early.
-- * the gauge fills at 7 frames per arcade unit, i.e. 350 frames (2916.7 ms at 120 fps) for a
-- full bar. Our 50 ms per YataiDON unit x 87 units = 4350 ms, too slow at the corrected rate.
local F = 1000 / 120 -- ROUND 24 (r24-fpsaudit): Common.FPS is the cabinet's
-- 120 fps script-logic clock, not 60 -- see ENGINE_BINDINGS.md
local WAIT_GAUGE_MS = 100 * F -- 833.3 fade-in done -> the gauge starts filling
local GAUGE_MS_UNIT = 7 * F -- 58.33 ms per cell (kGageSpeed_ = 7 f per arcade unit)
local WAIT_SCORE_MS = 100 * F -- 833.3 gauge full -> StartScoreDisplay
local SCORE_TO_CROWN = 500 * F -- 4166.7 UpdateScore state length
local CROWN_TO_ACTION = 150 * F -- 1250.0 Crown state length -> MoveResultAction
-- ScoreRank state (between UpdateScore and Crown) when the score earned a rank: the
-- arcade result/result.nulm `score_rank_mc` (`start` f5 .. `end` f120 at 60 fps).
local RANK_STATE_MS = 120 * (1000 / 60) -- 2000.0
local FRAME_MS = 1000 / 120
local WAIT_GAUGE_MS = 100 * FRAME_MS -- 833.3 fade-in done -> gauge starts filling
local GAUGE_MS_UNIT = 7 * FRAME_MS -- 58.33 ms per cell
local WAIT_SCORE_MS = 100 * FRAME_MS -- 833.3 gauge full -> score display
local SCORE_TO_CROWN = 500 * FRAME_MS -- 4166.7 UpdateScore state length
local CROWN_TO_ACTION = 150 * FRAME_MS -- 1250.0 Crown state length
local RANK_STATE_MS = 120 * (1000 / 60) -- 2000.0 ScoreRank state length
local ClipPlayer = require("game/clip_player")
-- score_rank_mc / score_rank_kiwami_mc placements inside the 1920x1080 result stage
local RANK_POS = { L = { 181.1, 500.7 }, R = { 1126.9, 501.0 } }
@@ -57,61 +27,26 @@ local function rank_of(score)
return r
end
-- ROUND 15 — the digit pop. Every arcade digit MC plays a scale keyframe when it lands on
-- `numN`; we drew the settled frame flat. Both curves are exported straight out of
-- result/result.nulm (ANIM_PIPELINE.md), so they are the cabinet's numbers, not a guess:
-- judge counters (sprite 167, digit4): scale 1.300 -> 1.000 LINEAR over 4 frames (66.7 ms)
-- score counters (sprite 245, digit7): a 19-frame squash-and-stretch --
-- 0.90 -> 1.40 (f0..6) -> 1.00 (f11) -> 0.90 (f15) -> 1.00 (f19), 316.7 ms
-- `center = true` scales about the digit cell's own centre, so a settled digit (scale 1)
-- lands on exactly the texture.json pixel it landed on before this change.
local _dg_ok, dg_anim = pcall(require, "anim/sampler")
if not _dg_ok then dg_anim = { safe = function()
local digit_ok, dg_anim = pcall(require, "anim/sampler")
if not digit_ok then dg_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 DGJ, DGJ_TRACK = dg_anim.safe("anim/result_digit_judge"), "#147@0"
local DGS, DGS_TRACK = dg_anim.safe("anim/result_digit_score"), "#225@0"
local DGJ_F0, DGJ_F1 = DGJ:first(), DGJ:last()
local DGS_F0, DGS_F1 = DGS:first(), DGS:last()
local digit_judge, DGJ_TRACK = dg_anim.safe("anim/result_digit_judge"), "#147@0"
local digit_score, DGS_TRACK = dg_anim.safe("anim/result_digit_score"), "#225@0"
local DGJ_F0, DGJ_F1 = digit_judge:first(), digit_judge:last()
local DGS_F0, DGS_F1 = digit_score:first(), digit_score:last()
-- ROUND 53 (r53-anim-lumen-sweep): the success backdrop cross-fade, sampled from the
-- cabinet's own timeline. `Scripts/anim/result_bg_success.lua` is sprite 88 (`bg_1p_l`)
-- of `result/result_bg.nulm`: at the `success` label (f5) it places `crear_1P_l` at
-- alpha 0 and fades it to 1 over f5..f14 — 9 frames = 150 ms, not the 333 ms the
-- inherited PyTaikoGreen animation.json id 1 plays (R17 measured this ramp but only used
-- it to re-arm the Fuji clock; the fade itself stayed on the fan value). Fail-soft: with
-- the table missing, success_fade_alpha() returns nil and every caller falls back to the
-- engine animation's attribute (ROUND 8 rule).
local SBG = dg_anim.safe("anim/result_bg_success")
local SBG_F0 = SBG:label("success") or 5
local success_bg = dg_anim.safe("anim/result_bg_success")
local SBG_F0 = success_bg:label("success") or 5
local function success_fade_alpha(t0, now_ms)
if not t0 then return nil end
return SBG:get("crear_1P_l", SBG_F0 + (now_ms - t0) * 0.06, "a")
return success_bg:get("crear_1P_l", SBG_F0 + (now_ms - t0) * 0.06, "a")
end
-- ROUND 53 (r53-anim-lumen-sweep): 魂 fire + rainbow gauge, per-frame from the cabinet's
-- own `enso_normal/enso/tamashiigage/tamashii_gage.nulm`.
-- * `anim/fever_fire` = sprite 58 (`fire` MC): fever_start f1..9 (alpha fade-in + growth),
-- fever loop f10..33 (8 flame cels x 3 frames = 400 ms), fever_end f34..44. The flame
-- cels are the display-list leaves #40/#44/../#56 -> texture frames 0..7.
-- * The disc blink (#35 bright / #42 plain at depth 1) and the sheen (#39 at depth 2,
-- alpha 0.6016 -- the old hand value was 0.5) are INTERMITTENT display-list presence,
-- which the exporter flattens into one continuous row span (known pipeline limitation,
-- recorded in ANIM_COVERAGE.md). The parity rule below -- bright disc + sheen on for
-- (frame - fever) % 4 < 2 -- was verified against the dense dump on every frame of the
-- f10..f33 loop.
-- * `anim/gauge_rainbow` = sprite 31 (41 f): each rainbow cel holds alpha 1 while the next
-- fades in linearly over 5 frames = 83.33 ms/step, 666.7 ms full cycle. The old hand
-- period was 75 ms.
-- The clip's fever_start GROWTH channel (sx 0.14 -> 0.80, bottom-anchored via its ty) is
-- in the table but deliberately NOT applied yet: draw_texture scales around the frame's
-- top-left here and the anchor compensation is unverified -- same class as best_score's
-- unapplied sy (C-res-4). Fail-soft throughout: with a table missing these return nil and
-- the callers fall back to the pre-R53 engine-animation ladder.
local FIRE = dg_anim.safe("anim/fever_fire")
local FIRE_LEAVES = { { "#40@0", 0 }, { "#44@0", 1 }, { "#46@0", 2 }, { "#48@0", 3 },
{ "#50@0", 4 }, { "#52@0", 5 }, { "#54@0", 6 }, { "#56@0", 7 } }
@@ -170,9 +105,7 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.is_2p = is_2p
self.has_2p = has_2p
self.gauge_length = gauge_length
-- Fill target in cells: gauge_length is the engine's 87-unit display value
-- (points / max * 87), so cells = points * 50 / max, floored like the HUD gauge.
self.gauge_cells = math.floor(gauge_length * GAUGE_CELLS / 87 + 1e-4)
self.gauge_cells = math.floor(gauge_length * GAUGE_CELLS / 87 + 1e-4) -- 87-unit display -> 50-cell gauge
self.difficulty = difficulty
self.selected_difficulty = selected_difficulty
self.crown_type = crown_type
@@ -183,13 +116,6 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.mod_random = mod_random
self.mod_speed = mod_speed
-- ROUND 48 (r48-soulgauge-chn-port): the reference build norma semantics replace the
-- Green-era clear_start {52,60,69} table. The clear line is TamashiiNorm
-- (easy 6000 / normal 7000 / HARD 7000 / oni 8000 of the raw 0..10000
-- soul, corpus-constant per tier) — hard shares normal's norma and thus
-- the segment-35 ("_normal") clear-zone art, not Green's hard-at-79%
-- layout. gauge_length arrives as the 87-unit display value
-- (soul / 10000 * 87), so the raw soul is recovered for the test.
local ART = { [0] = 0, [1] = 1, [2] = 1, [3] = 2 }
local NORMA = { [0] = 6000, [1] = 7000, [2] = 7000, [3] = 8000 }
local d = math.min(math.max(difficulty, 0), 3)
@@ -227,34 +153,13 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.crown_at = nil
self.msg_at = nil -- MoveResultAction: message bubble + success backdrop + Don pose
self.skipped = false
-- ROUND 19: read by C++ every frame (`ResultPlayer::reveal_end_ms`, ENGINE_BINDINGS
-- ROUND 19). 0/nil = the reveal is still running, so a drum press is a SKIP; once it
-- carries the MoveResultAction timestamp a press is an ADVANCE instead. That is the
-- cabinet's own boundary: MoveResultAction is the last thing ResultMain's `Crown`
-- state does, and every state after it (Medal / WaitEffectEnd / WaitNextScene)
-- reveals nothing and ignores isEffectSkip_.
self.reveal_end_ms = 0
-- ROUND 15: the engine's ResultPlayer reads these three fields once, right after the
-- script loads (src/objects/result/player.cpp). `count_up_instant` turns off the
-- per-digit ScoreAnimator roll: on the cabinet, ScoreBase.lua only ever calls
-- SkipCounter(), so every row LANDS on its final value -- no digit ever spins. The two
-- cadences are ScoreBase.lua's own `timer_ > 50` / `timer_ > 100`.
self.count_up_instant = true
self.count_up_row_ms = 50 * F
self.count_up_score_ms = 100 * F
-- ROUND 52 (r52-lua-divergence-fixes): the arcade count-up SE names, read
-- by the same engine block (result/player.cpp update_score_animation).
-- ScoreBase.lua:191 plays count_stop_c per landed row and :174/:198
-- don_big on the total score, BOTH gated `this.player_ == 1` (1P-only, in
-- duet too); the engine used to hardcode "num_up"/"don" for every seat.
self.count_up_row_ms = 50 * FRAME_MS
self.count_up_score_ms = 100 * FRAME_MS
self.count_up_row_sound = "count_stop"
self.count_up_score_sound = "don_big"
self.count_up_sound_1p_only = true
-- ROUND 52: seat suffixes for the arcade result cues.
-- count_up_loop / atmos_clear: _c solo, _l/_r in duet
-- (ResultMain.lua:459-464 / :1153-1162)
-- achieve_tamashii: _l for 1P even solo, _r for 2P
-- (ResultMain.lua:1086-1088: idx==1 && entryType 0|2 -> _l)
self.seat_c = has_2p and (is_2p and "_r" or "_l") or "_c"
self.seat_lr = is_2p and "_r" or "_l"
self.count_loop_on = false
@@ -268,8 +173,7 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.crown_msg_obj = nil
self.high_score_obj = nil
self.fade_in_bottom = tex.get_animation(1, "result") -- success bg fade -- ROUND 53:
-- fallback only; the drawn alpha is sampled from anim/result_bg_success (150 ms)
self.fade_in_bottom = tex.get_animation(1, "result") -- success bg fade (fallback)
self.tamashii_fire = tex.get_animation(20, "result")
-- background success overlay (1P full / 2P halves)
@@ -289,11 +193,6 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.t_score_shinuchi_lbl = tex.get_texture("result/score/score_shinuchi")
self.t_score_num = tex.get_texture("result/score/score_num")
self.t_donback = tex.get_texture("result/bottom/chara_0") -- frames: 0 = 1P red, 1 = 2P blue
-- NO control guide: the arcade's control_guide_L/R (common/control_guide_xs) is created by
-- ResultReward:InitControlGuide with SetVisible(false) and is only turned on inside the
-- reward-choice phase (StateId.RewardChoicePanel_, SetupMenFuchiKR("select","decide")), then
-- FadeOut() once the choice is made. A plain result screen -- which is all YataiDON has --
-- never shows it. `result/bottom/chara_center` is an 8x8 blank. See MAPPING.md ROUND 6.
-- gauge (HUD bakes, drawn at 0.7)
self.t_gauge_unfilled = tex.get_texture("result/gauge/" .. p .. "_unfilled" .. self.diff_str)
@@ -336,9 +235,6 @@ function ResultPlayer:update(current_ms, fade_in_finished,
score, good, ok, bad, max_combo, total_drumroll, is_skipped)
self.fade_in_finished_flag = fade_in_finished
self.now_ms = current_ms
-- ROUND 15: record when each row first shows a value, so the digits can play the
-- cabinet's pop. With `count_up_instant` a row goes "" -> final in one frame, which
-- is exactly the cabinet's SkipCounter().
local prev = { self.good, self.ok, self.bad, self.total_drumroll, self.max_combo }
self.score, self.good, self.ok, self.bad = score, good, ok, bad
self.max_combo, self.total_drumroll = max_combo, total_drumroll
@@ -354,10 +250,6 @@ function ResultPlayer:update(current_ms, fade_in_finished,
if is_skipped and not self.skipped then
self.skipped = true
self.gauge_shown = self.gauge_cells
-- ROUND 52: skip cuts the count-up loop (UpdateTamashiiGage's
-- isEffectSkip branch does TSound:StopSlot, ResultMain.lua:1069) and
-- lands everything on one don_big (ScoreBase UpdateScoreDisplay's
-- skip branch, :174 -- 1P-only; the per-row count_stop_c never fires).
if self.count_loop_on then
self.count_loop_on = false
if audio.stop_sound then audio.stop_sound("count_up_loop" .. self.seat_c) end
@@ -382,33 +274,20 @@ function ResultPlayer:update(current_ms, fade_in_finished,
if self.skipped then
self.gauge_shown = target
elseif elapsed > 0 then
-- ROUND 52: WaitUpdateTamashiiGage ends here -- start the arcade
-- count-up loop (ResultMain.lua:459-464, PlaySlot count_up_loop_*;
-- looped by the new engine binding, cut below when the fill lands).
if not self.count_loop_on and not self.gauge_done then
self.count_loop_on = true
audio.play_sound("count_up_loop" .. self.seat_c, "sound", true)
end
self.gauge_shown = math.min(target, math.floor(elapsed / GAUGE_MS_UNIT))
-- ROUND 52: the fill reaching the rainbow top plays
-- achieve_tamashii (ResultMain.lua:1086-1088; non-skip path only).
if not self.achieve_played and self.gauge_state == 2 and self.gauge_shown >= GAUGE_CELLS then
self.achieve_played = true
audio.play_sound("achieve_tamashii" .. self.seat_lr, "sound")
end
end
-- ROUND 49 (r49-lua-chn-crosscheck): completion may only be declared after the
-- WaitUpdateTamashiiGage dwell has actually elapsed (ResultMain.lua:457 `timer_ > 100`
-- runs UNCONDITIONALLY, even when the gauge has nothing to fill). Without the
-- `elapsed > 0` arm, a total-miss run (gauge_length == 0) satisfied
-- `gauge_shown >= target` on the very first gauge frame and armed score_delay
-- 833 ms early, skipping the state the cabinet always sits through.
if self.gauge_shown >= target and not self.gauge_done
and (self.skipped or elapsed > 0) then
self.gauge_done = true
self.score_delay = current_ms + (self.skipped and 0 or WAIT_SCORE_MS)
-- ROUND 52: StopSlot the moment the fill lands on its target
-- (ResultMain.lua:1094 `current == g_tamashiiGage` -> StopSlot).
if self.count_loop_on then
self.count_loop_on = false
if audio.stop_sound then audio.stop_sound("count_up_loop" .. self.seat_c) end
@@ -420,12 +299,8 @@ function ResultPlayer:update(current_ms, fade_in_finished,
end
end
self.fade_in_bottom:update(current_ms)
-- ROUND 11: publish the success cross-fade so ResultBackground / ResultFadeIn can swap the
-- live backdrop layers (gold sky, cyan pulsing Fuji, clear cloud bars) under the fireworks
-- overlay this same animation fades in. 1P only -- the arcade 2P backdrop has no clouds.
-- publish the success cross-fade for the live backdrop (1P only)
if not self.has_2p then
-- ROUND 53: sampled cabinet ramp (150 ms, f5..f14) wins; engine anim 1 (333 ms) is
-- the fail-soft fallback.
local sfa = success_fade_alpha(self.success_fade_t0, current_ms)
or self.fade_in_bottom.attribute
__hss_result_bg_clear = (self.gauge_state >= 1) and sfa or 0.0
@@ -433,10 +308,6 @@ function ResultPlayer:update(current_ms, fade_in_finished,
if self.score_delay ~= nil and current_ms > self.score_delay and not self.fade_in_started then
self.fade_in_started = true
-- UpdateScore (500 f). ScoreRank is skipped outright -- YataiDON has no 粋/雅/極 rank,
-- and ResultMain skips that whole state when g_scoreRank == 0. Then the Crown state:
-- the crown flies in at its start, MoveResultAction fires 150 f later. With no crown
-- (a failed run) the Crown state is skipped too, so MoveResultAction is immediate.
self.crown_at = current_ms + SCORE_TO_CROWN
self.msg_at = self.crown_at + ((self.gauge_state >= 1) and CROWN_TO_ACTION or 0)
end
@@ -446,9 +317,6 @@ function ResultPlayer:update(current_ms, fade_in_finished,
self.msg_at = math.min(self.msg_at or current_ms, current_ms)
end
-- ScoreRank state (arcade: UpdateScore -> ScoreRank -> Crown), entered when the
-- score would otherwise hand over to the crown: the total is only known by then.
-- Skipped at rank 0 and on a skipped run, as the arcade skips the state.
if self.crown_at and current_ms >= self.crown_at and not self.rank_done then
self.rank_done = true
local rank = (not self.skipped) and rank_of(math.floor(tonumber(self.score) or 0)) or 0
@@ -472,8 +340,6 @@ function ResultPlayer:update(current_ms, fade_in_finished,
-- Crown state entry: StartCrownDisplay (crownMc_ GotoAndPlay "start" + crown_<colour>_c)
if self.crown_at and current_ms >= self.crown_at
and self.gauge_state >= 1 and not self.crown_obj then
-- ROUND 52: has_2p passed through so the crown SE picks its arcade
-- seat suffix (_c solo, _l/_r duet -- ScoreBase.lua:322-342).
self.crown_obj = ResultCrown.new(self.crown_type, self.is_2p, self.has_2p)
if self.crown_type == 1 or self.crown_type == 2 then
audio.play_sound("full_combo_voice_" .. tostring(self.player_num) .. "p", "voice")
@@ -481,10 +347,6 @@ function ResultPlayer:update(current_ms, fade_in_finished,
end
if self.crown_obj then self.crown_obj:update(current_ms) end
-- MoveResultAction: DisplayMessage + MoveBg(StartSuccessEffect) on the same beat.
-- The arcade message state is chosen from the SOUL GAUGE, not the crown:
-- perfect = gauge at max, success = cleared, near = failed but gauge > 30 % of max,
-- miss = everything else (ResultMain.DisplayMessage).
if self.msg_at and current_ms >= self.msg_at and not self.crown_msg_obj then
local frame
if self.gauge_state == 2 then frame = 3 -- perfect
@@ -496,21 +358,13 @@ function ResultPlayer:update(current_ms, fade_in_finished,
[2] = "clear_voice_", [3] = "max_clear_voice_" }
audio.play_sound(voices[frame] .. tostring(self.player_num) .. "p", "voice")
if self.gauge_state >= 1 then
self.fade_in_bottom:start() -- MoveBg: success background
-- ROUND 53: arm the sampled f5..f14 cross-fade on the same beat
self.fade_in_bottom:start() -- success background
self.success_fade_t0 = current_ms
-- ROUND 52: the cleared-run ambience rides the same
-- MoveResultAction beat as the glad voice (ResultMain.lua
-- MoveDonResult :1153-1162 -- atmos_clear_c solo, _l/_r duet;
-- a failed run plays only the sad voice, no atmos).
audio.play_sound("atmos_clear" .. self.seat_c, "sound")
end
end
if self.crown_msg_obj then self.crown_msg_obj:update(current_ms) end
-- ROUND 19: MoveResultAction has fired -> the reveal is over. Publish the beat
-- itself (not `current_ms`), so the two cabinet dwells C++ adds on top of it are
-- measured from the same instant the arcade measures them from.
if self.reveal_end_ms == 0 and self.msg_at and current_ms >= self.msg_at then
self.reveal_end_ms = self.msg_at
end
@@ -525,8 +379,7 @@ end
function ResultPlayer:draw()
local p_idx = self.is_2p and 1 or 0
-- success background (ResultBg "success") — ROUND 53: sampled f5..f14 ramp, engine
-- anim 1 as fail-soft fallback (see success_fade_alpha above).
-- success background cross-fade
local success_a = success_fade_alpha(self.success_fade_t0, tex.get_current_ms())
or self.fade_in_bottom.attribute
if self.gauge_state >= 1 and success_a > 0 and self.t_success then
@@ -559,43 +412,15 @@ function ResultPlayer:draw()
end
-- ROUND 9 (engine binding `ResultPlayer:chara_pos` -> {x, y, scale}, see
-- ENGINE_BINDINGS.md). Two arcade facts the engine used to make unreachable:
-- * the result Don is placed at scale 1.12 (don1p placeholder (237,773));
-- Chara3D's own scale was a fixed 650 with no multiplier.
-- * don_R sits at (1682,787), the mirror of don_L across the screen centre.
-- ResultPlayer::draw only ever added screen_height/2 for 2P, which put the
-- 2P Don 540 px below the 1P one - off the bottom of the screen.
-- Chara3D's visible centre is (x,y) + (5,-130)*scale and the feet land another
-- 125*scale below it, so holding the round-6 tuned centre (210, 805) / feet 935
-- at scale 1.12 gives (204, 941).
local CHARA_SCALE = 1.12
local CHARA_X = 204
local CHARA_Y = 941
local CHARA_X_2P = 1649 -- arcade don_R 1682 minus the same -27 the 1P value carries
local CHARA_X_2P = 1649
function ResultPlayer:chara_pos()
return { self.is_2p and CHARA_X_2P or CHARA_X, CHARA_Y, CHARA_SCALE }
end
-- ROUND 17 (engine binding `ResultPlayer:nameplate_pos` -> {x, y, fade}, see
-- ENGINE_BINDINGS.md), the exact mirror of `chara_pos` above.
--
-- `result.nulm --sprite 319 --range 25,25` (`duet`) measured this round:
-- name_plate_L tx -754.0 ty +430 alpha 1
-- name_plate_R tx +753.2 ty +430 alpha 1
-- and at `use_left` (frame 5) only L is visible, at `use_right` (frame 15) only R
-- is -- so the cabinet's rule is SEAT-fixed: left board = left plate, right board
-- = right plate, both in duet. `Result2PScreen` builds player_1 on the left seat
-- and player_2 on the right, so this needs no join-order logic at all.
--
-- screen = (960,540) + (tx,ty) => L (206,970) R (1713.2,970)
-- our plate canvas is 408x96 with the MC-280 registration at canvas (204,48), so
-- the draw origin is centre - (204,48):
-- L (206-204, 970-48) = (2, 922) == skin_config `result_nameplate`
-- R (1713.2-204, 922) = (1509.2, 922) -> 1509, whole-pixel like every other
-- value in this skin (the plate art and its text are drawn 1:1, so a .2
-- destination would blur both -- ROUND 11 text-sharpness note).
local NAMEPLATE_X = 2
local NAMEPLATE_Y = 922
local NAMEPLATE_X_2P = 1509
@@ -609,19 +434,8 @@ function ResultPlayer:draw_gauge()
local p_idx = self.is_2p and 1 or 0
local scale = GAUGE_SCALE
-- ROUND 26 (r26-resultgaugebox): the gauge's own box/frame used to cross-fade in over
-- animation 17 (366 ms) starting only once WaitUpdateTamashiiGage begins, so it visibly
-- lagged behind the score board -- which pops to full opacity the instant Fadein's wipe
-- clears, with no fade of its own. Nothing in ResultMain.lua/ScoreBase.lua's documented
-- state machine (see the ROUND 15 comment above) puts a box-opacity ramp anywhere in
-- Fadein -> WaitUpdateTamashiiGage; the only delay that state actually has is on the FILL
-- (gauge_shown / GAUGE_MS_UNIT below). This animation was inherited unaudited from
-- PyTaikoGreen (whose own player.lua still runs it) and has no cabinet counterpart, so the
-- box itself now snaps to full opacity together with the rest of the board.
local base_fade = 1.0
local gl_int = self.gauge_shown
-- ROUND 48: the clear-zone position follows the norma art tier
-- (hard = normal's segment-35 zone), see the init block above.
local clear_pts = { 30, 35, 40 } -- easy / normal+hard / oni norma cells
local clear_pt = clear_pts[self.art_index + 1]
local u = GAUGE_UNIT
@@ -631,9 +445,6 @@ function ResultPlayer:draw_gauge()
end
if self.gauge_state == 2 and gl_int >= GAUGE_CELLS then
-- ROUND 53: rainbow cycle sampled from anim/gauge_rainbow (sprite 31): 83.33 ms per
-- step, base cel at 1 with the next fading in over it — the cabinet's own display
-- list, drawn leaf by leaf in depth order. Fallback = pre-R53 75 ms hand cycle.
local drew_rainbow = false
if self.t_gauge_rainbow and RBW.tracks and RBW_LEN > 0 and self.rainbow_start_ms >= 0 then
local f = math.fmod((tex.get_current_ms() - self.rainbow_start_ms) * 0.06, RBW_LEN)
@@ -662,11 +473,6 @@ function ResultPlayer:draw_gauge()
local n = math.min(gl_int, clear_pt - 1)
tex.draw_texture(self.t_gauge_bar, { scale = scale, x2 = (n - 1) * u, fade = base_fade, index = p_idx })
end
-- The transition piece is the first gold cell (the rounded cap of the clear zone).
-- Light it only when the run is cleared, like the HUD gauge (Gauge::get_is_clear):
-- the norma sits inside that cell (oni 8000 = cell 40.0 of 50, but a failed run at
-- 7800..7999 soul already shows 39 cells), so a cell-count test lit it a cell early
-- and a failed result grew a gold cell.
if gl_int >= clear_pt - 1 and self.gauge_state >= 1 and self.t_gauge_bar_clear_transition then
tex.draw_texture(self.t_gauge_bar_clear_transition,
{ scale = scale, x = (clear_pt - 1) * u, fade = base_fade, index = p_idx })
@@ -689,17 +495,10 @@ function ResultPlayer:draw_gauge()
end
local clear_idx = self.art_index + p_idx * 3
-- ROUND 48: a failed run never lights the クリア caption, even when the
-- final fill pixel-touches the clear zone (the reference build lights it on
-- IsNormaClear, i.e. gauge_state >= 1).
if gl_int >= clear_pt - 1 and self.gauge_state >= 1 then
if self.t_gauge_clear then
tex.draw_texture(self.t_gauge_clear, { scale = scale, fade = base_fade, index = clear_idx })
end
-- ROUND 53: flame cel + fade-in and the sheen duty cycle now come from
-- anim/fever_fire (sprite 58); see the fire_state block up top. ROUND 17's anchor
-- findings stand: the flame holds local (+43,-43) scale **0.800** through the
-- `fever` loop, which is why the drawn scale stays a constant 0.8 here.
local fire_sheen = nil
if self.gauge_state == 2 and gl_int >= GAUGE_CELLS and self.t_gauge_tamashii_fire then
local ff, fa2, sheen_a = fire_state(self.rainbow_start_ms, tex.get_current_ms())
@@ -708,7 +507,7 @@ function ResultPlayer:draw_gauge()
tex.draw_texture(self.t_gauge_tamashii_fire,
{ frame = ff, scale = 0.8 * scale, fade = fa2 * base_fade, index = p_idx })
else
-- fail-soft: pre-R53 engine ladder (animation 20, 8 x 50 ms)
-- fail-soft: engine animation ladder
local fire_f = math.floor(self.tamashii_fire.attribute)
tex.draw_texture(self.t_gauge_tamashii_fire,
{ frame = fire_f, scale = 0.8 * scale, fade = base_fade, index = p_idx })
@@ -742,19 +541,12 @@ function ResultPlayer:draw_gauge()
end
function ResultPlayer:draw_score_info()
-- Index order == the arcade's own ScoreBase.lua numMc_[1..5]:
-- good 良 (y 314), ok 可 (376), bad 不可 (438), dramroll 連打数 (500), maxcombo 最大コンボ数 (562)
-- measured off result/result.nulm `score_base_L` at `use_left` (good_num_mc … max_combo_num_mc).
-- ROUND 15 bug fix: `result/score/texture.json` had judge_num[3].y = 538 and [4].y = 476,
-- i.e. slot 3 was the BOTTOM (最大コンボ数) row -- so since round 5, when this array was
-- reordered to fill top-down, the drumroll count was printed on the 最大コンボ数 line and
-- the max combo on the 連打数 line. texture.json now runs 290/352/414/476/538 (and
-- 1798 + the same five for 2P), which is top-down, matching this array and the label block.
-- judge rows top-down: good, ok, bad, drumroll, max combo
local scores = { self.good, self.ok, self.bad, self.total_drumroll, self.max_combo }
local p_idx = self.is_2p and 1 or 0
for j, s in ipairs(scores) do
if s ~= "" and self.t_judge_num then
local pop = pop_scale(DGJ, DGJ_TRACK, DGJ_F0, DGJ_F1,
local pop = pop_scale(digit_judge, DGJ_TRACK, DGJ_F0, DGJ_F1,
self.row_land_ms[j], self.now_ms or 0)
local n = math.floor(tonumber(s) or 0)
local str = tostring(n)
@@ -781,12 +573,11 @@ function ResultPlayer:draw_total_score()
end
if not self.fade_in_finished_flag then return end
if self.score ~= "" and self.t_score_num then
local pop = pop_scale(DGS, DGS_TRACK, DGS_F0, DGS_F1,
local pop = pop_scale(digit_score, DGS_TRACK, DGS_F0, DGS_F1,
self.score_land_ms, self.now_ms or 0)
local n = math.floor(tonumber(self.score) or 0)
local str = tostring(n)
-- arcade draws total_score_back_mc (all outlines) first, then total_score_mc (all fills) on top:
-- score_num frames 0..9 = outline layer, 10..19 = fill layer (see MAPPING.md ROUND 5)
-- score_num frames 0..9 outline, 10..19 fill
for layer = 0, 10, 10 do
for i = 1, #str do
local d = tonumber(string.sub(str, #str - i + 1, #str - i + 1)) or 0
+6 -255
View File
@@ -1,52 +1,14 @@
-- YataiDON-HSS — the 段位道場 shutter (doors + scrim + kanji stamp).
--
-- ROUND 23 RECONSTRUCTION. This file and its Graphics tree (`Graphics/**/dan_doors*`)
-- were confirmed missing from disk by ROUND 22 (see Graphics/dan_select/MAPPING.md
-- ROUND 22) — gone since at least a round-19-era scratchpad snapshot, silently, because
-- both call sites `pcall`-guard the `require`. This round rebuilt it from three sources
-- ONLY, per the standing "never invent data" rule:
-- 1. Graphics/dan_select/MAPPING.md ROUND 11 / 14 / 17 — every frame number, every
-- constant (CLOSE_FROM/TO, STAMP_FROM/TO, OPEN_FROM/TO, the kanji cell centres,
-- the se_daniodai_v12e/intro -> dan_intro sound history) is documented there.
-- 2. scratchpad/r17dn/patch_doors.py and patch_doors2.py — the FINAL, tested ROUND 17
-- function bodies (draw_frame/draw/draw_open/phases/frame_of/set_progress),
-- recovered verbatim (they were python string-patches against this exact file, so
-- their "old"/"new" blocks ARE the shipped Lua source at that point in history).
-- 3. Scripts/anim/dani_intro.lua — the exported the reference build track data itself, which was
-- NEVER deleted (only this consuming module was). Its rows for #1@3 (scrim),
-- #5@1/#5@2 (right/left door), #7@4/#9@5/#11@6/#13@7 (段/位/道/場) match the
-- MAPPING doc's frame numbers exactly, confirming the reconstruction.
--
-- What could NOT be recovered from those three sources and was therefore NOT invented:
-- nothing load-bearing turned out to be missing. `Graphics/song_select/dan_transition/
-- background.png` — the 2000x1080 atlas ROUND 11's "What ships" table describes
-- (door panel (0,0,1200,1080); kanji cells (1200,0)/(1600,0)/(1200,400)/(1600,400),
-- 400x400 each; scrim patch (1200,800,64,64)) — turned out to still be on disk, verified
-- this round by cropping all six regions and confirming non-transparent content in each
-- (see ROUND 23 section below for the exact bboxes measured). Only the *Lua module*
-- that reads it was gone, not the art. `Sounds/song_select/dan_intro.ogg` was also
-- still present, and `Graphics/song_select/animation.json` id 38's `duration` (2300 ms)
-- and comment ("Scripts/song_select/dan_doors.lua draws both halves") had ALSO survived
-- untouched — this file is the only piece that needed rebuilding.
local anim = require("anim/sampler")
local CLIP = anim.safe("anim/dani_intro")
local M = {}
-- Stage geometry, from the MAPPING doc's "arcade rig, decoded" table (ROUND 11).
local SCREEN_W, SCREEN_H = 1920, 1080
local DOOR_W = 1200 -- door panel local width (main__shape5)
local KANJI = 400 -- each kanji cell is 400x400 (main__shape7/9/11/13)
-- background.png atlas layout, ROUND 11 "What ships":
-- (0,0,1200,1080) door panel | (1200,0)/(1600,0)/(1200,400)/(1600,400) the four
-- kanji, 400x400 each | (1200,800,64,64) an opaque black patch for the scrim.
local SCRIM_SRC = { x = 1200, y = 800, width = 64, height = 64 }
-- kanji cell -> its arcade track name, in reading order 段位道場 (ROUND 11 table:
-- #7@4=段 centre(455,255), #9@5=位 centre(1420,255), #11@6=道 centre(455,765),
-- #13@7=場 centre(1420,765) — the (i<=2)/(i odd) fallback centres below mirror that).
local CELLS = {
{ track = "#7@4", sx = 1200, sy = 0 }, -- 段
{ track = "#9@5", sx = 1600, sy = 0 }, -- 位
@@ -54,50 +16,6 @@ local CELLS = {
{ track = "#13@7", sx = 1600, sy = 400 }, -- 場
}
-- Frame map.
--
-- ROUND 64 (r64-danselect-fidelity) RE-DERIVATION. The report was 「段位道場的
-- entry動畫有誤,重新查」, and re-reading the cabinet's own script found two
-- real errors in ROUND 14/17's map. The export itself is NOT at fault (the
-- ROUND 61 trap): `anim/dani_intro.lua` was dumped `--range 0,262 --all` and
-- `lumen_anim_dump loading_dani_intro.nulm --list` reports sprite 15 with
-- exactly 263 frames and labels init@0 / in@5 / stop@248 / out@253 / end@262,
-- so every leg below is covered by real data.
--
-- WHO PLAYS IT, and WHERE. `script_lua/dani_select/dani_select_introduction_
-- main.lua` loads `lumen/000_default/enso_dani/loading_dani/loading_dani_intro`
-- as `introduction_instance` -- root depth 16 of dani_select.nulm, i.e. the
-- TOP-most layer of the DAN_SELECT scene -- and `dani_select_all.lua`'s state
-- machine plays it from the `IntroductionMain` state, which runs AFTER
-- Begin/Loading/SetUp/StartWait, i.e. after the whole screen has loaded:
-- PlayIntroduction : TSound:Play("se_daniodai_v12e","intro")
-- mc_main.main:GotoAndPlay("in") -- frame 5
-- Is_EndAnimation : if not IsPlay() then GotoAndPlay("out") end
-- -> DaniSelectMain: timer_:StartCount(); name_plate_:SetVisible(true)
-- So on the cabinet the ENTIRE 4.2 s runs on the dan-select screen and nothing
-- of it runs on song select. Our engine has to cover its own (blocking,
-- multi-second) DAN_SELECT load, which the cabinet does in a state with
-- nothing on screen, so the song-select half stays -- but it must stop
-- inventing motion the cabinet does not have.
--
-- ERROR 1 (fixed here): the close leg played the DOOR track BACKWARDS,
-- f200 -> f182, so our shutter slid in from off-screen. There is no such move
-- anywhere in the clip. `#5@1`/`#5@2` are PLACED at f35 already shut (tx 960,
-- sx +-1) and do not move again until f187. What actually happens over f0..62
-- is the SCRIM: `#1@3` is alpha 1 (a full-screen black) from f0 to f55 and
-- eases to 0.5 by f62 -- the screen is simply black, then 50 % black over the
-- shut shutter. The doors never move on the way in. The close leg is therefore
-- now a pure OPACITY ramp over the f62 pose: invented (we are fading out of a
-- live wheel, the cabinet is fading out of nothing), but it no longer
-- fabricates a door slide.
--
-- ERROR 2 (fixed here): the DAN_SELECT half stopped at f203. It is not movie
-- tail past there: `#3@0` -- the dan-dojo BACKGROUND, the same night-torch art
-- as Graphics/dan_select/global/bg.png, verified by rendering
-- `lumen_shape_dump -m -r 15 -f 215` -- pushes in from scale 1.0 to 1.25 over
-- f187..253 and only then cross-fades out over f253..262 (`out`). Cutting at
-- f203 threw away the push-in and the cross-fade, which is the half of the
-- entry animation the player actually reads as "entering the dojo".
local CLOSE_POSE = 62 -- shut doors, scrim already eased to 0.5
local STAMP_FROM = 62 -- the arcade's scrim has just settled to 0.5
local STAMP_TO = 182 -- all four landed, before the fade
@@ -105,19 +23,11 @@ local OPEN_FROM = 182 -- kanji fade (182-185), doors part (187-200),
local OPEN_TO = 262 -- scrim clears (203), bg push-in + `out` fade
local MS_PER_FRAME = 1000.0 / 60.0
-- INVENTED, and the only invented number left in the map: the wheel has to go
-- somewhere. 300 ms is ROUND 17's value, kept so Graphics/song_select/
-- animation.json id 38's `duration` (2300.0) still equals CLOSE_MS + STAMP_MS
-- exactly and no timing outside this file moves.
local CLOSE_MS = 300.0
local STAMP_MS = (STAMP_TO - STAMP_FROM) * MS_PER_FRAME -- 120 frames = 2000 ms
local OPEN_MS = (OPEN_TO - OPEN_FROM) * MS_PER_FRAME -- 80 frames = 1333 ms
-- Total ms of the SONG_SELECT half. MUST agree with Graphics/song_select/animation.json
-- id 38 `duration` (2300.0, confirmed still on disk this round: exactly CLOSE_MS+STAMP_MS).
-- The engine hands the real progress in via set_progress() every frame on a build with
-- `SongSelect:draw_top`, so this constant is only load-bearing on an engine WITHOUT it
-- (the self-clocked fallback in M.progress() below).
local DURATION_MS = 2300.0
local function duration_ms() return DURATION_MS end
@@ -127,7 +37,6 @@ local function clamp01(v) return math.max(0.0, math.min(1.0, v)) end
local function clip() return CLIP end
-- ── the atlas ────────────────────────────────────────────────────────────────
local _id, _id_tried
local function tex_id()
if not _id_tried then
@@ -137,9 +46,6 @@ local function tex_id()
return _id
end
-- ROUND 64 -- the DAN_SELECT screen's own background, which is also the intro
-- clip's `#3@0`. Resident only while DAN_SELECT is live; nil everywhere else,
-- which is exactly the gate the push-in needs.
local _bg, _bg_tried
local function bg_id()
if not _bg_tried then
@@ -149,13 +55,6 @@ local function bg_id()
return _bg
end
-- ── fallbacks, reached only if anim/dani_intro.lua is itself missing/malformed
-- (anim.safe hands back the NULL clip) — the closed-form end states the frame map
-- above degrades to, so a renamed/corrupted data table cannot take the screen down.
-- ROUND 64: the shut pose is the ONLY pose the fallback can honestly assert --
-- the doors never travel except over f187..200, whose curve is exactly the data
-- this fallback exists because we lost. Right door (#5@1) tx 960; left door
-- (#5@2) tx 960 with sx -1, i.e. its drawn left edge at 960 - 1200 = -240.
local function fallback_door()
return -240, 960, DOOR_W, 0, FB_SCRIM
end
@@ -165,21 +64,12 @@ local function fallback_kanji(i, shown)
return 1.0, 1.0 -- scale 1, alpha 1 — the landed pose, nothing finer is recoverable
end
-- ── sound. ROUND 14b: resident name is the file's STEM, not its skin-relative path
-- (`audio.cpp` prefixes only files inside a screen subfolder); the corrected cue is
-- `Sounds/song_select/dan_intro.ogg`, still present on disk.
local function fire_sfx()
if M.sfx_done then return end
if audio and audio.play_sound then audio.play_sound("dan_intro", "sound") end
M.sfx_done = true
end
-- ROUND 17: two segments only — close then stamp — and the half ENDS parked on
-- STAMP_TO. A `duration` shorter than close+stamp still compresses the two
-- proportionally (that is what keeps the old 1100 ms value producing the round-11
-- look); anything longer parks the leftover on STAMP_TO, which is the cabinet's
-- own hold pose. The shipped 2300 ms leaves none, because our hold is now the
-- real DAN_SELECT load rather than invented dead time.
local function phases()
local d = duration_ms()
local move = CLOSE_MS + STAMP_MS
@@ -190,8 +80,6 @@ local function phases()
end
-- u (0..1 across our transition) -> arcade frame.
-- ROUND 64: the close leg no longer travels; it holds CLOSE_POSE and only the
-- overall opacity moves (see the frame-map comment).
local function frame_of(u)
local close_u, stamp_u = phases()
if u <= close_u then
@@ -203,13 +91,6 @@ local function frame_of(u)
return STAMP_TO -- parked: doors shut, kanji up
end
-- ── the shared frame painter ────────────────────────────────────────────────
-- ROUND 17: both halves paint the SAME arcade frame map, so the painting is one
-- function taking an arcade frame.
-- ROUND 64: the second argument is now an OVERALL OPACITY (nil = 1), not a
-- "closedness". The cabinet's shutter has exactly one pose on the way in -- shut
-- -- so our invented close is a fade of that pose rather than a fabricated
-- slide, and the fade has to reach the doors too, not only the scrim.
local function draw_frame(f, op)
local id = tex_id()
if not id then return false end
@@ -217,14 +98,6 @@ local function draw_frame(f, op)
op = (op == nil) and 1.0 or clamp01(op)
if op <= 0 then return true end
-- ROUND 64 -- the background, `#3@0`, arcade depth 0: BELOW both doors and
-- the scrim. It is the dan-dojo night art (= Graphics/dan_select/global/
-- bg.png), placed at f35 and holding scale 1 until f187, then pushing in to
-- 1.25 by f253 and cross-fading out over f253..262. Only the DAN_SELECT
-- half ever sees it (during the stamp the opaque shutter covers it, and on
-- SONG_SELECT the texture is not even resident) -- bg_id() is nil there and
-- the whole block is skipped, so nothing regresses on an engine or screen
-- without it.
local btx, bty, bsx, _, ba = c:at("#3@0", f)
if btx and ba and ba > 0 then
local bid = bg_id()
@@ -237,12 +110,7 @@ local function draw_frame(f, op)
end
end
-- doors -----------------------------------------------------------------
-- A track row is (tx, ty, sx, sy, a) in the arcade's 1920x1080 stage. The
-- panel's local box is x 0..1200, y 0..1080 with its origin at the placement,
-- so screen span = tx .. tx + 1200*sx and top = ty - 540. sx < 0 IS the
-- mirror (the left door is the same shape flipped), which is why the left
-- door's x is tx + 1200*sx, i.e. tx minus the drawn width.
-- doors
local lx, rx, dw, dtop, scrim
local rtx, rty, rsx = c:at("#5@1", f)
local ltx, _, lsx = c:at("#5@2", f)
@@ -269,14 +137,12 @@ local function draw_frame(f, op)
fade = scrim * op })
end
-- kanji ------------------------------------------------------------------
-- kanji
for i = 1, #CELLS do
local cell = CELLS[i]
local cx, cy, s, a
local tx, ty, sx, _, al = c:at(cell.track, f)
-- A track's rows start at the frame the glyph is PLACED; the sampler
-- clamps below that, so an explicit gate is needed or 段 would be on
-- screen from frame 0.
-- glyph tracks start at their placed frame, so gate explicitly
local first = c:has(cell.track) and c.tracks[cell.track][1][1] or nil
if tx and first and f >= first then
cx, cy, s, a = tx, ty, sx, al
@@ -301,13 +167,6 @@ local function draw_frame(f, op)
return true
end
-- ── arming / progress ───────────────────────────────────────────────────────
-- `M.arm(active)` is called every SONG_SELECT frame from `draw_overlays(state)`
-- with `state == STATE_DAN_SELECTED`; it is the ONLY signal on an engine without
-- `draw_top` (the self-clocked fallback below). `M.set_progress(p)` is called
-- every SONG_SELECT frame from `draw_top` with `DanTransition::progress()` (0..1,
-- or a negative value when no transition is running) on an engine that has it —
-- when present it always wins over the fallback clock.
M.armed = false
M.engine_u = nil
M.has_draw_top = false
@@ -329,13 +188,6 @@ function M.set_progress(p)
M.engine_u = p
else
M.engine_u = nil
-- ROUND 17 -- TEARDOWN. This is called from SongSelect:draw_top on EVERY
-- song-select frame, with -1 when no transition is running, so it is the one
-- place guaranteed to run when the rig must not be on screen. Clearing the
-- DAN_SELECT half's state here is what stops a stale overlay surviving a
-- back-out of DAN_SELECT (or an aborted transition) and re-appearing over the
-- wheel: whatever half-open pose was left is dropped the first frame the
-- song-select screen is live again.
M.armed_open = false
M.open_t0 = nil
end
@@ -357,13 +209,6 @@ function M.progress()
return u
end
-- ── half 1: SONG_SELECT (close + stamp) ─────────────────────────────────────
-- Returns true when it drew anything.
-- The rig only belongs to SONG_SELECT (close leg) and DAN_SELECT (open leg).
-- Teardown used to rely on SongSelect:draw_top calling set_progress(-1), which never
-- happens when the player backs out of DAN_SELECT straight to TITLE (Esc): the last
-- engine_u stayed put and the global Indicator fallback kept painting the shut doors
-- over the title screen. Drop every bit of state as soon as we are anywhere else.
local function off_screen()
local scr = tex.current_screen and tex.current_screen() or nil
if scr == nil or scr == "SONG_SELECT" or scr == "DAN_SELECT" then return false end
@@ -385,27 +230,8 @@ function M.draw()
local close_u = phases()
local f = frame_of(u)
-- ROUND 64: over the invented close leg the pose is frozen at CLOSE_POSE and
-- only the opacity ramps in; from there on the clip's own alphas rule.
if not draw_frame(f, (u <= close_u) and (u / close_u) or nil) then return false end
-- ROUND 17: arm the DAN_SELECT half. Set on every frame the rig is live, so
-- an abort (the transition never completing) leaves it armed only until the
-- next set_progress(-1), which is the very next song-select frame.
--
-- ROUND 23 FIX: guard the re-arm to first-arm only. `Scripts/global/
-- indicator.lua`'s own fallback slot (`Indicator:draw()`, kept for an engine
-- without draw_top) calls `dan_doors.draw()` on EVERY screen `Indicator` is
-- used on, DAN_SELECT included -- and once `set_progress` has been called at
-- least once, `M.progress()`'s engine-driven branch returns the LAST
-- `engine_u` it was given (SONG_SELECT stops calling `set_progress` the
-- instant the screen changes to DAN_SELECT, so that value is never updated
-- to nil again). The fallback therefore kept calling `M.draw()` -> this
-- branch -> `M.open_t0 = nil` on every single DAN_SELECT frame, which reset
-- `M.draw_open()`'s wall clock before it could ever accumulate past 0 --
-- the doors visibly never opened, verified as stuck at the shut/stamped
-- pose for 6.5+ seconds in `scratchpad/r23dn/open_probe/`. Only re-arming
-- when NOT already armed lets `draw_open()`'s own clock run once started.
if not M.armed_open then
M.armed_open = true
M.open_t0 = nil
@@ -414,83 +240,13 @@ function M.draw()
return true
end
-- Pure layering helper for `Scripts/global/indicator.lua`'s fallback slot: true
-- iff `M.draw()` already painted this exact engine frame, so the fallback call
-- (kept only for an engine without the real SONG_SELECT top slot) never draws
-- the rig twice in one frame.
-- true iff M.draw() already painted this exact engine frame
function M.drawn_this_frame()
return M._drawn_ms ~= nil and M._drawn_ms == tex.get_current_ms()
end
-- ── half 2: DAN_SELECT (the reveal) ─────────────────────────────────────────
-- Called from `Indicator:draw_top()`, the top-most slot on DAN_SELECT (added
-- round 17: DanSelectScreen::draw() calls it after the coin overlay, the
-- DAN_SELECT plate and the allnet chip). ROUND 64: plays arcade f182..262 -- the
-- WHOLE remaining clip, not the f182..203 stub -- on a plain wall clock at 1:1
-- with the cabinet, then tears itself down so nothing survives into the screen.
-- Returns true while it is drawing. 80 frames = 1333 ms, which is the window
-- DanSelectScreen::tick_timer waits out before StartCount()ing the credit clock
-- (the cabinet's IntroductionMain -> DaniSelectMain hand-off).
--
-- The clock starts on the FIRST frame this runs rather than at the screen
-- change, which is deliberate: DAN_SELECT's init can block the render loop for
-- a couple of seconds and anchoring to the change would make the doors jump
-- straight to open on the first frame the loop comes back. Starting here means
-- the open plays in full, right after the load.
--
-- ROUND 86 (r86) -- THE SECOND ENTRY PATH, and the defect it caused.
--
-- Reported: 「段位道場的進入動畫消失了」. ROUND 83 promoted 段位道場 to a mode board on
-- the ENTRY screen, so the dojo is now reachable as ENTRY -> DAN_SELECT with no
-- SONG_SELECT in between. `M.armed_open` is set in exactly ONE place -- `M.draw()`
-- above, the SONG_SELECT half -- so on that route it was never set and this function
-- returned false on every frame. Nothing about the movie was broken; it was never armed.
--
-- What the cabinet does on ITS mode-board route is play the WHOLE clip here:
-- `dani_select_all.lua`'s IntroductionMain runs `mc_main.main:GotoAndPlay("in")` and `in`
-- is frame 5, through stop@248 / out@253..262. f5..55 is the full-black scrim (`#1@3`
-- alpha 1.0), i.e. the blackout the player reads as leaving the entry screen, then the
-- kanji stamp, then the reveal. So:
-- __hss_dan_intro == "full" (engine: previous screen was ENTRY) -> arm here, f5..262
-- __hss_dan_intro == "open" (previous screen was SONG_SELECT) -> ROUND 64 f182..262,
-- armed by M.draw()
-- The engine (`DanSelectScreen::on_screen_start`) writes this global once per screen
-- entry; we consume it. A skin/engine pair without it keeps the old behaviour exactly,
-- because `open_from` then falls back to OPEN_FROM.
local INTRO_FROM = 5 -- clip label `in`
-- ── ROUND 95 (r95-danselect-concurrent-intro) ────────────────────────────────
--
-- Reported: 「他現在比較像先載入dan, 才載入段位道場動畫,你要邊放動畫邊讓他同時載入
-- dan」. The engine used to run its (blocking, multi-second) course scan BEFORE
-- arming this movie, so the movie whose entire purpose is to hide the load played
-- after it. `DanSelectScreen::on_screen_start` now arms first and scans on a
-- worker, which means the ribbon behind these doors may still be empty when the
-- clip reaches the reveal.
--
-- f182 (`OPEN_FROM`) is the first frame that shows the player anything behind the
-- doors, so it is the clamp frame -- the exact analogue of ROUND 85's f113 clamp
-- one frame short of the genre-board swap. While `__hss_dan_ready` is false the
-- clip is held on f182 (doors shut, scrim up); the frame it turns true the clock
-- is RE-ANCHORED so f == 182 at that instant and the 80-frame / 1333.3 ms reveal
-- runs from there at 1:1.
--
-- scan wins the race -> nothing is ever clamped, the clip plays f5..f262 (or
-- f182..f262) in full. It is NOT cut short: the cabinet's
-- `Is_EndAnimation` is `mc_main.main:IsPlay() == false`,
-- purely the clip (dani_select_introduction_main.lua).
-- clip wins the race -> hold on f182 for as long as it takes. The cabinet's
-- equivalent wait is `DaniSelectMain`'s per-frame
-- `if daniselectdani_.is_setUpAllOk == true` gate before
-- `timer_:StartCount()` (dani_select_all.lua:126-128).
-- The CLAMP itself is ours, not the cabinet's -- the
-- cabinet's `CreateDaniData` reads a resident datatable
-- and is instant, so it never has anything to hide.
--
-- `DanSelectScreen::tick_timer` derives the credit clock's start from the same
-- three numbers, so the clock and the movie cannot disagree.
-- OLD ENGINE: `__hss_dan_ready` nil is treated as ready, so a skin on a
-- pre-ROUND-95 engine behaves exactly as ROUND 86.
local REVEAL_F = OPEN_FROM -- 182
function M.draw_open()
@@ -503,9 +259,6 @@ function M.draw_open()
M.open_t0 = nil
M.open_held = false
M.open_from = INTRO_FROM
-- the cabinet's PlayIntroduction fires TSound "se_daniodai_v12e"/"intro" at
-- the top of IntroductionMain; on the SONG_SELECT route M.draw() already fired
-- it during the close leg and `M.sfx_done` swallows this call.
fire_sfx()
elseif req == "open" then
M.open_from = OPEN_FROM
@@ -517,8 +270,6 @@ function M.draw_open()
local from = M.open_from or OPEN_FROM
local f = from + (now - M.open_t0) / MS_PER_FRAME
-- ROUND 95: clamp at f182 while the courses are still loading, re-anchor on
-- f182 the frame they land. See the block above.
local ready = (__hss_dan_ready ~= false)
if not ready then
if f > REVEAL_F then
@@ -530,7 +281,7 @@ function M.draw_open()
M.open_t0 = now - (REVEAL_F - from) * MS_PER_FRAME
f = REVEAL_F
end
M.open_f = f -- for the r95 trace (YATAIDON_R95_TRACE)
M.open_f = f
if f >= OPEN_TO then
M.armed_open = false
File diff suppressed because it is too large. Load diff
-58
View File
@@ -1,58 +0,0 @@
-- Ported from src/objects/title/attract_camera.cpp. Everything but the physical
-- webcam capture + its textured quad (the `camera.*` bindings, script.cpp) lives here:
-- the background/live-icon/live-text draw, the live-icon animation, and the
-- 30s-fallback finished-timer.
AttractCamera = {}
AttractCamera.__index = AttractCamera
function AttractCamera.new()
local self = setmetatable({}, AttractCamera)
self.start_ms = tex.get_current_ms()
self.finished = false
camera.open()
audio.play_sound("camera", "attract_mode")
self.live_icon_texture_change = tex.get_animation(36, "title")
self.live_icon_texture_change:start()
local ok, timer = pcall(tex.get_animation, 37, "title")
self.scene_timer = ok and timer or nil
if self.scene_timer then self.scene_timer:start() end
self.t_background = tex.get_texture("title/camera/background")
self.t_live_icon = tex.get_texture("title/camera/live_icon")
self.t_live_text = tex.get_texture("title/camera/live_text")
self.viewport = tex.get_skin_config("attract_camera_viewport")
return self
end
function AttractCamera:update(current_ms)
camera.update()
self.live_icon_texture_change:update(current_ms)
if self.scene_timer then
self.scene_timer:update(current_ms)
self.finished = self.scene_timer.is_finished
else
self.finished = (current_ms - self.start_ms >= 30000)
end
end
function AttractCamera:draw()
tex.draw_texture(self.t_background)
if self.viewport then
camera.draw(self.viewport.x, self.viewport.y, self.viewport.width, self.viewport.height)
end
tex.draw_texture(self.t_live_icon, { frame = math.floor(self.live_icon_texture_change.attribute) })
tex.draw_texture(self.t_live_text)
end
function AttractCamera:is_finished()
return self.finished
end
function AttractCamera:close()
camera.close()
end
-40
View File
@@ -1,40 +0,0 @@
-- Owns everything drawn during TitleState::ATTRACT_CAMERA: the camera
-- background, the two bana adverts, and the cloud. Replaces the separate
-- AttractCamera/BanaAdvertisement/CameraCloud C++ wrappers with one.
require("title/attract_camera")
require("title/camera_cloud")
require("title/bana_advert")
AttractScene = {}
AttractScene.__index = AttractScene
function AttractScene.new()
local self = setmetatable({}, AttractScene)
self.attract_camera = AttractCamera.new()
self.camera_cloud = CameraCloud.new()
self.bana_advert_1 = BanaAdvertisement.new()
self.bana_advert_2 = BanaAdvertisement.new()
return self
end
function AttractScene:update(current_ms)
self.attract_camera:update(current_ms)
self.camera_cloud:update(current_ms)
self.bana_advert_1:update(current_ms)
self.bana_advert_2:update(current_ms)
end
function AttractScene:draw()
self.attract_camera:draw()
self.camera_cloud:draw()
self.bana_advert_1:draw(33, 136)
self.bana_advert_2:draw(1023, 136)
end
function AttractScene:is_finished()
return self.attract_camera:is_finished()
end
function AttractScene:close()
self.attract_camera:close()
end
-322
View File
@@ -1,322 +0,0 @@
-- Ported 1:1 from src/objects/title/bana_advert.cpp (pure visual, no input).
BanaAdvertisement = {}
BanaAdvertisement.__index = BanaAdvertisement
function BanaAdvertisement.new()
local self = setmetatable({}, BanaAdvertisement)
self.current_state = "bana_options"
self.fade_in = tex.get_animation(15, "title")
self.fade_in:start()
self.resize_1 = tex.get_animation(16, "title")
self.resize_2 = tex.get_animation(16, "title")
self.bana_rotate = tex.get_animation(17, "title")
self.bana_move = tex.get_animation(18, "title")
self.touch_fade = tex.get_animation(19, "title")
self.touch_move_up = tex.get_animation(20, "title")
self.touch_move_down = tex.get_animation(22, "title")
self.touch_resize = tex.get_animation(21, "title")
self.touch_fade_out = tex.get_animation(23, "title")
self.donchan_move = tex.get_animation(24, "title")
self.get_text_scale_down = tex.get_animation(25, "title")
self.get_text_scale_up = tex.get_animation(26, "title")
self.get_text_scale_down_2 = tex.get_animation(27, "title")
self.get_text_fade_in = tex.get_animation(28, "title")
self.get_chara_bounce_up = tex.get_animation(29, "title")
self.get_chara_bounce_down = tex.get_animation(30, "title")
self.get_chara_1_bounce_up = tex.get_animation(29, "title")
self.get_chara_1_bounce_down = tex.get_animation(30, "title")
self.get_fade_out = tex.get_animation(31, "title")
self.t_background = tex.get_texture("title/camera/bana_advert_background")
self.t_outline = tex.get_texture("title/camera/bana_advert_outline")
self.t_outline_info = tex.get_texture_info("camera", "bana_advert_outline")
self.t_reader = tex.get_texture("title/camera/reader")
self.t_tap = tex.get_texture("title/camera/tap")
self.t_touch = tex.get_texture("title/camera/touch")
self.t_touch_gleam = tex.get_texture("title/camera/touch_gleam")
self.t_card_or_phone_bg = tex.get_texture("title/camera/card_or_phone_bg")
self.t_card_or_phone_card = tex.get_texture("title/camera/card_or_phone_card")
self.t_card_or_phone_phone = tex.get_texture("title/camera/card_or_phone_phone")
self.t_banapassport = tex.get_texture("title/camera/banapassport")
self.t_or = tex.get_texture("title/camera/or")
self.t_chara_0 = tex.get_texture("title/camera/chara_0")
self.t_osaifukeitai = tex.get_texture("title/camera/osaifukeitai")
self.t_rewards_donchan = tex.get_texture("title/camera/rewards_donchan")
self.t_rewards = tex.get_texture("title/camera/rewards")
self.t_rewards_info = tex.get_texture_info("camera", "rewards")
self.t_rewards_top_text = tex.get_texture("title/camera/rewards_top_text")
self.t_rewards_bottom_text = tex.get_texture("title/camera/rewards_bottom_text")
self.t_get_text = tex.get_texture("title/camera/get_text")
self.t_get_chara_0 = tex.get_texture("title/camera/get_chara_0")
self.t_get_chara_1 = tex.get_texture("title/camera/get_chara_1")
self.steps_cursor = 0
self.loop_counter = 0
self.timer = 0
self.rewards_angle = 0.0
self.rewards_last_ms = nil
self.chara_1_timer = nil
return self
end
function BanaAdvertisement:update(current_ms)
self.fade_in:update(current_ms)
self.resize_1:update(current_ms)
self.resize_2:update(current_ms)
self.bana_rotate:update(current_ms)
self.bana_move:update(current_ms)
self.touch_fade:update(current_ms)
self.touch_move_up:update(current_ms)
self.touch_move_down:update(current_ms)
self.touch_resize:update(current_ms)
self.touch_fade_out:update(current_ms)
self.donchan_move:update(current_ms)
self.get_text_scale_down:update(current_ms)
self.get_text_scale_up:update(current_ms)
self.get_text_scale_down_2:update(current_ms)
self.get_text_fade_in:update(current_ms)
self.get_chara_bounce_up:update(current_ms)
self.get_chara_bounce_down:update(current_ms)
self.get_chara_1_bounce_up:update(current_ms)
self.get_chara_1_bounce_down:update(current_ms)
self.get_fade_out:update(current_ms)
if self.current_state == "rewards" then
if self.rewards_last_ms then
self.rewards_angle = self.rewards_angle + (current_ms - self.rewards_last_ms) * 0.002
end
self.rewards_last_ms = current_ms
end
local sc = self.steps_cursor
if sc == 0 then
if self.fade_in.is_finished then
self.resize_1:start()
self.steps_cursor = 1
end
elseif sc == 1 then
if self.resize_1.is_finished then
self.resize_2:start()
self.steps_cursor = 2
end
elseif sc == 2 then
if self.resize_2.is_finished then
self.resize_1:start()
self.steps_cursor = 3
end
elseif sc == 3 then
if self.resize_1.is_finished then
self.resize_2:start()
self.steps_cursor = 4
end
elseif sc == 4 then
if self.resize_2.is_finished then
self.timer = current_ms
self.steps_cursor = 5
end
elseif sc == 5 then
if current_ms - self.timer >= 300 then
self.current_state = "tap_to_reader"
self.steps_cursor = 6
self.bana_rotate:start()
self.bana_move:start()
end
elseif sc == 6 then
if self.bana_rotate.is_finished then
self.touch_fade:start()
self.touch_move_up:start()
self.touch_move_down:start()
self.touch_resize:start()
self.steps_cursor = 7
end
elseif sc == 7 then
if self.touch_move_down.is_finished then
self.timer = current_ms
self.steps_cursor = 8
end
elseif sc == 8 then
if current_ms - self.timer >= 300 then
self.touch_fade_out:start()
self.steps_cursor = 9
end
elseif sc == 9 then
if self.touch_fade_out.is_finished then
self.bana_rotate:start()
self.bana_move:start()
self.touch_fade:reset()
self.touch_move_up:reset()
self.touch_move_down:reset()
self.touch_resize:reset()
self.touch_fade_out:reset()
self.steps_cursor = 10
end
elseif sc == 10 then
if self.bana_rotate.is_finished then
self.touch_fade:start()
self.touch_move_up:start()
self.touch_move_down:start()
self.touch_resize:start()
self.steps_cursor = 11
end
elseif sc == 11 then
if self.touch_move_down.is_finished then
self.timer = current_ms
self.steps_cursor = 12
end
elseif sc == 12 then
if current_ms - self.timer >= 600 then
self.current_state = "rewards"
self.steps_cursor = 13
self.loop_counter = 0
self.donchan_move:start()
end
elseif sc == 13 then
if self.loop_counter == 6 then
self.steps_cursor = 14
self.loop_counter = 0
self.resize_1:start()
self.current_state = "card_or_phone"
end
if self.donchan_move.is_finished then
self.donchan_move:restart()
self.loop_counter = self.loop_counter + 1
end
elseif sc == 14 then
if self.loop_counter == 6 then
self.current_state = "get"
self.get_text_scale_down:start()
self.get_text_scale_up:start()
self.get_text_scale_down_2:start()
self.get_text_fade_in:start()
self.get_chara_bounce_up:start()
self.get_fade_out:start()
self.chara_1_timer = current_ms
self.steps_cursor = 15
end
if self.resize_1.is_finished then
self.resize_2:start()
self.resize_1:reset()
end
if self.resize_2.is_finished then
self.resize_1:start()
self.resize_2:reset()
self.loop_counter = self.loop_counter + 1
end
elseif sc == 15 then
if self.get_fade_out.is_finished then
self.current_state = "bana_options"
self.fade_in:start()
self.steps_cursor = 0
end
if self.get_chara_bounce_up.is_finished and not self.get_chara_bounce_down.is_started then
self.get_chara_bounce_down:start()
elseif self.get_chara_bounce_down.is_finished then
self.get_chara_bounce_down:reset()
self.get_chara_bounce_up:reset()
self.get_chara_bounce_up:start()
end
if self.chara_1_timer and current_ms - self.chara_1_timer >= 116 then
self.get_chara_1_bounce_up:start()
self.chara_1_timer = nil
end
if self.get_chara_1_bounce_up.is_finished and not self.get_chara_1_bounce_down.is_started then
self.get_chara_1_bounce_down:start()
elseif self.get_chara_1_bounce_down.is_finished then
self.get_chara_1_bounce_down:reset()
self.get_chara_1_bounce_up:reset()
self.get_chara_1_bounce_up:start()
end
end
end
function BanaAdvertisement:draw(x, y)
tex.draw_texture(self.t_background, { x = x, y = y })
local ox, oy = tex.get_draw_offset()
local sx = self.t_outline_info.x[1] + ox + x
local sy = self.t_outline_info.y[1] + oy + y
local sw = self.t_outline_info.x2[1] * 2
local sh = self.t_outline_info.y2[1] - 13 * tex.get_screen_scale()
tex.begin_scissor(sx, sy, sw, sh)
if self.current_state == "tap_to_reader" then
tex.draw_texture(self.t_reader, { x = x, y = y })
local fade = self.touch_fade_out.is_started and self.touch_fade_out.attribute or self.touch_fade.attribute
tex.draw_texture(self.t_tap, {
x = x + self.bana_move.attribute, y = y + self.bana_move.attribute,
rotation = self.bana_rotate.attribute, fade = self.touch_fade_out.attribute,
})
tex.draw_texture(self.t_touch, {
x = x, y = y + self.touch_move_up.attribute + self.touch_move_down.attribute,
y2 = self.touch_resize.attribute, fade = fade,
})
local gleam_fade = self.touch_fade.attribute > 0 and self.touch_fade_out.attribute or 0
tex.draw_texture(self.t_touch_gleam, { x = x, y = y, y2 = self.touch_resize.attribute, fade = gleam_fade })
elseif self.current_state == "card_or_phone" then
tex.draw_texture(self.t_card_or_phone_bg, { x = x, y = y })
tex.draw_texture(self.t_card_or_phone_card, { scale = self.resize_1.attribute, center = true, x = x, y = y })
tex.draw_texture(self.t_card_or_phone_phone, { scale = self.resize_2.attribute, center = true, x = x, y = y })
elseif self.current_state == "bana_options" then
tex.draw_texture(self.t_banapassport, { scale = self.resize_1.attribute, center = true, x = x, y = y, fade = self.fade_in.attribute })
tex.draw_texture(self.t_or, { x = x, y = y, fade = self.fade_in.attribute })
tex.draw_texture(self.t_chara_0, { x = x, y = y, fade = self.fade_in.attribute })
tex.draw_texture(self.t_osaifukeitai, { scale = self.resize_2.attribute, center = true, x = x, y = y, fade = self.fade_in.attribute })
elseif self.current_state == "rewards" then
tex.draw_texture(self.t_rewards_donchan, { x = x, y = y + self.donchan_move.attribute })
local base_x = self.t_rewards_info.x[1]
local base_y = self.t_rewards_info.y[1]
for i = 0, 3 do
local pos = (self.rewards_angle + i) % 4.0
local slot_from = math.floor(pos) % 4
local slot_to = (slot_from + 1) % 4
local t = pos - math.floor(pos)
local dx = (1.0 - t) * self.t_rewards_info.x[slot_from + 1] + t * self.t_rewards_info.x[slot_to + 1] - base_x
local dy = (1.0 - t) * self.t_rewards_info.y[slot_from + 1] + t * self.t_rewards_info.y[slot_to + 1] - base_y
tex.draw_texture(self.t_rewards, { frame = i, x = x + dx, y = y + dy, index = 0 })
end
tex.draw_texture(self.t_rewards_top_text, { x = x, y = y })
tex.draw_texture(self.t_rewards_bottom_text, { x = x, y = y })
elseif self.current_state == "get" then
local get_text_scale
if self.get_text_scale_up.is_finished then
get_text_scale = self.get_text_scale_down_2.attribute
elseif self.get_text_scale_down.is_finished then
get_text_scale = self.get_text_scale_up.attribute
else
get_text_scale = self.get_text_scale_down.attribute
end
tex.draw_texture(self.t_get_text, {
scale = get_text_scale, center = true, x = x, y = y,
fade = math.min(self.get_text_fade_in.attribute, self.get_fade_out.attribute),
})
tex.draw_texture(self.t_get_chara_0, {
x = x, y = y + self.get_chara_bounce_up.attribute + self.get_chara_bounce_down.attribute,
fade = self.get_fade_out.attribute,
})
tex.draw_texture(self.t_get_chara_1, {
x = x, y = y + self.get_chara_1_bounce_up.attribute + self.get_chara_1_bounce_down.attribute,
fade = self.get_fade_out.attribute,
})
end
tex.end_scissor()
tex.draw_texture(self.t_outline, { x = x, y = y, index = 0 })
tex.draw_texture(self.t_outline, { mirror = "horizontal", x = x, y = y, index = 1 })
end
-43
View File
@@ -1,43 +0,0 @@
-- Ported 1:1 from src/objects/title/camera_cloud.cpp (pure visual, no input).
CameraCloud = {}
CameraCloud.__index = CameraCloud
function CameraCloud.new()
local self = setmetatable({}, CameraCloud)
self.fade_in = tex.get_animation(32, "title")
self.fade_in:start()
self.text_fade = tex.get_animation(33, "title")
self.text_fade:start()
self.move_up = tex.get_animation(34, "title")
self.move_up:start()
self.breathing = tex.get_animation(35, "title")
self.breathing:start()
self.t_cloud = tex.get_texture("title/camera/camera_cloud")
self.t_cloud_text_1 = tex.get_texture("title/camera/camera_cloud_text_1")
self.t_cloud_text_2 = tex.get_texture("title/camera/camera_cloud_text_2")
return self
end
function CameraCloud:update(current_ms)
self.fade_in:update(current_ms)
self.text_fade:update(current_ms)
self.move_up:update(current_ms)
self.breathing:update(current_ms)
end
function CameraCloud:draw()
tex.draw_texture(self.t_cloud, {
scale = self.breathing.attribute, center = true, y = self.move_up.attribute, fade = self.fade_in.attribute,
})
tex.draw_texture(self.t_cloud_text_1, {
scale = self.breathing.attribute, center = true, y = self.move_up.attribute,
fade = math.min(self.fade_in.attribute, 1 - self.text_fade.attribute),
})
tex.draw_texture(self.t_cloud_text_2, {
scale = self.breathing.attribute, center = true, y = self.move_up.attribute,
fade = math.min(self.fade_in.attribute, self.text_fade.attribute),
})
end
-4
View File
@@ -1,7 +1,3 @@
-- Ported 1:1 from src/objects/title/warning_screen.cpp (pure visual, no input).
-- WarningX/WarningBachiHit/WarningCharacters/Board are private helpers used
-- only by WarningScreen below (no longer exposed to sandbox fixtures).
local WarningX = {}
WarningX.__index = WarningX
Binary file not shown.
+67 -70
View File
@@ -2616,7 +2616,7 @@ d21dc53600bab83b80352bad38cd598d216c6e1279a4d54a0a085ad82ad1430d *./Graphics/res
684c22481d2a0e4bb2d6dd67b5df87650e081edfeadffcc70c377c393334c06d *./Graphics/result/score/score_shinuchi_en.png
1c160a1a14016ea3a5f761b040db433420476363224011d615613af660e7d0e1 *./Graphics/result/score/score_shinuchi_ja.png
89c4d6afafb8010ec4da0b9f72f10f677ec8c87677fcb3d2c9edde923a5b5f9f *./Graphics/result/score/texture.json
958b570bbdc822cccda619c5e3cc4c227751770287d0eead55950fa0ab2a3cbb *./Graphics/skin_config.json
95f69fb8de9d61a26ba20c2f4d32a5382e193fb006d6e1c0e8bbd8935d045223 *./Graphics/skin_config.json
0dce4781f045f0cd197797f76f0914af71e748d1cd661b44c760a479fa91a71c *./Graphics/song_select/animation.json
27e427b29774e04dc739b1c58a7205565de88cea6370876b623f8e13a7106b5c *./Graphics/song_select/box/back_graphic.png
b74432db46ec6d7ec080fbee6df6029c1451e243d30813fcf597a21704de4e74 *./Graphics/song_select/box/background/0.png
@@ -3096,24 +3096,24 @@ ca76d7a601b94e231072367097d7d0cfbe474603da87d5b05daf4cea784e7b6a *./Scripts/anim
e214a10342fb8f517b5d178c4778ed9160208d415f0b314f6f3faa881a3b4558 *./Scripts/anim/result_digit_judge.lua
41e457294ba042ef208b04c363b298b9626ad8ff8f949e88191d46ae924d1412 *./Scripts/anim/result_digit_score.lua
d1b3668d1dbfff23223765a4e617d41aa8cdf1012805bcb2bb739ddac4a2d94b *./Scripts/anim/result_msg.lua
aa4f7cb046e9e57237b538cc625228a2b887b2a4d91eca2ed7d532f8c648dae7 *./Scripts/anim/sampler.lua
734cb56b0ee748ce73aba7b3215830975866c83fcda8ef823f0e478c18ea066a *./Scripts/anim/sampler.lua
8dc28f07a72fac80115bbfce242e77b911b243e8c3d44d814baa2f053f143b95 *./Scripts/anim/skip_card.lua
224a891f4886bae975459092284df233f20e576dc80a208b378ed4428e8b66e2 *./Scripts/anim/skip_panel.lua
2e3d65444a943cd9d9934d85e329d47083fec9ca46a12b641d15338338cbddc1 *./Scripts/anim/song_board.lua
e08f82230e9e8f8c00deed9a7d0d0eaba6933021b9960b782a8b77eca3200b06 *./Scripts/anim/song_board_ura.lua
eb2bb15e87cc83d66b320e0e4cbcbd2d5385881f36eca27fe73740439055ef6c *./Scripts/anim/sort_cursor.lua
5789c13c7958010f2878f9a0b119a50af4f362e754dc06ca3683ce742d507d25 *./Scripts/anim/title_idle.lua
24e7afe3e93935774c14aebce3e635970ec045952224ea5246633ff05b37e3af *./Scripts/anim/title_idle_map.lua
34df97052c186c9588ff6a3c339501318c5e30ab9eecb97d1271c400ab9cfba1 *./Scripts/anim/title_idle_map.lua
66797bb60d6bacde5d882136a7dee9cda2f3e02282dd2ebaf2ee37346db14f70 *./Scripts/anim/wheel_decide.lua
4e61d41a6f50228b0d320290a99c7f6658843936448c61aff89c5a1e86891121 *./Scripts/background/background.lua
28f9069e2d499c9e216affc301a236ca365f2b16c84c70731b112da13f1d5ec4 *./Scripts/background/bg_collabs/a3.lua
85ba2770510e0c36ce4101dfff3d1e98e2268ee4cdbf43c5bb6397af9c2fc86f *./Scripts/background/bg_collabs/animal.lua
d9a2ba6bd0d2cb068c7f52503aa2cf481fa3f924c68164489c8a8a72fea42e6c *./Scripts/background/bg_collabs/buttoburst.lua
9079c8d288c9df1c123ff9c2c20907a367dc254be082d79936fdfb72e2a5a610 *./Scripts/background/bg_collabs/dan.lua
b3a7fd11edd028211c3b672719f3e1a7038dcf9e9abcda52cdaf9d30fc24475c *./Scripts/background/bg_collabs/funassyi.lua
bc7caa9155ab97fa2df21830088e6a04d506334af66e877024e7132e0d73871a *./Scripts/background/bg_collabs/imas.lua
4338723711a37bb9242203021259944c0b2105a8d2f943054f8aa7c5942d0481 *./Scripts/background/bg_collabs/imas_sidem.lua
cc25d797b97dbbfae3c8446e35adc0024e10b2dead57888980ff056ed94cc515 *./Scripts/background/bg_collabs/oshiri.lua
4bb649198612be96372f7f5e0cea56d0a43767b6d0140973feecc82c3688f1d8 *./Scripts/background/background.lua
fb8a2c66f9d119b5fee54026fa33aea52142033f297e13e914078cba249684e8 *./Scripts/background/bg_collabs/a3.lua
e5a938d840b8e28078d480ef0b0bd8533cefa6364b8ba75ea06523977493ad97 *./Scripts/background/bg_collabs/animal.lua
fb11c421ef506db47385896e2479d5de7640f4494a5815e12af29eff0682fe29 *./Scripts/background/bg_collabs/buttoburst.lua
59580ffa9de229cb372ab0b91f7bca7e4bfd22b79d78a50cf48ff815db342a95 *./Scripts/background/bg_collabs/dan.lua
2f02aeda60c74b77df3b83f3a09e2e5fa0e7c74eb9b9367bf3005a2be22943d5 *./Scripts/background/bg_collabs/funassyi.lua
f5e289024f8c2ea0f2b367a78117618a8a7a6afcc4f249bea7ea8279a5d645ac *./Scripts/background/bg_collabs/imas.lua
87e4daed71b454c9284457338052f63a6ba5b2fe7cff712fe2bb300fea1638a1 *./Scripts/background/bg_collabs/imas_sidem.lua
04494f7a1792a3dca2f1329e7e81d8064b13ce85fad4617050c9aa24f3768dc7 *./Scripts/background/bg_collabs/oshiri.lua
ea6d17fba190ecd605106d9b12e6fa79311ee637a00c0345f9c425f4286c1b27 *./Scripts/background/bg_collabs/practice.lua
a1b6594e92d4d6293b90545c37dc42040282e1d4a3c1bb91c92eaaf14ca0f13d *./Scripts/background/bg_collabs/tutorial.lua
4ff647ed2d5e89b4065ea3741c897c4e5f82847803b9716a4c2738d69662f932 *./Scripts/background/bg_data/bg_anim/bg_0.lua
@@ -3140,51 +3140,51 @@ b85248097bf701da651957088fa2944b1bce90d0be89b4ad00c5561a5cbcc284 *./Scripts/back
0bc106e4244904020f4e5a19a5268a8f0efad7105cf6375dd09f17c84177c211 *./Scripts/background/bg_data/donbg.lua
f1a738f092b1aa4aeb869dd11ec2b3bdf4fe0788f2d1796b622038d1df530ba9 *./Scripts/background/bg_data/fever_anim/f_1.lua
f154e429cb171d7ce72b029a4803ba74051ac23a3d7489f46fdcc45da72462e8 *./Scripts/background/bg_data/fever_anim/f_2.lua
06f2a3bd86d692bf21f143df0bf2f5d5e775c23b903e01de3f7812e4129a9c83 *./Scripts/background/bg_objects/arcade_gauge.lua
ae266319411a438bef00566dc89bc018da672e247c42e0b430f3fe063c0c754c *./Scripts/background/bg_objects/bg_anim.lua
9725a32eea28a8c1e6da04ecd0e908bce0424574099175677d36a3a2b1e913c5 *./Scripts/background/bg_objects/bg_fever.lua
3f7b7eea9e52b1f1c6f5fe4f0ed494daf315bc57b6dbd8d2711507a24be1bf99 *./Scripts/background/bg_objects/bg_normal.lua
ca8c1d80cf7a5e1633031f8e8d7c97149a12d92fb3ed0c4431c1a7413d91f5d9 *./Scripts/background/bg_objects/arcade_gauge.lua
5f4c5b958103331affd5ae56ae71c86d06ce1a161bbbf03989de2b186ba3040c *./Scripts/background/bg_objects/bg_anim.lua
907f308344ede7fe4d2e10b2158b9b4366f13eb6416c41f5c91d229f08e3aaa7 *./Scripts/background/bg_objects/bg_fever.lua
806804a5d2ec6b39993d7d53c0ca47e9123dc3b1dc535ca335c06233f1913f57 *./Scripts/background/bg_objects/bg_normal.lua
bc66cc31bfb3fac4483e2a1a9c04cf4d9c827ce4018c0858d0a7b162e91178f4 *./Scripts/background/bg_objects/chibi/0.lua
18417683592cb2da46d1e806a71d6c29b06d0ee695b5565a6c0d7ff5c0e4038d *./Scripts/background/bg_objects/chibi/13.lua
788e01149a0a7c52ba4242264df67680077ecfe9dbfcaaf3081018c5a127dfea *./Scripts/background/bg_objects/chibi/2.lua
4ff8913ee850bd04ef2e9ce80a9d09c8ad1f13edfa253b482c200df1d142fc01 *./Scripts/background/bg_objects/chibi/2.lua
2aa954c626b4ac0472501086b9c0bcf15e92529ca5fe97ccbe080c551512641e *./Scripts/background/bg_objects/chibi/4.lua
d63b0f392c96c2d4d6ad5043d84662674cf9995ba4d5e0370e8fe1fba30ef2b0 *./Scripts/background/bg_objects/chibi/5.lua
d16d6073ebcd811905f673989e0cfa87c76a6630d96987e42a702c854ed330de *./Scripts/background/bg_objects/chibi/8.lua
7971cf8c9e8599fed0891cc1de0626e1f0f653e3733b7ed8a3fdf1dc68950ef8 *./Scripts/background/bg_objects/chibi/bad.lua
d2b0af4afd579a7e897d90c3af7128f644b9cc45464b56cbae5fae2e0abcdaa6 *./Scripts/background/bg_objects/chibi/base.lua
a05635cfb1d2e7bf0f6ba59ddbc483890bac0a50e807d9af4d8656bccbf934ed *./Scripts/background/bg_objects/chibi.lua
91e96bfd91e7fd83d9fd5cd5faab2875792b6901706da98b92c4d4a7dc23841e *./Scripts/background/bg_objects/collab_dancer_model.lua
fbad1501c562ef9986cf12f58652ec58698565fa637362e24eaff679f8d72ae0 *./Scripts/background/bg_objects/dancer.lua
8ac408f441104d9e7dfc570996ded0986cbcf48c3641eb3663bbc63db3128180 *./Scripts/background/bg_objects/dan_info.lua
7ae42ec9cb485a1365747cb569b8f79d81497715a1a56e324e1a9a90afdffd82 *./Scripts/background/bg_objects/donbg/1.lua
1883a2f359908e1c65fc647a0ec0fa4c69a76884eee6cdb6cb8739de25bc9afc *./Scripts/background/bg_objects/donbg/2.lua
3675e742651ef664a9c92b8f5d821753f416af33d6049af737593ab5108d83c7 *./Scripts/background/bg_objects/donbg/3.lua
a4349fbd4c3d7d8a96f4d22dd43ad08b3966756f11e9912753208a440cd3ddfb *./Scripts/background/bg_objects/donbg/4.lua
c6dad415f43ae162eeda6ddc6cccafffadb98a842a2a7ad71288502f70d1a8a3 *./Scripts/background/bg_objects/donbg/5.lua
338aee029198cc84e037bcd7262ddc51cdceca4c403cde83106ca8619bfed01d *./Scripts/background/bg_objects/donbg/6.lua
65b47dc1588d3a31aaf85ddcde16111b2c2935c37793efe72c93bf9f79efd03e *./Scripts/background/bg_objects/donbg/anim_player.lua
21bb6944dfe95a0c1097d1207bc81a97b780415beaf2ee51aea5144496a0ca4d *./Scripts/background/bg_objects/donbg/base.lua
d398c362848a29b81603c57dbefac089a3b2f2aea5accb43bdc76a8695ec0e37 *./Scripts/background/bg_objects/chibi.lua
b0fcccefa79cc8a53b4581d944c4890fb6ae4c40efa0b1e96e008173c2229756 *./Scripts/background/bg_objects/collab_dancer_model.lua
fee3772a7144a4708038a0dfc1aa851a3daf4840f5a9d4ba69e8583a987100fd *./Scripts/background/bg_objects/dancer.lua
94670a496a7cc38c0f87a3dce12384aa5875e121e576afd9d955b834e5b142f3 *./Scripts/background/bg_objects/dan_info.lua
1ce550bce8d8d814b48847e564acb0bfae5062ecd6fa97ddc39914e8692f9569 *./Scripts/background/bg_objects/donbg/1.lua
1ce550bce8d8d814b48847e564acb0bfae5062ecd6fa97ddc39914e8692f9569 *./Scripts/background/bg_objects/donbg/2.lua
1ce550bce8d8d814b48847e564acb0bfae5062ecd6fa97ddc39914e8692f9569 *./Scripts/background/bg_objects/donbg/3.lua
537cf924dc02bdac7c48141d49ffd497b71ab7c3481148cd34b740e99118d414 *./Scripts/background/bg_objects/donbg/4.lua
1ce550bce8d8d814b48847e564acb0bfae5062ecd6fa97ddc39914e8692f9569 *./Scripts/background/bg_objects/donbg/5.lua
32428fe2b07e05a825f714db97f1bf2ac1714dcf9a6b01001221b48c33ca6040 *./Scripts/background/bg_objects/donbg/6.lua
e10c1e1c31774833a748fd646554dd35c2953b9e1a488d356443e56d25b80a10 *./Scripts/background/bg_objects/donbg/anim_player.lua
0772a9a29fd7cb04dbc52dcc32ddf2634a0994ab910afc833a3294abcf318471 *./Scripts/background/bg_objects/donbg/base.lua
63bd3b3d984c5c970fbe1a3b45aadb0eee6e1409a790889a3585b7e8fda44f4a *./Scripts/background/bg_objects/donbg.lua
92a4b04a004a0dc52c6384baad4595f70fb07004b28b386b86e2a20198922a92 *./Scripts/background/bg_objects/ending.lua
9c06d84f2343f80f726324341189881e77b8bf83a48323bf8882cff3b9c6c249 *./Scripts/background/bg_objects/enso_skip.lua
f12e43022e63defdeb24284f3ac16d696dd0d8170d0ac1d58bc583e40ca71eec *./Scripts/background/bg_objects/fever_anim.lua
995b5a7a0b3c0d5be564676d54c7c7c07ae63e1214abad4d90222d68cb32a8e0 *./Scripts/background/bg_objects/fever.lua
47343f73ac28526a6a9dfa585f3d0deca9b2be8ae81b4c58433133fd21829cb9 *./Scripts/background/bg_objects/footer.lua
16a13b0ad085f80bf4398ea3807c5f89df1678c5b79692988bf417b3ee68e157 *./Scripts/background/bg_objects/rank_badge.lua
85950d2bbc50b757f374afb674f705253bed8cb37c441f06d4c0be9de260b3ce *./Scripts/background/bg_objects/renda.lua
e74edba93a21b25327fd34f8cbb83181db74d4ebb7d18c16603fb3f24fe91e25 *./Scripts/background/bg_objects/ending.lua
2758cbad0f4279d2a82ad3fcbc8bc271671675c74e35957c81624d4c62760b2c *./Scripts/background/bg_objects/enso_skip.lua
997f33164cd87de37d415498346704f8af70c5150eafcd3902805b166fd21b9d *./Scripts/background/bg_objects/fever_anim.lua
1a01a12c661140da55f90e9d51c192d508e572feba1f169ad171a6bd99d09402 *./Scripts/background/bg_objects/fever.lua
232afea8689b47e2cf1218eb13a6dd0a4f459c4bd760035f66c55497aa441892 *./Scripts/background/bg_objects/footer.lua
5326e4a4f79705209165ccd71b97f8d1ea37546ed2b902948a0b39e75e8c09f1 *./Scripts/background/bg_objects/rank_badge.lua
2b138430504b8211eefc2b68df2d096416a1777185cfef6b088d414ceeaee1b7 *./Scripts/background/bg_objects/renda.lua
1ae115fb3bd3def72f7661160da68e7175aa9215f865ae49d236aa61ff9459d1 *./Scripts/background/dancer_data/dancer_0.lua
7772658a03732f632e8dc6b4d3d41386718908e6f9bc0e1c557e185b40c575e0 *./Scripts/background/dancer_data/dancer_1.lua
f204455c907d3e0e7affb89db3649a709a27d528d84cc58d51a7d8cfaa1d3deb *./Scripts/background/dancer_data/dancer_2.lua
4e17bf53815d1e586fdba5f3501d6f2d9935ad5604da871d2e7e89c87499aedd *./Scripts/background/dancer_data/dancer_3.lua
6e2b3c136611c688cdfbe44b0bbcb9d25c2ab647903246b07f2aef9baa6bab0b *./Scripts/background/dancer_data/dancer_4.lua
3a4f1f45bb54c4303dc97afcc6f67df831d6d6011a2264baed3d933c789d210e *./Scripts/background/dancer_data/dancer_5.lua
35beb6070f85692d3440ff694db0f13a9891250e08df97c4aefd94a44892ff2b *./Scripts/dan_select/dan_select.lua
88470fc661194770df409a221c377fbebe43af1df057027633c4eee4df5d09f3 *./Scripts/entry/box.lua
564178e298e38924b5e0930fa9f6f2247badd0f09ee93549de85f9c779460ee7 *./Scripts/entry/costume_menu.lua
d1038af8ae314279f77fce5d29f6b0b0d66321687b4499f7d43975b0171ccccd *./Scripts/entry/entry.lua
09f57050379e4d4ced4566a9bc2ee47075cdd57a71be2a75d74554a01f7c37e1 *./Scripts/entry/player.lua
d3a07103893cc61862c1e6617c4860b715688df553915237dcde0205fc3e9ac6 *./Scripts/game/clip_player.lua
6986617f4561a17b307b6535cbd6021c348336f74812f2b4d74fde5849c4b75c *./Scripts/game/combo_announce.lua
d1da72658ffef36b6e9245043adfc51e7824d771b403b878a589c54f296f7e91 *./Scripts/dan_select/dan_select.lua
020f929896e3f58c27bf54881afbbf1597ebd14951b5476f9a97a29fa670e2fc *./Scripts/entry/box.lua
0a941cea73b6a81f2c92219be139a43f5902067ce468ffd189520395b4739c91 *./Scripts/entry/costume_menu.lua
7b4fae9b61bfca46db0c6c0647b884f94edf495636271d14a99e544d472ac74b *./Scripts/entry/entry.lua
261b2a42b2d9b78326c7a9599852173a29954ac8f451a5fa9def9c18383cf08a *./Scripts/entry/player.lua
8a993e86d3d6e653d47bc6d9bae67e0648ad27ae89293959c26de04869ef0219 *./Scripts/game/clip_player.lua
13f6d670d95b75abcbbddeddcb442ab32e6c32afeefc8ebf1b8bc703b57a9827 *./Scripts/game/combo_announce.lua
5749994b631898c91e246110bb4b5e67aa3fdc395c4d59b49c4ee72a44e0043d *./Scripts/game/ending_anim/ending_clear.lua
33764ed8566bf003802816710a84f7bb01ef7a6ccd549b130d896f20c6e2a3b0 *./Scripts/game/ending_anim/ending_donderful.lua
1e5c509028b1a67acc2e40eed6222b51bc0e87a3d5e7eb8a4e3a3b1da4bc319b *./Scripts/game/ending_anim/ending_fail.lua
@@ -3194,31 +3194,27 @@ f4de8faceba1417bbdf0c408275f36b91022c93539178bd2d59018623bac759b *./Scripts/game
826c02cac90412a42722d0fa23e5f08878b00f32ca564607d08bd36fdecc0de4 *./Scripts/game/ending_anim/rank_hud_out.lua
6c885099876d77ea14d4588edb3de5105ecef7f8b83e2787a3bdb25918e5b920 *./Scripts/game/ending_anim/rank_result_kiwami.lua
02a2a424b68fce7b172807ce60df5c2b41121cb65f2edcc5a1e43b77702a4008 *./Scripts/game/ending_anim/rank_result.lua
66ad0c99972d48e4b613951bd3532cb59b12f164c7562ff52a01d8b26d882f10 *./Scripts/game_over/game_over.lua
b650428ef3ea6163096cb6b5a802efeb735d541da56c6461d8957fb0e1720004 *./Scripts/global/allnet_indicator.lua
db24b1fb9a9863a8aa36dad69e1e677b0a1b435fd43bb726cb21d77cac0514c1 *./Scripts/global/coin_overlay.lua
6b8ffa09d0cd5512514ead340b53f8f31ab8a09d81bd5a329267ebb8e87b1ac6 *./Scripts/global/entry_overlay.lua
09af1fe70f253447dba892a6f33cf2ce932a3cadad76389d2826c7b506250b7b *./Scripts/global/indicator.lua
04627f2e9e80f62611562206a56478926ba75026490ffae30d803f74112acca7 *./Scripts/global/nameplate.lua
9e59f03d1622d7091433a17135b72347feca12b3e91a338f81c144ef07323656 *./Scripts/global/result_transition.lua
4f27c03dd20ba5fea85497b2cb2ae02e27abc4f074438f395753240b5ee35b18 *./Scripts/global/timer.lua
517c126460bfe1c9bb449d0b5660e982cf6e702d0d6c845a4f50e3aae2424917 *./Scripts/global/transition.lua
53ac15fdeee0ffc98670e128ff3cddc0ba66e0e8cbd91ab440628b0fd7d804ff *./Scripts/game_over/game_over.lua
09b35b2ebdb82a2912f198b8d3c0ae55e7366550912c0d608ca86be38262f64f *./Scripts/global/allnet_indicator.lua
a24e5eb9ac289325facb2fe023b16a343ef1ecf7e57cbb988bb81e2514665cf3 *./Scripts/global/coin_overlay.lua
b8828910262c4b19f53589abdcf1f77faa0eb2d326eb524c6b46399548c260ae *./Scripts/global/entry_overlay.lua
5c9562b2bf9a3502b8a2e9b61cec4a187e45819a19f5e882e7548fe14d223032 *./Scripts/global/indicator.lua
8c51e468799275c017323400b092b77408db34213465813abbbad1bb446e004b *./Scripts/global/nameplate.lua
34645d4c05dcb582ddaad6e3d2e1075c476471c81e0ce68e33f7b0bbfa3c0f2b *./Scripts/global/result_transition.lua
97bc832add42c186d57dec5bc774b9c42424051124c6e754b0a32bcf711fff21 *./Scripts/global/timer.lua
a76dc303c9054e422e5939c20195ea2f9f2a6f1673ff52467c6a9da59f477f4a *./Scripts/global/transition.lua
12afbce120fa933331efd27d5eb2b2c87367ef5b3d761930ec7ab154e12343ba *./Scripts/result/bottom_characters.lua
513e990bf907afb909aa9b55bd87416d05b9b5dd3e5798b0050ac12b5bfacfd6 *./Scripts/result/dan_result_draw.lua
a98561bc8c5f6b3d308c43634b1e824c142b3a366df6d4fca47ae92c063b291a *./Scripts/result/high_score_indicator.lua
5c0bcfb76695c2788e8ff1e62b74446b1dc965c1e915278d0a28b1d8287cb54c *./Scripts/result/result_background.lua
3002fb2d7ad5dd57555063487b43571e1da33338fef97f1cafa26e0711273b6f *./Scripts/result/result_bg_layers.lua
e39e9ca5f3ba12beb260e69aae01ef4acc7e99e8423e21845d8303d673c93f5e *./Scripts/result/result_crown.lua
c8780c19e87b4af9eab9a888c1c53624972e9f36f66eb628d57dddfa1ebfde03 *./Scripts/result/result_crown_message.lua
d5684c6d2a5b2276dc64d24e450bf5b4590563939dd5a06b13d39bc14ab09790 *./Scripts/result/result_fade_in.lua
cb1911058f4b53a7f403f863762e3aae422cae053feba70cd21d85bab177c18b *./Scripts/result/result_player.lua
5158a9726a695ed946e3db458bb2226c396dee0b146ab8413613b37d9dc2e7fa *./Scripts/song_select/dan_doors.lua
3ecb9fbecc0ee8766f49342ab0fdca4c4e90e5c9635b1ecee421378506b7a19f *./Scripts/song_select/song_select.lua
cae2d2785e82671e4ff11df8d8dec83c8354e714df4f44bcb1938288f375d29a *./Scripts/title/attract_camera.lua
28aa7c8cf9969059682c72bfb065e16df48cb2608f3512e694d19cf53be70783 *./Scripts/title/attract_scene.lua
6b1cd4fa907aaa87fd734c980dc11210069619dd9baaa81e487265f5ed53ebb2 *./Scripts/title/bana_advert.lua
bda7e07727e8892c539982a21aa8861015140dd651c19b344055e8214e192b65 *./Scripts/title/camera_cloud.lua
779154b2f83fb825c1bb6d6535901285a4bbc1ada7f7d73677b09885594a0822 *./Scripts/title/warning_screen.lua
bc1567d77db047e2473a4279bacf902d857312b6d5b005c20563ae098bc45c8c *./Scripts/result/dan_result_draw.lua
65d5a4450c6d657789e69b4e17b893698f2920726e08d98bf9265ac763408f6f *./Scripts/result/high_score_indicator.lua
be8afab142d2a20bd98b0ac22b2d5db474d0a80a5a921c0415fe482c672ee498 *./Scripts/result/result_background.lua
21ef39a4e4d3906f952922dedd590db7a210a128e7b1e9081b658735052582ef *./Scripts/result/result_bg_layers.lua
d0e71023f6442b34b35e8fa92f34b29960f259c0255bf97ad468467b615808d3 *./Scripts/result/result_crown.lua
31979bee96d855ac40b979274f2cabeb657ae128af3883a32f33a596d3c6879c *./Scripts/result/result_crown_message.lua
f9b910c01ba733a57187174cc29b0eece3064f149f4c6ca7e21a43e93a47e6ee *./Scripts/result/result_fade_in.lua
9a51da8dd62bc9d4b87ce4cfef83b39f107c9598c26c36a0b6969655a5607781 *./Scripts/result/result_player.lua
6290618e48db87ceea755e91d02af8bbbb233392181bac4b3984a2b52924fbb5 *./Scripts/song_select/dan_doors.lua
a537199faff903d8c4bf0dce7c436ad0b9b83a823ab5ba2dffd7329d419f2c72 *./Scripts/song_select/song_select.lua
a4b7febf5d36106c41d957bfdb1e4f0f565782fb57c28e699b29960508578bd9 *./Scripts/title/warning_screen.lua
89047ab5b642696922ca2c6e098ca288580a9c0bb3e197a08f17b821a31fc519 *./.skin-repo
40c378cdcaea2e2c9ec3da4c7fa72911d13a849a5b51e9645300f9571a7e12c8 *./Sounds/dan_result/achieve_tamashii.ogg
606f2d3558badc06e54b2c59a36f0580f4954774918d69a712ad6de45518cba0 *./Sounds/dan_result/advance_intro.ogg
@@ -3498,6 +3494,7 @@ f485739fe95456ed2c9b261731bb5984398befa97c0d5e2965d389c40666d8b7 *./Videos/attra
630b2ad2fa9a473dd3cf2246d4fd09527e5be3614556e163cf428baeab4d2963 *./Videos/attract_videos/attract_00000018.mp4
3a52959013e54ebf4f99f5baf1f58a41a8c51d518bd16947d06c7d446c350396 *./Videos/attract_videos/attract_00000019.mp4
381b6706c8fa6664df810acbd7ae0db155e081aa5a04fcff8d09cae8141c859f *./Videos/attract_videos/attract_00000020.mp4
c567acc69bf6e9a239e66a195f2d65a7ec6455acff3ee248f4302477d01c193d *./Videos/op_videos/attractdemo_cn_tw.mp4
3ae7d8f807a3bd693fd4a380e6f6ebedae4d443d796049880f69bc23456a9f85 *./Videos/op_videos/attractdemo_en_us.mp4
54f561d89b48feff2fa31999e7485e370f2c55b9f360102b2014310078d20fc3 *./Videos/op_videos/attractdemo_kor.mp4
c567acc69bf6e9a239e66a195f2d65a7ec6455acff3ee248f4302477d01c193d *./Videos/attract_videos/attractdemo_cn_tw.mp4
3ae7d8f807a3bd693fd4a380e6f6ebedae4d443d796049880f69bc23456a9f85 *./Videos/attract_videos/attractdemo_en_us.mp4
54f561d89b48feff2fa31999e7485e370f2c55b9f360102b2014310078d20fc3 *./Videos/attract_videos/attractdemo_kor.mp4
3801f84ff85f56376828abef0b12bd8a1e336b4bc739557bc0ccfa386d79668e *./Videos/op_videos/title.mp4