318 lines
12 KiB
Lua
318 lines
12 KiB
Lua
local Timing = require("background/bg_objects/timing")
|
|
local pose_at = Timing.pose_at
|
|
|
|
-- BaseDancer: a background dancer. It enters (from its N_start frames), dances a
|
|
-- looping routine (N_loop) in step with the rest of its group, and leaves (N_end) when
|
|
-- the gauge drops. Each part is a pose table (see timing.lua), timed in beats:
|
|
-- at: { x, y } where the routine stands; poses are offsets from the origin, x drawn relative to it
|
|
-- enter: { beats = length, poses... } or { beats = length, layer, layer, ... }
|
|
-- dance: a pose table (usually Timing.steps of phrases), or a list of layers
|
|
-- leave: like enter (optional: without it the dancer just disappears)
|
|
-- a part can carry `move`: a pose table whose x, y is added to all its layers, for a path
|
|
-- that glides across frame changes (spinning while hopping in)
|
|
-- origin: { x, y } where the dancer's origin (its anchor point) sits in its images (plus `loop = { x, y }` etc.
|
|
-- for keys whose images differ). Texture.json puts that
|
|
-- point at the dancer's place on screen; poses move it, and it's the pivot for
|
|
-- scaling and turning.
|
|
-- A layer is a pose table drawn from `key` ("start", "loop", "end" or "_name" for this
|
|
-- dancer's own textures, else a shared key such as "ink_l"), with its own `origin` if it differs
|
|
-- and a `blend` (e.g. "additive") its poses can turn off with blend = false.
|
|
-- Dancer.define(specs) makes a class for BaseDancerGroup; specs[index] or specs.default.
|
|
local BaseDancer = {}
|
|
BaseDancer.__index = BaseDancer
|
|
|
|
local DEFAULT_KEY = { enter = "start", dance = "loop", leave = "end" }
|
|
|
|
-- a part is either one layer (a pose table) or a list of layers
|
|
-- a part's `beats` ends its layers, so a last pose's `to` glide finishes on time
|
|
local function layers(part)
|
|
if not part then return {} end
|
|
local list = (type(part[1]) == "table" and type(part[1][1]) == "number") and { part } or part
|
|
for _, layer in ipairs(list) do layer.length = layer.length or part.beats end
|
|
return list
|
|
end
|
|
|
|
function BaseDancer.new(cls, name, index, bpm, path)
|
|
local self = setmetatable({}, cls)
|
|
self.name = name
|
|
self.index = index
|
|
self.path = path
|
|
self.spec = cls.specs[index] or cls.specs.default
|
|
if self.spec.plain then -- not ported yet: step through the loop frames every half beat
|
|
local info = tex.get_texture_info(name, index .. "_loop")
|
|
local frames = {}
|
|
for f = 0, (info and info.frame_count or 1) - 1 do frames[#frames + 1] = { f, 0.5 } end
|
|
self.spec = { enter = { beats = 0 }, dance = Timing.steps(frames) }
|
|
end
|
|
self.state = nil -- nil (hidden), "enter", "dance", "leave"
|
|
self.beat = 60000 / bpm
|
|
self.ids = {}
|
|
self.parts = {
|
|
enter = layers(self.spec.enter),
|
|
dance = layers(self.spec.dance),
|
|
leave = layers(self.spec.leave),
|
|
}
|
|
return self
|
|
end
|
|
|
|
function BaseDancer:texture(key)
|
|
if key == "start" or key == "loop" or key == "end" then key = self.index .. "_" .. key end
|
|
if key:sub(1, 1) == "_" then key = self.index .. key end -- this dancer's own, e.g. "_poof"
|
|
if not self.ids[key] then
|
|
self.ids[key] = { id = tex.get_texture(self.path .. "/" .. self.name .. "/" .. key) or 0,
|
|
info = tex.get_texture_info(self.name, key) }
|
|
end
|
|
return self.ids[key]
|
|
end
|
|
|
|
function BaseDancer:play(state, now)
|
|
self.state, self.t0 = state, now
|
|
end
|
|
|
|
function BaseDancer:start() self.start_now = true end
|
|
|
|
function BaseDancer:leave()
|
|
if self.spec.leave then self.leave_now = true else self.state = nil end
|
|
end
|
|
|
|
function BaseDancer:update(current_time_ms, bpm)
|
|
local now = current_time_ms
|
|
if bpm then
|
|
local beat = 60000 / bpm
|
|
if self.t0 and beat ~= self.beat then -- keep our place in the part at the new tempo
|
|
self.t0 = now - (now - self.t0) / self.beat * beat
|
|
end
|
|
self.beat = beat
|
|
end
|
|
if self.start_now then self.start_now = nil; self:play("enter", now) end
|
|
if self.leave_now then self.leave_now = nil; self:play("leave", now) end
|
|
self.now = now
|
|
if not self.state then return end
|
|
local t = (now - self.t0) / self.beat
|
|
if self.state == "enter" and t >= self.spec.enter.beats then
|
|
self:play("dance", now)
|
|
elseif self.state == "leave" and t >= self.spec.leave.beats then
|
|
self.state = nil
|
|
end
|
|
end
|
|
|
|
-- beats into the current part; the routine follows the group's clock
|
|
function BaseDancer:part_time()
|
|
if self.state == "dance" then
|
|
local t = self.group and self.group:dance_clock(self.now, self.beat) or (self.now - self.t0) / self.beat
|
|
local period = self.parts.dance[1].length or self.spec.dance.beats
|
|
return t % period
|
|
end
|
|
return (self.now - self.t0) / self.beat
|
|
end
|
|
|
|
local NO_AT, NO_ORIGIN = { 0, 0 }, { 0, 0 }
|
|
|
|
-- where the dancer's origin sits in `key`'s images: spec.origin, or its entry for that key
|
|
function BaseDancer:origin(key)
|
|
local o = self.spec.origin or NO_ORIGIN
|
|
return o[key] or o
|
|
end
|
|
|
|
-- draw_texture reads these straight away, so every dancer shares one pair
|
|
local PARAMS, ORIGIN = {}, { 0, 0 }
|
|
-- x: where the group puts this dancer's routine (left edge of its loop texture)
|
|
function BaseDancer:draw(x)
|
|
if not self.state then return end
|
|
local t, at = self:part_time(), self.spec.at or NO_AT
|
|
local move = self.spec[self.state].move and pose_at(self.spec[self.state].move, t)
|
|
local mx, my = move and move.x or 0, move and move.y or 0
|
|
for _, layer in ipairs(self.parts[self.state]) do
|
|
local pose = pose_at(layer, t)
|
|
if pose then
|
|
local tx = self:texture(pose.key or layer.key or DEFAULT_KEY[self.state])
|
|
local info = tx.info
|
|
if info and info.width then
|
|
local w, h = info.width, info.height
|
|
local W, H = w * math.abs(pose.sx), h * pose.sy
|
|
local o = layer.origin or self:origin(pose.key or layer.key or DEFAULT_KEY[self.state])
|
|
ORIGIN[1], ORIGIN[2] = o[1] * math.abs(pose.sx), o[2] * pose.sy
|
|
PARAMS.frame = pose.frame
|
|
PARAMS.x = x + self:origin("loop")[1] + (pose.x or at[1]) - at[1] + mx
|
|
PARAMS.y = (pose.y or at[2]) + my
|
|
PARAMS.x2 = W - w
|
|
PARAMS.y2 = H - h
|
|
PARAMS.origin = ORIGIN
|
|
PARAMS.rotation = pose.rot
|
|
PARAMS.mirror = (pose.mirror or pose.sx < 0) and "horizontal" or nil
|
|
PARAMS.fade = pose.alpha
|
|
PARAMS.blend = (pose.blend == nil and layer.blend or pose.blend) or nil
|
|
PARAMS.color = nil -- donbg's dark wrapper may have set one
|
|
tex.draw_texture(tx.id, PARAMS)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- A dancer class for BaseDancerGroup from specs (by dancer index, or `default`)
|
|
local function define(specs)
|
|
local cls = setmetatable({ specs = specs }, { __index = BaseDancer })
|
|
cls.__index = cls
|
|
cls.new = function(...) return BaseDancer.new(cls, ...) end
|
|
return cls
|
|
end
|
|
|
|
local PLAIN = define({ default = { plain = true } })
|
|
|
|
-- 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
|
|
|
|
-- dancer_class: a class from Dancer.define; collabs not ported yet get the plain dancer
|
|
function BaseDancerGroup.new(index, bpm, max_dancers, path, dancer_class)
|
|
local self = setmetatable({}, BaseDancerGroup)
|
|
dancer_class = dancer_class or PLAIN
|
|
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
|
|
local dancer = dancer_class.new(self.name, variant_idx, bpm, self.path)
|
|
dancer.group = self
|
|
table.insert(self.dancers, dancer)
|
|
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
|
|
|
|
-- Draws dancers on fixed slot centres instead of even spacing
|
|
function BaseDancerGroup:draw_slots(centers, order)
|
|
local screen_scale = tex.get_screen_scale()
|
|
for _, i in ipairs(order) do
|
|
local dancer = self.active_dancers[i]
|
|
if dancer then
|
|
local info = tex.get_texture_info(self.name, tostring(dancer.index) .. "_loop")
|
|
dancer:draw(math.floor(centers[i] * screen_scale - info.width / 2))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Beats since the first dancer started dancing: the routine's shared clock, so dancers that
|
|
-- join later dance in step with the rest. Re-anchored when the bpm changes so it doesn't jump.
|
|
function BaseDancerGroup:dance_clock(now, beat)
|
|
local c = self.clock
|
|
if not c then
|
|
c = { t0 = now, beat = beat }
|
|
self.clock = c
|
|
elseif c.beat ~= beat then
|
|
c.t0, c.beat = now - (now - c.t0) / c.beat * beat, beat
|
|
end
|
|
return (now - c.t0) / beat
|
|
end
|
|
|
|
-- Like remove_dancer, but the dancer stays in its slot to play its exit
|
|
function BaseDancerGroup:leave_dancer()
|
|
if self.active_count > 1 then
|
|
self.active_dancers[self.spawn_positions[self.active_count] + 1]:leave()
|
|
self.active_count = self.active_count - 1
|
|
end
|
|
end
|
|
|
|
function BaseDancerGroup:destroy()
|
|
tex.unload_folder(self.path, "dancer/" .. self.name)
|
|
end
|
|
|
|
local SETS = require("background/bg_objects/dancer/sets")
|
|
|
|
local Dancer = {}
|
|
Dancer.Base = BaseDancer
|
|
Dancer.BaseDancerGroup = BaseDancerGroup
|
|
Dancer.define = define
|
|
Dancer.Plain = PLAIN
|
|
|
|
-- The default dancer sets (dancer_0..20), from the hand-written specs in dancer/sets.lua
|
|
function Dancer.create(index, bpm, path, max_dancers)
|
|
return BaseDancerGroup.new(index, bpm, max_dancers or 5, path, define(SETS[index]))
|
|
end
|
|
|
|
return Dancer
|