Files
PyTaikoGreen/Scripts/bg_objects/dancer.lua
T
2026-04-27 16:19:26 -04:00

401 lines
14 KiB
Lua

-- 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(self.name, tostring(self.index) .. "_start", {
frame = self.s_texture_change.attribute, x = x, y = y
})
else
tex.draw_texture(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, ease_out = "quadratic", delay = duration * 2
})
self.bounce_down = anim.move(duration, {
total_distance = 20, ease_in = "quadratic", delay = duration * 3
})
self.bounce_up:start()
self.bounce_down:start()
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.name, "4_start", {
frame = 7, x = x, y = (-50 * screen_scale) + sy
})
tex.draw_texture(self.name, "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.name, "4_loop", { frame = 54, x = x, y = by })
elseif attr >= 5 and attr <= 8 then
tex.draw_texture(self.name, "4_loop", { frame = 56, x = x, y = by })
elseif attr == 4 then
tex.draw_texture(self.name, "4_loop", { frame = 55, x = x, y = by })
end
tex.draw_texture(self.name, "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()
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.name, "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(self.name, tostring(self.index) .. "_start", {
frame = self.s_texture_change.attribute, x = x, y = sy
})
else
tex.draw_texture(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(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.width 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
-- Factory: maps dancer index 0-20 to the appropriate group class
local Dancer = {}
Dancer.Base = BaseDancer
Dancer.BaseDancerGroup = BaseDancerGroup
function Dancer.create(index, bpm, path, max_dancers)
max_dancers = max_dancers or 5
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