- In game: the rank plate slides in at the player panel's top-left as the score crosses each rank line (500k white, 600k bronze, 700k silver, 800k gold, 900k pink, 950k purple, 1M rainbow), swapping through the out/in pair on the next line. Uses the engine's Background:handle_score hook. - Result: a ScoreRank state between the score count-up and the crown, playing the original's plate clip (極 with its own burst) under the score box. - Song select already draws the chips from the stored rank. Clips exported from the original with the rank plates as swappable shapes; Scripts/game/clip_player.lua is the shared frame player. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
88 lines
3.0 KiB
Lua
88 lines
3.0 KiB
Lua
-- Generic replay of an exported cabinet clip (scratchpad/lyric/clip_export.py):
|
|
-- data.frames[f+1] = {{shape, tx, ty, sx, sy, rot, a}, ...}, data.shapes[c] = {w, h, ox, oy},
|
|
-- data.nframes, data.loop_at (the frame the clip continues from after its last one).
|
|
-- Plays at 60 fps from the moment start() is called. `shape_map` (optional) swaps
|
|
-- one shape for another at draw time -- the way the original picks a variant with a
|
|
-- label (rank_1 .. rank_7) on a clip whose motion is otherwise identical.
|
|
local ClipPlayer = {}
|
|
ClipPlayer.__index = ClipPlayer
|
|
|
|
local FRAME_MS = 1000.0 / 60.0
|
|
|
|
-- module: "game/ending_anim/<name>"; path/subset: texture folder (e.g. "game", "<name>_anim")
|
|
function ClipPlayer.new(module, path, subset, x, y)
|
|
local ok, data = pcall(require, module)
|
|
if not ok or type(data) ~= "table" or not data.frames then return nil end
|
|
local self = setmetatable({}, ClipPlayer)
|
|
self.data, self.path, self.subset = data, path, subset
|
|
tex.load_folder(path, subset)
|
|
self.x, self.y = x or 0, y or 0
|
|
self.ids = {}
|
|
self.shape_map = nil
|
|
self.t0 = nil
|
|
self.hold_last = false -- true: stay on the last frame instead of looping
|
|
return self
|
|
end
|
|
|
|
function ClipPlayer:id_of(shape)
|
|
local id = self.ids[shape]
|
|
if id == nil then
|
|
id = tex.get_id(self.subset, "s" .. shape) or false
|
|
self.ids[shape] = id
|
|
end
|
|
return id or nil
|
|
end
|
|
|
|
function ClipPlayer:start(now) self.t0 = now end
|
|
function ClipPlayer:started() return self.t0 ~= nil end
|
|
|
|
function ClipPlayer:frame(now)
|
|
if not self.t0 then return 0 end
|
|
local f = math.floor((now - self.t0) / FRAME_MS)
|
|
local n, loop_at = self.data.nframes, self.data.loop_at
|
|
if f >= n then
|
|
if self.hold_last then return n - 1 end
|
|
local span = n - loop_at
|
|
f = (span > 0) and (loop_at + (f - loop_at) % span) or (n - 1)
|
|
end
|
|
return f
|
|
end
|
|
|
|
-- true once the clip has passed its last frame at least once
|
|
function ClipPlayer:finished(now)
|
|
return self.t0 ~= nil and (now - self.t0) / FRAME_MS >= self.data.nframes
|
|
end
|
|
|
|
function ClipPlayer:draw(now, fade)
|
|
if not self.t0 then return end
|
|
fade = fade or 1.0
|
|
local rows = self.data.frames[self:frame(now) + 1]
|
|
if not rows then return end
|
|
local shapes, map = self.data.shapes, self.shape_map
|
|
for i = 1, #rows do
|
|
local r = rows[i]
|
|
local shape = r[1]
|
|
if map and map[shape] then shape = map[shape] end
|
|
local s = shapes[shape]
|
|
local id = s and self:id_of(shape)
|
|
if id then
|
|
local sx, sy = r[4], r[5]
|
|
tex.draw_texture(id, {
|
|
x = self.x + r[2],
|
|
y = self.y + r[3],
|
|
x2 = s[1] * (sx - 1),
|
|
y2 = s[2] * (sy - 1),
|
|
origin = { s[3] * sx, s[4] * sy },
|
|
rotation = r[6],
|
|
fade = r[7] * fade,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
function ClipPlayer:destroy()
|
|
tex.unload_folder(self.path, self.subset)
|
|
end
|
|
|
|
return ClipPlayer
|