local BGNormal = require("background/bg_objects/bg_normal") local Fever = require("background/bg_objects/fever") 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") local COLLAB_PATH = "background/collab/pzd" local MAX_DANCERS = 5 -- No footer: Lumen has no dodai, the dungeon floor is painted into the stage. -- Renda is the default renda_a_03 (same art and flight), loaded from "background". local function clamp01(v) return math.max(0, math.min(1, v)) end local function ease_out(p) return p * (2 - p) end local function ease_in(p) return p * p end -- Don band: a row of orbs on a stone board. Clearing fades the orbs out and drops in rows of -- one-colour orb columns that clear one by one, left to right, like a combo; a new row every -- 3.625 beats in two colour orders. Losing the clear fades the combo out and drops the orbs -- back in. Timed in beats. local TILE_X = { 0, 244, 488, 732, 976, 1218 } -- Lumen overlaps the last tile by 2px local COLUMN_X = { 0, 243, 487, 731, 975, 1218 } -- from texture.json x -61 -- combo key per column: 0 red, 1 yellow, 2 purple, 3 green, 4 blue local ROWS = { { 0, 1, 2, 3, 4, 0 }, { 4, 3, 0, 1, 2, 4 } } local ROW_EVERY = 3.625 local ROW_DROP = { from = -184, beats = 0.25 } local MATCH = { at = 0.75, step = 0.525, fade = 0.25 } -- column k fades from at + k * step local SWAP_FADE = 0.225 -- orbs out on clearing, combo out on losing it local FIRST_ROW = 0.25 local ORB_DROP = { from = -190, beats = 0.375 } local DIM, GLOW = 0.1, 0.7 local PzdDonBG = {} PzdDonBG.__index = PzdDonBG function PzdDonBG.new(player_num, bpm) local self = setmetatable({}, PzdDonBG) self.name = "0_" .. player_num tex.load_folder(COLLAB_PATH, "donbg/" .. self.name) local function id(key) return tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. key) or 0 end self._id_tile, self._id_orbs = id("tile"), id("orbs") self._id_dim, self._id_glow = id("dim"), id("glow") self._id_combo = {} for k = 0, 4 do self._id_combo[k] = id("combo" .. k) end self.beat = 60000 / bpm self.is_clear = false return self end function PzdDonBG:set_clear(is_clear) if is_clear == self.is_clear then return end self.left_row = not is_clear and self:row_time() or nil self.left = not is_clear self.is_clear, self.t0 = is_clear, self.now end function PzdDonBG:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end self.now = current_time_ms self.t0 = self.t0 or current_time_ms end -- beats since the last clear change function PzdDonBG:since() return ((self.now or 0) - (self.t0 or self.now or 0)) / self.beat end -- beats into the combo since it started (nil before the first row) function PzdDonBG:row_time() local t = self:since() - FIRST_ROW if not self.is_clear or t < 0 then return nil end return t end function PzdDonBG:draw_row(t, fade) local row = math.floor(t / ROW_EVERY) local r = t - row * ROW_EVERY local y = r < ROW_DROP.beats and ROW_DROP.from * (1 - ease_out(r / ROW_DROP.beats)) or 0 for k, key in ipairs(ROWS[row % 2 + 1]) do local a = 1 - clamp01((r - MATCH.at - (k - 1) * MATCH.step) / MATCH.fade) if a > 0 then tex.draw_texture(self._id_combo[key], { x = COLUMN_X[k], y = y, fade = a * fade }) end end end function PzdDonBG:draw_orbs(y, fade) for _, x in ipairs(COLUMN_X) do tex.draw_texture(self._id_orbs, { x = x, y = y, fade = fade }) end end function PzdDonBG:draw() for _, x in ipairs(TILE_X) do tex.draw_texture(self._id_tile, { x = x }) end local t = self:since() if self.is_clear then if t < SWAP_FADE then self:draw_orbs(0, 1 - t / SWAP_FADE) end local r = self:row_time() if r then self:draw_row(r, 1) end elseif self.left then if self.left_row and t < SWAP_FADE then self:draw_row(self.left_row + t, 1 - t / SWAP_FADE) end local d = clamp01((t - SWAP_FADE) / ORB_DROP.beats) if t >= SWAP_FADE then self:draw_orbs(ORB_DROP.from * (1 - ease_out(d)), 1) end else self:draw_orbs(0, 1) end tex.draw_texture(self._id_dim, { fade = DIM }) tex.draw_texture(self._id_glow, { fade = GLOW }) end function PzdDonBG:destroy() tex.unload_folder(COLLAB_PATH, "donbg/" .. self.name) end -- Clear background, over the normal stage. Clearing fans out eight light rays from the floor -- centre and flashes a growing star three times, a quarter turn each, then the screen whites -- out and fades back onto the gold floor. Sparkles then keep rising off the floor, turning once a beat. Losing -- the clear fades the floor and sparkles out over 1.45 beats. Timed in beats. local RAY_AT = { 640, 660 } -- { angle, width scale, alpha } local RAYS = { { -20, 1.7, 0.7 }, { 39, 0.64, 0.7 }, { -10, 1.7, 0.4 }, { -69, 1.28, 0.34 }, { -116, 2.58, 0.7 }, { 93, 2.36, 0.34 }, { 76, 0.43, 0.48 }, { 122, 0.64, 0.7 }, } local RAYS_IN = { beats = 0.725, alpha = 0.6 } local FLOOR_AT = 0.85 -- rays and star cut, gold floor under the flash local STAR = { from = 0.35, beats = 0.375, size = 15.19, flashes = { 0.35, 0.575, 0.825 } } local FLASH = { from = 0.475, peak = 0.725, out = 1.475 } local SPARKLES_AT = 1.5 local FADE_OUT = 1.45 -- sparkles: { beat, kind (frame: pink, green, yellow), x, y } in a 3.75-beat loop; each -- rises 320px eased out over 0.75 beat and fades over the last quarter of its beat, pink at -- full size, green 0.8, yellow 0.6. Lumen cuts the last two short when the loop restarts. local SPARKLE_AT = { 640, 730 } local SPARKLE_LOOP = 3.75 local SPARKLE_SIZE = { [0] = 1, 0.8, 0.6 } local SPARKLE_ALPHA = 0.7 local SPARKLES = { { 0, 0, 0, 0 }, { 0.3, 1, 248, -90 }, { 0.8, 2, -160, -140 }, { 1.1, 0, -394, 0 }, { 1.575, 1, 133, -90 }, { 1.9, 2, -259, -140 }, { 2.375, 0, 139, 0 }, { 2.85, 1, 371, -90 }, { 3.175, 2, -129, -140 }, } local PzdBGFever = {} PzdBGFever.__index = PzdBGFever function PzdBGFever.new(bpm) local self = setmetatable({}, PzdBGFever) self.name = "bg_fever_0" tex.load_folder(COLLAB_PATH, "bg_fever/" .. self.name) local function id(key) return tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. key) or 0 end self._id_floor, self._id_flash = id("floor"), id("flash") self._id_ray, self._id_star = id("ray"), id("star") self._id_sparkle = id("sparkle") self.beat = 60000 / bpm return self end function PzdBGFever:t() return ((self.now or 0) - (self.t0 or self.now or 0)) / self.beat end -- how much of the floor shows: none until the flash, then all; fading out after a stop function PzdBGFever:floor_alpha() if self.out then return self.out.from * (1 - clamp01(((self.now or 0) - self.out.t0) / (FADE_OUT * self.beat))) end return (self.t0 and self:t() >= FLOOR_AT) and 1 or 0 end function PzdBGFever:start() self.out, self.t0 = nil, self.now end function PzdBGFever:stop() if not self.t0 or self.out then return end self.out = { t0 = self.now, from = self:floor_alpha() } end function PzdBGFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end self.now = current_time_ms end function PzdBGFever:draw_transition(t) if t < FLOOR_AT then local k = RAYS_IN.alpha * clamp01(t / RAYS_IN.beats) for _, r in ipairs(RAYS) do local w = r[2] tex.draw_texture(self._id_ray, { x = RAY_AT[1], y = RAY_AT[2], x2 = 32 * w - 32, origin = { 16 * w, 320 }, rotation = r[1], fade = k * r[3] }) end for i, at in ipairs(STAR.flashes) do if t >= at and t < at + 1000 / 60 / self.beat then -- one Lumen frame local p = clamp01((t - STAR.from) / STAR.beats) local s = 64 * (1 + (STAR.size - 1) * p) tex.draw_texture(self._id_star, { x = RAY_AT[1], y = RAY_AT[2], x2 = s - 64, y2 = s - 64, origin = { s / 2, s / 2 }, rotation = 90 * (i - 1), fade = 0.6 + 0.4 * p }) end end end local flash if t < FLASH.peak then flash = clamp01((t - FLASH.from) / (FLASH.peak - FLASH.from)) elseif t < FLOOR_AT then flash = 1 else flash = 1 - clamp01((t - FLOOR_AT) / (FLASH.out - FLOOR_AT)) end if flash > 0 then tex.draw_texture(self._id_flash, { fade = flash }) end end function PzdBGFever:draw_sparkles(t, fade) local lt = t % SPARKLE_LOOP for _, s in ipairs(SPARKLES) do local life = lt - s[1] if life >= 0 and life < 1 then local size = SPARKLE_SIZE[s[2]] local rise = -320 * ease_out(clamp01(life / 0.75)) * size local alpha = life < 0.1 and life / 0.1 or life >= 0.75 and 1 - ease_in((life - 0.75) / 0.25) or 1 local w = 64 * size tex.draw_texture(self._id_sparkle, { frame = s[2], x = SPARKLE_AT[1] + s[3], y = SPARKLE_AT[2] + s[4] + rise, x2 = w - 64, y2 = w - 64, origin = { w / 2, w / 2 }, rotation = 360 * life, blend = "additive", fade = SPARKLE_ALPHA * alpha * fade }) end end end function PzdBGFever:draw() if not self.t0 then return end local t, floor = self:t(), self:floor_alpha() if floor > 0 then tex.draw_texture(self._id_floor, { fade = floor }) end if t >= SPARKLES_AT and floor > 0 then self:draw_sparkles(t - SPARKLES_AT, floor) end if not self.out then self:draw_transition(t) end end function PzdBGFever:destroy() tex.unload_folder(COLLAB_PATH, "bg_fever/" .. self.name) end -- Fever line-up: two halves of the cast jump up 230px, drop 70px onto their line and bounce -- 70px a beat, swapping poses on every bounce local FEVER_SPEC = { keys = { "left", "right" }, rise = 160, peak = 70, bounce = 70, drop = 230, poses = true } -- Dancers -- Timed in beats; poses are offsets of the dancers' feet (origin { 128, 256 }, texture.json -- y 698), the pivot for their squash. Each jumps up from below and lands with a squash, -- dances an 8-beat routine (A B A B C D, then A B turned round: A sinks into a squash over half -- a beat, B springs up from a crouch and settles) and hops up and drops away when removed. -- d: frames (enter, a, b, c, d, out) and per-dancer squash/hop sizes local function dancer(d) local f = d.frames local function a(frame, mirror) return { { frame, 0.5, x = 0, y = 0, mirror = mirror, to = { 0, 0, nil, sy = d.squash } }, { frame, 0.5, sy = d.squash, mirror = mirror } } end local function b(frame, mirror, x) x = x or 0 local hop = d.hop or 0 return { { frame, 0.25, x = x, y = d.dip or 0, sy = d.low, mirror = mirror, to = { x, hop, nil, sy = d.high } }, { frame, 0.25, y = hop, sy = d.high, mirror = mirror, to = { x, 0, nil, sy = 1 } }, { frame, 0.5, y = 0, mirror = mirror } } end local spec = { origin = { 128, 256 }, enter = { beats = 2, key = "loop", { 0, f[1], 0, 281, hide = true }, { 0.25, f[1], 0, d.from or 281, sy = d.from_sy, to = { 0, d.peak, "out", sy = d.peak_sy } }, { 1.075, f[1], 0, d.peak, sy = d.peak_sy, to = { 0, d.land or 0, "in", sy = d.land_sy or 1 } }, { 1.45, f[1], 0, d.land or 0, sy = d.land_sy }, { 1.525, f[2], 0, 0, sy = d.thud_from, to = { 0, 0, nil, sy = d.thud } }, { 1.75, f[2], 0, 0, sy = d.thud, to = { 0, 0, nil, sy = 1 } }, }, dance = Timing.steps(a(f[2]), b(f[3], nil, d.bx), a(f[2]), b(f[3], nil, d.bx), a(f[4]), b(f[5], nil, d.dx), a(f[2], true), b(f[3], true, d.bx)), } if f[6] then local o = d.out spec.leave = { beats = 1.5, key = "loop", { 0, f[6], 0, o.from or 0, sy = o.from_sy, to = { 0, o.peak, "out", sy = o.peak_sy } }, { 0.525, f[6], 0, o.peak, sy = o.peak_sy, to = { 0, 281, "in", sy = 1 } }, } end return spec end -- skin index i is Lumen's dancer (5 - i); dancer1 (the centre) has no exit and just vanishes local PZD_DANCERS = Dancer.define({ [0] = dancer({ frames = { 0, 1, 2, 3, 4, 5 }, squash = 0.961, low = 0.91, high = 1.027, peak = -20, peak_sy = 1.047, thud = 0.945, out = { peak = -10, peak_sy = 1.05 } }), [1] = dancer({ frames = { 0, 1, 2, 3, 4, 5 }, squash = 0.953, low = 1, high = 1.051, dip = 10, hop = -10, peak = -30, peak_sy = 1.066, thud = 0.938, out = { from = -29, from_sy = 0.852, peak = -40 } }), [2] = dancer({ frames = { 0, 1, 2, 3, 4, 5 }, squash = 0.961, low = 0.91, high = 1.039, bx = -10, dx = 10, from = 265, from_sy = 0.875, peak = -40, land = -20, land_sy = 0.957, thud = 0.899, out = { from = -10, peak = -30, peak_sy = 1.043 } }), [3] = dancer({ frames = { 0, 1, 2, 3, 4, 5 }, squash = 0.953, low = 0.903, high = 1.04, peak = -30, peak_sy = 1.055, thud = 0.93, out = { peak = -30, peak_sy = 1.035 } }), [4] = dancer({ frames = { 0, 1, 0, 2, 3 }, squash = 0.926, low = 0.949, high = 1.039, peak = -30, peak_sy = 1.055, thud_from = 0.926, thud = 0.887 }), }) -- Fixed slots (far-left .. far-right), from Lumen's dancer positions local DANCER_CENTERS = { 173, 407, 640, 873, 1107 } 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 } -- Chibis for good hits: one of six orb spirits fades in sliding from behind the lane, then -- bounces across the don band in straight zigzags (33px, seven halves) at a steady pace. -- Bad hits are the default spirit (pzd's tamashii is the same art and motion). local CHIBI_LIFE = 5 local CHIBI_ENTER = { beats = 0.525, from = { -426, 13 }, to = { -201, -24 } } local CHIBI_END_X = 692 local CHIBI_ZIG = { top = -24, bottom = 9, halves = 7 } local PzdChibi = setmetatable({}, { __index = BaseChibi }) PzdChibi.__index = PzdChibi function PzdChibi.new(spec, path, folder, bpm, is_2p) local self = setmetatable({}, PzdChibi) self:init(spec, path, folder, bpm, is_2p) return self end function PzdChibi: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 PzdChibi:draw() if not self.t then return end local t, e, at = self.t, CHIBI_ENTER, { frame = self.first } if t < e.beats then local p = t / e.beats at.x, at.y, at.fade = e.from[1] + (e.to[1] - e.from[1]) * p, e.from[2] + (e.to[2] - e.from[2]) * p, p else local p = (t - e.beats) / (CHIBI_LIFE - e.beats) local zig = p * CHIBI_ZIG.halves local half = zig - math.floor(zig) if math.floor(zig) % 2 == 1 then half = 1 - half end -- odd halves rise at.x = e.to[1] + (CHIBI_END_X - e.to[1]) * p at.y = CHIBI_ZIG.top + (CHIBI_ZIG.bottom - CHIBI_ZIG.top) * half end self:draw_image(at) end -- Background local PzdBackground = {} PzdBackground.__index = PzdBackground function PzdBackground.new(player_num, bpm) local self = setmetatable({}, PzdBackground) self.donbg = PzdDonBG.new(player_num, bpm) self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH) self.bg_fever = PzdBGFever.new(bpm) self.fever = Fever.new(bpm, COLLAB_PATH, FEVER_SPEC) self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, PZD_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("background", 2) self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1) self.is_clear = false self.is_rainbow = false self.last_milestone = 1 return self end function PzdBackground: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 PzdBackground:add_chibi(player_num) BaseChibi.add(self.chibi, { key = "0", first = math.random(0, 5) }, COLLAB_PATH, player_num, PzdChibi) end function PzdBackground:handle_good(player_num) self:add_chibi(player_num) end function PzdBackground:handle_ok(player_num) self:add_chibi(player_num) end function PzdBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function PzdBackground:handle_drumroll(player_num) self.renda:add_renda() end function PzdBackground:handle_balloon(player_num) self.renda:add_renda() end function PzdBackground: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 PzdBackground:draw_back() self.bg_normal:draw() self.bg_fever:draw() self.donbg:draw() self.renda:draw() self.dancer:draw_slots(DANCER_CENTERS, DANCER_DRAW_ORDER) self.fever:draw() self.chibi:draw() end function PzdBackground:draw_fore() end function PzdBackground:destroy() self.donbg:destroy() self.bg_normal:destroy() self.bg_fever:destroy() self.fever:destroy() self.dancer:destroy() self.renda:destroy() self.chibi:destroy() end return PzdBackground