Files
PyTaikoGreen/Scripts/background/bg_collabs/tonkatsudj.lua
T
2026-10-10 09:54:09 -04:00

323 lines
13 KiB
Lua

local DonBGBase = require("background/bg_objects/donbg/base")
local Fever = require("background/bg_objects/fever")
local RendaController = require("background/bg_objects/renda")
local ChibiController = require("background/bg_objects/chibi")
local Timing = require("background/bg_objects/timing")
local pose_at = Timing.pose_at
local COLLAB_PATH = "background/collab/tonkatsudj"
-- No footer: Lumen has no dodai, the crowd runs to the bottom of the screen.
-- No dancers or fever line-up in Lumen: the line-up is a random default one.
-- Chibis are the default chibi_a_01 (same art and motion), loaded from "background".
-- Renda is renda_a_01's flight with Shibukatsu's own pictures.
-- Lumen clocks run at 60 frames a second; these loops are ambient, counted in Lumen frames
local function lumen_frame(ms) return ms * 60 / 1000 end
-- Don band: Shibukatsu tiles scroll one tile left every 5.33s under a row of logo heads that
-- scroll at their own pace (a 413.8px spacing in the same time) and tick 2 degrees every
-- 25 frames. Clearing fades in the yellow tiles, with sparkles twinkling on them in three
-- groups; a miss streak (only while not cleared) fades in the grey set. Ambient, in ms.
local DONBG = { layers = { { key = "background", ms = 5333 } } }
local HEAD_SPACING = 413.8
local HEAD_MS = 5333
local HEAD_SIZE = { 160, 140 }
local HEAD_TICK = {
{ 0, 0, 0, 0, to = { 0, 0, rot = 2 } },
{ 4, 0, 0, 0, rot = 2 },
{ 5, 0, 0, 1, to = { 0, 0 } }, -- snaps back with a 1px dip
{ 7, 0, 0, 0 },
length = 25,
}
-- sparkles on the clear tiles: { x, y, group } centres, scrolling with the tiles; the six
-- repeat 724px on. Each group grows a sparkle from 10% over 9 frames, then fades it in 7,
-- groups starting at frames 0, 25 and 67 of a 100-frame loop.
local SPARKLE_SIZE = 50
local SPARKLE_REPEAT = 724
local SPARKLES = {
{ 61, 156, 1 }, { 194, 50, 2 }, { 299, 138, 3 }, { 427, 42, 1 }, { 512, 157, 2 }, { 658, 45, 3 },
}
local SPARKLE_START = { 0, 25, 67 }
local TWINKLE = {
{ 0, 0, 0, 0, scale = 0.1, to = { 0, 0, scale = 1 } },
{ 9, 0, 0, 0, to = { 0, 0, alpha = 0 } },
{ 16, 0, hide = true },
length = 100,
}
local TonkatsudjDonBG = setmetatable({}, { __index = DonBGBase })
TonkatsudjDonBG.__index = TonkatsudjDonBG
function TonkatsudjDonBG.new(player_num, path)
local self = setmetatable(DonBGBase.new(1, player_num, path, DONBG), TonkatsudjDonBG)
local function id(key) return tex.get_texture(path .. "/" .. self.name .. "/" .. key) or 0 end
self._id_heads = id("overlay")
self._id_sparkle = id("star")
self.head_drift = anim.move(HEAD_MS, { total_distance = -HEAD_SPACING, loop = true })
self.head_drift:start()
return self
end
function TonkatsudjDonBG:update(current_time_ms, bpm)
DonBGBase.update(self, current_time_ms, bpm)
self.head_drift:update(current_time_ms)
end
function TonkatsudjDonBG:draw_heads(fade, y, frame)
local f = lumen_frame(self.now or 0)
local tick = pose_at(HEAD_TICK, f % HEAD_TICK.length)
local w, h = HEAD_SIZE[1], HEAD_SIZE[2]
local x = self.head_drift.attribute % HEAD_SPACING - HEAD_SPACING
while x < tex.get_screen_width() do
tex.draw_texture(self._id_heads, {
frame = frame, fade = fade, x = x + w / 2, y = y + tick.y + h / 2,
origin = { w / 2, h / 2 }, rotation = tick.rot,
})
x = x + HEAD_SPACING
end
end
function TonkatsudjDonBG:draw_sparkles(fade, y)
local f = lumen_frame(self.now or 0)
local drift = self.layers[1].drift_x.attribute
for _, s in ipairs(SPARKLES) do
local p = pose_at(TWINKLE, (f - SPARKLE_START[s[3]]) % TWINKLE.length)
if p then
local size = SPARKLE_SIZE * p.sx
for copy = 0, 1 do
tex.draw_texture(self._id_sparkle, {
x = s[1] + copy * SPARKLE_REPEAT + drift - size / 2, y = y + s[2] - size / 2,
x2 = size - SPARKLE_SIZE, y2 = size - SPARKLE_SIZE, fade = fade * p.alpha,
})
end
end
end
end
-- frame 0 normal, 1 clear (yellow tiles, sparkles), 2 miss (grey tiles and heads)
function TonkatsudjDonBG:draw_textures(fade, y, frame)
DonBGBase.draw_textures(self, fade, y, frame)
self:draw_heads(fade, y, frame == 2 and 1 or 0)
if frame == 1 then self:draw_sparkles(fade, y) end
end
-- Stage: a DJ booth under a screen cycling five manga panels (each crossfading in over 5
-- frames, 99 frames apiece and 109 for the first), the DJ bobbing behind the table, two
-- dim beams swinging 10 degrees from the screen, the crowd bobbing 10px, four stage beams
-- sweeping from the side lamps, yellow bursts flashing in four groups and a band of sparkles
-- turning past like a carousel. Ambient, every loop counted in Lumen frames.
-- { frame, Lumen frame it starts fading in }, in a 505-frame cycle
local SCREEN_CYCLE = 505
local SCREEN_FADE = 5
local SCREENS = { { 1, 25 }, { 2, 124 }, { 3, 223 }, { 4, 322 }, { 0, 421 } }
local DJ = {
{ 0, 0, 0, 0 }, { 12, 0, 0, -1 }, { 24, 0, 0, 0 },
{ 50, 1, 0, 0 }, { 63, 1, 0, -1 }, { 76, 1, 0, 0 }, { 88, 1, 0, -1 },
length = 100,
}
local CROWD_BOB = { { 0, 0, 0, 10, to = { 0, 0 } }, { 18, 0, 0, 0, to = { 0, 10 } }, length = 25 }
-- Beams: the lamp (the image's narrow right end, centred) stays put while the beam swings
-- between two angles and lengths and back, linearly, over `frames` each way. Lumen keeps
-- the beam's width horizontal as it swings (a skew); a rotation is close enough.
-- { lamp x, lamp y, from angle, to angle, from length, to length, width, frames, mirror }
local BEAM_SIZE = { 660, 218 }
local DIM_ALPHA = 0.8
local DIM_BEAMS = {
{ 396, 344.3, -90, -100, 0.7, 0.71, 0.648, 50 },
{ 872.3, 346.1, -90, -80, 0.7, 0.754, 0.7, 50 },
}
local STAGE_BEAMS = {
{ 1193, 303.4, -70, -97, 0.829, 0.786, 0.8, 75 },
{ 91, 303.4, -110, -83, 0.829, 0.786, 0.8, 75, true },
{ 269.5, 157.7, -72.7, -123.1, 1.047, 1.195, 1, 150 },
{ 1012.5, 157.7, -107.3, -56.9, 1.047, 1.195, 1, 150, true },
}
-- Bursts: { group, centre x, centre y, size, rotation, mirror }. A group flashes in over 3
-- frames, holds, drops to half and goes, starting at frames 0, 11, 24 and 34 of 50.
local BURST_SIZE = { 118, 126 }
local BURST_START = { 0, 11, 24, 34 }
local BURSTS = {
{ 1, 237.4, 447.05, 0.5, 0, true }, { 1, 1029.35, 655, 0.6, 90 },
{ 2, 897.35, 504, 0.8, -90 }, { 2, 251.4, 667, 0.6, -90 },
{ 3, 1194.95, 453.05, 1, 0, true }, { 3, 686, 667, 0.6, 90 }, { 3, 67.05, 431.05, 0.6, 90 },
{ 4, 71.05, 584.45, 0.5, 0 }, { 4, 478, 642.95, 0.5, 0 }, { 4, 1240.95, 521.45, 0.5, 0 },
}
local FLASH = {
{ 0, 0, 0, 0, alpha = 0, to = { 0, 0, alpha = 1 } },
{ 3, 0, 0, 0 },
{ 14, 0, 0, 0, alpha = 0.5 },
{ 15, 0, hide = true },
length = 50,
}
-- Sparkle carousel: five copies a pass apart (100 frames) ride 1740px right to left over 500
-- frames, widest (1.4x), lowest and brightest at the edges and narrowest, highest (68px up)
-- and dimmest (30%) in the middle. Lumen's later copies are a little brighter.
local CAROUSEL = { copies = 5, frames = 500, x = 640, half = 870, y = 524.2, dip = 68.3 }
local SPARKLE_FIELD = { 316, 330 }
local TonkatsudjBGNormal = {}
TonkatsudjBGNormal.__index = TonkatsudjBGNormal
function TonkatsudjBGNormal.new()
local self = setmetatable({}, TonkatsudjBGNormal)
self.name = "bg_0"
tex.load_folder(COLLAB_PATH, "bg_normal/" .. self.name)
local function id(key) return tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. key) or 0 end
self._id_bg = id("background")
self._id_screen = id("screen")
self._id_dj = id("dj")
self._id_djtable = id("djtable")
self._id_beam = id("beam")
self._id_confetti = id("confetti")
self._id_crowd = id("crowd")
self._id_burst = id("star")
self._id_sparkle = id("sparkle")
self.f = 0
return self
end
function TonkatsudjBGNormal:update(current_time_ms)
self.f = lumen_frame(current_time_ms - (self.t0 or current_time_ms))
self.t0 = self.t0 or current_time_ms
end
function TonkatsudjBGNormal:draw_screen()
local t = self.f % SCREEN_CYCLE
local k = #SCREENS
for i, s in ipairs(SCREENS) do
if t >= s[2] then k = i end
end
local prev = SCREENS[(k - 2) % #SCREENS + 1]
local cur = SCREENS[k]
local into = (t - cur[2]) % SCREEN_CYCLE
tex.draw_texture(self._id_screen, { frame = prev[1] })
tex.draw_texture(self._id_screen, { frame = cur[1], fade = math.min(1, into / SCREEN_FADE) })
end
function TonkatsudjBGNormal:draw_beams(beams, fade)
for _, b in ipairs(beams) do
local p = (self.f % (2 * b[8])) / b[8]
local k = p <= 1 and p or 2 - p
local w, h = BEAM_SIZE[1] * (b[5] + (b[6] - b[5]) * k), BEAM_SIZE[2] * b[7]
tex.draw_texture(self._id_beam, {
x = b[1], y = b[2], x2 = w - BEAM_SIZE[1], y2 = h - BEAM_SIZE[2],
origin = { w, h / 2 }, rotation = b[3] + (b[4] - b[3]) * k,
mirror = b[9] and "vertical" or nil, fade = fade, blend = "additive",
})
end
end
function TonkatsudjBGNormal:draw_bursts()
for _, b in ipairs(BURSTS) do
local p = pose_at(FLASH, (self.f - BURST_START[b[1]]) % FLASH.length)
if p then
local w, h = BURST_SIZE[1] * b[4], BURST_SIZE[2] * b[4]
tex.draw_texture(self._id_burst, {
x = b[2], y = b[3], x2 = w - BURST_SIZE[1], y2 = h - BURST_SIZE[2],
origin = { w / 2, h / 2 }, rotation = b[5], mirror = b[6] and "horizontal" or nil,
fade = p.alpha,
})
end
end
end
function TonkatsudjBGNormal:draw_carousel()
local c = CAROUSEL
local pass = c.frames / c.copies
for k = 0, c.copies - 1 do
local u = 1 - 2 * ((self.f + k * pass) % c.frames) / c.frames -- 1 right edge, -1 left
local a = math.abs(u)
local w = SPARKLE_FIELD[1] * (1 + 0.4 * a)
tex.draw_texture(self._id_sparkle, {
x = c.x + c.half * u - w / 2, y = c.y + c.dip * u * u - SPARKLE_FIELD[2] / 2,
x2 = w - SPARKLE_FIELD[1], fade = 0.3 + 0.7 * a, blend = "additive",
})
end
end
function TonkatsudjBGNormal:draw()
tex.draw_texture(self._id_bg)
self:draw_screen()
local dj = pose_at(DJ, self.f % DJ.length)
tex.draw_texture(self._id_dj, { frame = dj.frame, y = dj.y })
tex.draw_texture(self._id_djtable)
self:draw_beams(DIM_BEAMS, DIM_ALPHA)
tex.draw_texture(self._id_confetti)
tex.draw_texture(self._id_crowd, { y = pose_at(CROWD_BOB, self.f % CROWD_BOB.length).y })
self:draw_beams(STAGE_BEAMS, 1)
self:draw_bursts()
self:draw_carousel()
end
function TonkatsudjBGNormal:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name) end
local TonkatsudjBackground = {}
TonkatsudjBackground.__index = TonkatsudjBackground
function TonkatsudjBackground.new(player_num, bpm)
local self = setmetatable({}, TonkatsudjBackground)
self.donbg = TonkatsudjDonBG.new(player_num, COLLAB_PATH)
self.bg_normal = TonkatsudjBGNormal.new()
self.fever = Fever.create(math.random(0, 3), bpm, "background")
self.renda = RendaController.new(COLLAB_PATH, 0)
self.chibi = ChibiController.new(bpm, "background", 0)
self.is_rainbow = false
return self
end
function TonkatsudjBackground:update(current_ms, bpm)
self.donbg:update(current_ms, bpm)
self.bg_normal:update(current_ms)
self.fever:update(current_ms, bpm)
self.renda:update(current_ms)
self.chibi:update(current_ms, bpm)
end
function TonkatsudjBackground:handle_good(player_num) self.chibi:add_chibi(player_num, false) end
function TonkatsudjBackground:handle_ok(player_num) self.chibi:add_chibi(player_num, false) end
function TonkatsudjBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
function TonkatsudjBackground:handle_drumroll(player_num) self.renda:add_renda() end
function TonkatsudjBackground:handle_balloon(player_num) self.renda:add_renda() end
function TonkatsudjBackground:handle_miss_streak(player_num, on) self.donbg:set_miss(on) end
function TonkatsudjBackground:handle_gauge(player_num, progress, is_clear, is_rainbow)
self.donbg:set_clear(is_clear)
if is_rainbow and not self.is_rainbow then self.fever:start() end
if not is_rainbow and self.is_rainbow then self.fever:stop() end
self.is_rainbow = is_rainbow
end
function TonkatsudjBackground:draw_back()
self.bg_normal:draw()
self.donbg:draw()
self.renda:draw()
self.fever:draw()
self.chibi:draw()
end
function TonkatsudjBackground:draw_fore() end
function TonkatsudjBackground:destroy()
self.donbg:destroy()
self.bg_normal:destroy()
self.fever:destroy()
self.renda:destroy()
self.chibi:destroy()
end
return TonkatsudjBackground