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