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.get_texture("result/background/background_2p")
|
|
self.t_header = tex.get_texture("result/background/bg_header")
|
|
return self
|
|
end
|
|
|
|
self.t_sky = tex.get_texture("result/background/background_1p")
|
|
self.t_sky_clear = tex.get_texture("result/background/bg_sky_clear")
|
|
self.t_fuji = tex.get_texture("result/background/bg_fuji")
|
|
self.t_fuji_clear = tex.get_texture("result/background/bg_fuji_clear")
|
|
self.t_header = tex.get_texture("result/background/bg_header")
|
|
self.t_cloud, self.t_cloud_clear = {}, {}
|
|
for i = 0, 7 do
|
|
self.t_cloud[i] = tex.get_texture("result/background/bg_cloud_" .. i)
|
|
self.t_cloud_clear[i] = tex.get_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
|