local DonBGBase = require("background/bg_objects/donbg/base") local Fever = require("background/bg_objects/fever") local Dancer = require("background/bg_objects/dancer") 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/mario" local MAX_DANCERS = 5 local pose_at, steps, concat = Timing.pose_at, Timing.steps, Timing.concat -- Ambient clocks count Lumen's 60fps frames local function frames(ms) return ms * 60 / 1000 end -- ? blocks (and the clear band's coins) blink dark for three short steps every 50 frames; -- `f` is one frame in the table's unit local function blink(f) return steps({ { 0, 35 * f, hide = true }, { 0, 5 * f }, { 1, 5 * f }, { 0, 5 * f } }) end local BLINK = blink(1) -- The course (drawn 3x, 3840 wide) and the don band's course strip scroll left 1px a frame local COURSE_W = 3840 -- Don band: the course strip with its blinking ? blocks; the clear set is a strip of coins -- and clouds (1536 wide) whose coins blink the same way. Ambient, in ms. local BLINK_MS = blink(1000 / 60) local COURSE_MS = COURSE_W * 1000 / 60 local DONBG_SPEC = { layers = { { key = "background", ms = COURSE_MS, dx = -COURSE_W, tile = COURSE_W }, { key = "blocks", ms = COURSE_MS, dx = -COURSE_W, tile = COURSE_W, blink = BLINK_MS }, { key = "coins", ms = 1536 * 1000 / 60, dx = -1536, tile = 1536, clear_only = true }, { key = "coin_blink", ms = 1536 * 1000 / 60, dx = -1536, tile = 1536, clear_only = true, blink = BLINK_MS }, } } -- Normal background: the course scrolling under the lane, ? blocks blinking local MarioBGNormal = {} MarioBGNormal.__index = MarioBGNormal function MarioBGNormal.new() local self = setmetatable({}, MarioBGNormal) self.name = "bg_0" tex.load_folder(COLLAB_PATH, "bg_normal/" .. self.name) self._id_bg = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/background") or 0 self._id_blocks = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/blocks") or 0 self.now = 0 return self end function MarioBGNormal:update(current_time_ms) self.now = current_time_ms end function MarioBGNormal:draw() local scale = tex.get_screen_scale() local x = -(frames(self.now) % COURSE_W) local blink = pose_at(BLINK, frames(self.now) % BLINK.length) for _, dx in ipairs({ x, x + COURSE_W }) do tex.draw_texture(self._id_bg, { x = dx * scale }) if blink then tex.draw_texture(self._id_blocks, { frame = blink.frame, x = dx * scale }) end end end function MarioBGNormal:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name) end -- Clear background: fades in over the normal course and races to the goal, speeding up -- until it stops on the flagpole and castle. The flag slides down the pole, the castle's flag -- goes up, then four fireworks burst over the castle in turn, each every 3 beats. Losing the -- clear fades it out; the next clear starts the race again. Timed in beats. local FADE = 0.5 local RACE, GOAL_X = 2.25, -4089 -- beats to reach the goal, and the course's scroll there local FLAG_DOWN = { at = 2.25, beats = 0.75, y = 203 } local CASTLE_AT = 2.25 local CASTLE_FLAG = { at = 3.025, beats = 0.75, y = -70 } local FIREWORK = steps({ { 0, 0.075 }, { 1, 0.075 }, { 2, 0.35 }, { 0, 2.5, hide = true } }) local FIREWORKS_AT = { 4.225, 5, 5.75, 6.525 } local MarioBGFever = {} MarioBGFever.__index = MarioBGFever function MarioBGFever.new(bpm) local self = setmetatable({}, MarioBGFever) 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.ids = {} for _, key in ipairs({ "background", "goal", "block", "castle_flag", "castle", "flag", "firework" }) do self.ids[key] = id(key) end self.beat = 60000 / bpm self.now = 0 self.alpha = 0 self.state = nil -- nil (hidden), "on", "off" return self end function MarioBGFever:start() self.state, self.t0, self.alpha = "on", self.now, 0 end -- fades out from wherever the fade is function MarioBGFever:stop() if self.state == "on" then self.state, self.t_off, self.alpha_off = "off", self.now, self.alpha end end function MarioBGFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end self.now = current_time_ms if self.state == "on" then self.alpha = math.min(1, (self.now - self.t0) / self.beat / FADE) elseif self.state == "off" then self.alpha = self.alpha_off * math.max(0, 1 - (self.now - self.t_off) / self.beat / FADE) if self.alpha <= 0 then self.state = nil end end end function MarioBGFever:draw() if not self.state then return end local scale = tex.get_screen_scale() local t = (self.now - self.t0) / self.beat local race = math.min(t / RACE, 1) local x = GOAL_X * race * race * scale local fade = self.alpha local function draw(key, p) p.x, p.fade = (p.x or 0) + x, fade tex.draw_texture(self.ids[key], p) end local function slide(m) return math.max(0, math.min((t - m.at) / m.beats, 1)) * m.y * scale end draw("background", {}) draw("goal", {}) local blink = pose_at(BLINK, frames(self.now - self.t0) % BLINK.length) if blink then draw("block", { frame = blink.frame }) end if t >= CASTLE_FLAG.at then draw("castle_flag", { y = slide(CASTLE_FLAG) }) end if t >= CASTLE_AT then draw("castle", {}) end draw("flag", { y = slide(FLAG_DOWN) }) for i, at in ipairs(FIREWORKS_AT) do local burst = t >= at and pose_at(FIREWORK, (t - at) % FIREWORK.length) if burst then draw("firework", { index = i - 1, frame = burst.frame }) end end end function MarioBGFever:destroy() tex.unload_folder(COLLAB_PATH, "bg_fever/" .. self.name) end -- Fever: the line-up shoots up from below the lane and settles; the group then holds while -- each character keeps its own loop, timed in beats from the start. Offsets are screen px from -- each one's texture.json rest position. -- three hops a beat apart, then a higher fourth one; `f` = up and down frames (and the fourth -- hop's), `lift` = how far the up frame starts above the down one (the koopas) local function hops(h, last, f, lift) lift = lift or 0 local out = {} for i = 0, 3 do local top = i == 3 and last or h table.insert(out, { i, f[i == 3 and 3 or 1] or f[1], 0, -lift, to = { 0, -top, "out" } }) table.insert(out, { i + 0.5, f[i == 3 and 4 or 2] or f[2] or f[1], 0, lift - top, to = { 0, 0, "in" } }) end out.length = 4 return out end -- the blooper squats, springs up and right, sinks back, and does it again ten times, drifting -- across and off the screen before starting over from the left local function blooper() local out = {} for k = 0, 9 do local x = k * 192 table.insert(out, { 2 * k, 0, x, 0, to = { x, 48 } }) table.insert(out, { 2 * k + 0.25, 1, x, 0, to = { x + 192, -192, "out" } }) table.insert(out, { 2 * k + 1, 1, to = { x + 192, 0 } }) end out.length = 20 return out end local FLAP = steps({ { 0, 0.5 }, { 1, 0.5 } }) local CAST = { -- cheep cheep waits below the right edge, leaps up-left and falls back out of sight { key = "cheep", frames = FLAP, path = { { 0, 0, 0, 0 }, { 4, 0, 0, 0, to = { -534, -344 } }, { 5, 0, to = { -930, -437 } }, { 6, 0, to = { -1174, -350, "in" } }, { 7, 0, to = { -1406, 0 } }, length = 8, } }, -- Lakitu bobs on his cloud, turning to the front one beat in four { key = "lakitu", frames = steps({ { 0, 3 }, { 1, 1 } }), path = { { 0, 0, 0, 0, to = { 0, -20, "out" } }, { 0.5, 0, to = { 0, 0, "in" } }, length = 1, } }, -- a Bullet Bill crosses from right to left every 11 beats { key = "bullet", path = { { 0, 0, 0, 0 }, { 2, 0, 0, 0, to = { -1514, 0 } }, { 5.2, 0, -1514, 0 }, length = 11.075, } }, { key = "blooper", path = blooper() }, { key = "toad", path = hops(12, 16.8, { 0 }) }, { key = "peach", path = hops(12, 16.8, { 0 }) }, { key = "buzzy", path = hops(22, 32.8, { 0, 1 }) }, { key = "koopa", path = hops(41, 57.5, { 0, 1 }, 8) }, -- Bowser's hammer: thrown in an arc down to the left, gone a beat, back in his hand as he -- jumps, then swung up behind his head; spinning while it flies { key = "hammer", size = 153.6, spin = steps({ { 0, 0.125 }, { 1, 0.125, rot = 180 }, { 0, 0.125, rot = 180 }, { 1, 0.125 } }), path = { { 0, 0, -203, -159, spin = true, to = { -320, -86, "in" } }, { 1, 0, spin = true, to = { -499, 258 } }, { 2, 0, hide = true }, { 3, 0, 0, 59, to = { 0, 0, "out" } }, { 3.5, 0, 0, 0, spin = true, to = { -86, -112 } }, { 3.75, 0, spin = true, to = { -189, -160 } }, length = 4, } }, { key = "bowser", path = hops(19.5, 58.6, { 0, 1, 2, 0 }) }, { key = "red_koopa", path = hops(35.5, 52, { 0, 1 }, 8) }, } -- whether the path pose on at t is a spinning one local function spinning(path, t) local current for _, p in ipairs(path) do if t >= p[1] then current = p end end return current and current.spin end local MarioFever = setmetatable({}, { __index = Fever }) MarioFever.__index = MarioFever function MarioFever.new(bpm) local self = Fever.new(bpm, COLLAB_PATH, { rise = 530, peak = 38, bounce = 30, drop = 545, keys = {} }) setmetatable(self, MarioFever) self.ids = {} for _, c in ipairs(CAST) do self.ids[c.key] = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. c.key) or 0 end self.now = 0 return self end function MarioFever:start() Fever.start(self) self.t0 = self.now end -- the group holds once in; only leaving ends it function MarioFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end self.now = current_time_ms if not self.state then return end for _, m in ipairs(self.moves) do m:update(current_time_ms) end if self.state == "out" and self.moves[#self.moves].is_finished then self.state = nil end end function MarioFever:draw() if not self.state then return end local scale, y = tex.get_screen_scale(), self:y() local t = (self.now - self.t0) / self.beat for _, c in ipairs(CAST) do local pose = pose_at(c.path, t % c.path.length) if pose then local frame, rot = pose.frame, 0 if c.frames then frame = pose_at(c.frames, t % c.frames.length).frame end if c.spin and spinning(c.path, t % c.path.length) then local spin = pose_at(c.spin, t % c.spin.length) frame, rot = spin.frame, spin.rot end local p = { frame = frame, x = pose.x * scale, y = pose.y * scale + y } if rot ~= 0 then -- turned about its centre local o = c.size / 2 * scale p.x, p.y, p.origin, p.rotation = p.x + o, p.y + o, { o, o }, rot end tex.draw_texture(self.ids[c.key], p) end end end -- Dancers -- Timed in beats; y is the offset from Lumen's dancer line (texture.json y 690), images -- centred on it. Mario and Luigi jump in, walk a step left and back, then jump twice turning -- round; leaving, they jump and drop off. local WALK = { { 1, 0.125 }, { 2, 0.125 }, { 1, 0.125 }, { 3, 0.125 } } local function walk(mirror) local out = {} for i = 1, 15 do out[i] = { WALK[(i - 1) % 4 + 1][1], 0.125, mirror = mirror } end return out end local function jump_in(from, top, land, frame) return { { 0, frame, 0, from, mirror = true, to = { 0, top, "out" } }, { 0.825, frame, mirror = true, to = { 0, land, "in" } }, { 1.5, frame + 1, 0, land, mirror = true }, beats = 2, } end local BROS = { origin = { 64, 64 }, enter = jump_in(235, -175, -65, 0), dance = { steps({ { 0, 0.125, mirror = true } }, walk(false), { { 0, 0.125 } }, walk(true), { { 4, 1.75, mirror = true }, { 5, 0.125, mirror = true }, { 5, 0.125 } }, { { 4, 1.75 }, { 5, 0.125 }, { 5, 0.125, mirror = true } }), move = { { 0, 0, 0, -65, to = { -20, -65 } }, { 2, 0, to = { 0, -65 } }, { 4, 0, to = { 0, -125, "out" } }, { 4.775, 0, to = { 0, -65, "in" } }, { 5.75, 0, 0, -65 }, { 6, 0, to = { 0, -125, "out" } }, { 6.775, 0, to = { 0, -65, "in" } }, { 7.75, 0, 0, -65 }, length = 8, }, }, leave = { { 0, 0, 0, -65, key = "end", mirror = true, to = { 0, -175, "out" } }, { 0.5, 0, key = "end", mirror = true, to = { 0, 165, "in" } }, { 1.5, 0, hide = true }, beats = 2, }, } -- frames played in turn every `step` beats over `beats` local function cycle(beats, step, list, fields) local out = {} for i = 0, math.ceil(beats / step) - 1 do local s = { list[i % #list + 1], math.min(step, beats - i * step) } for k, v in pairs(fields or {}) do s[k] = v end out[#out + 1] = s end return out end -- Goomba hops in, toddles left and back facing the other way, then hops twice; leaving, it -- flips over and falls local GOOMBA = { origin = { 64, 64 }, enter = { { 0, 0, 0, 185, to = { 0, -185, "out" } }, { 0.825, 0, to = { 0, -65, "in" } }, { 1.5, 1, 0, -65, to = { 0, -75, "out" } }, { 1.75, 1, to = { 0, -65, "in" } }, beats = 2, }, dance = { steps(cycle(8, 0.375, { 0, 1 }, { mirror = true })), move = { { 0, 0, 0, -65, to = { -20, -65 } }, { 2, 0, to = { 0, -65 } }, { 4, 0, to = { 0, -81, "out" } }, { 4.75, 0, to = { 0, -65, "in" } }, { 5.5, 0, 0, -65 }, { 6, 0, to = { 0, -81, "out" } }, { 6.75, 0, to = { 0, -65, "in" } }, { 7.5, 0, 0, -65 }, length = 8, }, }, leave = { steps(cycle(1.5, 0.375, { 0, 1 }, { key = "loop", rot = 180 })), move = { { 0, 0, 0, -65, to = { 0, -155, "out" } }, { 0.5, 0, to = { 0, 185, "in" } }, length = 1.5, }, beats = 2, }, } -- Bowser stomps in, sways left and back, then jumps twice with a little hop after each; -- leaving, he jumps, flips over and falls local function bowser_jump(at) return { { at, 0, to = { 0, -158, "out" } }, { at + 1, 0, to = { 0, -128, "in" } }, { at + 1.65, 0, to = { 0, -138, "out" } }, { at + 1.825, 0, to = { 0, -128, "in" } }, } end local BOWSER = { origin = { 128, 128 }, enter = jump_in(243, -172, -129, 0), dance = { steps(cycle(2, 0.5, { 2, 1 }, { mirror = true }), cycle(2, 0.5, { 2, 1 }), cycle(2, 0.5, { 3, 1 }), cycle(2, 0.5, { 0, 2 }, { mirror = true })), move = concat({ { 0, 0, 0, -128, to = { -20, -128 } }, { 2, 0, to = { 0, -128 } }, }, bowser_jump(4), bowser_jump(6)), }, leave = { { 0, 2, 0, -128, key = "loop", mirror = true, to = { 0, -166, "out" } }, { 0.475, 2, key = "loop", rot = 180, to = { 0, 194, "in" } }, { 1.475, 2, hide = true }, beats = 2, }, } BOWSER.dance.move.length = 8 -- The paratroopa flaps in, hops in arcs left, back, right and back (turning round), then -- flaps up and down twice; leaving, it drops into its shell, flips and falls local function flap(beats, mirror) return cycle(beats, 0.25, { 0, 1 }, { mirror = mirror }) end local PARATROOPA = { origin = { 64, 99.5 }, enter = { steps(flap(2, true)), move = { { 0, 0, 0, 60, to = { 0, -128, "out" } }, { 0.825, 0, to = { 0, -100, "in" } }, { 1.5, 0, to = { 0, -116, "out" } }, { 1.75, 0, to = { 0, -100, "in" } }, length = 2, }, beats = 2, }, dance = { steps(flap(1, false), flap(2, true), flap(5, false)), move = { { 0, 0, 0, -100, to = { -20, -150, "out" } }, { 0.45, 0, to = { -45, -100, "in" } }, { 1, 0, to = { -20, -150, "out" } }, { 1.45, 0, to = { 0, -100, "in" } }, { 2, 0, to = { 25, -159, "out" } }, { 2.45, 0, to = { 47, -100, "in" } }, { 3, 0, to = { 25, -159, "out" } }, { 3.45, 0, to = { 0, -100, "in" } }, { 4, 0, to = { 0, -150, "out" } }, { 5, 0, to = { 0, -100, "in" } }, { 6, 0, to = { 0, -150, "out" } }, { 7, 0, to = { 0, -100, "in" } }, length = 8, }, }, leave = { { 0, 0, 0, -90, key = "end", mirror = true, to = { 0, -150, "out" } }, { 0.5, 0, key = "end", rot = 180, to = { 0, 10, "in" } }, { 1.45, 0, hide = true }, beats = 2, }, } -- skin index = Lumen's dancerN - 1: Mario, Luigi, Goomba, Bowser, the paratroopa local MARIO_DANCERS = Dancer.define({ [0] = BROS, [1] = BROS, [2] = GOOMBA, [3] = BOWSER, [4] = PARATROOPA }) -- Lumen's slots, left to right; the group fills centre, left, right, far-left, far-right, -- which is Lumen's dancer1..5 (skin dancers 0..4). Dancer1 is drawn on top. local DANCER_CENTERS = { 213, 427, 640, 853, 1067 } local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- Chibis for good hits: Mario, Luigi or a koopa hops in at full size and runs local MARIO_RUN = { { 0, 1 }, { 0.125, 2 }, { 0.25, 1 }, { 0.375, 3 } } local KOOPA_RUN = { { 0, 0 }, { 0.25, 1 } } local MARIO_CHIBIS = { { key = "0", run = { y = 16 }, start_size = 1, period = 0.5, poses = MARIO_RUN }, { key = "1", run = { y = 16 }, start_size = 1, period = 0.5, poses = MARIO_RUN }, { key = "2", run = { y = 16 }, start_size = 1, period = 0.5, poses = KOOPA_RUN }, { key = "3", run = { y = 16 }, start_size = 1, period = 0.5, poses = KOOPA_RUN }, } -- Drumroll: every other hit, two ? blocks appear at random spots in the lower half; each -- bumps up as a spinning coin pops out of it, and both vanish after 21 frames. In frames. local RENDA_LIMIT = 25 local BUMP = { { 0, 0, 0, 0, to = { 0, -10, "out" } }, { 6, 0, to = { 0, 0, "in" } }, { 11, 0, 0, 0 }, length = 21, } local COIN = { { 0, 0, 0, -46, to = { 0, -116, "out" } }, { 11, 0, to = { 0, -96, "in" } }, length = 21, } local MarioRenda = {} MarioRenda.__index = MarioRenda function MarioRenda.new() local self = setmetatable({}, MarioRenda) tex.load_folder(COLLAB_PATH, "renda") self._id = tex.get_texture(COLLAB_PATH .. "/renda/renda_0") or 0 self.blocks = {} self.calls = 0 self.now = 0 return self end function MarioRenda:add_renda() self.calls = self.calls + 1 if self.calls % 2 == 0 then return end for _ = 1, 2 do if #self.blocks >= RENDA_LIMIT then return end table.insert(self.blocks, { t0 = self.now, x = math.random(0, 1279), y = math.random(420, 719) }) end end function MarioRenda:update(current_time_ms) self.now = current_time_ms for i = #self.blocks, 1, -1 do if frames(self.now - self.blocks[i].t0) >= BUMP.length then table.remove(self.blocks, i) end end end function MarioRenda:draw() local scale = tex.get_screen_scale() for _, b in ipairs(self.blocks) do local f = frames(self.now - b.t0) local bump, coin = pose_at(BUMP, f), pose_at(COIN, f) if bump then tex.draw_texture(self._id, { frame = 0, x = b.x * scale, y = (b.y + bump.y) * scale }) end if coin then tex.draw_texture(self._id, { frame = 1 + math.floor(f / 2) % 4, x = b.x * scale, y = (b.y + coin.y) * scale }) end end end function MarioRenda:destroy() tex.unload_folder(COLLAB_PATH, "renda") end -- Background -- Lumen's dodai is fully transparent and the course already paints its own ground, so there -- is no footer. local MarioBackground = {} MarioBackground.__index = MarioBackground function MarioBackground.new(player_num, bpm) local self = setmetatable({}, MarioBackground) self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DONBG_SPEC) self.bg_normal = MarioBGNormal.new() self.bg_fever = MarioBGFever.new(bpm) self.fever = MarioFever.new(bpm) self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, MARIO_DANCERS) table.sort(self.dancer.dancers, function(a, b) return a.index < b.index end) self.renda = MarioRenda.new() self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1) self.is_clear = false self.is_rainbow = false self.last_milestone = 1 return self end function MarioBackground: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 MarioBackground:add_chibi(player_num) BaseChibi.add(self.chibi, MARIO_CHIBIS[math.random(#MARIO_CHIBIS)], COLLAB_PATH, player_num) end function MarioBackground:handle_good(player_num) self:add_chibi(player_num) end function MarioBackground:handle_ok(player_num) self:add_chibi(player_num) end function MarioBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function MarioBackground:handle_drumroll(player_num) self.renda:add_renda() end function MarioBackground:handle_balloon(player_num) self.renda:add_renda() end function MarioBackground: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 MarioBackground: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 MarioBackground:draw_fore() end function MarioBackground: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 MarioBackground