Files
PyTaikoGreen/Scripts/background/bg_objects/donbg/base.lua
T
2026-10-10 14:52:27 -04:00

231 lines
9.1 KiB
Lua

local Timing = require("background/bg_objects/timing")
local chain_moves, sum, Loop, pose_at = Timing.chain_moves, Timing.sum, Timing.Loop, Timing.pose_at
-- DonBGBase: the don band behind the lane. A stack of layers, each a texture
-- tiled across the band (frame 0 normally, frame 1 for the clear set, which fades in over the
-- normal set and back out over `fade` beats). Ambient, in ms. spec (optional): fade, layers =
-- list of { key, ... } drawn in order:
-- ms, dx, dy: drift dx, dy px every ms, looping (dx defaults to one tile left; no ms = still)
-- tile: x spacing of the copies (default the texture width); x: phase of the first copy
-- y: offset on top of texture.json, for one texture drawn as several rows
-- period, bob, squash, pulse: a loop of { ms, distance, ease } moves for the y offset, the
-- height taken off the top (feet stay planted) and the opacity in thousandths
-- clear_only: drawn only in the clear set (with frame 0); a one-frame layer is drawn in both
-- alpha, blend: the layer's opacity (default 1) and blend mode (e.g. "additive")
-- rotation: degrees clockwise about the texture's centre (texture.json x, y stay the top-left)
-- blink: a looping pose table in ms (Timing.steps): the layer shows its pose's frame, and is
-- hidden while no pose (or a `hide` one) is on
-- dark (optional): { min, ms }, the miss-streak colour and fade time (default 0.65, 300)
-- A band with its own grey art keeps it as frame 2 and calls set_miss instead of set_dark.
-- Without a spec the band is its background scrolling one tile every 3s.
local LEGACY = { layers = { { key = "background", ms = 3000 } } }
local DEFAULT_FADE = 0.25 -- beats
local DEFAULT_BEAT = 600 -- ms, until update() is given the bpm
local DARK_MIN, DARK_FADE_MS = 0.65, 300 -- miss streak colour and fade
DonBGBase = {}
DonBGBase.__index = DonBGBase
local function period(layer)
if layer.period then return layer.period end
local total = 0
for _, m in ipairs(layer.bob or {}) do total = total + m[1] end
return total
end
function DonBGBase.new(index, player_num, path, spec)
local self = setmetatable({}, DonBGBase)
self.name = string.format("%d_%d", index - 1, player_num)
self.path = path
tex.load_folder(path, string.format("donbg/%s", self.name))
self.bg_texture = tex.get_texture_info(self.name, "background")
-- kept for subclasses that draw their own layers
self.move = anim.move(3000, {
total_distance = -self.bg_texture.width,
loop = true
})
self.move:start()
spec = spec or LEGACY
self.fade_beats = spec.fade or DEFAULT_FADE
self.beat = DEFAULT_BEAT
self.is_clear = false
self.clear = 0
self.miss = 0
self.dark_min = spec.dark and spec.dark.min or DARK_MIN
self.dark_ms = spec.dark and spec.dark.ms or DARK_FADE_MS
self._id_bg = tex.get_texture(self.path .. "/" .. self.name .. "/" .. "background") or 0
local scale = tex.get_screen_scale()
self.layers = {}
for _, l in ipairs(spec.layers) do
local info = tex.get_texture_info(self.name, l.key) or {}
local tile = math.floor((l.tile or info.width or 0) * scale)
local layer = {
l = l,
_id = tex.get_texture(self.path .. "/" .. self.name .. "/" .. l.key) or 0,
tile = tile,
x = math.floor((l.x or 0) * scale),
y = math.floor((l.y or 0) * scale),
frames = info.frame_count or 1,
}
if l.rotation then
layer.origin = { (info.width or 0) * scale / 2, (info.height or 0) * scale / 2 }
end
if l.ms then
layer.drift_x = anim.move(l.ms, { total_distance = l.dx and math.floor(l.dx * scale) or -tile, loop = true })
layer.drift_x:start()
if l.dy then
layer.drift_y = anim.move(l.ms, { total_distance = math.floor(l.dy * scale), loop = true })
layer.drift_y:start()
end
end
if l.bob or l.squash or l.pulse then
layer.loop = Loop.new(period(l), function()
return {
bob = chain_moves(l.bob or {}, 1, scale),
squash = chain_moves(l.squash or {}, 1, scale),
pulse = chain_moves(l.pulse or {}, 1, 1),
}
end)
end
table.insert(self.layers, layer)
end
return self
end
-- miss streak: the band is multiplied towards black (colour of draw_texture), eased in/out
function DonBGBase:set_dark(on)
self.dark_target = on and 1 or 0
end
function DonBGBase:update_dark(current_time_ms)
local dt = current_time_ms - (self.dark_last_ms or current_time_ms)
self.dark_last_ms = current_time_ms
local d, target = self.dark or 0, self.dark_target or 0
local step = dt / self.dark_ms
self.dark = d + math.max(-step, math.min(step, target - d))
end
function DonBGBase:dark_color()
if not self.dark or self.dark <= 0 then return nil end
local v = math.floor(255 * (1 - (1 - self.dark_min) * self.dark))
return { v, v, v, 255 }
end
-- miss streak with a grey set (frame 2): fades in over the normal set like the clear set,
-- only while not cleared
function DonBGBase:set_miss(on)
self.miss_on = on
end
-- fades the clear set in, or back out, from wherever the fade is
function DonBGBase:set_clear(is_clear)
if is_clear == self.is_clear then return end
self.is_clear = is_clear
self.clear_fade = anim.fade(self.fade_beats * self.beat, {
initial_opacity = self.clear,
final_opacity = is_clear and 1 or 0,
})
self.clear_fade:start()
end
function DonBGBase:update(current_time_ms, bpm)
if bpm then self.beat = 60000 / bpm end
local dt = current_time_ms - (self.now or current_time_ms)
self.now = current_time_ms
local target = (self.miss_on and not self.is_clear) and 1 or 0
local step = dt / (self.fade_beats * self.beat)
self.miss = self.miss + math.max(-step, math.min(step, target - self.miss))
self:update_dark(current_time_ms)
self.move:update(current_time_ms)
if self.clear_fade then
self.clear_fade:update(current_time_ms)
self.clear = self.clear_fade.attribute
end
for _, layer in ipairs(self.layers) do
if layer.drift_x then layer.drift_x:update(current_time_ms) end
if layer.drift_y then layer.drift_y:update(current_time_ms) end
if layer.loop then layer.loop:update(current_time_ms) end
end
end
local function loop_sum(layer, key)
local anims = layer.loop and layer.loop.anims
return anims and sum(anims[key]) or 0
end
local NO_ORIGIN = { 0, 0 }
function DonBGBase:draw_textures(fade, y, frame)
local screen_width = tex.get_screen_width()
for _, layer in ipairs(self.layers) do
local l = layer.l
local blink = l.blink and pose_at(l.blink, (self.now or 0) % l.blink.length)
if (not l.clear_only or frame == 1) and (not l.blink or blink) then
local tile = layer.tile
local x = layer.x + (layer.drift_x and layer.drift_x.attribute or 0)
x = tile > 0 and (x % tile) - 2 * tile or x
local squash = loop_sum(layer, "squash")
local f = fade * (l.alpha or 1) * (1 + loop_sum(layer, "pulse") / 1000)
local ly = y + layer.y + (layer.drift_y and layer.drift_y.attribute or 0) + loop_sum(layer, "bob") + squash
local o = layer.origin or NO_ORIGIN
local P = layer._p
if not P then P = {}; layer._p = P end -- per layer, refilled below: no table per tile per frame
repeat
P.frame = blink and blink.frame or l.clear_only and 0 or math.min(frame, layer.frames - 1)
P.fade = f
P.x = x + o[1]
P.y = ly + o[2]
P.y2 = l.squash and -squash or nil
P.rotation = l.rotation
P.origin = layer.origin
P.blend = l.blend
P.color = nil -- draw_dark's wrapper sets it per call
tex.draw_texture(layer._id, P)
x = x + tile
until tile <= 0 or x >= screen_width
end
end
end
function DonBGBase:draw_band(y)
y = y or 0
self:draw_textures(1.0, y, 0)
if self.miss > 0 then self:draw_textures(self.miss, y, 2) end
if self.clear > 0 then
self:draw_textures(self.clear, y, 1)
end
end
-- Draws the band through `inner`, multiplying every texture it draws by the miss-streak
-- darkness. Subclasses draw with their own tex.draw_texture calls, so wrap that once here.
function DonBGBase:draw_dark(inner, y)
local c = self:dark_color()
if not c then return inner(self, y) end
local orig = tex.draw_texture
tex.draw_texture = function(id, p)
p = p or {}
p.color = c
return orig(id, p)
end
local ok, err = pcall(inner, self, y)
tex.draw_texture = orig
if not ok then error(err, 0) end
end
function DonBGBase:draw(y)
self:draw_dark(DonBGBase.draw_band, y or 0)
end
function DonBGBase:destroy()
tex.unload_folder(self.path, string.format("donbg/%s", self.name))
end
return DonBGBase