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

299 lines
12 KiB
Lua

local DonBGBase = require("background/bg_objects/donbg/base")
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 ChibiWave = require("background/bg_objects/chibi/wave")
local Timing = require("background/bg_objects/timing")
local chain_moves, sum, steps = Timing.chain_moves, Timing.sum, Timing.steps
local COLLAB_PATH = "background/collab/yokai"
local MAX_DANCERS = 5
-- Renda is renda_a_01's flight with Yo-kai Watch pictures; bad hits get the default tamashii.
-- Don band: purple medal tiles scroll one tile left every 5.33 s under two rows of logos that
-- scroll one 640px pair in the same time. Clearing fades in the yellow tiles, a miss streak
-- (only while not cleared) the grey set. Ambient, in ms.
local DONBG = { layers = {
{ key = "background", ms = 5333 },
{ key = "overlay", ms = 5333, dx = -640, tile = 640, x = 1 },
{ key = "overlay", ms = 5333, dx = -640, tile = 640, x = 321, y = 66 },
} }
-- Stage: the street. Clearing slides it 175px down to show the sky, where fireworks burst
-- once it has settled; losing the clear slides it back up. The slide is in beats, the
-- fireworks ambient (Lumen frames).
local SLIDE = { distance = 175, beats = 1.475 }
-- The fireworks: a 294-frame show of bursts { frame, centre x, centre y, size }, each 16
-- pictures two frames apiece, drawn additive.
local FIREWORK_SIZE, FIREWORK_FRAMES, SHOW_FRAMES = 256, 16, 294
local SHOW = {
{ 0, 316, 472, 0.5 }, { 7, 686, 472, 0.5 }, { 12, 442, 415, 0.4 }, { 15, 1203, 339, 1.12 },
{ 19, 877, 480, 0.4 }, { 65, 1224, 413, 0.6 }, { 68, 948, 510, 0.6 }, { 73, 340, 524, 0.6 },
{ 84, 594, 523, 0.5 }, { 87, 690, 427, 0.6 }, { 90, 82, 363, 0.6 }, { 95, 906, 483, 0.6 },
{ 137, 38, 443, 1.12 }, { 142, 1222, 443, 0.6 }, { 145, 389, 423, 0.3 }, { 148, 50, 427, 0.6 },
{ 153, 282, 511, 0.5 }, { 159, 976, 517, 1.12 }, { 164, 634, 463, 0.8 }, { 170, 692, 499, 0.6 },
{ 175, 405, 434, 0.5 }, { 216, 1060, 463, 1.12 }, { 221, 381, 502, 0.6 }, { 224, 749, 576, 0.6 },
{ 227, 589, 545, 0.5 }, { 232, 1088, 389, 0.5 }, { 238, 1163, 426, 1.12 }, { 243, 870, 438, 0.8 },
{ 246, 367, 500, 1.0 }, { 249, 668, 457, 0.6 }, { 254, 399, 414, 0.3 },
}
local function lumen_frame(ms) return ms * 60 / 1000 end
local YokaiStage = {}
YokaiStage.__index = YokaiStage
function YokaiStage.new(bpm)
local self = setmetatable({}, YokaiStage)
tex.load_folder(COLLAB_PATH, "bg_normal/bg_0")
tex.load_folder(COLLAB_PATH, "bg_fever/bg_fever_0")
self._id_bg = tex.get_texture(COLLAB_PATH .. "/bg_0/background") or 0
self._id_firework = tex.get_texture(COLLAB_PATH .. "/bg_fever_0/firework") or 0
self.beat = 60000 / bpm
self.offset = 0
self.moves = {}
self.clear = false
return self
end
function YokaiStage:y() return self.offset + sum(self.moves) end
-- slides to the sky (or back to the street) from wherever it is
function YokaiStage:set_clear(is_clear)
local scale = tex.get_screen_scale()
self.offset = self:y()
self.clear, self.show_t0 = is_clear, nil
local target = is_clear and SLIDE.distance * scale or 0
self.moves = chain_moves({ { SLIDE.beats, (target - self.offset) / scale } }, self.beat, scale)
end
function YokaiStage:update(current_time_ms, bpm)
if bpm then self.beat = 60000 / bpm end
self.now = current_time_ms
for _, m in ipairs(self.moves) do m:update(current_time_ms) end
local settled = not self.moves[1] or self.moves[1].is_finished
if self.clear and settled and not self.show_t0 then self.show_t0 = current_time_ms end
end
function YokaiStage:draw_fireworks()
local scale = tex.get_screen_scale()
local f = lumen_frame(self.now - self.show_t0) % SHOW_FRAMES
for _, b in ipairs(SHOW) do
local k = math.floor((f - b[1]) / 2)
if k >= 0 and k < FIREWORK_FRAMES then
local size = FIREWORK_SIZE * b[4]
tex.draw_texture(self._id_firework, {
frame = k, blend = "additive",
x = (b[2] - size / 2) * scale, y = (b[3] - size / 2) * scale,
x2 = (size - FIREWORK_SIZE) * scale, y2 = (size - FIREWORK_SIZE) * scale,
})
end
end
end
function YokaiStage:draw()
tex.draw_texture(self._id_bg, { y = self:y() })
if self.show_t0 then self:draw_fireworks() end
end
function YokaiStage:destroy()
tex.unload_folder(COLLAB_PATH, "bg_normal/bg_0")
tex.unload_folder(COLLAB_PATH, "bg_fever/bg_fever_0")
end
-- Fever: two yokai crowds (left, right) jump up, bounce 40px a beat and drop away
local FEVER = { rise = 260, peak = 40, bounce = 40, drop = 300 }
-- Dancers: five yo-kai and kids jump up from below the stage, land with a squash and dance
-- an 8-beat routine of half- and whole-beat poses; removed, they hop up and drop away. Timed
-- in beats; poses are offsets of their feet (texture.json y 686), the pivot for the squash.
local ORIGIN = { 128, 312 }
local function enter(land)
return {
beats = 2, key = "loop",
{ 0.2, 0, 0, 394, to = { 0, -40, "out" } },
{ 0.9, 0, 0, -40, to = { 0, 0, "in" } },
{ 1.375, land, 0, 0, to = { nil, nil, "out", sy = 0.94 } },
{ 1.6, land, 0, 0, sy = 0.94, to = { nil, nil, "in", sy = 1 } },
{ 1.85, land, 0, 0 },
}
end
local function leave(frame)
return {
beats = 1.125, key = "loop",
{ 0, frame, 0, 0 },
{ 0.2, frame, 0, 0, to = { 0, -40, "out" } },
{ 0.45, frame, 0, -40, to = { 0, 394, "in" } },
}
end
-- the routine most of them share, poses a..e; `last` adds fields to the e pose
local function routine(a, b, c, d, e, last)
local e_step = { e, 1 }
for k, v in pairs(last or {}) do e_step[k] = v end
return steps({ { a, 0.5, x = 0, y = 0 }, { b, 0.5 }, { a, 0.5 }, { c, 0.5 }, { d, 1 }, { b, 0.5 },
{ a, 1 }, { b, 0.5 }, { a, 0.5 }, { c, 0.5 }, e_step, { b, 1, x = 0 } })
end
-- Whisper floats: his body (8 frames a quarter beat each) leans 5 degrees on the d and e poses,
-- between arm outlines (under) and arm fills (over) that follow the routine; all of it bobs
-- 10px down and back every 2 beats
local function whisper_body()
local poses = { length = 8 }
for i = 0, 31 do
local beat = i * 0.25
local rot = (beat >= 2 and beat < 3) and 5 or (beat >= 6 and beat < 7) and -5 or 0
poses[#poses + 1] = { beat, 2 + i % 8, 0, -156, rot = rot }
end
return poses
end
local function whisper_bob()
local poses = { length = 8 }
for i = 0, 3 do
poses[#poses + 1] = { 2 * i, 0, 0, 10, to = { 0, 0 } }
poses[#poses + 1] = { 2 * i + 1, 0, 0, 0, to = { 0, 10 } }
end
return poses
end
local WHISPER_DANCE = {
beats = 8, move = whisper_bob(),
routine(0, 1, 2, 3, 4, { x = -6 }), -- arm outlines
whisper_body(),
routine(0, 1, 2, 3, 3, { mirror = true }), -- arm fills
}
WHISPER_DANCE[1].key, WHISPER_DANCE[2].key, WHISPER_DANCE[3].key = "_arms", "loop", "_hands"
WHISPER_DANCE[2].origin = { 128, 156 }
local DANCERS = Dancer.define({
-- Komasan, far right: a routine of his own
[0] = {
origin = ORIGIN, enter = enter(0), leave = leave(0),
dance = steps({ { 1, 0.5, x = 0, y = 0 }, { 2, 0.5 }, { 1, 0.5 }, { 2, 0.5 }, { 0, 0.5 }, { 3, 1 },
{ 1, 0.5 }, { 2, 1 }, { 1, 0.5 }, { 2, 0.5 }, { 0, 0.5 }, { 4, 1 }, { 0, 0.5 } }),
},
-- Whisper, far left
[1] = { origin = ORIGIN, enter = enter(1), leave = leave(10), dance = WHISPER_DANCE },
-- Fumika, right, and Keita, left
[2] = { origin = ORIGIN, enter = enter(1), leave = leave(6), dance = routine(2, 0, 3, 4, 5) },
[3] = { origin = ORIGIN, enter = enter(1), leave = leave(6), dance = routine(2, 0, 3, 4, 5) },
-- Jibanyan, centre: Lumen has no exit for him (he is never removed)
[4] = { origin = ORIGIN, enter = enter(0), dance = routine(1, 2, 3, 4, 5) },
})
local DANCER_CENTERS = { 173, 407, 640, 873, 1107 } -- Whisper, Keita, Jibanyan, Fumika, Komasan
local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- outer pairs first, the centre on top
-- dancer index by join order: joins fill the centre, left, right, far-left, far-right slots
local DANCER_JOIN_ORDER = { 4, 3, 2, 1, 0 }
-- Chibi for good hits: Jibanyan's head fades in growing from half size, then zigzags across,
-- 33px up and down every half beat
local CHIBI = {
key = "1", start_size = 0.5, fade_in = true,
hop = { from = { -424, 13 }, peak = { -199, -24 }, up = 0.5 },
run = { x = -199, y = -24, distance = 1130 },
wave = { first = 33, height = 33, beats = 0.5 },
}
-- Background
local YokaiBackground = {}
YokaiBackground.__index = YokaiBackground
function YokaiBackground.new(player_num, bpm)
local self = setmetatable({}, YokaiBackground)
self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DONBG)
self.stage = YokaiStage.new(bpm)
self.footer = Footer.new(COLLAB_PATH, 0)
self.fever = Fever.new(bpm, COLLAB_PATH, FEVER)
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, DANCERS)
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)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1)
self.is_clear = false
self.is_rainbow = false
self.last_milestone = 1
return self
end
function YokaiBackground:update(current_ms, bpm)
self.donbg:update(current_ms, bpm)
self.stage: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 YokaiBackground:add_chibi(player_num)
BaseChibi.add(self.chibi, CHIBI, COLLAB_PATH, player_num, ChibiWave)
end
function YokaiBackground:handle_good(player_num) self:add_chibi(player_num) end
function YokaiBackground:handle_ok(player_num) self:add_chibi(player_num) end
function YokaiBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
function YokaiBackground:handle_drumroll(player_num) self.renda:add_renda() end
function YokaiBackground:handle_balloon(player_num) self.renda:add_renda() end
function YokaiBackground:handle_miss_streak(player_num, on) self.donbg:set_miss(on) end
function YokaiBackground:handle_gauge(player_num, progress, is_clear, is_rainbow)
self.donbg:set_clear(is_clear)
if is_clear ~= self.is_clear then self.stage:set_clear(is_clear) 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 YokaiBackground:draw_back()
self.stage: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 YokaiBackground:draw_fore() end
function YokaiBackground:destroy()
self.donbg:destroy()
self.stage:destroy()
self.fever:destroy()
self.dancer:destroy()
self.footer:destroy()
self.renda:destroy()
self.chibi:destroy()
end
return YokaiBackground