local DonBGBase = require("background/bg_objects/donbg/base") 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 ChibiWave = require("background/bg_objects/chibi/wave") local Timing = require("background/bg_objects/timing") local pose_at = Timing.pose_at -- THE IDOLM@STER (IMAS, IMAS_CG, IMAS_ML). The three share the stage, drumroll and footer -- (same Lumen data and art), loaded from IMAS's folder; the don band, dancers, chibis and -- line-up are each collab's own. local IMAS_PATH = "background/collab/imas" -- Spotlights: additive beams, each tinted one colour local CYAN, YELLOW, MAGENTA = { 0, 255, 255, 255 }, { 255, 255, 0, 255 }, { 255, 0, 255, 255 } -- 0..1 opacity of a light that fades in over `fade` beats from `on`, holds, and fades out over -- `fade` from `off`, in a cycle of `period` beats (a fade-out may wrap past the cycle's end) local function window(t, on, off, fade, period) local u = t % period if u < on then u = u + period end return math.max(0, math.min(1, (u - on) / fade, (off + fade - u) / fade)) end -- the stage's glow strip pulses out and back in every 2 beats local function overlay_fade(t) return math.abs(1 - t % 2) end -- Normal background: the stage under a glowing strip and five spotlights (cyan, yellow, -- magenta, cyan, yellow). Two groups take turns: 1, 3, 5 shine for 1.25 beats, then 2 and 4, -- in a 2.75-beat cycle. Timed in beats. local NORMAL_SPOT_COLORS = { CYAN, YELLOW, MAGENTA, CYAN, YELLOW } local NORMAL_SPOT_CYCLE = 2.75 local SPOT_FADE = 0.25 local function normal_spot_fade(t, i) local on = (i % 2 == 0) and 0 or 1.25 return window(t, on, on + 1.25, SPOT_FADE, NORMAL_SPOT_CYCLE) end local function draw_normal_spots(id, t) for i = 0, 4 do local fade = normal_spot_fade(t, i) if fade > 0 then tex.draw_texture(id, { index = i, color = NORMAL_SPOT_COLORS[i + 1], fade = fade, mirror = "horizontal", blend = "additive" }) end end end local ImasBGNormal = {} ImasBGNormal.__index = ImasBGNormal function ImasBGNormal.new(bpm) local self = setmetatable({}, ImasBGNormal) self.name = "bg_0" tex.load_folder(IMAS_PATH, "bg_normal/" .. self.name) local function id(key) return tex.get_texture(IMAS_PATH .. "/" .. self.name .. "/" .. key) or 0 end self._id_bg = id("background") self._id_overlay = id("overlay") self._id_spotlight = id("spotlight") self.beat = 60000 / bpm self.t = 0 return self end function ImasBGNormal:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end self.t = self.t + (current_time_ms - (self.last_ms or current_time_ms)) / self.beat self.last_ms = current_time_ms end function ImasBGNormal:draw_background() tex.draw_texture(self._id_bg) end function ImasBGNormal:draw() self:draw_background() local fade = overlay_fade(self.t) for _ = 1, 2 do tex.draw_texture(self._id_overlay, { fade = fade, blend = "additive" }) end draw_normal_spots(self._id_spotlight, self.t) end function ImasBGNormal:destroy() tex.unload_folder(IMAS_PATH, "bg_normal/" .. self.name) end -- Clear background: the stage cuts to its lit-up version 0.375 beat in, under four big stars -- spinning around the stage on their own paths, and two sets of five spotlights taking turns -- every 1.23 beats. Orange and green floor lights pulse against each other every 2 beats. -- Losing clear drops the stars and lights at once, switches to the normal spotlights and cuts -- back to the normal stage after 0.375 beat (it's gone at 0.75). Timed in beats; drawn over -- bg_normal, which takes over again after. local CUT_BEATS = 0.375 local LEAVE_BEATS = 0.75 local FEVER_SPOTS = { { first = 5, colors = { CYAN, MAGENTA, YELLOW, CYAN, MAGENTA } }, { first = 10, colors = { YELLOW, MAGENTA, CYAN, YELLOW, MAGENTA } }, } local FEVER_SPOT_START = 0.775 -- beats after the clear local FEVER_SPOT_TURN = 7.4 / 6 -- each set shines this long -- Stars: { beat, x, y, sx, sy, ease } corners of each star's path in a 7.4-beat loop that -- starts a beat after the clear, as offsets from the star's texture.json position (Lumen -- mirrors it). sx narrows as the star turns edge-on; it glides straight between corners. local STAR_LOOP = 7.4 local STAR_START = 1 local STAR_PIVOT = { 142, 128 } -- the star's centre in its image local STARS = { { -- from the far left across to the right edge and back { 0, -1228, -145, 0.3, 1.5 }, { 0.35, -1125, -11, 0.33, 1.34 }, { 0.725, -952, 44, 0.6, 1.19, "out" }, { 0.875, -888, 40, 1, 1.16 }, { 1.725, -480, -86, 1, 1 }, { 2.275, -220, -30, 0.95, 1.07 }, { 2.825, 33, 32, 0.67, 1.13 }, { 3.425, 282, -43, 0.3, 1.2 }, { 3.95, 41, 32, 0.52, 1.14, "out" }, { 4.125, -42, 26, 0.9, 1.12 }, { 4.65, -296, -59, 0.9, 1.06 }, { 4.975, -451, -87, 0.9, 1.03 }, { 5.55, -680, -20, 0.8, 1 }, { 6.35, -966, 44, 0.6, 1 }, { 6.875, -1127, -13, 0.45, 1.1 }, { 7.15, -1190, -80, 0.37, 1.16, "in" }, }, { -- leaps at the far left, crosses along the top and leaps at the right { 0, -495, -72, 1, 1 }, { 0.625, -783, -12, 0.93, 1 }, { 0.85, -889, -24, 0.9, 1 }, { 1, -956, -48, 0.4, 1.14 }, { 1.35, -1082, -155, 0.42, 1.45 }, { 1.7, -1148, -308, 0.43, 1.77 }, { 2.15, -1075, -145, 0.39, 1.43 }, { 2.575, -943, -42, 0.35, 1.11 }, { 2.725, -886, -23, 1, 1 }, { 2.925, -781, -12, 1, 1 }, { 3.85, -303, -118, 1, 1 }, { 4.425, -11, -37, 1, 1 }, { 4.675, 112, 2, 0.48, 1.2 }, { 4.925, 211, -75, 0.51, 1.4 }, { 5.175, 252, -202, 0.53, 1.6 }, { 5.65, 211, -76, 0.49, 1.36 }, { 6.125, 112, 2, 0.44, 1.13 }, { 6.375, 43, -14, 0.41, 1 }, { 7.025, -298, -118, 0.9, 1 }, }, { -- from the right, across the top to a leap at the far left and back { 0, 298, -7, 0.6, 1.5 }, { 0.7, -1, -108, 0.42, 1.29 }, { 0.85, -67, -118, 0.95, 1.25 }, { 1.225, -246, -131, 0.92, 1.16 }, { 1.975, -600, -89, 0.86, 0.98 }, { 2.725, -935, 30, 0.8, 0.8 }, { 3.125, -1048, 40, 0.73, 1.11 }, { 3.9, -1248, -28, 0.6, 1.73 }, { 4.425, -1049, 40, 0.56, 1.35 }, { 4.625, -968, 34, 0.54, 1.2 }, { 4.95, -814, -1, 1, 1.34 }, { 5.6, -537, -103, 1, 1.3 }, { 6.225, -254, -131, 1, 1.26 }, { 6.65, -62, -118, 1, 1.23 }, { 6.875, 36, -100, 0.44, 1.22 }, }, { -- wobbles at the right, then runs the whole stage to the far left and back { 0, -460, -73, 1, 1 }, { 1.025, -10, 27, 1, 1.25 }, { 1.275, 108, 38, 0.54, 1.33 }, { 1.675, 280, -14, 0.4, 1.57 }, { 2, 162, 35, 0.4, 1.55 }, { 2.275, 53, 35, 0.4, 1.53 }, { 2.575, -64, 17, 1.1, 1.1 }, { 4.025, -637, -90, 1.1, 1.1 }, { 4.625, -876, -34, 1.1, 1.1 }, { 4.95, -996, -6, 0.35, 1.56 }, { 5.475, -1202, -3, 0.43, 1.98 }, { 5.675, -1122, 5, 0.49, 1.88 }, { 6.95, -629, -90, 0.87, 1.22 }, }, } -- as pose tables: each corner glides `to` the next (the last back to the first) for s, corners in ipairs(STARS) do local poses = { length = STAR_LOOP } for i, c in ipairs(corners) do local n = corners[i + 1] or corners[1] poses[i] = { c[1], 0, c[2], c[3], sx = c[4], sy = c[5], to = { n[2], n[3], c[6], sx = n[4], sy = n[5] } } end STARS[s] = poses end local ImasBGFever = {} ImasBGFever.__index = ImasBGFever function ImasBGFever.new(bpm) local self = setmetatable({}, ImasBGFever) self.name = "bg_fever_0" tex.load_folder(IMAS_PATH, "bg_fever/" .. self.name) local function id(key) return tex.get_texture(IMAS_PATH .. "/" .. self.name .. "/" .. key) or 0 end self._id_bg = id("background") self._id_overlay = id("overlay") self._id_spotlight = id("spotlight") self._id_star = id("star") self._id_light_orange = id("light_orange") self._id_light_green = id("light_green") self.star_info = tex.get_texture_info(self.name, "star") self.beat = 60000 / bpm self.state = nil -- nil (hidden), "in", "out" return self end function ImasBGFever:start() self.state, self.t, self.overlay_t = "in", 0, 0 end function ImasBGFever:stop() if self.state ~= "in" then return end self.state, self.t = "out", 0 end function ImasBGFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end local dt = (current_time_ms - (self.last_ms or current_time_ms)) / self.beat self.last_ms = current_time_ms if not self.state then return end self.t, self.overlay_t = self.t + dt, self.overlay_t + dt if self.state == "out" and self.t >= LEAVE_BEATS then self.state = nil end end function ImasBGFever:draw_star(poses, t) local p = pose_at(poses, (t - STAR_START) % STAR_LOOP) local w, h = self.star_info.width, self.star_info.height local ox, oy = STAR_PIVOT[1] * p.sx, STAR_PIVOT[2] * p.sy tex.draw_texture(self._id_star, { x = p.x + STAR_PIVOT[1], y = p.y + STAR_PIVOT[2], x2 = w * (p.sx - 1), y2 = h * (p.sy - 1), origin = { ox, oy }, mirror = "horizontal", blend = "additive", }) end -- Draws over bg_normal while clear (or leaving it); bg_normal shows its stage before the cut function ImasBGFever:draw(bg_normal) if not self.state then return false end local t = self.t local fever_bg = (self.state == "in") == (t >= CUT_BEATS) if fever_bg then tex.draw_texture(self._id_bg) else bg_normal:draw_background() end tex.draw_texture(self._id_overlay, { fade = overlay_fade(self.overlay_t), blend = "additive" }) if self.state == "out" then draw_normal_spots(self._id_spotlight, t) return true end for _, star in ipairs(STARS) do self:draw_star(star, t) end for k, set in ipairs(FEVER_SPOTS) do local on = FEVER_SPOT_START + (k - 1) * FEVER_SPOT_TURN local fade = t >= on and window(t, on, on + FEVER_SPOT_TURN, SPOT_FADE, 2 * FEVER_SPOT_TURN) or 0 if fade > 0 then for i = 0, 4 do tex.draw_texture(self._id_spotlight, { index = set.first + i, color = set.colors[i + 1], fade = fade, mirror = "horizontal", blend = "additive" }) end end end local pulse = math.abs(1 - t % 2) -- orange brightens as green dims, and back tex.draw_texture(self._id_light_orange, { fade = 1 - pulse / 2, blend = "additive" }) tex.draw_texture(self._id_light_green, { fade = 0.5 + pulse / 2, blend = "additive" }) return true end function ImasBGFever:destroy() tex.unload_folder(IMAS_PATH, "bg_fever/" .. self.name) end -- Drumroll: notes fly up and to the right, the default renda_c flight local RENDA = { ms = { 2000 }, dx = 1560, dy = -1648, x_range = 500, y_range = 20 } -- IMAS --------------------------------------------------------------------------------------- -- Don band: the base scrolls one tile left every 5.33s; the clear set scrolls with it under -- a pink glow strip that pulses every second local IMAS_DONBG = { fade = 0.2, layers = { { key = "background", ms = 5333 }, { key = "glow", clear_only = true, tile = 0, alpha = 0.148, blend = "additive", period = 1000, pulse = { { 500, -1000 }, { 500, 1000, "out" } } }, }, } -- Dancers: all five jump up from below, land with a squash, dance a 4-beat routine (a pose a -- quarter beat, two held for a half) and hop up and drop away when removed. Timed in beats; -- poses are offsets from y 670, pivoting about Lumen's origin in the images. local function imas_dancer(rest) return { origin = { 152, 152 }, at = { 0, rest }, enter = { beats = 2, { 0, 0, 0, rest + 320, to = { 0, rest - 60, "out" } }, -- jumps up from below { 0.675, 0, 0, rest - 60, to = { 0, rest, "in" } }, { 0.925, 1, 0, rest, to = { nil, nil, nil, sy = 0.92 } }, -- lands with a squash { 1.025, 1, 0, rest, sy = 0.95, to = { nil, nil, nil, sy = 1 } }, { 1.1, 1, 0, rest }, }, dance = Timing.steps({ { 0, 0.25, x = 0, y = rest }, { 1, 0.25 }, { 2, 0.25 }, { 3, 0.25 }, { 4, 0.25 }, { 5, 0.5 }, { 6, 0.25 }, { 7, 0.25 }, { 8, 0.25 }, { 9, 0.25 }, { 10, 0.25 }, { 11, 0.25 }, { 12, 0.5 }, { 13, 0.25 }, }), leave = { beats = 1.45, { 0, 0, 0, rest }, { 0.1, 1, 0, rest, to = { 0, rest - 60, "out" } }, -- hops up and drops away { 0.5, 1, 0, rest - 60, to = { 0, rest + 320, "in" } }, }, } end local IMAS_DANCERS = Dancer.define({ default = imas_dancer(-145), [1] = imas_dancer(-142), [2] = imas_dancer(-148) }) -- Fixed slots (far-left .. far-right), from Lumen's dancer positions; each dancer has its own local DANCER_CENTERS = { 213, 427, 640, 853, 1067 } -- (joins fill the centre, left, right, far-left, far-right slots) -- Chibis for good hits: one of four idols hops in growing from 70% size, then bobs across -- the don band in a zig-zag, flapping between two poses every half beat local IMAS_CHIBIS = {} for key = 0, 3 do IMAS_CHIBIS[key + 1] = { key = tostring(key), run = { y = -1 }, wave = { first = 17, height = 40, beats = 0.85 }, period = 1, poses = { { 0, 0 }, { 0.5, 1 } }, } end -- IMAS CG ------------------------------------------------------------------------------------ -- Three dancers on fixed slots (Lumen's dancer2, dancer1, dancer3 at x 427, 640, 853; index 1, -- 2, 0), standing 8px under the dancer's base. Same routine as IMAS, but their images are -- bottom-anchored and the routine borrows frame 0 of the entry once. Timed in beats; poses -- are offsets from the rest position. local function cg_dancer(origin) return { origin = origin, at = { 0, 0 }, enter = { beats = 2, { 0, 0, 20, 310, to = { 20, -46, "out" } }, -- jumps up from below { 0.65, 0, 20, -46, to = { 20, 4, "in" } }, { 0.925, 1, 0, 4, to = { 0, 3, "out", sy = 0.95 } }, -- lands with a squash { 1.025, 1, 0, 3, sy = 0.95, to = { 0, 0, sy = 1 } }, { 1.1, 1, 0, 0 }, }, dance = Timing.steps({ { 0, 0.25, x = 0, y = 0 }, { 1, 0.25 }, { 2, 0.25 }, { 3, 0.25 }, { 4, 0.25 }, { 5, 0.5 }, { 6, 0.25 }, { 7, 0.25 }, { 0, 0.25, key = "start" }, { 8, 0.25 }, { 9, 0.25 }, { 10, 0.25 }, { 11, 0.625 }, { 12, 0.125 }, }), leave = { beats = 1.45, { 0, 0, 0, 4, key = "start", to = { 0, 3, "in", sy = 0.935 } }, { 0.125, 0, 0, -4, key = "start", sy = 0.943, to = { 0, -50, "out", sy = 1 } }, -- hops up { 0.475, 0, 0, -46, key = "end", to = { 0, 374, "in" } }, -- and drops away }, } end local CG_DANCERS = Dancer.define({ [0] = cg_dancer({ 112, 280 }), [1] = cg_dancer({ 112, 280 }), [2] = cg_dancer({ 116, 288 }), }) -- Chibis for good hits: one of two idols (frames 0, 1) hops in from 70% size, then bobs across -- the don band in a zig-zag. One image each; the bob is 33px high, a swing every 0.45 beat. local CG_CHIBIS = {} for frame = 0, 1 do CG_CHIBIS[frame + 1] = { key = "0", first = frame, hop = { from = { -428, 13 }, peak = { -354, -36 }, up = 0.25 }, run = { x = -294, y = 5, distance = 1222 }, wave = { first = -29, height = 33, beats = 0.45 }, } end local VARIANTS = { [IMAS_PATH] = { donbg = IMAS_DONBG, dancers = IMAS_DANCERS, chibis = IMAS_CHIBIS, fever = { rise = 210, bounce = 70, drop = 400, poses = true }, -- dancer index by join order, and the slots' draw order join_order = { 4, 3, 2, 1, 0 }, draw_order = { 5, 1, 4, 2, 3 }, }, ["background/collab/imas_cg"] = { donbg = IMAS_DONBG, dancers = CG_DANCERS, chibis = CG_CHIBIS, fever = { rise = 249, bounce = 70, drop = 439, poses = true }, join_order = { 2, 1, 0 }, draw_order = { 2, 4, 3 }, }, -- not reviewed yet: the default band, dancers and chibis ["background/collab/imas_ml"] = { fever = { rise = 249, bounce = 70, drop = 439, poses = true } }, } -- Background factory (IMAS, IMAS_CG, IMAS_ML) local ImasBackground = {} ImasBackground.__index = ImasBackground function ImasBackground.new(player_num, bpm, path, max_dancers) local self = setmetatable({}, ImasBackground) local v = VARIANTS[path] self.variant = v self.path = path self.donbg = DonBGBase.new(1, player_num, path, v.donbg) self.bg_normal = ImasBGNormal.new(bpm) self.bg_fever = ImasBGFever.new(bpm) self.footer = Footer.new(IMAS_PATH, 0) self.fever = Fever.new(bpm, path, v.fever) self.dancer = Dancer.BaseDancerGroup.new(0, bpm, max_dancers, path, v.dancers) if v.dancers then local by_index = {} for _, d in ipairs(self.dancer.dancers) do by_index[d.index] = d end for i, index in ipairs(v.join_order) do self.dancer.dancers[i] = by_index[index] end end self.renda = RendaController.new(IMAS_PATH, 0, RENDA) self.chibi = ChibiController.new(bpm, path, 0) self.max_dancers = max_dancers self.is_clear = false self.is_rainbow = false self.last_milestone = 1 return self end function ImasBackground: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 ImasBackground:add_chibi(player_num) local chibis = self.variant.chibis if chibis then BaseChibi.add(self.chibi, chibis[math.random(#chibis)], self.path, player_num, ChibiWave, "chibi_0") else self.chibi:add_chibi(player_num, false) end end function ImasBackground:handle_good(player_num) self:add_chibi(player_num) end function ImasBackground:handle_ok(player_num) self:add_chibi(player_num) end function ImasBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function ImasBackground:handle_drumroll(player_num) self.renda:add_renda() end function ImasBackground:handle_balloon(player_num) self.renda:add_renda() end function ImasBackground: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 md = self.max_dancers local current_milestone if is_clear then current_milestone = md else current_milestone = math.min(md - 1, math.floor(progress * md)) 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 ImasBackground:draw_back() if not self.bg_fever:draw(self.bg_normal) then self.bg_normal:draw() end self.footer:draw() self.donbg:draw() self.renda:draw() if self.variant.dancers then self.dancer:draw_slots(DANCER_CENTERS, self.variant.draw_order) else self.dancer:draw() end self.fever:draw() self.chibi:draw() end function ImasBackground:draw_fore() end function ImasBackground: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 -- Export the factory local Imas = {} Imas.new = ImasBackground.new -- Callable as a collab constructor: Imas.new(player_num, bpm, path, max_dancers) setmetatable(Imas, { __call = function(_, player_num, bpm, path, max_dancers) return ImasBackground.new(player_num, bpm, path, max_dancers) end }) return Imas