Files
PyTaikoGreen/Scripts/background/bg_objects/yokai.lua
T

288 lines
11 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 = Timing.chain_moves, Timing.sum
-- Every Yo-kai Watch enso (yokai, hatsukoi, matsuri, tokoroten, yougota) shares the street, don
-- band, fever, chibi, renda and footer byte for byte: those load from SHARED. Only the dancers
-- differ; each collab file passes its dancer class and folder.
local SHARED = "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(SHARED, "bg_normal/bg_0")
tex.load_folder(SHARED, "bg_fever/bg_fever_0")
self._id_bg = tex.get_texture(SHARED .. "/bg_0/background") or 0
self._id_firework = tex.get_texture(SHARED .. "/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,
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(SHARED, "bg_normal/bg_0")
tex.unload_folder(SHARED, "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 jump up from below the stage, land with a squash and dance; removed, they hop up and
-- drop away. Timed in beats; poses are offsets from where each stands. The landing squashes to
-- `sy` (0.94); `dip` moves it down for dancers pivoting above their feet.
local function enter(land, dip, sy)
sy = sy or 0.94
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, dip or 0, "out", sy = sy } },
{ 1.6, land, 0, dip or 0, sy = sy, to = { nil, 0, "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
-- one pose that squashes (sy < 1) or stretches about the feet: `down` beats to sy, `hold`,
-- `back` beats to 1
local function squash(frame, sy, down, hold, back)
return { frame, down, to = { nil, nil, "out", sy = sy } }, { frame, hold, sy = sy },
{ frame, back, sy = sy, to = { nil, nil, "in", sy = 1 } }
end
-- Whisper floats: up 10px over a beat, back down over the next
local function bob(beats)
local poses = { length = beats }
for i = 0, beats - 1, 2 do
poses[#poses + 1] = { i, 0, 0, 10, to = { 0, 0 } }
poses[#poses + 1] = { i + 1, 0, 0, 0, to = { 0, 10 } }
end
return poses
end
-- an 8-picture loop a quarter beat each from `first` (Whisper's body and outline)
local function cycle(beats, first, key)
local poses = { length = beats, key = key }
for i = 0, beats * 4 - 1 do poses[#poses + 1] = { i * 0.25, first + i % 8, 0, 0 } end
return poses
end
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:init(player_num, bpm)
self.donbg = DonBGBase.new(1, player_num, SHARED, DONBG)
self.stage = YokaiStage.new(bpm)
self.footer = Footer.new(SHARED, 0)
self.fever = Fever.new(bpm, SHARED, FEVER)
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, self.path, self.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(SHARED, 0)
self.chibi = ChibiController.new(bpm, SHARED, 1)
self.is_clear = false
self.is_rainbow = false
self.last_milestone = 1
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, SHARED, 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
-- the background class for a collab whose dancers (a Dancer.define class) live in `path`
local function background(path, specs)
local cls = setmetatable({ path = path, dancers = Dancer.define(specs) }, { __index = YokaiBackground })
cls.__index = cls
cls.new = function(player_num, bpm)
local self = setmetatable({}, cls)
self:init(player_num, bpm)
return self
end
return cls
end
return {
background = background,
enter = enter,
leave = leave,
squash = squash,
bob = bob,
cycle = cycle,
}