Files
PyTaikoGreen/Scripts/background/bg_collabs/waiwai.lua
T
2026-11-04 18:45:35 -05:00

425 lines
15 KiB
Lua

local RendaController = require("background/bg_objects/renda")
local COLLAB = "background/collab/"
local S = tex.get_screen_scale()
local RISE_MS, FALL_MS = 233, 150
local A, B = "hop_late", "hop_early"
-- x comes from texture.json: the dancer's hop and whether the second prop is a mirror image
local function row(...)
local out = {}
for i, d in ipairs({ ... }) do
out[i] = { key = "dancer_" .. i, prop = "dancer_" .. i .. "_prop", hop = d[1], props = d[2], mirror_prop = d[3] }
end
return out
end
local PACKS = {
taiko = { renda = { dx = 915, dy = -1006 } },
i7id7 = {
slides = 7,
renda = { dx = 1560, dy = -1648 },
dancers = row({ A, 2 }, { B, 2 }, { B, 2, true }, { B, 1 }, { A, 2 }, { B, 1 }, { A, 1 }),
},
i7tri = {
slides = 3,
renda = { dx = 1560, dy = -1648 },
dancers = row({ A, 1 }, { B, 1 }, { B, 2, true }),
},
i7rev = {
slides = 2,
renda = { dx = 1560, dy = -1648 },
dancers = row({ B, 1 }, { A, 1 }),
},
i7natsu = {
slides = 10,
renda = { dx = 1560, dy = -1648 },
dancers = row({ A, 2 }, { B, 2 }, { B, 2, true }, { B, 1 }, { A, 2 }, { B, 1 }, { A, 1 }, { A, 1 }, { B, 1 },
{ A, 2, true }),
},
a3 = { street = true, renda = { dx = 915, dy = -1006 } },
}
local function clamp01(t) return math.max(0, math.min(1, t)) end
local function lerp(a, b, t) return a + (b - a) * t end
-- 0 -> 1 -> 0 over one cycle
local function arc(t)
t = t % 1
return t < 0.5 and t * 2 or (1 - t) * 2
end
local Waiwai = {}
Waiwai.__index = Waiwai
local function pack_path(name)
return COLLAB .. (name == "taiko" and "waiwai" or ("waiwai_" .. name))
end
function Waiwai.create(name, bpm)
local self = setmetatable({}, Waiwai)
self.spec = PACKS[name]
self.path = pack_path(name)
self.base_path = self.spec.street and self.path or pack_path("taiko")
self.bpm = bpm
self.beat = 0
self.last_ms = nil
self.progress = { 0, 0 }
self.level = 0
self.second_a = 0
self.fever_a = 0
self.ids = {}
tex.load_folder(self.base_path, "stage")
if self.path ~= self.base_path then tex.load_folder(self.path, "stage") end
self.renda = RendaController.new(self.path, 0, {
ms = { 2000 }, dx = self.spec.renda.dx, dy = self.spec.renda.dy, x_range = 500, y_range = 20,
})
return self
end
function Waiwai.new(_, bpm) return Waiwai.create("taiko", bpm) end
-- A collab constructor for Background's table
function Waiwai.pack(name)
return function(_, bpm) return Waiwai.create(name, bpm) end
end
function Waiwai:id(key, own)
local t = self.ids[key]
if not t then
t = tex.get_texture((own and self.path or self.base_path) .. "/stage/" .. key)
self.ids[key] = t
end
return t
end
function Waiwai:draw(key, fade, frame)
if fade <= 0 then return end
tex.draw_texture(self:id(key), { fade = fade, frame = frame })
end
-- both sides of a mirrored pair
function Waiwai:draw_pair(key, fade, frame)
if fade <= 0 then return end
tex.draw_texture(self:id(key), { fade = fade, frame = frame })
tex.draw_texture(self:id(key), { fade = fade, frame = frame, index = 1, mirror = "horizontal" })
end
function Waiwai:handle_gauge(player_num, progress)
self.progress[player_num == 2 and 2 or 1] = progress
local mean = (self.progress[1] + self.progress[2]) / 2
self.level = mean >= 0.7 and 2 or (mean >= 0.3 and 1 or 0)
end
function Waiwai:handle_good() end
function Waiwai:handle_ok() end
function Waiwai:handle_bad() end
function Waiwai:handle_drumroll() self.renda:add_renda() end
function Waiwai:handle_balloon() self.renda:add_renda() end
function Waiwai:update(current_ms, bpm)
local dt = self.last_ms and current_ms - self.last_ms or 0
self.last_ms = current_ms
self.bpm = bpm
self.now = current_ms
self.beat = self.beat + dt * bpm / 60000
self.renda:update(current_ms)
-- a level only starts once the one below it is finished, in both directions
local up, down = dt / RISE_MS, dt / FALL_MS
if self.level >= 1 then
self.second_a = math.min(1, self.second_a + up)
elseif self.fever_a <= 0 then
self.second_a = math.max(0, self.second_a - down)
end
if self.level >= 2 and self.second_a >= 1 then
self.fever_a = math.min(1, self.fever_a + up)
elseif self.level < 2 then
self.fever_a = math.max(0, self.fever_a - down)
end
end
-- Pops up past its rest while rising, then falls back out accelerating
local function pop_offset(a, rising, depth)
if rising then
if a < 0.7 then return lerp(depth, -18, a / 0.7) end
return lerp(-18, 0, (a - 0.7) / 0.3)
end
return depth * (1 - a) * (1 - a)
end
-- Two poses a beat, bobbing 8 px
function Waiwai:draw_mascots(key, y)
tex.draw_texture(self:id(key), {
frame = math.floor(self.beat) % 2,
y = (y + 8 * arc(self.beat)) * S,
})
end
-- A folder of pictures, each up for `slot` beats with the next fading in over its last 0.2
function Waiwai:draw_slides(id, count, slot, fade)
local i = math.floor(self.beat / slot)
local into = self.beat / slot - i
tex.draw_texture(id, { frame = i % count, fade = fade })
local next_in = (into * slot - (slot - 0.2)) / 0.2
if next_in > 0 then tex.draw_texture(id, { frame = (i + 1) % count, fade = fade * next_in }) end
end
-- Falling sparkles, each stream starting a third of its flight after the last
local SPARKLE_STREAMS = {
{ x = { -160, 320, 800 }, y = 340, from = { 327, -449 }, to = { -43, 520 }, ms = 10000, stagger = 3333, scale = 0.75, fade = 0.7 },
{ x = { -160, 320, 800 }, y = 340, from = { 573, -449 }, to = { 203, 520 }, ms = 10000, stagger = 3333, scale = 0.75, fade = 0.7, mirror = true },
{ x = { 95, 545, 995 }, y = 150, from = { 260, -310 }, to = { -60, 730 }, ms = 8000, stagger = 2667, scale = 1, fade = 1 },
}
local SPARKLE_SIZE = 328
function Waiwai:draw_confetti(fade)
if fade <= 0 then return end
local id = self:id("confetti")
for _, s in ipairs(SPARKLE_STREAMS) do
local half = SPARKLE_SIZE * s.scale / 2
for phase = 0, 2 do
local t = ((self.now + phase * s.stagger) % s.ms) / s.ms
for _, x in ipairs(s.x) do
tex.draw_texture(id, {
x = (x + lerp(s.from[1], s.to[1], t) - half) * S,
y = (s.y + lerp(s.from[2], s.to[2], t) - half) * S,
scale = s.scale,
fade = fade * s.fade,
mirror = s.mirror and "horizontal" or nil,
})
end
end
end
end
-- The stage up to the screen's floor; the fever layers fade over what they replace
function Waiwai:draw_stage()
local second, fever = self.second_a, self.fever_a
local beat = self.beat
local not_full = fever < 1
local flip = math.floor(beat) % 2
local band = self.spec.slides and self:id("fever_band", true) or self:id("fever_band")
local slides = self.spec.slides and self:id("screen", true) or self:id("screen")
if not_full then self:draw("eq_band", 1, self.level >= 1 and flip or 0) end
if fever > 0 then tex.draw_texture(band, { frame = flip, fade = fever }) end
self:draw("dome", 1)
self:draw("fever_dome", fever)
self:draw("fever_dome_glow", fever * lerp(0.4, 1, 1 - arc(beat / 2)))
self:draw("stage", 1)
if fever > 0 then
self:draw("fever_dots", 1, flip)
else
self:draw("lights_dots", second)
end
-- the bulbs change colour once fever has fully arrived, and only change back when it has gone
local fever_lit = fever >= 1 or (self.level < 2 and fever > 0)
self:draw_pair(fever_lit and "fever_corner_lights" or "corner_lights", 1)
if fever > 0 then
tex.draw_texture(self:id("fever_crowd_back"), { y = 3 * arc(beat) * S, fade = fever })
end
if not_full then self:draw("rim_top", 1) end
self:draw("fever_rim_top", fever)
if not_full then self:draw_slides(slides, self.spec.slides or 2, 4, 1) end
if fever > 0 then self:draw_slides(self:id("fever_screen"), 3, 3, fever) end
local glow = lerp(0.6, 1, 1 - arc(beat))
if not_full then self:draw("screen_glow", glow) end
self:draw("fever_screen_glow", fever * glow)
self:draw("stage_front", 1)
self:draw_pair("spot_beams", second)
self:draw_pair("fever_bulbs", fever)
if not_full then
-- two sets of floor lights, swapping every two beats
local into = beat / 2 % 1
self:draw_pair("floor_lights", clamp01(into * 8), math.floor(beat / 2) % 2)
end
self:draw_pair("fever_floor_lights", fever)
local half = beat % 1
self:draw_pair("fever_floor_flash", fever * (half < 0.5 and lerp(0.6, 1, 1 - arc(half * 2)) or 1))
self:draw("stage_floor", 1)
self:draw("balcony", 1)
self:draw("fever_stage_floor", fever)
if not_full then self:draw("rim_floor", 1) end
self:draw("fever_rim_floor", fever)
end
local LANTERN_MS = 333
-- A3: the festival street stays as it is at every level, only the balcony warms up
function Waiwai:draw_street()
local fever = self.fever_a
self:draw("street", 1)
self:draw("top_band", 1)
if fever >= 1 or (self.level < 2 and fever > 0) then self:draw_pair("fever_corner_lights", 1) end
if fever > 0 then
tex.draw_texture(self:id("fever_crowd_back"), { y = 3 * arc(self.beat) * S, fade = fever })
end
local flicker = lerp(0.55, 0.7, arc(self.now / LANTERN_MS))
tex.draw_texture(self:id("lantern_glow"), { fade = flicker, blend = "additive" })
self:draw_a3_dancers()
self:draw("curtain", 1)
end
-- Sixteen beats, shared by all four; each mirrors for the middle eight
local A3_POSES = {
{ 0, 0 }, { 0.35, 1 }, { 0.6, 2 }, { 2, 1 }, { 2.1, 0 }, { 2.55, 1 }, { 2.6, 2 }, { 3.45, 3 },
{ 4, 0 }, { 4.35, 1 }, { 4.6, 2 }, { 6, 1 }, { 6.1, 0 }, { 6.35, 1 }, { 6.6, 2 }, { 7.45, 3 },
{ 8, "rest_8" }, { 8.6, 4 }, { 8.8, 5 }, { 9, 6 }, { 9.85, 7 }, { 10.45, 8 },
{ 12, "rest_12" }, { 12.6, 4 }, { 12.8, 5 }, { 13, 6 }, { 13.85, 7 }, { 14.45, 8 },
}
local A3_HOPS = { 0.6, 2.6, 4.6, 6.6 }
local A3_DANCERS = {
{ key = "dancer_4", x = 422, rest_8 = 0, rest_12 = 0 },
{ key = "dancer_3", x = 857, rest_8 = 0, rest_12 = 0 },
{ key = "dancer_2", x = 562, rest_8 = 9, rest_12 = 9 },
{ key = "dancer_1", x = 717, rest_8 = 0, rest_12 = 1, notes = { { 3.45, 46, -26 }, { 7.45, -41.5, -24.5 } } },
}
local A3_SCALE, A3_FLOOR = 0.5, 633
function Waiwai:draw_a3_dancers()
local beat = self.beat % 16
local pose = 0
for _, p in ipairs(A3_POSES) do
if beat >= p[1] then pose = p[2] end
end
local hop = 0
for _, h in ipairs(A3_HOPS) do
local t = beat - h
if t >= 0 and t < 1 then hop = 8 * (t < 0.35 and t / 0.35 or (1 - t) / 0.65) end
end
local mirror = beat >= 4 and beat < 12 and "horizontal" or nil
local shadow_scale = A3_SCALE * lerp(1, 0.9, arc(clamp01((beat % 2 - 0.6) / 0.7) / 2))
for _, d in ipairs(A3_DANCERS) do
tex.draw_texture(self:id("shadow"), {
x = (d.x + 2.5 - 60 * shadow_scale) * S,
y = (A3_FLOOR + 73.5 - 25 * shadow_scale) * S,
scale = shadow_scale,
fade = 0.449,
})
local frame = type(pose) == "string" and d[pose] or pose
tex.draw_texture(self:id(d.key), { frame = frame, y = -hop * S, scale = A3_SCALE, mirror = mirror })
for _, n in ipairs(d.notes or {}) do
local t = beat - n[1]
if t >= 0 and t < 0.65 then
tex.draw_texture(self:id("note"), {
x = (d.x + n[2] - 21.5) * S,
y = (A3_FLOOR + n[3] - 21.5) * S,
fade = t < 0.3 and t / 0.3 or (0.65 - t) / 0.35,
})
end
end
end
end
-- Four-beat routine: first pose, a hop into the second, a hop back. Half hop in beat 1, half in beat 3.
local HOP_START = { hop_early = 0.75, hop_late = 2.75 }
local PROP_START = { hop_early = 1.45, hop_late = 3.33 }
local SWAP_TO, SWAP_BACK = 1.325, 3.325
local HOP_PX = 16
local DANCER_H = 184
function Waiwai:draw_i7_dancers()
local dancers = self.spec.dancers
if not dancers then return end
local beat = self.beat % 4
for _, d in ipairs(dancers) do
local t = beat - HOP_START[d.hop]
local y, squash = 0, 0
if t >= 0 and t < 0.575 then
y = -HOP_PX * (t < 0.25 and t / 0.25 or (0.575 - t) / 0.325)
else
-- crouch in the second half of every beat
local half = beat % 1
if half >= 0.5 then squash = 0.019 * arc(half * 2 - 1) end
end
local id = self:id(d.key, true)
local h = DANCER_H
tex.draw_texture(id, {
frame = (beat >= SWAP_TO and beat < SWAP_BACK) and 1 or 0,
y = (y + h * squash) * S,
y2 = -h * squash * S,
})
local age = beat - PROP_START[d.hop]
if age >= 0 and age < 0.65 then
local id_prop = self:id(d.prop, true)
local size = 56 * clamp01(age / 0.2)
local fade = age < 0.35 and 1 or (0.65 - age) / 0.3
for i = 0, d.props - 1 do
local inset = (56 - size) / 2
tex.draw_texture(id_prop, {
index = i,
x = inset * S,
y = (inset - 12 * age) * S,
x2 = (size - 56) * S,
y2 = (size - 56) * S,
fade = fade,
mirror = (d.mirror_prop and i == 1) and "horizontal" or nil,
})
end
end
end
end
-- The balcony's crowd and mascots, common to every stage
function Waiwai:draw_balcony()
local second, fever = self.second_a, self.fever_a
local crowd_bob = fever > 0 and 5 * arc(self.beat) * S or 0
if second > 0 then tex.draw_texture(self:id("side_crowd"), { y = crowd_bob, fade = second }) end
tex.draw_texture(self:id("crowd"), { y = crowd_bob })
if not self.spec.street and fever > 0 then
-- the speakers swing in from below the sides, thumping on the beat
local slide = 1 - fever
local thump = 0.1 * clamp01(1 - (self.beat % 1) / 0.25)
local grow = 160 * thump
local grow_h = 320 * thump
local dy = -120 * slide
tex.draw_texture(self:id("speaker"), {
x = (-106 * slide - grow / 2) * S,
y = (dy - grow_h / 2) * S,
x2 = grow * S,
y2 = grow_h * S,
})
tex.draw_texture(self:id("speaker"), {
index = 1,
mirror = "horizontal",
x = (106 * slide - grow / 2) * S,
y = (dy - grow_h / 2) * S,
x2 = grow * S,
y2 = grow_h * S,
})
end
if fever > 0 then self:draw_mascots("mascots_fever", pop_offset(fever, self.level >= 2, 90)) end
if second > 0 then self:draw_mascots("mascots_second", pop_offset(second, self.level >= 1, 128)) end
self:draw_mascots("mascots_first", 0)
self:draw("rail", 1)
if not self.spec.street and fever > 0 then self:draw("fever_rail_glow", lerp(0.1, 1, fever)) end
self:draw_confetti(fever)
end
function Waiwai:draw_back()
if not self.now then return end
if self.spec.street then
self:draw_street()
else
self:draw_stage()
self:draw_i7_dancers()
end
self:draw_balcony()
self.renda:draw()
end
function Waiwai:draw_fore() end
function Waiwai:destroy()
self.renda:destroy()
tex.unload_folder(self.base_path, "stage")
if self.path ~= self.base_path then tex.unload_folder(self.path, "stage") end
end
return Waiwai