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 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/gumi" -- fever line-up: two poses, 70px bounce local FEVER_SPEC = { bounce = 70, poses = true } local MAX_DANCERS = 5 local chain_moves, frame_change, sum = Timing.chain_moves, Timing.frame_change, Timing.sum -- Normal background: the stage screens spell out G, U, M, I, one letter every 1.5 beats, -- the next letter fading in over the last half beat. Timed in beats. local BG_NORMAL_PANEL = { { 1, 0 }, { 0.5, 1000 } } -- next panel's opacity in thousandths local GumiBGNormal = {} GumiBGNormal.__index = GumiBGNormal function GumiBGNormal.new(bpm) local self = setmetatable({}, GumiBGNormal) self.name = "bg_0" tex.load_folder(COLLAB_PATH, "bg_normal/" .. self.name) self.frames = tex.get_texture_info(self.name, "background").frame_count self._id_bg = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. "background") or 0 self.beat = 60000 / bpm self.panel = 0 self.fade = chain_moves(BG_NORMAL_PANEL, self.beat, 1) return self end -- rebuilt per panel so a tempo change takes effect on the next letter function GumiBGNormal:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end for _, m in ipairs(self.fade) do m:update(current_time_ms) end if self.fade[#self.fade].is_finished then self.panel = (self.panel + 1) % self.frames self.fade = chain_moves(BG_NORMAL_PANEL, self.beat, 1) end end function GumiBGNormal:draw() tex.draw_texture(self._id_bg, { frame = self.panel }) tex.draw_texture(self._id_bg, { frame = (self.panel + 1) % self.frames, fade = sum(self.fade) / 1000 }) end function GumiBGNormal:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name) end -- Clear background: a white flash over the stage, then the screens cycle through eight -- panels, one a beat, with a white pulse and pulsing spotlights on every beat. Losing clear -- cuts to the dark stage, flickers the last panel and fades back to the normal stage. -- Timed in beats; drawn over bg_normal, which shows through before and after. local BG_FEVER_PANELS = 8 -- frame_order in texture.json local BG_FEVER_FLASH = { { 0.375, 1000, "out" }, { 0.75, -1000, "in" } } -- entry flash local BG_FEVER_LEAD = 0.375 -- panels start under it local BG_FEVER_SPOTS = { { 0.475, 0 }, { 1.025, 1000 } } -- spotlights fade in -- per beat: the next panel fades in at the end; additive white pulse; spotlight pulse local BG_FEVER_FADE = { { 0.625, 0 }, { 0.375, 1000 } } -- a full white pulse is too strong in game; toned down to 40% (peak in thousandths) so the stage stays visible local BG_FEVER_PULSE = { { 0.375, 400, "in" }, { 0, -55 }, { 0.325, -345, "out" } } local BG_FEVER_SPOT = { { 0.375, 0 }, { 0, -43 }, { 0.575, -957 } } -- from 1000 -- exit: the first panel flickers over the dark stage, then fades out local BG_FEVER_FLICKER = { { 0.025, 0 }, { 0, 1000 }, { 0.15, 0 }, { 0, -1000 }, { 0.025, 0 }, { 0, 1000 }, { 0.1, 0 }, { 0, -1000 }, { 0.025, 0 }, { 0, 1000 }, { 0.1, 0 }, { 0, -1000 }, { 0.025, 0 }, { 0, 1000 }, { 0.525, -1000 }, } local BG_FEVER_FLICKER_FRAMES = { { 0, 0.2 }, { 2, 0.125 }, { 0, 0.65 } } local BG_FEVER_SPOTS_OUT = 0.55 local LIGHT_FRAMES = { 0, 1, 1, 2, 2, 3, 3 } -- texture.json "light" indices local LIGHT_MIRROR = { false, false, true, false, true, false, true } local GumiBGFever = {} GumiBGFever.__index = GumiBGFever function GumiBGFever.new(bpm) local self = setmetatable({}, GumiBGFever) 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_bg = id("background") self._id_flash = id("flash") self._id_dark = id("dark") self._id_light = id("light") self.beat = 60000 / bpm self.state = nil -- nil (hidden), "in", "out" return self end local function update_all(moves, current_time_ms) for _, m in ipairs(moves) do m:update(current_time_ms) end end function GumiBGFever:spots() return (self.spots_offset + sum(self.spot_env)) / 1000 end function GumiBGFever:next_beat() self.fade = chain_moves(BG_FEVER_FADE, self.beat, 1) self.pulse = chain_moves(BG_FEVER_PULSE, self.beat, 1) self.spot = chain_moves(BG_FEVER_SPOT, self.beat, 1) end function GumiBGFever:start() self.state = "in" self.flash = chain_moves(BG_FEVER_FLASH, self.beat, 1) self.spots_offset = 0 self.spot_env = chain_moves(BG_FEVER_SPOTS, self.beat, 1) self.lead = anim.move(BG_FEVER_LEAD * self.beat, { total_distance = 1 }) self.lead:start() self.panel = 0 self.fade = nil -- the beat loop starts once the lead is over end -- spotlights fade from wherever they are function GumiBGFever:stop() if self.state ~= "in" then return end local spots = math.floor(self:spots() * 1000) self.state = "out" self.spots_offset = spots self.spot_env = chain_moves({ { BG_FEVER_SPOTS_OUT, -spots } }, self.beat, 1) self.flicker = chain_moves(BG_FEVER_FLICKER, self.beat, 1) self.flicker_frame = frame_change(BG_FEVER_FLICKER_FRAMES, self.beat) self.flicker_frame:start() end function GumiBGFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end if not self.state then return end update_all(self.spot_env, current_time_ms) update_all(self.flash, current_time_ms) self.lead:update(current_time_ms) if self.state == "out" then update_all(self.flicker, current_time_ms) self.flicker_frame:update(current_time_ms) if self.flicker[#self.flicker].is_finished then self.state = nil end end if not self.lead.is_finished then return end if not self.fade then self:next_beat() end update_all(self.fade, current_time_ms) update_all(self.pulse, current_time_ms) update_all(self.spot, current_time_ms) if self.fade[#self.fade].is_finished then self.panel = (self.panel + 1) % BG_FEVER_PANELS self:next_beat() end end function GumiBGFever:draw_spots() if not self.fade then return end local fade = self:spots() * (1000 + sum(self.spot)) / 1000 for i, frame in ipairs(LIGHT_FRAMES) do tex.draw_texture(self._id_light, { index = i - 1, frame = frame, fade = fade, blend = "additive", mirror = LIGHT_MIRROR[i] and "horizontal" or nil, }) end end function GumiBGFever:draw() if not self.state then return end if self.state == "in" then if self.fade then tex.draw_texture(self._id_bg, { frame = self.panel }) tex.draw_texture(self._id_bg, { frame = (self.panel + 1) % BG_FEVER_PANELS, fade = sum(self.fade) / 1000 }) tex.draw_texture(self._id_flash, { fade = sum(self.pulse) / 1000, blend = "additive" }) end tex.draw_texture(self._id_flash, { fade = sum(self.flash) / 1000 }) else tex.draw_texture(self._id_dark) tex.draw_texture(self._id_bg, { frame = self.flicker_frame.attribute, fade = sum(self.flicker) / 1000 }) end self:draw_spots() end function GumiBGFever: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. Each jumps up from below, -- lands with a squash, then loops a 16-beat routine; the small ones hop off when removed. -- Small dancers: pose 0.725 beat, in-between 0.275 beat. Dancer 3 ends its routine on frame 18. local function small_dance(last) return { { 1, 0.725 }, { 2, 0.275 }, { 3, 0.725 }, { 4, 0.275 }, { 1, 0.725 }, { 2, 0.275 }, { 3, 0.725 }, { 4, 0.275 }, { 0, 0.725 }, { 5, 0.275 }, { 6, 0.725 }, { 7, 0.275 }, { 0, 0.725 }, { 8, 0.275 }, { 9, 0.45 }, { 10, 0.275 }, { 0, 0.275 }, { 11, 0.725 }, { 12, 0.275 }, { 13, 0.725 }, { 14, 0.275 }, { 11, 0.725 }, { 12, 0.275 }, { 13, 0.725 }, { 15, 0.275 }, { 16, 1 }, { 17, 0.475 }, { 16, 0.525 }, { 17, 1.725 }, { last, 0.275 }, } end -- poses are offsets from where each dancer stands, pivoting about its feet (bottom-left) local function small_dancer(last) return { origin = { 0, 200 }, enter = { beats = 2, { 0, 0, 0, 213 }, { 0.35, 0, 0, 213, to = { 0, -43, "out" } }, -- jumps up from below { 0.925, 0, 0, -43, to = { 0, 0, "in" } }, { 1.2, 0, 0, 0, to = { nil, nil, "out", sy = 0.94 } }, -- lands with a squash { 1.3, 0, 0, 0, sy = 0.94, to = { nil, nil, "in", sy = 1 } }, { 1.425, 0, 0, 0 }, }, dance = Timing.steps(small_dance(last)), leave = { beats = 1.325, { 0, 0, 0, 0, to = { nil, nil, "out", sy = 0.93 } }, -- crouches { 0.125, 0, 0, 0, sy = 0.93, to = { nil, nil, "in", sy = 1 } }, { 0.25, 0, 0, -21, to = { 0, -100, "out" } }, -- hops up and drops away { 0.475, 0, 0, -100, to = { 0, 214, "in" } }, }, } end -- GUMI in the centre: lands on her second entry frame, which sits 13px higher local GUMI_DANCER = { origin = { 0, 296 }, enter = { beats = 2, { 0, 0, 0, 366 }, { 0.35, 0, 0, 366, to = { 0, -40, "out" } }, { 0.925, 0, 0, -40, to = { 0, 13, "in" } }, { 1.175, 0, 0, 0 }, { 1.2, 1, 0, 0, to = { nil, nil, "out", sy = 0.95 } }, { 1.325, 1, 0, 0, sy = 0.95, to = { nil, nil, "in", sy = 1 } }, { 1.45, 1, 0, 0 }, }, dance = Timing.steps({ { 2, 0.725 }, { 3, 0.275 }, { 4, 0.725 }, { 5, 0.275 }, { 2, 0.725 }, { 3, 0.275 }, { 6, 0.725 }, { 7, 0.275 }, { 8, 0.725 }, { 9, 0.275 }, { 10, 0.725 }, { 11, 0.275 }, { 12, 0.275 }, { 13, 0.45 }, { 14, 0.275 }, { 15, 0.725 }, { 16, 0.275 }, { 17, 0.725 }, { 18, 0.275 }, { 19, 0.725 }, { 20, 0.275 }, { 17, 0.725 }, { 18, 0.275 }, { 19, 0.725 }, { 20, 0.275 }, { 21, 1 }, { 22, 0.5 }, { 23, 0.5 }, { 24, 1.725 }, { 11, 0.275 }, }), } local GUMI_INDEX = 4 local GUMI_DANCERS = Dancer.define({ [0] = small_dancer(0), [1] = small_dancer(0), [2] = small_dancer(0), [3] = small_dancer(18), [GUMI_INDEX] = GUMI_DANCER, }) -- Dancers stand on fixed slots (far-left .. far-right) instead of the group's even -- spacing, since the centre dancer is wider than the others local DANCER_CENTERS = { 183, 417, 620, 863, 1097 } local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- outer pairs first, centre on top -- Chibi GUMI for good hits: hops in on her carrot growing from 70% size, then rides it -- straight across the don band local GUMI_CHIBI = { key = "1", hop = { from = { -418, 21 }, peak = { -334, -36 }, up = 0.3 }, hop_frames = { { 0, 0.3 }, { 1, 0.175 }, { 0, 0.025 } }, period = 2, poses = { { 0, 0 }, { 0.3, 1 }, { 0.65, 2 }, { 1, 3 }, { 1.325, 4 }, { 1.675, 5 } }, } -- Background local GumiBackground = {} GumiBackground.__index = GumiBackground function GumiBackground.new(player_num, bpm) local self = setmetatable({}, GumiBackground) self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DonBG.parallax()) self.bg_normal = GumiBGNormal.new(bpm) self.bg_fever = GumiBGFever.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, GUMI_DANCERS) for i, d in ipairs(self.dancer.dancers) do if d.index == GUMI_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 GumiBackground:update(current_ms, bpm) self.donbg:update(current_ms, bpm) self.bg_normal: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 GumiBackground:handle_good(player_num) BaseChibi.add(self.chibi, GUMI_CHIBI, COLLAB_PATH, player_num) end function GumiBackground:handle_ok(player_num) BaseChibi.add(self.chibi, GUMI_CHIBI, COLLAB_PATH, player_num) end function GumiBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function GumiBackground:handle_drumroll(player_num) self.renda:add_renda() end function GumiBackground:handle_balloon(player_num) self.renda:add_renda() end function GumiBackground: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 GumiBackground: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 GumiBackground:draw_fore() end function GumiBackground: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 GumiBackground