Files
PyTaikoGreen/Scripts/background/bg_collabs/touken.lua
T
2026-10-10 01:55:40 -04:00

236 lines
9.6 KiB
Lua

local DonBGBase = require("background/bg_objects/donbg/base")
local BGNormal = require("background/bg_objects/bg_normal")
local PetalFever = require("background/bg_objects/petal_fever")
local Fever = require("background/bg_objects/fever")
local Footer = require("background/bg_objects/footer")
local Dancer = require("background/bg_objects/dancer")
local RendaController = require("background/bg_objects/renda")
local ChibiController = require("background/bg_objects/chibi")
local BaseChibi = require("background/bg_objects/chibi/base")
local Timing = require("background/bg_objects/timing")
-- Touken Ranbu
local COLLAB_PATH = "background/collab/touken"
local MAX_DANCERS = 3
-- Don band: the base scrolls two tiles left every 5.33s; the clear set scrolls with it under
-- a pink glow strip that pulses every second (IMAS's band with half-width tiles). Ambient, in ms.
local TOUKEN_DONBG = {
fade = 0.2,
layers = {
{ key = "background", ms = 5333, dx = -328 },
{ key = "glow", clear_only = true, tile = 0, alpha = 0.148, blend = "additive",
period = 1000, pulse = { { 500, -1000 }, { 500, 1000, "out" } } },
},
}
-- Clear background: the shrine courtyard (bg_normal) stays underneath; a pink glow fades in
-- over it and two streams of cherry petals fall across, a new pass every 1.25s. Lumen screens
-- the glow onto the courtyard; glow.png is that screen baked as an additive layer.
local TOUKEN_FEVER = {
path = COLLAB_PATH, haze = { key = "glow", blend = "additive" }, haze_in = 1.5, haze_out = 1,
petals_out = 0.225,
streams = {
{ key = "petals", flight = 2500, dx = -332, dy = 622, warm = true },
{ key = "petals_2", flight = 2500, dx = -259, dy = 650, warm = true },
},
}
-- Dancers: three swordsmen jump up from below, land with a squash, dance an 8-beat routine and
-- hop up and drop away when removed. Timed in beats; poses are offsets of their feet (texture.json
-- y 686), the pivot for the squash. Dancer 2 (left) has an extra landing frame, so its routine
-- frames are one higher.
local SQUASH = 0.978
-- one 2-beat phrase: two poses with a dip, a hop on the third, then two quarter-beat poses
local function phrase(a, b, c, d, e)
return {
{ a, 0.225, y = 0 }, { a, 0.25, sy = SQUASH }, { b, 0.25, sy = SQUASH }, { b, 0.275, y = 0 },
{ c, 0.2, y = -11 }, { c, 0.15, y = -6 }, { c, 0.15, y = 0, sy = SQUASH }, { d, 0.25 }, { e, 0.25 },
}
end
local function touken_dancer(shift)
local function routine(...)
local poses = Timing.steps(...)
for _, p in ipairs(poses) do p[2] = p[2] + shift end
return poses
end
local dance = routine(phrase(1, 2, 3, 4, 5), phrase(6, 7, 8, 9, 10), phrase(1, 2, 3, 4, 5),
phrase(6, 7, 11, 12, 13))
dance[1][3] = 0
return {
origin = { 128, 312 },
enter = {
beats = 2, key = "loop",
{ 0.2, 0, 0, 394, to = { 0, -40, "out" } }, -- jumps up from below
{ 0.875, 0, 0, -40, to = { 0, -4, "in" } },
{ 1.35, 1, 0, 0, to = { nil, nil, "out", sy = 0.94 } }, -- lands with a squash
{ 1.575, 1, 0, 0, sy = 0.94, to = { nil, nil, "in", sy = 1 } },
{ 1.825, 1, 0, 0 },
},
dance = dance,
leave = {
beats = 1.125, key = "loop",
{ 0, 14 + shift, 0, 0, to = { 0, -40, "out" } }, -- hops up and drops away
{ 0.45, 14 + shift, 0, -40, to = { 0, 394, "in" } },
},
}
end
local TOUKEN_DANCERS = Dancer.define({ default = touken_dancer(0), [1] = touken_dancer(1) })
-- Slots (left, centre, right) from Lumen's dancer positions; centre on top
local DANCER_CENTERS = { 427, 640, 853 }
local DANCER_DRAW_ORDER = { 3, 1, 2 }
local DANCER_SPAWN = { 1, 0, 2 } -- 0-indexed slots in join order: centre, left, right
-- dancer index by join order: dancer1 (centre), dancer2 (left), dancer3 (right)
local DANCER_JOIN_ORDER = { 2, 1, 0 }
-- Chibis for good hits: a crest flower fades in growing from half size as it rises onto the
-- don band, zigzags 33px down and up seven times at a steady pace, then rolls on level.
-- Offsets from texture.json (Lumen's attach point), in px; timed in beats.
local CHIBI_LIFE = 5
local CHIBI_HOP = { length = 0.5,
{ 0, 0, -424, 13, scale = 0.5, alpha = 0, to = { -355, 17, scale = 0.65, alpha = 0.3 } },
{ 0.15, 0, -355, 17, scale = 0.65, alpha = 0.3, to = { -206, -24, scale = 1, alpha = 1 } },
}
local CHIBI_RUN = { from = -206, to = 938 }
local CHIBI_ZIG = { top = -24, bottom = 9, half = 0.51, halves = 7 }
local CHIBI_KEYS = { "1", "2", "3" } -- red, blue, purple
local ToukenChibi = setmetatable({}, { __index = BaseChibi })
ToukenChibi.__index = ToukenChibi
function ToukenChibi.new(spec, path, folder, bpm, is_2p)
local self = setmetatable({}, ToukenChibi)
self:init(spec, path, folder, bpm, is_2p)
return self
end
function ToukenChibi:update(current_time_ms)
self.t0 = self.t0 or current_time_ms
self.t = (current_time_ms - self.t0) / self.beat
self.done = self.t >= CHIBI_LIFE
end
function ToukenChibi:draw()
if not self.t then return end
local t = self.t
if t < CHIBI_HOP.length then
local p = Timing.pose_at(CHIBI_HOP, t)
self:draw_image({ x = p.x, y = p.y, sx = p.sx, sy = p.sy, fade = p.alpha })
return
end
local run, z = t - CHIBI_HOP.length, CHIBI_ZIG
local leg, y = run / z.half, z.bottom
if leg < z.halves then
local half = leg - math.floor(leg)
if math.floor(leg) % 2 == 1 then half = 1 - half end -- even legs fall, odd ones rise
y = z.top + (z.bottom - z.top) * half
end
local x = CHIBI_RUN.from + (CHIBI_RUN.to - CHIBI_RUN.from) * run / (CHIBI_LIFE - CHIBI_HOP.length)
self:draw_image({ x = x, y = y })
end
-- Drumroll: notes fly up and to the right, two turns over the 2s flight, centred on Lumen's
-- spawn area (x 0-640, y 720-820)
local RENDA = { ms = { 2000 }, spin = 720, x = -55, x_range = 640, y = -55, y_range = 100 }
-- Background
local ToukenBackground = {}
ToukenBackground.__index = ToukenBackground
function ToukenBackground.new(player_num, bpm)
local self = setmetatable({}, ToukenBackground)
self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, TOUKEN_DONBG)
self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH)
self.bg_fever = PetalFever.new(bpm, TOUKEN_FEVER)
-- no dodai: the courtyard is the stage. No fever of its own either: the game uses a default
self.footer = Footer.new(COLLAB_PATH, -1)
self.fever = Fever.create(math.random(0, 3), bpm, "background")
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, TOUKEN_DANCERS)
self.dancer.spawn_positions = DANCER_SPAWN
local by_index = {}
for _, d in ipairs(self.dancer.dancers) do by_index[d.index] = d end
for i, index in ipairs(DANCER_JOIN_ORDER) do self.dancer.dancers[i] = by_index[index] end
self.renda = RendaController.new(COLLAB_PATH, 0, RENDA)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1)
self.is_clear = false
self.is_rainbow = false
self.last_milestone = 1
return self
end
function ToukenBackground:update(current_ms, bpm)
self.donbg:update(current_ms, bpm)
self.bg_fever:update(current_ms, bpm)
self.fever:update(current_ms, bpm)
self.dancer:update(current_ms, bpm)
self.renda:update(current_ms)
self.chibi:update(current_ms, bpm)
end
function ToukenBackground:add_chibi(player_num)
BaseChibi.add(self.chibi, { key = CHIBI_KEYS[math.random(#CHIBI_KEYS)] }, COLLAB_PATH, player_num, ToukenChibi)
end
function ToukenBackground:handle_good(player_num) self:add_chibi(player_num) end
function ToukenBackground:handle_ok(player_num) self:add_chibi(player_num) end
function ToukenBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
function ToukenBackground:handle_drumroll(player_num) self.renda:add_renda() end
function ToukenBackground:handle_balloon(player_num) self.renda:add_renda() end
function ToukenBackground:handle_gauge(player_num, progress, is_clear, is_rainbow)
self.donbg:set_clear(is_clear)
if is_clear and not self.is_clear then self.bg_fever:start() end
if not is_clear and self.is_clear then self.bg_fever:stop() end
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_clear = is_clear
self.is_rainbow = is_rainbow
local current_milestone
if is_clear then
current_milestone = MAX_DANCERS
else
current_milestone = math.min(MAX_DANCERS - 1, math.floor(progress * MAX_DANCERS))
end
if current_milestone > self.last_milestone then
for _ = 1, current_milestone - self.last_milestone do self.dancer:add_dancer() end
self.last_milestone = current_milestone
elseif current_milestone < self.last_milestone then
for _ = 1, self.last_milestone - current_milestone do self.dancer:leave_dancer() end
self.last_milestone = current_milestone
end
end
function ToukenBackground:draw_back()
self.bg_normal:draw()
self.bg_fever:draw()
self.footer:draw()
self.donbg:draw()
self.renda:draw()
self.dancer:draw_slots(DANCER_CENTERS, DANCER_DRAW_ORDER)
self.fever:draw()
self.chibi:draw()
end
function ToukenBackground:draw_fore() end
function ToukenBackground:destroy()
self.donbg:destroy()
self.bg_normal:destroy()
self.bg_fever:destroy()
self.footer:destroy()
self.fever:destroy()
self.dancer:destroy()
self.renda:destroy()
self.chibi:destroy()
end
return ToukenBackground