Files
YataiDONNijiiro/Scripts/background/bg_objects/fever_anim.lua
T
TaikoZhongandClaude Fable 5.1 1b74eb9c99 Soul-max crowd: replay the per-group movie instead of a flat overlay
The 魂 max crowd was two flattened PNGs, so every figure sat at one fixed
position, the turtle/shiba groups lost their depth order, and nothing moved.
The crowd is now data-driven: one texture per crowd group
(background/fevera_N) plus a per-frame display list
(bg_data/fever_anim/f_N.lua) covering the rise, the loop and the fall, replayed
by bg_objects/fever_anim.lua at 60 fps. Every group bobs and squashes on its
own phase and layers exactly as authored; the base of the crowd lands on the
band. Three crowds are shipped; the old flat overlays stay as a fallback for
skins without the data.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 09:48:44 +08:00

132 lines
4.4 KiB
Lua

-- 魂-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
-- at the movie's fixed 60 fps. 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.
local FeverAnim = {}
FeverAnim.__index = FeverAnim
local FRAME_MS = 1000.0 / 60.0
local STAGE_Y = 540 -- the 1920x540 crowd stage covers the lower half of the screen
function FeverAnim.available(index)
local ok, data = pcall(require, "background/bg_data/fever_anim/f_" .. index)
return ok and type(data) == "table"
end
function FeverAnim.new(index, bpm, path)
local ok, data = pcall(require, "background/bg_data/fever_anim/f_" .. index)
if not ok or type(data) ~= "table" then return nil end
local self = setmetatable({}, FeverAnim)
self.name = "fevera_" .. index
self.path = path or "background"
tex.load_folder(self.path, self.name)
self.data = data
self.enter_f = data.labels.normal_fever or 0
self.loop_f = data.labels.fever or self.enter_f
self.exit_f = data.labels.fever_normal or data.nframes
self.ids = {}
self.state = "wait"
self.cursor = 0
self.last_ms = nil
return self
end
function FeverAnim:id_of(shape)
local id = self.ids[shape]
if id == nil then
id = tex.get_id(self.name, "s" .. shape) or false
self.ids[shape] = id
end
return id or nil
end
-- rising edge: GotoAndPlay("normal_fever") -- also restarts a crowd that was leaving
function FeverAnim:start()
if self.state == "enter" or self.state == "loop" then return end
self.state = "enter"
self.cursor = self.enter_f
end
-- falling edge: GotoAndPlay("fever_normal")
function FeverAnim:stop()
if self.state == "wait" or self.state == "exit" then return end
self.state = "exit"
self.cursor = self.exit_f
end
function FeverAnim:is_visible()
return self.state ~= "wait"
end
function FeverAnim:alpha()
return self.state ~= "wait" and 1.0 or 0.0
end
function FeverAnim:update(current_ms, bpm)
local last = self.last_ms
self.last_ms = current_ms
if self.state == "wait" or not last then return end
local dt = current_ms - last
if dt <= 0 then return end
if dt > 500 then dt = 500 end
self.cursor = self.cursor + dt / FRAME_MS
if self.state == "enter" and self.cursor >= self.loop_f then
self.state = "loop"
end
if self.state == "loop" then
local span = self.exit_f - self.loop_f
if span > 0 then
self.cursor = self.loop_f + (self.cursor - self.loop_f) % span
else
self.cursor = self.loop_f
end
elseif self.state == "exit" and self.cursor >= self.data.nframes then
self.state = "wait"
end
end
function FeverAnim:draw()
if self.state == "wait" then return end
local f = math.floor(self.cursor)
if f < 0 then f = 0 elseif f >= self.data.nframes then f = self.data.nframes - 1 end
local rows = self.data.frames[f + 1]
if not rows then return end
local shapes = self.data.shapes
for i = 1, #rows do
local r = rows[i]
local a = r[7]
if a > 0.002 then
local s = shapes[r[1]]
local id = s and self:id_of(r[1])
if id then
local sx, sy = r[4], r[5]
tex.draw_texture(id, {
x = r[2],
y = r[3] + STAGE_Y,
x2 = s[1] * (sx - 1),
y2 = s[2] * (sy - 1),
origin = { s[3] * sx, s[4] * sy },
rotation = r[6],
fade = a,
})
end
end
end
end
function FeverAnim:destroy()
tex.unload_folder(self.path, self.name)
end
return FeverAnim