local DonBGBase = require("background/bg_objects/donbg/base") local DonBG = require("background/bg_objects/donbg") local Fever = require("background/bg_objects/fever") local Footer = require("background/bg_objects/footer") local Dancer = require("background/bg_objects/dancer") local ChibiController = require("background/bg_objects/chibi") local BaseChibi = require("background/bg_objects/chibi/base") local RendaController = require("background/bg_objects/renda") local Timing = require("background/bg_objects/timing") local COLLAB_PATH = "background/collab/miku" -- fever line-up: two poses, 70px bounce local FEVER_SPEC = { bounce = 70, poses = true } local MAX_DANCERS = 5 local chain_moves, frame_change, sum, Loop = Timing.chain_moves, Timing.frame_change, Timing.sum, Timing.Loop -- Normal background: a DJ booth whose three monitors spell out MIKU, a flickering -- glow over the stage and thumping speakers on both sides. Plain ms timings: the -- monitors run on their own loops rather than the song's beat. -- Right monitor: powers on, glitches into M, I, K, U, glitches out and powers off again local SCREEN_R = { { 0, 333 }, { 1, 117 }, { 2, 67 }, { 1, 117 }, { 2, 67 }, { 1, 133 }, { 2, 167 }, { 3, 667 }, { 4, 667 }, { 5, 667 }, { 6, 683 }, { 1, 117 }, { 2, 67 }, { 1, 117 }, { 2, 67 }, { 1, 133 }, { 2, 200 }, { 0, 350 }, } -- its opacity, in thousandths: fades in when the loop starts and again when it powers off local SCREEN_R_FADE = { { 100, 1000, "out" }, { 4283, 0 }, { 0, -1000 }, { 333, 1000 } } local SCREEN_C = { { 0, 917 }, { 1, 800 }, { 2, 883 }, { 3, 117 }, { 4, 50 }, { 3, 117 }, { 4, 83 }, { 3, 117 }, { 4, 67 }, { 3, 117 }, { 4, 50 }, { 3, 117 }, { 4, 83 }, { 3, 117 }, { 5, 700 }, } local SCREEN_L = { { 0, 567 }, { 1, 350 }, { 2, 67 }, { 3, 67 }, { 2, 67 }, { 3, 117 }, { 2, 67 }, { 3, 33 }, { 2, 67 }, { 3, 200 }, { 2, 67 }, { 3, 33 }, { 2, 67 }, { 3, 383 }, { 4, 833 }, { 5, 783 }, } local BAND_MOVE = { { 3250, -56 } } -- scanline sweeping up the centre monitor, then gone local BAND_START = 28 -- glow opacity in thousandths: three quick dips and a slow one local GLOW_FLICKER = { { 67, -200 }, { 67, 200 }, { 67, -200 }, { 67, 200 }, { 67, -200 }, { 117, 200 }, { 250, -290 }, { 83, 290 }, } local GLOW_OPACITY = 0.84 local SPEAKER_PULSE = { { 150, -50, "out" }, { 183, 50, "in" } } -- scale in thousandths local MikuBGNormal = {} MikuBGNormal.__index = MikuBGNormal function MikuBGNormal.new() local self = setmetatable({}, MikuBGNormal) self.name = "bg_0" tex.load_folder(COLLAB_PATH, "bg_normal/" .. self.name) local function id(key) return tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. key) or 0 end self._id_bg = id("background") self._id_screen_l = id("screen_l") self._id_screen_c = id("screen_c") self._id_screen_r = id("screen_r") self._id_band = id("band") self._id_glow = id("glow") self._id_speaker = id("speaker") self.speaker_width = tex.get_texture_info(self.name, "speaker").width local scale = tex.get_screen_scale() self.screen_l = Loop.new(3767, function() local f = frame_change(SCREEN_L, 1) f:start() return { frames = f } end) self.screen_c = Loop.new(4333, function() local f = frame_change(SCREEN_C, 1) f:start() return { frames = f, band = chain_moves(BAND_MOVE, 1, scale) } end) self.screen_r = Loop.new(4733, function() local f = frame_change(SCREEN_R, 1) f:start() return { frames = f, fade = chain_moves(SCREEN_R_FADE, 1, 1) } end) self.glow = Loop.new(850, function() return { flicker = chain_moves(GLOW_FLICKER, 1, 1) } end) self.speaker = Loop.new(333, function() return { pulse = chain_moves(SPEAKER_PULSE, 1, 1) } end) self.band_start = math.floor(BAND_START * scale) return self end function MikuBGNormal:update(current_time_ms) self.screen_l:update(current_time_ms) self.screen_c:update(current_time_ms) self.screen_r:update(current_time_ms) self.glow:update(current_time_ms) self.speaker:update(current_time_ms) end function MikuBGNormal:draw() if not self.glow.anims then return end -- not updated yet tex.draw_texture(self._id_bg) -- the right monitor is off (opacity 0) at the start of its loop tex.draw_texture(self._id_screen_r, { frame = self.screen_r.anims.frames.attribute, fade = sum(self.screen_r.anims.fade) / 1000, }) tex.draw_texture(self._id_screen_c, { frame = self.screen_c.anims.frames.attribute }) local band = self.screen_c.anims.band[1] if not band.is_finished then tex.draw_texture(self._id_band, { y = self.band_start + band.attribute }) end tex.draw_texture(self._id_screen_l, { frame = self.screen_l.anims.frames.attribute }) tex.draw_texture(self._id_glow, { fade = GLOW_OPACITY * (1 + sum(self.glow.anims.flicker) / 1000), blend = "additive", }) -- speakers shrink towards their outer edge local s = 1 + sum(self.speaker.anims.pulse) / 1000 local shift = self.speaker_width * (1 - s) / 2 tex.draw_texture(self._id_speaker, { index = 0, scale = s, center = true, x = -shift }) tex.draw_texture(self._id_speaker, { index = 1, scale = s, center = true, x = shift, mirror = "horizontal" }) end function MikuBGNormal:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name) end local MIKU_DONBG_FAST_X = -164 -- don band fast layer's tile phase -- Chibi Miku for good hits: hops in growing from 70% size, then runs straight across -- the don band (no bounce). Bad hits play the shared tamashii. local MIKU_CHIBI = { key = "1", period = 1, poses = { { 0, 0 }, { 0.2, 1 }, { 0.4, 2 }, { 0.6, 3 }, { 0.8, 4 } }, } -- Clear background: the panel drops in from above and bounces, then the light rays fade -- in; losing clear drops it away again. Transitions are timed in beats, the tile scroll -- in plain ms. local BG_FEVER_SCROLL_MS = 1333 -- one tile local BG_FEVER_DROP_Y0 = -360 local BG_FEVER_DROP = { { 1, 360, "in" }, { 0.425, -50, "out" }, { 0.325, 50, "in" } } local BG_FEVER_RAYS_IN = { { 1, 0 }, { 0.75, 1000 } } -- opacity in thousandths local BG_FEVER_FALL = { { 1, 390 } } local BG_FEVER_RAYS_OUT = 0.65 local MikuBGFever = {} MikuBGFever.__index = MikuBGFever function MikuBGFever.new(bpm) local self = setmetatable({}, MikuBGFever) self.name = "bg_fever_0" tex.load_folder(COLLAB_PATH, "bg_fever/" .. self.name) self.tile = tex.get_texture_info(self.name, "background").width self._id_bg = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. "background") or 0 self._id_ray = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. "ray") or 0 self.beat = 60000 / bpm self.scroll = anim.move(BG_FEVER_SCROLL_MS, { total_distance = -self.tile, loop = true }) self.scroll:start() self.state = nil -- nil (hidden), "in", "out" return self end function MikuBGFever:y() return self.offset + sum(self.drop) end function MikuBGFever:ray_opacity() return (self.ray_offset + sum(self.rays)) / 1000 end function MikuBGFever:start() local scale = tex.get_screen_scale() self.state = "in" self.offset = math.floor(BG_FEVER_DROP_Y0 * scale) self.drop = chain_moves(BG_FEVER_DROP, self.beat, scale) self.ray_offset = 0 self.rays = chain_moves(BG_FEVER_RAYS_IN, self.beat, 1) end -- falls away from wherever it is, rays fading from their current opacity function MikuBGFever:stop() if not self.state then return end local opacity = math.floor(self:ray_opacity() * 1000) self.state = "out" self.offset = self:y() self.drop = chain_moves(BG_FEVER_FALL, self.beat, tex.get_screen_scale()) self.ray_offset = opacity self.rays = chain_moves({ { BG_FEVER_RAYS_OUT, -opacity } }, self.beat, 1) end function MikuBGFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end self.scroll:update(current_time_ms) if not self.state then return end for _, m in ipairs(self.drop) do m:update(current_time_ms) end for _, m in ipairs(self.rays) do m:update(current_time_ms) end if self.state == "out" and self.drop[#self.drop].is_finished then self.state = nil end end function MikuBGFever:draw() if not self.state then return end local y = self:y() for i = 0, 4 do tex.draw_texture(self._id_bg, { x = i * self.tile + self.scroll.attribute, y = y }) end local fade = self:ray_opacity() tex.draw_texture(self._id_ray, { index = 0, fade = fade }) tex.draw_texture(self._id_ray, { index = 1, fade = fade, mirror = "horizontal" }) end function MikuBGFever:destroy() tex.unload_folder(COLLAB_PATH, "bg_fever/" .. self.name) end -- Dancers -- All timings are in beats so the dancers follow the song's BPM. -- One beat of the small dancers: a pose held most of the beat, then a short in-between local function small_dance(s) return { { s, 0.175 }, { s + 1, 0.55 }, { s + 2, 0.275 }, { s + 3, 0.725 }, { s + 4, 0.275 }, { s + 1, 0.725 }, { s + 2, 0.275 }, { s + 3, 0.725 }, { s + 4, 0.275 }, } end -- poses are offsets from where each dancer stands, pivoting about its feet (bottom-left) local SMALL_DANCER = { origin = { 0, 200 }, enter = { beats = 2, { 0, 0, 0, 213 }, { 0.35, 0, 0, 213, to = { 0, -43, "out" } }, -- jumps up from below, hangs { 0.825, 0, 0, -43 }, { 1.125, 0, 0, -43, to = { 0, 0, "in" } }, { 1.275, 0, 0, 0, to = { sy = 0.95 } }, -- lands with a squash { 1.35, 0, 0, 0, sy = 0.95, to = { sy = 1 } }, { 1.45, 0, 0, 0 }, }, dance = Timing.steps(small_dance(0), small_dance(5)), leave = { beats = 1.425, { 0, 0, 0, 0 }, { 0.1, 0, 0, 0, to = { 0, -100, "out" } }, -- hops up and drops away { 0.475, 0, 0, -100, to = { 0, 214, "in" } }, }, } local MIKU_A = { { 2, 0.1 }, { 3, 0.075 }, { 4, 0.55 }, { 5, 0.075 }, { 6, 0.2 }, { 7, 0.725 }, { 8, 0.075 }, { 9, 0.2 }, { 4, 0.725 }, { 5, 0.075 }, { 6, 0.2 }, { 7, 0.725 }, { 8, 0.075 }, { 9, 0.2 }, } local MIKU_B = { { 10, 0.1 }, { 11, 0.075 }, { 12, 0.55 }, { 13, 0.075 }, { 14, 0.2 }, { 15, 0.725 }, { 16, 0.075 }, { 17, 0.2 }, { 12, 0.725 }, { 13, 0.075 }, { 14, 0.2 }, { 18, 0.725 }, { 16, 0.075 }, { 17, 0.2 }, } local MIKU_FINALE = { { 19, 0.35 }, { 20, 0.325 }, { 21, 0.325 }, { 22, 0.3 }, { 23, 0.35 }, { 24, 0.35 }, { 25, 0.35 }, { 26, 0.375 }, { 27, 1.275 }, } local MIKU_DANCER = { origin = { 0, 304 }, enter = { beats = 2, { 0, 0, 0, 343 }, { 0.35, 0, 0, 343, to = { 0, -69, "out" } }, { 0.925, 0, 0, -69, to = { 0, 0, "in" } }, { 1.2, 1, 0, 0 }, { 1.225, 1, 0, 0, to = { sy = 0.95 } }, { 1.25, 1, 0, 0, sy = 0.95, to = { sy = 1 } }, { 1.325, 1, 0, 0 }, }, dance = Timing.steps(MIKU_A, MIKU_B, MIKU_A, MIKU_FINALE), } local MIKU_INDEX = 4 local MIKU_DANCERS = Dancer.define({ [MIKU_INDEX] = MIKU_DANCER, default = SMALL_DANCER }) -- Dancers stand on fixed slots (far-left .. far-right) instead of the group's even spacing, -- since Miku's frames are wider than the others local DANCER_CENTERS = { 173, 387, 640, 893, 1107 } local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- outer pairs first, centre on top -- Background local MikuBackground = {} MikuBackground.__index = MikuBackground function MikuBackground.new(player_num, bpm) local self = setmetatable({}, MikuBackground) self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DonBG.parallax(MIKU_DONBG_FAST_X)) self.bg_normal = MikuBGNormal.new() self.bg_fever = MikuBGFever.new(bpm) self.footer = Footer.new(COLLAB_PATH, 0, { false, true }) self.fever = Fever.new(bpm, COLLAB_PATH, FEVER_SPEC) self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, MIKU_DANCERS) for i, d in ipairs(self.dancer.dancers) do if d.index == MIKU_INDEX then self.dancer.dancers[i], self.dancer.dancers[1] = self.dancer.dancers[1], self.dancer.dancers[i] break end end self.renda = RendaController.new(COLLAB_PATH, 0, RendaController.ACROSS) self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1) self.is_clear = false self.is_rainbow = false self.last_milestone = 1 return self end function MikuBackground:update(current_ms, bpm) self.donbg:update(current_ms, bpm) self.bg_normal:update(current_ms) 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 MikuBackground:handle_good(player_num) BaseChibi.add(self.chibi, MIKU_CHIBI, COLLAB_PATH, player_num) end function MikuBackground:handle_ok(player_num) BaseChibi.add(self.chibi, MIKU_CHIBI, COLLAB_PATH, player_num) end function MikuBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function MikuBackground:handle_drumroll(player_num) self.renda:add_renda() end function MikuBackground:handle_balloon(player_num) self.renda:add_renda() end function MikuBackground: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 MikuBackground:draw_back() self.bg_normal:draw() self.bg_fever: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 MikuBackground:draw_fore() end function MikuBackground:destroy() self.donbg:destroy() self.bg_normal:destroy() self.bg_fever:destroy() self.fever:destroy() self.dancer:destroy() self.footer:destroy() self.renda:destroy() self.chibi:destroy() end return MikuBackground