local DonBGBase = require("background/bg_objects/donbg/base") local Fever = require("background/bg_objects/fever") local PetalFever = require("background/bg_objects/petal_fever") local Footer = require("background/bg_objects/footer") 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 COLLAB_PATH = "background/collab/a3" local MAX_DANCERS = 4 local chain_moves, frame_change, sum, concat, Loop = Timing.chain_moves, Timing.frame_change, Timing.sum, Timing.concat, Timing.Loop -- Don band: one tile scrolls by every 21.3 seconds; clearing fades the festival set in -- and back out over a quarter beat (the default) local A3_DONBG = { layers = { { key = "background", ms = 21333 } } } -- Normal background: the festival street under the stage curtain, with the lanterns' glow -- flickering additively over it (ambient, in ms; opacity in thousandths from 699) local FLICKER_MS = 333 local FLICKER_BASE = 699 local FLICKER = { { 83, -97 }, { 83, 46 }, { 84, -97 }, { 83, 148 } } local A3BGNormal = {} A3BGNormal.__index = A3BGNormal function A3BGNormal.new() local self = setmetatable({}, A3BGNormal) 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_overlay = id("overlay") self._id_curtain = id("curtain") self.flicker = Loop.new(FLICKER_MS, function() return { glow = chain_moves(FLICKER, 1, 1) } end) return self end function A3BGNormal:update(current_time_ms) self.flicker:update(current_time_ms) end function A3BGNormal:draw() tex.draw_texture(self._id_bg) if self.flicker.anims then local glow = (FLICKER_BASE + sum(self.flicker.anims.glow)) / 1000 tex.draw_texture(self._id_overlay, { fade = glow, blend = "additive" }) end tex.draw_texture(self._id_curtain) end function A3BGNormal:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name) end -- Clear background: a golden haze and two streams of petals over the normal one. Petal -- flights in ms. local A3_FEVER = { path = COLLAB_PATH, haze = { key = "overlay" }, haze_in = 1.5, haze_out = 1, petals_out = 0.25, streams = { -- falling petals: glow additively at 80%, fading out over the second half of the flight { key = "petals", flight = 6667, dx = -340, dy = 690, first = 0, alpha = 0.8, fade_out = true, blend = "additive" }, -- turned a quarter: drift across at full opacity (164 = half the petal texture) { key = "petals", flight = 5000, dx = -260, dy = 670, first = 3, rotation = 90, half = 164 }, }, } -- Dancers: four fox-hooded dancers on fixed slots. They pop in and out with no -- entrance, all following one 16-beat routine, so a dancer joining mid-song is in step. -- Each dancer's N_loop holds its poses, then the same poses mirrored. Timed in beats. -- Routine steps: { pose, beats, mirrored }; "x"/"after_x"/"y" are per-dancer poses local DANCE = { { 0, 0.35 }, { 1, 0.25 }, { 2, 1.4 }, { 1, 0.1 }, { 0, 0.45 }, { 1, 0.05 }, { 2, 0.85 }, { 3, 0.55 }, { 0, 0.35, true }, { 1, 0.25, true }, { 2, 1.4, true }, { 1, 0.1, true }, { 0, 0.25, true }, { 1, 0.25, true }, { 2, 0.85, true }, { 3, 0.475, true }, { "x", 0.075, true }, { "after_x", 0.6, true }, { 4, 0.2, true }, { 5, 0.2, true }, { 6, 0.85, true }, { 7, 0.6, true }, { 8, 1.55, true }, { "y", 0.6 }, { 4, 0.2 }, { 5, 0.2 }, { 6, 0.85 }, { 7, 0.6 }, { 8, 1.55 }, } local DANCE_BEATS = 16 -- index = skin folder; centre = the slot's x. Dancer 1 has an extra pose (4) and shifts the -- rest up by one. local DANCERS = { [0] = { center = 775, poses = 9, shift = 0, x = 1, after_x = 0, y = 1 }, [1] = { center = 505, poses = 10, shift = 1, x = 4, after_x = 4, y = 4 }, [2] = { center = 1035, poses = 9, shift = 0, x = 1, after_x = 0, y = 0 }, [3] = { center = 245, poses = 9, shift = 0, x = 1, after_x = 0, y = 0 }, } local DRAW_ORDER = { 3, 2, 1, 0 } -- back to front -- a hop every two beats for the first half (px), and the shadow shrinking under it local HOP = { { 0.375, -16 }, { 0.5, 23 }, { 0.15, -7 } } local HOPS = concat({ { 0.6, 0 } }, HOP, { { 0.975, 0 } }, HOP, { { 0.975, 0 } }, HOP, { { 0.975, 0 } }, HOP) local SHADOW = { { 0.6, 0 }, { 0.35, -100 }, { 0.375, 100 }, { 0.675, 0 } } -- scale, per 2 beats -- squash: px off the 312px height, feet planted. A dip on the pose changes, B a dip and a -- stretch on the turn, C a slow sink on the last pose local SQUASH_A = { { 0.2, 16 }, { 0.225, -16 } } local SQUASH_B = { { 0.4, 16 }, { 0.6, -20 }, { 0.25, 4 } } local SQUASH_C = { { 0, 6 }, { 0.525, 6 }, { 0.275, -12 } } local SQUASH = concat({ { 0.125, 0 } }, SQUASH_A, { { 1.55, 0 } }, SQUASH_A, { { 1.6, 0 } }, SQUASH_A, { { 1.55, 0 } }, SQUASH_A, { { 1.475, 0 } }, SQUASH_B, { { 1.2, 0 } }, SQUASH_C, { { 0.75, 0 } }, SQUASH_B, { { 1.2, 0 } }, SQUASH_C) -- dancer 0's music note pops by its head twice (texture.json "note" indices 0, 1) local NOTES = { { { 3.45, 0 }, { 0.3, 1000 }, { 0.35, -1000 } }, { { 7.45, 0 }, { 0.325, 1000 }, { 0.325, -1000 } }, } local SHADOW_ALPHA = 0.45 local function dance_steps(spec) local steps = {} for _, step in ipairs(DANCE) do local pose = step[1] if type(pose) == "string" then pose = spec[pose] elseif pose >= 4 then pose = pose + spec.shift end table.insert(steps, { pose + (step[3] and spec.poses or 0), step[2] }) end return steps end local A3Dancers = {} A3Dancers.__index = A3Dancers function A3Dancers.new(bpm) local self = setmetatable({}, A3Dancers) self.name = "dancer_0" tex.load_folder(COLLAB_PATH, "dancer/" .. self.name) local function id(key) return tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. key) or 0 end self._id_shadow = id("shadow") self._id_note = id("note") self._ids = {} self.steps = {} for i, spec in pairs(DANCERS) do self._ids[i] = id(i .. "_loop") self.steps[i] = dance_steps(spec) end self.width = tex.get_texture_info(self.name, "0_loop").width self.shown = 1 self:set_bpm(bpm) return self end function A3Dancers:set_bpm(bpm) self.bpm = bpm local beat = 60000 / bpm local scale = tex.get_screen_scale() self.routine = Loop.new(DANCE_BEATS * beat, function() local frames = {} for i in pairs(DANCERS) do frames[i + 1] = frame_change(self.steps[i], beat) frames[i + 1]:start() end local shadow = {} for _ = 1, 4 do shadow = concat(shadow, SHADOW) end return { frames = frames, hop = chain_moves(HOPS, beat, scale), squash = chain_moves(SQUASH, beat, scale), shadow = chain_moves(shadow, beat, 1), note1 = chain_moves(NOTES[1], beat, 1), note2 = chain_moves(NOTES[2], beat, 1), } end) end function A3Dancers:set_count(count) self.shown = count end function A3Dancers:update(current_time_ms, bpm) if bpm and bpm ~= self.bpm then self:set_bpm(bpm) end self.routine:update(current_time_ms) end function A3Dancers:draw() local a = self.routine.anims if not a then return end local scale = tex.get_screen_scale() local squash = sum(a.squash) local shadow = (1000 + sum(a.shadow)) / 1000 for _, i in ipairs(DRAW_ORDER) do if i < self.shown then local x = math.floor(DANCERS[i].center * scale - self.width / 2) tex.draw_texture(self._id_shadow, { x = x, center = true, scale = shadow, fade = SHADOW_ALPHA * shadow }) tex.draw_texture(self._ids[i], { frame = a.frames[i + 1].attribute, x = x, y = sum(a.hop) + squash, y2 = -squash, }) if i == 0 then for n, note in ipairs({ a.note1, a.note2 }) do local fade = sum(note) / 1000 if fade > 0 then tex.draw_texture(self._id_note, { index = n - 1, x = x, fade = fade }) end end end end end end function A3Dancers:destroy() tex.unload_folder(COLLAB_PATH, "dancer/" .. self.name) end -- Chibi for good hits: one of four festival tiles hops in growing from 70% size, then bobs -- across the don band local A3_CHIBI_MOTION = { key = "1", hop = { from = { -418, 0 }, peak = { -335, -47 }, up = 0.3 }, run = { y = -3 }, wave = { first = -12, height = 22, beats = 0.5 }, -- a steady bob } local A3_CHIBIS = {} for frame = 0, 3 do -- one character per frame of chibi_1/1 A3_CHIBIS[frame + 1] = setmetatable({ first = frame }, { __index = A3_CHIBI_MOTION }) end -- Background local A3Background = {} A3Background.__index = A3Background function A3Background.new(player_num, bpm) local self = setmetatable({}, A3Background) self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, A3_DONBG) self.bg_normal = A3BGNormal.new() self.bg_fever = PetalFever.new(bpm, A3_FEVER) -- A3 has no dodai or fever of its own; the game uses the regular ones self.footer = Footer.new("background", math.random(0, 2)) self.fever = Fever.create(math.random(0, 3), bpm, "background") self.dancer = A3Dancers.new(bpm) self.renda = RendaController.new(COLLAB_PATH, 0) -- the default drumroll flight self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1) self.is_clear = false self.is_rainbow = false return self end function A3Background: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 local function add_chibi(self, player_num) BaseChibi.add(self.chibi, A3_CHIBIS[math.random(#A3_CHIBIS)], COLLAB_PATH, player_num, ChibiWave) end function A3Background:handle_good(player_num) add_chibi(self, player_num) end function A3Background:handle_ok(player_num) add_chibi(self, player_num) end function A3Background:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function A3Background:handle_drumroll(player_num) self.renda:add_renda() end function A3Background:handle_balloon(player_num) self.renda:add_renda() end function A3Background: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 -- one dancer to start, up to three with the gauge, all four once cleared if is_clear then self.dancer:set_count(MAX_DANCERS) else self.dancer:set_count(math.max(1, math.min(MAX_DANCERS - 1, math.floor(progress * MAX_DANCERS)))) end end function A3Background:draw_back() self.bg_normal:draw() self.bg_fever:draw() self.footer:draw() self.donbg:draw() self.renda:draw() self.dancer:draw() self.fever:draw() self.chibi:draw() end function A3Background:draw_fore() end function A3Background:destroy() self.donbg:destroy() self.bg_normal:destroy() self.bg_fever:destroy() self.footer:destroy() self.fever:destroy() self.dancer:destroy() self.renda:destroy() self.chibi:destroy() end return A3Background