681 lines
27 KiB
Lua
681 lines
27 KiB
Lua
-- YataiDON-HSS override of PyTaikoGreen/background/bg_objects/dancer.lua: Dancer0_4 bounce 20*sc; dancer spacing from info.x2[1]
|
|
-- round 11: the normal-gameplay dancers are now the reference cabinet rig (bottom of file).
|
|
-- BaseDancer: single dancer that starts off-screen and bounces in, then loops
|
|
local BaseDancer = {}
|
|
BaseDancer.__index = BaseDancer
|
|
|
|
function BaseDancer.new(name, index, bpm)
|
|
local self = setmetatable({}, BaseDancer)
|
|
self.name = name
|
|
self.index = index
|
|
self.bpm = bpm
|
|
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_id(self.name, tostring(self.index) .. "_start"), {
|
|
frame = self.s_texture_change.attribute, x = x, y = y
|
|
})
|
|
else
|
|
tex.draw_texture(tex.get_id(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)
|
|
local self = setmetatable({}, Dancer0_4)
|
|
local base = BaseDancer.new(name, index, bpm)
|
|
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_id(self.name, "4_start") or 0
|
|
self._id_4_loop = tex.get_id(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)
|
|
local self = setmetatable({}, DancerPoof)
|
|
local base = BaseDancer.new(name, index, bpm)
|
|
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_id(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)
|
|
local self = setmetatable({}, DancerPoof2)
|
|
local base = DancerPoof.new(name, index, bpm)
|
|
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_id(self.name, tostring(self.index) .. "_start"), {
|
|
frame = self.s_texture_change.attribute, x = x, y = sy
|
|
})
|
|
else
|
|
tex.draw_texture(tex.get_id(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_id(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))
|
|
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),
|
|
BaseDancer.new(self.name, 1, bpm),
|
|
BaseDancer.new(self.name, 2, bpm),
|
|
BaseDancer.new(self.name, 3, bpm),
|
|
Dancer0_4.new(self.name, 4, bpm)
|
|
}
|
|
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
|
|
|
|
-- ============================================================================
|
|
-- ROUND 11 -- reference cabinet dancers (enso_normal/enso/dancer/dancer_*.nulm)
|
|
-- ----------------------------------------------------------------------------
|
|
-- Everything above this line is the PyTaikoGreen rig, kept verbatim because the
|
|
-- collab backdrops (a3.lua, funassyi.lua) still build their dancers out of it.
|
|
-- The NORMAL gameplay backdrop now uses the classes below instead, which replay
|
|
-- the cabinet's own dancer timelines: every arcade dancer frame is a single
|
|
-- shape placement with an affine transform (no skeletal deformation), so the
|
|
-- rig ports exactly -- the shapes become the frames of `<v>_loop` and the
|
|
-- per-frame {slot,tx,ty,sx,sy,rot,alpha} rows live in
|
|
-- Scripts/background/dancer_data/dancer_<g>.lua (generated).
|
|
--
|
|
-- Timing: the clips are authored at 60 fps with every pose boundary on a
|
|
-- multiple of 20 frames and every loop an exact multiple of 80 frames, i.e. a
|
|
-- beat is 40 frames (90 BPM). We replay them at BEAT_FRAMES per beat so the
|
|
-- dancers stay locked to the song.
|
|
--
|
|
-- ROUND 15 (r15-dancer) -- "dancer的舞步應該要同步". The round-11 rig gave
|
|
-- every slot its OWN `fpos` accumulator, started at whatever wall-clock moment
|
|
-- `add_dancer()` happened to fire (a soul-gauge milestone), so the five dancers
|
|
-- ran at five unrelated phases: slot 1 could be mid-hop while slot 2 was
|
|
-- landing. That is not what the cabinet does, and the cabinet PHYSICALLY
|
|
-- CANNOT do it. Measured, from the arcade movies themselves
|
|
-- (`lumen_anim_dump.exe dancer_*.nulm --sprite <root> --all`):
|
|
--
|
|
-- * the root timeline places all five slots `pos0..pos4` on frame 0 and has
|
|
-- a bare stop() there, so the root never advances and never re-places
|
|
-- anything -- root frames 0..4 are identical display lists;
|
|
-- * every slot is an instance of the SAME one-frame "slot" sprite, which
|
|
-- itself carries ALL of that movie's `DancerN` variant clips at once
|
|
-- (tetsuo: 5 slots x 5 variants = 25 dancer sprite instances live at all
|
|
-- times), and the game picks what you see with visibility, not the timeline;
|
|
-- * dumped densely, all 25 instances report the SAME frame cursor on every
|
|
-- single stage frame ("f":0,1,2,... in lockstep).
|
|
--
|
|
-- So the arcade rig has exactly ONE playhead for the whole troupe. A per-slot
|
|
-- phase does not exist in the data, which is why the steps are always together.
|
|
-- This port now models that directly: the GROUP owns `gframe` and every slot
|
|
-- reads it. Sync is structural, not maintained -- there is no second clock to
|
|
-- drift against.
|
|
--
|
|
-- Entering / leaving. Each variant clip is `in` [0,80) -> `dance` [80,out) ->
|
|
-- `out` [out,n). `in` is 80 frames in all six movies, and 80 frames is a pose
|
|
-- boundary at every multiple inside every dance loop (measured: run starts land
|
|
-- on 0, 80, 160, ... for all six), so 80 is the phrase quantum. A dancer that
|
|
-- joins therefore begins its hop so that it LANDS on a phrase boundary of the
|
|
-- shared loop, and a dancer that leaves peels off at the next phrase boundary.
|
|
-- Worst-case wait is one phrase (2 beats), and the joiner is on the group's own
|
|
-- frame number from its first danced frame -- never near it, exactly it.
|
|
local BEAT_FRAMES = 40
|
|
local ARCADE_GROUPS = 6
|
|
local PHRASE = 80
|
|
|
|
local ArcadeDancer = {}
|
|
ArcadeDancer.__index = ArcadeDancer
|
|
|
|
function ArcadeDancer.new(group_name, variant, vd)
|
|
local self = setmetatable({}, ArcadeDancer)
|
|
self.vd = vd
|
|
self.tex = tex.get_id(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. Without this it would fall into the `out` branch below and
|
|
-- be drawn from the dance loop it has not joined yet.
|
|
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 (texture.cpp:525), so every
|
|
-- coordinate here is in the child's own 1920x1080 space -- do NOT multiply
|
|
-- by tex.get_screen_scale(), which is the 1280x720 PARENT-space factor.
|
|
for i = 1, #row do
|
|
local d = row[i]
|
|
local sx, sy = d[4], d[5]
|
|
-- every frame is padded to the same size (the engine draws them all at
|
|
-- frame 0's) but keeps its OWN origin, which `origin` supplies per draw.
|
|
local o = vd.o[d[1] + 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 = { o[1] * sx, o[2] * sy },
|
|
rotation = d[6],
|
|
fade = d[7],
|
|
})
|
|
end
|
|
end
|
|
|
|
local ArcadeDancerGroup = {}
|
|
ArcadeDancerGroup.__index = ArcadeDancerGroup
|
|
|
|
function ArcadeDancerGroup.new(index, bpm, max_dancers, path)
|
|
local self = setmetatable({}, ArcadeDancerGroup)
|
|
local g = index % ARCADE_GROUPS
|
|
self.name = "dancer_" .. g
|
|
self.path = path or "background"
|
|
self.data = require("background/dancer_data/dancer_" .. g)
|
|
tex.load_folder(self.path, "dancer/" .. self.name)
|
|
|
|
-- pos0..pos4 on the cabinet are centre, left, right, far-left, far-right --
|
|
-- the same spawn order the parent rig used, so slot i is simply pos(i-1).
|
|
self.max_dancers = max_dancers or 5
|
|
-- ROUND 18 (r18-misc). The cabinet's troupe size is NOT "start at one and
|
|
-- earn the rest": `App::EnsoGraphicDancer::Process` (reference source
|
|
-- `EnsoGraphicDancer.obj.c:2723-2752`) computes, every frame,
|
|
-- r = gauge / clear_point
|
|
-- n = clamp(floor(2r + 3), 3, 5) -- when kParamTable's min3 flag is set
|
|
-- and hands `n` to `CheckDancerNum`. All SIX normal-enso troupes
|
|
-- (dancer_donko / _inu / _kame / _tetsuo / _omen / _unagi -- exactly the six
|
|
-- this class serves) carry that flag, so the floor really is THREE and the
|
|
-- very first Process call (gauge 0 => n = 3) makes slots 0,1,2 hop in
|
|
-- together. Only the collab `dance_b_*` troupes use the other branch,
|
|
-- floor(4r + 1) with a floor of one, and those do not come through here --
|
|
-- `Dancer.create` routes them to the parent PyTaikoGreen classes.
|
|
-- The count is what changed; the PHASE model (round 15) did not: a slot
|
|
-- added at gframe 0 goes through the same `begin_enter`, so the three
|
|
-- openers land on the same phrase boundary as any later joiner.
|
|
self.min_dancers = 3
|
|
self.now = nil
|
|
self.gframe = 0 -- THE playhead: one per troupe, exactly as in the movie
|
|
self.active = 0
|
|
self.slots = {}
|
|
|
|
local nvar = 0
|
|
for _ in pairs(self.data.variants) do nvar = nvar + 1 end
|
|
local pick = {}
|
|
for v = 0, nvar - 1 do pick[#pick + 1] = v end
|
|
for i = #pick, 2, -1 do
|
|
local j = math.random(1, i)
|
|
pick[i], pick[j] = pick[j], pick[i]
|
|
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])
|
|
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()
|
|
-- `CheckDancerNum` shrinks down to the value `Process` asked for, and for a
|
|
-- min3 troupe that value can never be below 3 -- so 3 is the floor here too.
|
|
-- (`min_dancers or 1` keeps the old behaviour if this class is ever reused
|
|
-- for a `dance_b_*` collab troupe, whose branch really does bottom out at 1.)
|
|
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
|
|
|
|
-- The cabinet's `CheckDancerNum(this, n)` (EnsoGraphicDancer.obj.c:2901-3008):
|
|
-- grow slot-by-slot in index order, shrink slot-by-slot in reverse. Expressing
|
|
-- the count ABSOLUTELY -- rather than as a milestone delta the caller has to
|
|
-- keep in sync -- is what stops the two from ever drifting apart.
|
|
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, so
|
|
-- a #BPMCHANGE cannot open a phase gap between slots either.
|
|
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 as the movie starts -- the cabinet's first `CheckDancerNum(0 -> 3)`.
|
|
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, matching the root
|
|
-- display-list depth order (pos4 deepest .. pos0 shallowest).
|
|
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 ported arcade rig; the collab
|
|
-- backdrops still ship the parent's art and 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
|