Files
YataiDONNijiiro/Scripts/background/bg_objects/fever_anim.lua
T
TaikoZhongandClaude Fable 5.1 41c27b11f4 Soul-max crowd: step at the song's beat like the original
The original re-times this movie to the song (the same frame sync the dancers
use): the 40-frame bob is one beat, so 60 fps only at 90 BPM.

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

138 lines
4.7 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
-- 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.
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.
local BEAT_FRAMES = 40
local function frame_ms(bpm)
if not bpm or bpm <= 0 then return 1000.0 / 60.0 end
return (60000.0 / bpm) / BEAT_FRAMES
end
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(bpm)
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