Files
TaikoZhongandClaude Opus 5.5 f239bc156c background: cabinet backdrops for the collab presets
Collab songs (#SCENEPRESET) now get their own cabinet backdrop, replayed by
the same objects as the default backdrop instead of the parent's collab art:

  miku, toho, gumi, ia, lovelive, i7 (id7 / tri / rev / natsu), imas
  (765 / cg / ml / sidem), yo-kai (base / matsuri / hatsukoi / tokoroten /
  yougota), mario, a3

Each preset is a pack in bg_data/collab_packs.lua naming its parts: donbg
band (1P and 2P), normal and clear backdrops, troupe, stand under the
troupe, drumroll effect and soul-max crowd.  Shared parts (the i7, imas
and yo-kai families) are shared the way the cabinet shares them.  A pack is
used only when every part it names is present, otherwise the preset falls
back to the collab script as before.  Packs also apply in 2P (donbg bands).

The objects take a part name where they took a random index:
DonBG.create_named, BGNormal.create_named, BGFever.create_named,
ArcadeDancerGroup.new("<name>"), Fever.Anim.new("<name>").  New:
renda_clip.lua (each hit replays one of the effect's parts along its own
path), and a beat-synced BGAnim for the whole-troupe dancer movies (i7,
a3).  Collab troupes stand in fixed positions (lead in the centre) and
grow from 1 to the full troupe with the gauge.

Exported placements may now mirror: bg_anim, donbg and dancer data carry a
mirror flag (drawn with mirror = horizontal and the origin moved to the
other side of the png).  Existing data has no flag and draws as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:55:05 +08:00

620 lines
22 KiB
Lua

-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/dancer.lua: Dancer0_4 bounce 20*sc; dancer spacing from info.x2[1]
-- BaseDancer: single dancer that starts off-screen and bounces in, then loops
local BaseDancer = {}
BaseDancer.__index = BaseDancer
function BaseDancer.new(name, index, bpm, path)
local self = setmetatable({}, BaseDancer)
self.name = name
self.index = index
self.bpm = bpm
self.path = path or "background"
self.is_started = false
local tex_info = tex.get_texture_info(self.name, tostring(self.index) .. "_loop")
local frame_count = (tex_info and tex_info.frame_count) or 1
self.keyframes = {}
for i = 0, frame_count - 1 do
table.insert(self.keyframes, i)
end
local duration = (60000 / bpm) / 2
local total_duration = duration * #self.keyframes
local textures = {}
for i, frame_idx in ipairs(self.keyframes) do
table.insert(textures, { duration * (i - 1), duration * i, frame_idx })
end
self.texture_change = anim.texture_change(total_duration, textures, { loop = true })
self.texture_change:start()
return self
end
function BaseDancer:start()
self.is_started = true
local screen_scale = tex.get_screen_scale()
local duration = 60000 / self.bpm
self.s_bounce_up = anim.move(duration / 2, {
start_position = math.floor(-200 * screen_scale),
total_distance = math.floor(350 * screen_scale),
ease_out = "quadratic",
delay = 500
})
self.s_bounce_down = anim.move(duration / 2, {
total_distance = math.floor(140 * screen_scale),
ease_in = "quadratic",
delay = duration / 2 + 500
})
local start_info = tex.get_texture_info(self.name, tostring(self.index) .. "_start")
local s_frame_count = (start_info and start_info.frame_count) or 1
local s_keyframes = {}
for i = 0, s_frame_count - 1 do
table.insert(s_keyframes, i)
end
local start_textures = {}
for i, frame_idx in ipairs(s_keyframes) do
table.insert(start_textures, { (duration / #s_keyframes) * (i - 1), (duration / #s_keyframes) * i, frame_idx })
end
self.s_texture_change = anim.texture_change(duration, start_textures, { delay = 500 })
self.s_texture_change:start()
self.s_bounce_up:start()
self.s_bounce_down:start()
end
function BaseDancer:update(current_time_ms, bpm)
self.texture_change:update(current_time_ms)
if self.is_started then
self.s_texture_change:update(current_time_ms)
self.s_bounce_up:update(current_time_ms)
self.s_bounce_down:update(current_time_ms)
end
if bpm and bpm ~= self.bpm then
self.bpm = bpm
local duration = (60000 / bpm) / 2
local total_duration = duration * #self.keyframes
local textures = {}
for i, frame_idx in ipairs(self.keyframes) do
table.insert(textures, { duration * (i - 1), duration * i, frame_idx })
end
self.texture_change = anim.texture_change(total_duration, textures, { loop = true })
self.texture_change:start()
end
end
function BaseDancer:draw(x)
if not self.is_started then return end
local y = -self.s_bounce_up.attribute + self.s_bounce_down.attribute
if not self.s_texture_change.is_finished then
tex.draw_texture(tex.get_texture(self.path .. "/" .. self.name .. "/" .. tostring(self.index) .. "_start"), {
frame = self.s_texture_change.attribute, x = x, y = y
})
else
tex.draw_texture(tex.get_texture(self.path .. "/" .. self.name .. "/" .. tostring(self.index) .. "_loop"), {
frame = self.texture_change.attribute, x = x
})
end
end
-- Dancer0_4: dancer 4 in group 0, has a bouncing object on top
local Dancer0_4 = setmetatable({}, { __index = BaseDancer })
Dancer0_4.__index = Dancer0_4
function Dancer0_4.new(name, index, bpm, path)
local self = setmetatable({}, Dancer0_4)
local base = BaseDancer.new(name, index, bpm, path)
for k, v in pairs(base) do self[k] = v end
self.keyframes = {}
for i = 0, 53 do
table.insert(self.keyframes, i)
end
local duration = (60000 / bpm) / 2
local total_duration = duration * #self.keyframes
local textures = {}
for i, frame_idx in ipairs(self.keyframes) do
table.insert(textures, { duration * (i - 1), duration * i, frame_idx })
end
self.texture_change = anim.texture_change(total_duration, textures, { loop = true })
self.texture_change:start()
self.bounce_up = anim.move(duration, {
total_distance = 20 * tex.get_screen_scale(), ease_out = "quadratic", delay = duration * 2
})
self.bounce_down = anim.move(duration, {
total_distance = 20 * tex.get_screen_scale(), ease_in = "quadratic", delay = duration * 3
})
self.bounce_up:start()
self.bounce_down:start()
self._id_4_start = tex.get_texture(self.path .. "/" .. self.name .. "/4_start") or 0
self._id_4_loop = tex.get_texture(self.path .. "/" .. self.name .. "/4_loop") or 0
return self
end
function Dancer0_4:update(current_time_ms, bpm)
BaseDancer.update(self, current_time_ms, bpm)
self.bounce_up:update(current_time_ms)
self.bounce_down:update(current_time_ms)
if self.bounce_down.is_finished then
self.bounce_up:restart()
self.bounce_down:restart()
end
end
function Dancer0_4:draw(x)
if not self.is_started then return end
local sy = -self.s_bounce_up.attribute + self.s_bounce_down.attribute
local screen_scale = tex.get_screen_scale()
if not self.s_texture_change.is_finished then
tex.draw_texture(self._id_4_start, {
frame = 7, x = x, y = (-50 * screen_scale) + sy
})
tex.draw_texture(self._id_4_start, {
frame = self.s_texture_change.attribute, x = x, y = sy
})
else
local attr = self.texture_change.attribute
local by = -self.bounce_up.attribute + self.bounce_down.attribute
if attr >= 0 and attr <= 3 then
tex.draw_texture(self._id_4_loop, { frame = 54, x = x, y = by })
elseif attr >= 5 and attr <= 8 then
tex.draw_texture(self._id_4_loop, { frame = 56, x = x, y = by })
elseif attr == 4 then
tex.draw_texture(self._id_4_loop, { frame = 55, x = x, y = by })
end
tex.draw_texture(self._id_4_loop, { frame = attr, x = x })
end
end
-- DancerPoof: dancer with a poof animation on spawn
local DancerPoof = setmetatable({}, { __index = BaseDancer })
DancerPoof.__index = DancerPoof
function DancerPoof.new(name, index, bpm, path)
local self = setmetatable({}, DancerPoof)
local base = BaseDancer.new(name, index, bpm, path)
for k, v in pairs(base) do self[k] = v end
local duration = 60000 / bpm
local poof_keyframes = {}
for i = 0, 6 do
table.insert(poof_keyframes, { (duration / 7) * i, (duration / 7) * (i + 1), i })
end
self.poof_texture_change = anim.texture_change(duration, poof_keyframes, { delay = 250 })
self.poof_texture_change:start()
self._id_poof = tex.get_texture(self.path .. "/" .. self.name .. "/poof") or 0
return self
end
function DancerPoof:update(current_time_ms, bpm)
BaseDancer.update(self, current_time_ms, bpm)
self.poof_texture_change:update(current_time_ms)
end
function DancerPoof:draw(x)
BaseDancer.draw(self, x)
if not self.poof_texture_change.is_finished then
tex.draw_texture(self._id_poof, { x = x, frame = self.poof_texture_change.attribute })
end
end
-- DancerPoof2: like DancerPoof but uses per-index poof texture
local DancerPoof2 = setmetatable({}, { __index = DancerPoof })
DancerPoof2.__index = DancerPoof2
function DancerPoof2.new(name, index, bpm, path)
local self = setmetatable({}, DancerPoof2)
local base = DancerPoof.new(name, index, bpm, path)
for k, v in pairs(base) do self[k] = v end
return self
end
function DancerPoof2:draw(x)
if not self.is_started then return end
local sy = -self.s_bounce_up.attribute + self.s_bounce_down.attribute
if not self.s_texture_change.is_finished then
tex.draw_texture(tex.get_texture(self.path .. "/" .. self.name .. "/" .. tostring(self.index) .. "_start"), {
frame = self.s_texture_change.attribute, x = x, y = sy
})
else
tex.draw_texture(tex.get_texture(self.path .. "/" .. self.name .. "/" .. tostring(self.index) .. "_loop"), {
frame = self.texture_change.attribute, x = x
})
end
if not self.poof_texture_change.is_finished then
tex.draw_texture(tex.get_texture(self.path .. "/" .. self.name .. "/" .. tostring(self.index) .. "_poof"), {
x = x, frame = self.poof_texture_change.attribute
})
end
end
-- BaseDancerGroup: manages a pool of dancers spread across the screen
local BaseDancerGroup = {}
BaseDancerGroup.__index = BaseDancerGroup
local function get_variant_count(dancer_name)
local keys = tex.get_texture_keys(dancer_name)
if not keys then return 1 end
local variants = {}
for _, key in ipairs(keys) do
local num = tonumber(key:match("^(%d+)"))
if num then variants[num] = true end
end
local count = 0
for _ in pairs(variants) do count = count + 1 end
return math.max(1, count)
end
function BaseDancerGroup.new(index, bpm, max_dancers, path, dancer_class)
local self = setmetatable({}, BaseDancerGroup)
dancer_class = dancer_class or BaseDancer
self.name = "dancer_" .. index
self.active_count = 0
self.path = path or "background"
tex.load_folder(self.path, "dancer/" .. self.name)
-- center(2), left(1), right(3), far-left(0), far-right(4) — 0-indexed
self.spawn_positions = { 2, 1, 3, 0, 4 }
self.active_dancers = {}
self.max_dancers = max_dancers
local variant_count = get_variant_count(self.name)
self.dancers = {}
for i = 1, max_dancers do
local variant_idx = (i - 1) % variant_count
table.insert(self.dancers, dancer_class.new(self.name, variant_idx, bpm, self.path))
end
-- Shuffle dancers
for i = #self.dancers, 2, -1 do
local j = math.random(1, i)
self.dancers[i], self.dancers[j] = self.dancers[j], self.dancers[i]
end
self.needs_initial_dancer = true
return self
end
function BaseDancerGroup:add_dancer()
if self.active_count < #self.dancers and self.active_count < #self.spawn_positions then
self.active_count = self.active_count + 1
local position = self.spawn_positions[self.active_count] + 1 -- convert to 1-indexed
local dancer = self.dancers[self.active_count]
self.active_dancers[position] = dancer
dancer:start()
end
end
function BaseDancerGroup:remove_dancer()
if self.active_count > 1 then
local position = self.spawn_positions[self.active_count] + 1
self.active_dancers[position] = nil
self.active_count = self.active_count - 1
end
end
function BaseDancerGroup:update(current_time_ms, bpm)
if self.needs_initial_dancer then
self.needs_initial_dancer = false
self:add_dancer()
end
for _, dancer in ipairs(self.dancers) do
dancer:update(current_time_ms, bpm)
end
end
function BaseDancerGroup:draw()
local screen_width = tex.get_screen_width()
local num_dancers = self.max_dancers
local first_dancer = nil
for i = 1, num_dancers do
local d = self.active_dancers[i]
if d then
first_dancer = d; break
end
end
if not first_dancer then return end
local info = tex.get_texture_info(self.name, tostring(first_dancer.index) .. "_loop")
local dancer_width = info and info.x2[1] or 0
local available_space = screen_width - (num_dancers * dancer_width)
local spacing = available_space / (num_dancers + 1)
for i = 1, num_dancers do
local dancer = self.active_dancers[i]
if dancer then
local x_pos = math.floor(spacing + (i - 1) * (dancer_width + spacing))
dancer:draw(x_pos)
end
end
end
function BaseDancerGroup:destroy()
tex.unload_folder(self.path, "dancer/" .. self.name)
end
-- DancerGroup0: index 0 uses BaseDancers 0-3 + Dancer0_4 at position 4
local DancerGroup0 = setmetatable({}, { __index = BaseDancerGroup })
DancerGroup0.__index = DancerGroup0
function DancerGroup0.new(index, bpm, max_dancers, path)
local self = setmetatable({}, DancerGroup0)
self.name = "dancer_" .. index
self.active_count = 0
self.path = path or "background"
tex.load_folder(self.path, "dancer/" .. self.name)
self.spawn_positions = { 2, 1, 3, 0, 4 }
self.active_dancers = {}
self.max_dancers = max_dancers
self.dancers = {
BaseDancer.new(self.name, 0, bpm, self.path),
BaseDancer.new(self.name, 1, bpm, self.path),
BaseDancer.new(self.name, 2, bpm, self.path),
BaseDancer.new(self.name, 3, bpm, self.path),
Dancer0_4.new(self.name, 4, bpm, self.path)
}
for i = #self.dancers, 2, -1 do
local j = math.random(1, i)
self.dancers[i], self.dancers[j] = self.dancers[j], self.dancers[i]
end
self.needs_initial_dancer = true
return self
end
-- DancerGroupPoof1: uses DancerPoof
local DancerGroupPoof1 = setmetatable({}, { __index = BaseDancerGroup })
DancerGroupPoof1.__index = DancerGroupPoof1
function DancerGroupPoof1.new(index, bpm, max_dancers, path)
return BaseDancerGroup.new(index, bpm, max_dancers, path, DancerPoof)
end
-- DancerGroupPoof2: uses DancerPoof2
local DancerGroupPoof2 = setmetatable({}, { __index = BaseDancerGroup })
DancerGroupPoof2.__index = DancerGroupPoof2
function DancerGroupPoof2.new(index, bpm, max_dancers, path)
return BaseDancerGroup.new(index, bpm, max_dancers, path, DancerPoof2)
end
-- Reference cabinet dancers: a single playhead per troupe, replayed at
-- BEAT_FRAMES per beat.
local BEAT_FRAMES = 40
local ARCADE_GROUPS = 6
local PHRASE = 80
local ArcadeDancer = {}
ArcadeDancer.__index = ArcadeDancer
function ArcadeDancer.new(group_name, variant, vd, path)
local self = setmetatable({}, ArcadeDancer)
self.vd = vd
self.tex = tex.get_texture((path or "background") .. "/" .. group_name .. "/" .. tostring(variant) .. "_loop") or 0
self.state = "idle"
self.enter = 0 -- group frame the `in` hop starts on
self.join = 0 -- group frame it joins the shared loop on
self.exit = 0 -- group frame the `out` segment starts on
return self
end
-- smallest f >= g with (f - dance) a whole number of phrases
local function phrase_at_or_after(g, dance)
return dance + math.ceil((g - dance) / PHRASE) * PHRASE
end
function ArcadeDancer:begin_enter(g)
self.join = phrase_at_or_after(g + PHRASE, self.vd.dance)
self.enter = self.join - PHRASE
self.state = "in"
end
function ArcadeDancer:begin_leave(g)
if self.state == "idle" or self.state == "out" then return end
if self.state == "in" and g < self.join then
-- earned and un-earned again before it ever landed: it simply never arrives
self.state = "idle"
return
end
self.exit = phrase_at_or_after(g, self.vd.dance)
self.state = "out"
end
-- The one place a frame number is produced; everything is a pure function of the
-- group's clock `g`, so two slots in the same state are on the same frame.
function ArcadeDancer:frame_at(g)
local vd = self.vd
local st = self.state
if st == "idle" then return nil end
if st == "in" then
if g < self.enter then return nil end -- earned, still waiting for the phrase
return g - self.enter
end
local span = vd.out - vd.dance
if st == "out" and g >= self.exit then
local f = vd.out + (g - self.exit)
if f >= vd.n then return nil end
return f
end
return vd.dance + (g - vd.dance) % span
end
local function find_key(keys, f)
local lo, hi = 1, #keys
while lo < hi do
local mid = math.floor((lo + hi + 1) / 2)
if keys[mid][1] <= f then lo = mid else hi = mid - 1 end
end
return keys[lo]
end
function ArcadeDancer:draw(px, py, g)
local f = self:frame_at(g)
if not f then return end
local vd = self.vd
local row = find_key(vd.keys, math.floor(f))[2]
-- child-skin textures are loaded at scale 1.0, so every coordinate here is in
-- the child's own 1920x1080 space (do NOT multiply by tex.get_screen_scale())
for i = 1, #row do
local d = row[i]
local sx, sy = d[4], d[5]
-- every frame keeps its own origin, which `origin` supplies per draw
local o = vd.o[d[1] + 1]
-- d[8] = 1: a mirroring placement (collab rigs); the frame flips inside its
-- rect, so the origin moves to the other side of the png
local mir = d[8] == 1
tex.draw_texture(self.tex, {
frame = d[1],
x = px + d[2],
y = py + d[3],
x2 = vd.w * (sx - 1),
y2 = vd.h * (sy - 1),
origin = { (mir and (vd.w - o[1]) or o[1]) * sx, o[2] * sy },
rotation = d[6],
fade = d[7],
mirror = mir and "horizontal" or nil,
})
end
end
local ArcadeDancerGroup = {}
ArcadeDancerGroup.__index = ArcadeDancerGroup
-- index: a default troupe (0..5), or a collab pack's troupe name ("b_001", whose data is
-- `fixed`: slot i always shows variant i - 1, as many slots as the movie has positions)
function ArcadeDancerGroup.new(index, bpm, max_dancers, path)
local self = setmetatable({}, ArcadeDancerGroup)
if type(index) == "string" then
self.name = index
else
self.name = "dancer_" .. (index % ARCADE_GROUPS)
end
self.path = path or "background"
self.data = require("background/dancer_data/" .. self.name)
tex.load_folder(self.path, "dancer/" .. self.name)
-- pos0..pos4 on the cabinet are centre, left, right, far-left, far-right
self.max_dancers = max_dancers or 5
self.min_dancers = 3
if self.data.fixed then
self.max_dancers = #self.data.pos
self.min_dancers = 1
end
self.now = nil
self.gframe = 0 -- the troupe's single playhead
self.active = 0
self.slots = {}
local nvar = 0
for _ in pairs(self.data.variants) do nvar = nvar + 1 end
-- a fixed troupe has one member per position, as many as the movie has dancers
if self.data.fixed and nvar < self.max_dancers then self.max_dancers = nvar end
local pick = {}
for v = 0, nvar - 1 do pick[#pick + 1] = v end
if not self.data.fixed then
for i = #pick, 2, -1 do
local j = math.random(1, i)
pick[i], pick[j] = pick[j], pick[i]
end
end
for i = 1, self.max_dancers do
local v = pick[((i - 1) % nvar) + 1]
self.slots[i] = ArcadeDancer.new(self.name, v, self.data.variants[v], self.path)
end
self.needs_initial = true
return self
end
function ArcadeDancerGroup:add_dancer()
if self.active >= self.max_dancers then return end
self.active = self.active + 1
self.slots[self.active]:begin_enter(self.gframe)
end
function ArcadeDancerGroup:remove_dancer()
if self.active <= (self.min_dancers or 1) then return end
self.slots[self.active]:begin_leave(self.gframe)
self.active = self.active - 1
end
-- Set the absolute troupe count; grows/shrinks slot by slot.
function ArcadeDancerGroup:set_dancer_count(n)
local lo = self.min_dancers or 1
if n < lo then n = lo elseif n > self.max_dancers then n = self.max_dancers end
while self.active < n do self:add_dancer() end
while self.active > n do self:remove_dancer() end
end
function ArcadeDancerGroup:update(current_time_ms, bpm)
if self.now == nil then self.now = current_time_ms end
local dt = current_time_ms - self.now
if dt < 0 then dt = 0 end
self.now = current_time_ms
-- one advance, one clock; BPM changes retime the whole troupe at once
local frame_ms = (60000 / ((bpm and bpm > 0) and bpm or 120)) / BEAT_FRAMES
self.gframe = self.gframe + dt / frame_ms
if self.needs_initial then
self.needs_initial = false
-- gframe is still 0, so all three get enter = 0 / join = dance and hop in together
self:set_dancer_count(self.min_dancers or 1)
end
local g = self.gframe
for i = 1, self.max_dancers do
local d = self.slots[i]
if d.state == "in" and g >= d.join then
d.state = "dance"
elseif d.state == "out" and g >= d.exit + (d.vd.n - d.vd.out) then
d.state = "idle"
end
end
end
function ArcadeDancerGroup:draw()
local pos = self.data.pos
local py = self.data.posy
local g = self.gframe
-- far dancers first so the centre one ends up in front
for i = self.max_dancers, 1, -1 do
local d = self.slots[i]
if d and d.state ~= "idle" then d:draw(pos[i] or 960, py, g) end
end
end
function ArcadeDancerGroup:destroy()
tex.unload_folder(self.path, "dancer/" .. self.name)
end
-- Factory: maps dancer index 0-20 to the appropriate group class
local Dancer = {}
Dancer.Base = BaseDancer
Dancer.BaseDancerGroup = BaseDancerGroup
Dancer.ArcadeDancerGroup = ArcadeDancerGroup
function Dancer.create(index, bpm, path, max_dancers)
max_dancers = max_dancers or 5
-- the normal gameplay backdrop uses the arcade rig; collab backdrops keep the parent's classes
if (path or "background") == "background" then
return ArcadeDancerGroup.new(index, bpm, max_dancers, path)
end
local map = {
DancerGroup0, DancerGroup0, DancerGroup0,
BaseDancerGroup, BaseDancerGroup, BaseDancerGroup,
BaseDancerGroup,
DancerGroupPoof1, DancerGroupPoof1,
BaseDancerGroup, BaseDancerGroup, BaseDancerGroup,
DancerGroupPoof2, DancerGroupPoof2,
BaseDancerGroup, BaseDancerGroup,
DancerGroupPoof2,
BaseDancerGroup, BaseDancerGroup, BaseDancerGroup, BaseDancerGroup
}
local cls = map[index + 1]
return cls.new(index, bpm, max_dancers, path)
end
return Dancer