Files
YataiDONNijiiro/Scripts/result/result_crown.lua
TaikoZhongandClaude Fable 5.1 4d3127d9ea Replay the cabinet's add-blended light effects as additive
The exported clips and backdrop movies were drawn source-over everywhere, but the
cabinet places a number of glow layers with its add blend mode: the lantern glows
of the lower backdrops (bg_nomal_a_01..05), the crown's radial burst, the score
rank plate's effect copy, and the full-combo / donderful letter glows.  Drawn
source-over, those textures show up as dark or grey patches.

- movie_export / clip_export record the placement's blend mode (`blend = 8` on a
  backdrop leaf, an 8th row value on a clip leaf; the root placement's mode is
  folded into its group), and bg_anim / clip_player / ending draw such leaves
  with blend = "additive".  Data re-exported; only the blend field changed.
- result_crown: the burst copies draw additively; the crown_loop shine sweep is a
  plain white polygon on the cabinet, so gleam frames 0..2 are white now instead
  of the atlas colour they were exported with.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 01:49:19 +08:00

211 lines
11 KiB
Lua

-- 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.
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
-- 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()
-- 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
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 CROWN = crown_anim.safe("anim/crown_mc")
local CROWN_START_F = CROWN:label("start") or 5 -- t = 0 is this arcade frame
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
-- GL_SHINE index visible at loop-frame k, or nil during the idle stretch / missing table
local function crown_loop_shine(k)
if not CLOOP.tracks then return nil, false end
local ki = math.floor(k)
for i = 1, #CLOOP_LEAVES do
local rows = CLOOP.tracks[CLOOP_LEAVES[i][1]]
if rows and #rows > 0 and ki >= rows[1][1] and ki <= rows[#rows][1] then
return CLOOP_LEAVES[i][2], true
end
end
return nil, true -- table present, idle frame
end
local function clamp01(v)
if v <= 0 then return 0 end
if v >= 1 then return 1 end
return v
end
local function lerp(a, b, p) return a + (b - a) * p end
function ResultCrown.new(crown_type, is_2p, has_2p)
local self = setmetatable({}, ResultCrown)
self.is_2p = is_2p
self.p_idx = is_2p and 1 or 0
self.t0 = tex.get_current_ms()
self.now = self.t0
self.sound_played = false
local crown_names = { [0] = "crown_clear", [1] = "crown_fc", [2] = "crown_dfc" }
self.t_crown = tex.load_texture("result/crown/" .. (crown_names[crown_type] or "crown_clear"))
self.t_crown_fade = tex.load_texture("result/crown/crown_fade")
self.t_gleam = tex.load_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
return self
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()).
audio.play_sound(self.sound_name or "crown", "sound")
self.sound_played = true
end
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
end
function ResultCrown:draw()
local t = (self.now - self.t0) / F
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
if a > 0.001 then
tex.draw_texture(self.t_crown_fade, { scale = s, center = true, index = idx, fade = a })
end
end
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.
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
local s0 = CROWN:get("#38@0", f, "sx") or 0.1
if a > 0.001 then
-- the cabinet places both burst copies with the add blend mode
tex.draw_texture(self.t_gleam, { frame = GL_BURST, scale = s1, center = true, index = idx, fade = a, blend = "additive" })
tex.draw_texture(self.t_gleam, { frame = GL_BURST, scale = s0, center = true, index = idx, fade = a, blend = "additive" })
end
end
-- ---- d4 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
local oy = CROWN:get("#50@4", f, "ty") or -2.2
local a = CROWN:get("#50@4", f, "a") or 1.0
tex.draw_texture(self.t_gleam,
{ 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.
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)
if shine then
tex.draw_texture(self.t_gleam,
{ frame = GL_SHINE[shine], center = true, index = idx })
elseif not have_table and k < 9 then
tex.draw_texture(self.t_gleam,
{ frame = GL_SHINE[math.floor(k / 3) + 1], center = true, index = idx })
end
end
end