local SparkleDonBG = require("background/bg_objects/donbg/sparkle") 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/lovelive" local MAX_DANCERS = 3 local pose_at, chain_moves, sum = Timing.pose_at, Timing.chain_moves, Timing.sum -- Don band: IMAS SideM's band with new art. The base scrolls one tile left every 5.33s (its -- tiles start 35px in); the clear set scrolls with it under a glow strip that pulses every -- second, with stars twinkling in clusters over it. Ambient, in ms. local LOVELIVE_DONBG = { layers = { { key = "background", ms = 5333, x = 35 }, { key = "glow", clear_only = true, tile = 0, alpha = 0.148, blend = "additive", period = 1000, pulse = { { 500, -1000 }, { 500, 1000, "out" } } }, }, } local LOVELIVE_STARS = { trio = { { 110, 34, 0, 0, 1, 2.5 }, { 45, 34, 0, 0, 1, 2.5 }, { 106, 20, 50, -100, 0.5, 1 }, { 31, 20, 50, -100, 0.5, 1 }, { 10, 20, 300, 0, 1, 1.5 }, { 50, 20, 300, 0, 1, 1.5 }, { 85, 20, 300, 0, 1, 1.5 }, }, pair = { { 11, 34, 0, 0, 1, 2.5 }, { 68, 34, 0, 0, 1, 2.5 }, { 0, 20, 300, 0, 1, 1.5 }, { 38, 20, 50, -100, 0.5, 1 }, { 64, 20, 300, 0, 1, 1.5 }, { 94, 20, 50, -100, 0.5, 1 }, }, } -- Stage: the rooftop stage by day. Over it, little sparkles fall at five spots in turn (a -- dotted streak and three stars that twinkle as they drop), and the screen glows purple, -- then gold. Ambient, counted in Lumen frames (60 a second). -- Clear: the night stage fades in over a beat with the screen pulsing every 4.5 beats; pink -- and purple spotlights take turns from both sides every 3 beats and green glows rise in the -- corners every 3. Losing clear fades the night stage out over a beat, and the lights go -- out with it. Timed in beats. local FRAMES_PER_MS = 60 / 1000 local STAGE_FADE = 1 -- the screen's purple, then gold glow (Lumen draws the gold one four times over) local TINT_PURPLE = { { 0, 0, 0, 0, alpha = 0, to = { 0, 0, alpha = 1 } }, { 90, 0, to = { 0, 0, alpha = 0 } }, { 180, 0, alpha = 0 }, length = 360 } local TINT_GOLD = { { 0, 0, 0, 0, alpha = 0 }, { 180, 0, alpha = 0, to = { 0, 0, alpha = 1 } }, { 270, 0, to = { 0, 0, "out", alpha = 0 } }, length = 360 } local TINT_ALPHA = { purple = 0.352, gold = 0.352 * 4 * 0.449 } -- Sparkles: { first frame, x, y } of each spot's centre on screen, every 600 frames. A spot's -- streak grows in, falls 80px over 180 frames and shrinks away; its stars start 30 frames -- apart, each bigger than the last, and fall 80px over 120 frames while they twinkle. local SPARKLE_FRAMES = 600 local SPARKLES = { { 0, 336, 493 }, { 90, 317.5, 382 }, { 180, 982.5, 493 }, { 0, 949, 382 }, { 240, 559, 454 } } local DOTS = { { 0, 0, 0, 0, scale = 0.5, alpha = 0, to = { 0, 0, scale = 1, alpha = 1 } }, { 60, 0 }, { 150, 0, to = { 0, 0, scale = 0.5, alpha = 0 } }, length = 180 } local TWINKLE = { { 0, 0, 0, 0, scale = 0.5, alpha = 0, to = { 0, 0, "in", scale = 1, alpha = 1 } }, { 50, 0, to = { 0, 0, scale = 0.5, alpha = 0 } }, length = 100 } local STARS = { { 30, 0.5 }, { 60, 0.75 }, { 90, 1 } } local FALL, STAR_FALL_FRAMES = 80, 120 -- clear lights, in beats from when the night stage starts coming in local SCREEN = { { 0, 0, 0, 0, alpha = 0, to = { 0, 0, alpha = 1 } }, { 2.25, 0, to = { 0, 0, alpha = 0 } }, length = 4.5 } local SPOT_PINK = { { 0, 0, 0, 0, alpha = 0, to = { 0, 0, "in", alpha = 1 } }, { 0.125, 0 }, { 2.9, 0, to = { 0, 0, "out", alpha = 0 } }, length = 6 } local SPOT_PURPLE = { { 0, 0, 0, 0, alpha = 0.5, to = { 0, 0, alpha = 0 } }, { 3, 0, alpha = 0, to = { 0, 0, alpha = 1 } }, { 3.15, 0 }, { 5.9, 0, to = { 0, 0, alpha = 0.5 } }, length = 6 } local GREEN = { { 0, 0, 0, 0, alpha = 0, to = { 0, 0, alpha = 1 } }, { 1.5, 0 }, length = 3 } local GREEN_ALPHA = 0.75 local LoveliveStage = {} LoveliveStage.__index = LoveliveStage local function load_ids(name, keys) local ids = {} for _, key in ipairs(keys) do ids[key] = { id = tex.get_texture(COLLAB_PATH .. "/" .. name .. "/" .. key) or 0, info = tex.get_texture_info(name, key) } end return ids end function LoveliveStage.new(bpm) local self = setmetatable({}, LoveliveStage) tex.load_folder(COLLAB_PATH, "bg_normal/bg_0") tex.load_folder(COLLAB_PATH, "bg_fever/bg_fever_0") self.day = load_ids("bg_0", { "background", "tint_purple", "tint_gold", "dots", "star" }) self.night = load_ids("bg_fever_0", { "background", "screen", "spot_pink", "spot_purple", "green" }) self.beat = 60000 / bpm self.beats = 0 self.now = 0 self.stage = { offset = 0, moves = {} } -- night stage opacity in thousandths return self end local function stage_value(self) return self.stage.offset + sum(self.stage.moves) end local function fade_stage(self, target) local from = stage_value(self) self.stage = { offset = from, moves = chain_moves({ { STAGE_FADE * math.abs(target - from) / 1000, target - from } }, self.beat, 1) } end -- the lights start over unless the night stage was still fading out function LoveliveStage:start() if stage_value(self) <= 0 then self.lit_at = self.beats end fade_stage(self, 1000) end function LoveliveStage:stop() fade_stage(self, 0) end function LoveliveStage:update(current_time_ms, bpm) if self.last then self.beats = self.beats + (current_time_ms - self.last) / self.beat end self.last, self.now = current_time_ms, current_time_ms if bpm then self.beat = 60000 / bpm end for _, m in ipairs(self.stage.moves) do m:update(current_time_ms) end end -- draws `t` (an id and info) with its centre at x, y (screen px) at `size` local function draw_centered(t, x, y, size, fade) local s, w, h = tex.get_screen_scale(), t.info.width, t.info.height local W, H = w * size, h * size tex.draw_texture(t.id, { x = x * s, y = y * s, x2 = W - w, y2 = H - h, origin = { W / 2, H / 2 }, fade = fade, blend = "additive" }) end function LoveliveStage:draw_sparkles(f) for _, spot in ipairs(SPARKLES) do local u = (f - spot[1]) % SPARKLE_FRAMES if u < DOTS.length then local p = pose_at(DOTS, u) draw_centered(self.day.dots, spot[2], spot[3] + FALL * u / DOTS.length, p.sx, p.alpha) end for _, star in ipairs(STARS) do local s = u - star[1] if s >= 0 and s < TWINKLE.length then local p = pose_at(TWINKLE, s) draw_centered(self.day.star, spot[2], spot[3] + FALL * s / STAR_FALL_FRAMES, p.sx * star[2], p.alpha) end end end end function LoveliveStage:draw() local day, night = self.day, self.night tex.draw_texture(day.background.id) local stage = stage_value(self) / 1000 local t = self.beats - (self.lit_at or 0) if stage > 0 then tex.draw_texture(night.background.id, { fade = stage }) tex.draw_texture(night.screen.id, { fade = stage * pose_at(SCREEN, t % SCREEN.length).alpha }) end local f = self.now * FRAMES_PER_MS self:draw_sparkles(f % SPARKLE_FRAMES) local tf = f % TINT_PURPLE.length tex.draw_texture(day.tint_purple.id, { fade = TINT_ALPHA.purple * pose_at(TINT_PURPLE, tf).alpha, blend = "additive" }) tex.draw_texture(day.tint_gold.id, { fade = TINT_ALPHA.gold * pose_at(TINT_GOLD, tf).alpha, blend = "additive" }) if stage <= 0 then return end local pink = pose_at(SPOT_PINK, t % SPOT_PINK.length).alpha local purple = pose_at(SPOT_PURPLE, t % SPOT_PURPLE.length).alpha local green = GREEN_ALPHA * pose_at(GREEN, t % GREEN.length).alpha for i = 0, 1 do local mirror = i == 1 and "horizontal" or nil tex.draw_texture(night.spot_pink.id, { index = i, mirror = mirror, fade = pink, blend = "additive" }) tex.draw_texture(night.spot_purple.id, { index = i, mirror = mirror, fade = purple, blend = "additive" }) end for i = 0, 1 do tex.draw_texture(night.green.id, { index = i, fade = green, blend = "additive" }) end end function LoveliveStage:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/bg_0") tex.unload_folder(COLLAB_PATH, "bg_fever/bg_fever_0") end -- Fever: μ's in two halves jumps up 249px, overshooting 70, bounces 70px a beat swapping -- poses, and drops 439px when the rainbow is lost (IMAS CG's line-up motion) local FEVER_SPEC = { rise = 249, bounce = 70, drop = 439, poses = true } -- Dancers: all three jump up from below, land with a squash, step through eight poses half a -- beat each, and (the two at the sides) squash, hop and drop away when removed. Timed in -- beats; poses are offsets of Lumen's dancer origin (y 670), 313px down the images and the -- pivot for the squash, so the feet stay put. local LEAVE = { beats = 1.425, { 0, 0, 0, 0, to = { nil, nil, "out", sy = 0.96 } }, { 0.125, 0, 0, 0, sy = 0.96, to = { nil, nil, "in", sy = 1 } }, { 0.175, 0, 0, 0, to = { 0, -57, "out" } }, -- hops up and drops away { 0.625, 1, 0, -57, to = { 0, 331, "in" } }, } local LOVELIVE_DANCER = { origin = { 150, 313 }, enter = { beats = 2, { 0, 0, 0, 331, to = { 0, -45, "out" } }, -- jumps up from below { 0.675, 0, 0, -45, to = { 0, 0, "in" } }, { 0.925, 1, 0, 0, to = { nil, nil, "out", sy = 0.95 } }, -- lands with a squash { 1.1, 1, 0, 0, sy = 0.95, to = { nil, nil, "in", sy = 1 } }, { 1.25, 1, 0, 0 }, }, dance = Timing.steps({ { 0, 0.5, x = 0, y = 0 }, { 1, 0.5 }, { 2, 0.5 }, { 3, 0.5 }, { 4, 0.5 }, { 5, 0.5 }, { 6, 0.5 }, { 7, 0.5 }, }), leave = LEAVE, } -- the centre dancer never leaves local LOVELIVE_DANCERS = Dancer.define({ [2] = setmetatable({ leave = false }, { __index = LOVELIVE_DANCER }), default = LOVELIVE_DANCER, }) -- Slots (left, centre, right) from Lumen's dancer positions; centre on top local DANCER_CENTERS = { 400, 640, 880 } 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: an alpaca (brown or white) hops in growing from 70% size, then -- trots across the don band, switching between two frames every quarter beat local LOVELIVE_CHIBIS = {} for key = 1, 2 do LOVELIVE_CHIBIS[key] = { key = tostring(key), period = 0.5, poses = { { 0, 1 }, { 0.25, 0 } } } end -- Drumroll: notes fly up and right from the bottom-left (renda_a_03's motion) local RENDA = { ms = { 2000 }, dx = 1560, dy = -1648, x_range = 500, y_range = 20 } -- Background local LoveliveBackground = {} LoveliveBackground.__index = LoveliveBackground function LoveliveBackground.new(player_num, bpm) local self = setmetatable({}, LoveliveBackground) self.donbg = SparkleDonBG.new(player_num, COLLAB_PATH, LOVELIVE_DONBG, LOVELIVE_STARS) -- the stage is painted down to the bottom of the screen, so there's no footer self.stage = LoveliveStage.new(bpm) self.fever = Fever.new(bpm, COLLAB_PATH, FEVER_SPEC) self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, LOVELIVE_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 LoveliveBackground: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 local function add_chibi(self, player_num) BaseChibi.add(self.chibi, LOVELIVE_CHIBIS[math.random(#LOVELIVE_CHIBIS)], COLLAB_PATH, player_num) end function LoveliveBackground:handle_good(player_num) add_chibi(self, player_num) end function LoveliveBackground:handle_ok(player_num) add_chibi(self, player_num) end function LoveliveBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function LoveliveBackground:handle_drumroll(player_num) self.renda:add_renda() end function LoveliveBackground:handle_balloon(player_num) self.renda:add_renda() end function LoveliveBackground: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.stage:start() end if not is_clear and self.is_clear then self.stage: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 LoveliveBackground:draw_back() self.stage:draw() self.donbg:draw() self.renda:draw() self.dancer:draw_slots(DANCER_CENTERS, DANCER_DRAW_ORDER) self.fever:draw() self.chibi:draw() end function LoveliveBackground:draw_fore() end function LoveliveBackground:destroy() self.donbg:destroy() self.stage:destroy() self.fever:destroy() self.dancer:destroy() self.renda:destroy() self.chibi:destroy() end return LoveliveBackground