The double-play result plate was baked from the wrong atlas layer, so it came out orange / teal with the header band burnt in. Rebuilt from the original's two sky halves (the left one placed mirrored) and the header band is now drawn by the layer code, the same way the 1P backdrop draws it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
236 lines
12 KiB
Lua
236 lines
12 KiB
Lua
-- 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".
|
|
|
|
local anim = require("anim/sampler")
|
|
|
|
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 MS_TO_F = 0.06 -- 60 fps
|
|
|
|
-- depth, cloud texture index, y, scale (|sx| == sy for every one of them), mirror, source width
|
|
-- (only a mirrored layer needs it -- and for both of those the base and clear bakes are the same
|
|
-- width), arcade track name, frame-0 tx, frame-0 alpha
|
|
local LAYERS = {
|
|
{ 3, 0, 162.60, 1.0, false, 592, "#67@3", 1705.70, 0 },
|
|
{ 4, 1, 905.25, 1.0, false, 960, "#69@4", 1240.00, 0 },
|
|
{ 5, 1, 934.00, 1.2, true, 960, "#69@5", 1106.30, 0 },
|
|
{ 6, 2, 622.00, 1.0, false, 840, "#71@6", 1382.90, 1 },
|
|
{ 7, 3, 873.25, 1.0, false, 824, "#73@7", -272.10, 1 },
|
|
{ 8, 4, 873.25, 1.0, false, 800, "#75@8", 668.65, 0 },
|
|
{ 9, 5, 595.25, 1.0, false, 848, "#77@9", 468.00, 0 },
|
|
{ 10, 6, -17.70, 1.0, false, 616, "#79@10", 1645.00, 1 },
|
|
{ 11, 2, 75.10, 0.6, true, 840, "#71@11", 346.40, 0 },
|
|
{ 12, 6, 214.30, 1.0, false, 616, "#79@12", 767.80, 1 },
|
|
{ 13, 7, 400.75, 1.0, false, 600, "#81@13", -190.35, 0 },
|
|
}
|
|
|
|
local FUJI_X = 583.00 -- shape 65, base
|
|
local FUJI_Y = 280.00
|
|
local FUJI_CLEAR_X = 584.65 -- shape 84 inside huji_1p_l_mc, clear
|
|
local FUJI_H = 800
|
|
local FUJI_TRACK = "huji_1p_l_mc/#84@0"
|
|
|
|
function ResultBgLayers.new(player_num)
|
|
local self = setmetatable({}, ResultBgLayers)
|
|
self.player_num = player_num
|
|
self.two_player = (player_num == 3)
|
|
self.t0 = nil
|
|
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.
|
|
self.t_bg = tex.load_texture("result/background/background_2p")
|
|
self.t_header = tex.load_texture("result/background/bg_header")
|
|
return self
|
|
end
|
|
|
|
self.t_sky = tex.load_texture("result/background/background_1p")
|
|
self.t_sky_clear = tex.load_texture("result/background/bg_sky_clear")
|
|
self.t_fuji = tex.load_texture("result/background/bg_fuji")
|
|
self.t_fuji_clear = tex.load_texture("result/background/bg_fuji_clear")
|
|
self.t_header = tex.load_texture("result/background/bg_header")
|
|
self.t_cloud, self.t_cloud_clear = {}, {}
|
|
for i = 0, 7 do
|
|
self.t_cloud[i] = tex.load_texture("result/background/bg_cloud_" .. i)
|
|
self.t_cloud_clear[i] = tex.load_texture("result/background/bg_cloud_clear_" .. i)
|
|
end
|
|
|
|
-- FAIL SOFT: `anim.safe` returns a NULL clip whose readers return nil, so a missing or
|
|
-- malformed table degrades to the LAYERS constants instead of raising.
|
|
self.clip = anim.safe("anim/result_bg")
|
|
self.fuji = anim.safe("anim/result_bg_fuji")
|
|
return self
|
|
end
|
|
|
|
-- arcade frame of the 720-frame loop, from the engine clock
|
|
function ResultBgLayers:frame()
|
|
local ms = tex.get_current_ms()
|
|
if not self.t0 then self.t0 = ms end
|
|
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.
|
|
function ResultBgLayers:fuji_frame(clear)
|
|
if clear <= 0 then
|
|
self.fuji_t0 = nil -- re-arm: the next clear starts from rest again
|
|
return 0
|
|
end
|
|
local ms = tex.get_current_ms()
|
|
if not self.fuji_t0 then self.fuji_t0 = ms end
|
|
-- monotonic; the sampler parks it on frame 89 (FUJI_LOOP - 1) forever after.
|
|
return (ms - self.fuji_t0) * MS_TO_F
|
|
end
|
|
|
|
local function draw_cloud(id, w, x, y, scale, mirror, a)
|
|
if not id or a <= 0.004 then return end
|
|
local left = mirror and (x - w * scale) or x
|
|
tex.draw_texture(id, {
|
|
x = left, y = y, scale = scale, fade = a,
|
|
mirror = mirror and "horizontal" or nil,
|
|
})
|
|
end
|
|
|
|
-- `fade` multiplies everything (the screen-entry wipe); `clear` is 0..1, the success cross-fade.
|
|
function ResultBgLayers:draw(fade, clear)
|
|
fade = fade or 1.0
|
|
clear = clear or 0.0
|
|
if fade <= 0 then return end
|
|
|
|
if self.two_player then
|
|
if self.t_bg then tex.draw_texture(self.t_bg, { fade = fade }) end
|
|
if self.t_header then tex.draw_texture(self.t_header, { fade = fade }) end
|
|
return
|
|
end
|
|
|
|
local f = self:frame()
|
|
|
|
-- 1. sky. Base: shape 3 at x=0 and mirrored at x=960. Clear: shape 5 (gold, a different
|
|
-- atlas) the other way round -- x=960 upright, x=0 mirrored.
|
|
if self.t_sky then
|
|
tex.draw_texture(self.t_sky, { index = 0, fade = fade })
|
|
tex.draw_texture(self.t_sky, { index = 1, fade = fade, mirror = "horizontal" })
|
|
end
|
|
if clear > 0 and self.t_sky_clear then
|
|
tex.draw_texture(self.t_sky_clear, { index = 0, fade = 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.
|
|
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
|
|
if clear > 0 and self.t_fuji_clear then
|
|
local ty, sy = self.fuji:at(FUJI_TRACK, self:fuji_frame(clear))
|
|
ty = ty or FUJI_Y
|
|
sy = sy or 1.0
|
|
tex.draw_texture(self.t_fuji_clear, {
|
|
x = FUJI_CLEAR_X, y = ty, y2 = FUJI_H * sy - FUJI_H, fade = 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.
|
|
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]
|
|
local tx, a = self.clip:at(track, f)
|
|
tx = tx or L[8]
|
|
a = a or L[9]
|
|
if clear < 1.0 then
|
|
draw_cloud(self.t_cloud[idx], w, tx, y, scale, mirror, fade * a * (1.0 - clear))
|
|
end
|
|
if clear > 0 then
|
|
draw_cloud(self.t_cloud_clear[idx], w, tx, y, scale, mirror, fade * a * 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.
|
|
if self.t_header then tex.draw_texture(self.t_header, { fade = fade }) end
|
|
end
|
|
|
|
return ResultBgLayers
|