Files
2026-10-21 22:14:26 -04:00

367 lines
15 KiB
Lua

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 Timing = require("background/bg_objects/timing")
-- Momoiro Clover Z
local COLLAB_PATH = "background/collab/momoclo"
local MAX_DANCERS = 5
-- Stage: a concert hall. The big screen cycles the Momoiro Clover Z logo and the five members
-- (3 beats each) under a scanline overlay; the side screens and the coloured washes on the
-- floor change colour with it, and four searchlights spin a full turn every 6 beats. Timed in
-- beats. The crowd's glowsticks flicker on their own 800 ms loop (ambient, in ms).
-- Clearing fades coloured lights in over the stage over 0.725 beat, and back out from
-- wherever they are.
local PANEL_BEATS, PANELS = 3, 6
local SPIN_BEATS = 6
local STICK_MS = 50 -- one glowstick step; the frames run up and back, the ends held twice as long
local STICKS = { 0, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 1 }
local SCANLINE_ALPHA = 0.5
local WASH_ALPHA = 0.398
local CLEAR_FADE = 0.725
local CLEAR_WASH_ALPHA = 0.2
local CLEAR_CROWD_ALPHA = 0.6
-- searchlights in Lumen's draw order: { texture.json index, turn at rest, spin direction, mirrored }
local SPOTS = { { 1, 180, 1, false }, { 2, 0, -1, true }, { 0, 0, 1, false }, { 3, 180, -1, true } }
local MomocloStage = {}
MomocloStage.__index = MomocloStage
function MomocloStage.new(bpm)
local self = setmetatable({}, MomocloStage)
tex.load_folder(COLLAB_PATH, "bg_normal/bg_0")
tex.load_folder(COLLAB_PATH, "bg_fever/bg_fever_0")
local function id(name, key) return tex.get_texture(COLLAB_PATH .. "/" .. name .. "/" .. key) or 0 end
self.ids = {}
for _, key in ipairs({ "stage", "sticks", "truss", "screen", "scanline", "side", "side_shade", "spot",
"tower", "wash", "glow", "bulbs", "floor" }) do
self.ids[key] = id("bg_0", key)
end
for _, key in ipairs({ "wash", "haze", "crowd", "light" }) do
self.ids["clear_" .. key] = id("bg_fever_0", key)
end
local spot = tex.get_texture_info("bg_0", "spot")
self.spot_origin = { spot.width / 2, spot.height / 2 }
self.beat = 60000 / bpm
self.t = 0 -- beats
self.now = 0
self.level = 0 -- 0..1 of the clear lights
return self
end
-- fades the clear lights in or out from wherever they are
function MomocloStage:set_clear(is_clear)
self.target, self.from, self.fade_t = is_clear and 1 or 0, self.level, 0
end
function MomocloStage: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, self.now = current_time_ms, current_time_ms
self.t = self.t + dt
if self.target then
self.fade_t = self.fade_t + dt
local k = math.min(1, self.fade_t / CLEAR_FADE)
self.level = self.from + (self.target - self.from) * k
if k >= 1 then self.target = nil end
end
end
-- a texture's two texture.json copies, the second mirrored
local function pair(id, p)
local a, b = { index = 0 }, { index = 1, mirror = "horizontal" }
for k, v in pairs(p or {}) do a[k], b[k] = v, v end
tex.draw_texture(id, a)
tex.draw_texture(id, b)
end
function MomocloStage:draw()
local ids, level, sc = self.ids, self.level, tex.get_screen_scale()
local panel = math.floor(self.t / PANEL_BEATS) % PANELS
local turn = 360 * (self.t % SPIN_BEATS) / SPIN_BEATS
pair(ids.stage)
tex.draw_texture(ids.sticks, { frame = STICKS[math.floor(self.now / STICK_MS) % #STICKS + 1] })
if level > 0 then tex.draw_texture(ids.clear_wash, { fade = CLEAR_WASH_ALPHA * level }) end
pair(ids.truss)
tex.draw_texture(ids.screen, { frame = panel })
tex.draw_texture(ids.scanline, { fade = SCANLINE_ALPHA })
pair(ids.side, { frame = panel })
pair(ids.side_shade)
local o = self.spot_origin
for _, s in ipairs(SPOTS) do
tex.draw_texture(ids.spot, {
index = s[1], x = o[1], y = o[2], origin = { o[1] * sc, o[2] * sc },
rotation = s[2] + s[3] * turn, mirror = s[4] and "horizontal" or nil,
})
end
pair(ids.tower)
pair(ids.wash, { frame = panel, fade = WASH_ALPHA })
tex.draw_texture(ids.glow, { index = 0 })
tex.draw_texture(ids.glow, { index = 1 })
if level > 0 then
tex.draw_texture(ids.clear_haze, { index = 0, fade = level })
tex.draw_texture(ids.clear_haze, { index = 1, fade = level })
tex.draw_texture(ids.clear_crowd, { fade = CLEAR_CROWD_ALPHA * level, mirror = "horizontal" })
end
tex.draw_texture(ids.bulbs)
pair(ids.floor)
if level > 0 then
for i = 0, 4 do
tex.draw_texture(ids.clear_light, { index = i, frame = i, fade = level, mirror = "horizontal" })
end
end
end
function MomocloStage:destroy()
tex.unload_folder(COLLAB_PATH, "bg_normal/bg_0")
tex.unload_folder(COLLAB_PATH, "bg_fever/bg_fever_0")
end
-- Footer: four stage lamps along the bottom edge, each with a glow (the left two mirrored)
local FOOTER_MIRRORS = { false, false, true, true }
local MomocloFooter = setmetatable({}, { __index = Footer })
MomocloFooter.__index = MomocloFooter
function MomocloFooter.new()
local self = setmetatable(Footer.new(COLLAB_PATH, 0, FOOTER_MIRRORS), MomocloFooter)
self._id_glow = tex.get_texture(COLLAB_PATH .. "/footer/glow") or 0
return self
end
function MomocloFooter:draw()
Footer.draw(self)
for i, mirror in ipairs(FOOTER_MIRRORS) do
tex.draw_texture(self._id_glow, { index = i - 1, mirror = mirror and "horizontal" or nil })
end
end
-- Don band: pink stripes with clover crests scrolling under a row of clouds and tomoe crests
-- (one 720px repeat, faster than the stripes), both every 9s; ambient, in ms. The clear set is
-- the same in pastel.
local DONBG = {
fade = 0.225,
layers = {
{ key = "background", ms = 9000, dx = -448, x = -81 },
{ key = "overlay", ms = 9000, dx = -720, x = -305 },
},
}
-- Fever: two crowds of fans with glowsticks jump up from below and bounce once a beat
local FEVER = { peak = 70, bounce = 70 }
-- Dancers: the five members, timed in beats. Each drops in from below with a soft glow behind
-- her, lands with a squash, then dances a 32-beat routine (an 8-beat phrase twice, a second
-- phrase, the first again and a closing run of quick poses); the routine's steps hit hard
-- with a squash, a little hop and a second squash. Removed members hop up and drop away;
-- Takagi Reni is dragged off by her manager instead. Poses pivot about the feet (bottom-left).
local GLOW_ALPHA = 0.3
-- a squash that eases into sy 0.96 over `down` beats, holds and springs back over `up`
local function squash(frame, down, hold, up)
return {
{ frame, down, to = { nil, nil, "out", sy = 0.96 } },
{ frame, hold, sy = 0.96 },
{ frame, up, sy = 0.96, to = { nil, nil, "in", sy = 1 } },
}
end
local function hop(frame)
return { { frame, 0.5, to = { nil, -8, "out" } }, { frame, 0.05, y = -8 }, { frame, 0.45, y = -8, to = { nil, 0, "in" } } }
end
-- the 8-beat phrase on poses a..: step, sway, step, sway, then a hit
local function phrase(a, last)
return Timing.concat(
{ { a, 0.75, x = 0, y = 0 }, { a + 1, 0.25 }, { a + 2, 0.75 }, { a + 3, 0.25 },
{ a, 0.75 }, { a + 4, 0.25 }, { a + 5, 0.5 }, { a + 6, 0.5 } },
squash(a + 7, 0.525, 0.25, 0.225), hop(a + 8), squash(a + 9, 0.4, 0.15, 0.45), last or {})
end
local ROUTINE = Timing.steps(
phrase(0, { { 10, 1 } }),
phrase(11, { { 21, 1 } }),
phrase(0, { { 22, 1 } }),
{ { 23, 0.325 }, { 24, 0.325 }, { 25, 0.35 } },
{ { 26, 0.5 }, { 27, 0.5 }, { 28, 0.5 }, { 29, 0.5 } },
{ { 30, 0.5 }, { 31, 0.5 }, { 32, 0.25 }, { 33, 0.25 }, { 34, 0.25 }, { 35, 0.25 } },
{ { 36, 0.5 }, { 37, 0.5 }, { 38, 0.5 }, { 39, 0.5 }, { 40, 0.5 }, { 41, 0.5 } })
local GLOW_IN = { key = "_glow", { 0, 0, hide = true }, { 0.6, 0, 0, 0, alpha = 0, to = { nil, nil, nil, alpha = GLOW_ALPHA } }, { 0.9, 0, 0, 0, alpha = GLOW_ALPHA } }
local GLOW_STAY = { key = "_glow", { 0, 0, 0, 0, alpha = GLOW_ALPHA } }
local GLOW_OUT = { key = "_glow", { 0, 0, 0, 0, alpha = GLOW_ALPHA, to = { nil, nil, nil, alpha = 0 } }, { 0.5, 0, hide = true } }
-- drops in from 396px below, rising 24px past the line, and lands with a squash on pose 1;
-- `settle` is the pose some members step into as they straighten up
local function enter(settle)
local land = settle
and { { 1.15, 1, sy = 0.96, to = { nil, nil, "in", sy = 0.99 } }, { 1.35, settle } }
or { { 1.15, 1, sy = 0.96, to = { nil, nil, "in", sy = 1 } }, { 1.375, 1 } }
return Timing.concat({
{ 0, 0, 0, 396 },
{ 0.05, 0, 0, 360, to = { 0, -24, "out" } },
{ 0.625, 0, 0, -24, to = { 0, 0, "in" } },
{ 0.875, 1, 0, 0, to = { nil, nil, "out", sy = 0.96 } },
{ 1.1, 1, sy = 0.96 },
}, land)
end
local function member(settle)
return {
origin = { 0, 360 },
enter = { beats = 2, GLOW_IN, enter(settle) },
dance = { beats = ROUTINE.length, GLOW_STAY, ROUTINE },
leave = { beats = 1.575, GLOW_OUT, {
key = "end",
{ 0, 0, 0, 0, to = { 0, -24, "out" } },
{ 0.5, 0, 0, -24, to = { 0, 396, "in" } },
} },
}
end
-- Momota Kanako in the centre: a sparkle pops as she lands; she never leaves
local KANAKO = member()
KANAKO.leave = nil
table.insert(KANAKO.enter, { key = "_sparkle", origin = { 56, 56 },
{ 0, 0, 0, 0, hide = true },
{ 1.1, 0, 0, 0, scale = 0.2, alpha = 0, to = { nil, nil, nil, scale = 1, alpha = 1 } },
{ 1.425, 0, 0, 0, to = { nil, nil, nil, scale = 1.4, alpha = 0 } },
{ 1.675, 0, hide = true },
})
-- Takagi Reni: a squash, her manager swings in from the right, grabs her and drags her off.
-- Her images and his pivot about their centres here.
local RENI = member()
RENI.leave = { beats = 1.975, GLOW_OUT,
{ key = "end", origin = { 160, 180 },
{ 0, 0, 0, 0 },
{ 0.325, 0, 0, 0, to = { nil, 6, "out", sy = 0.96 } },
{ 0.5, 0, 0, 6, sy = 0.96, to = { nil, 0, "in", sy = 1 } },
{ 0.625, 0, 0, 0 },
{ 0.75, 1, 14, -4, rot = 5 },
{ 1, 2, 0, 0, to = { 248, nil, "in" } },
{ 1.975, 2, hide = true } },
{ key = "_manager", origin = { 100, 180 },
{ 0, 0, 256, -46, rot = 45, to = { 231, nil, nil, rot = 38 } },
{ 0.1, 0, 231, -46, rot = 38, to = { 113, 0, "in", rot = 0 } },
{ 0.275, 0, 113, 0, to = { nil, 6, "out", sy = 0.96 } },
{ 0.45, 0, 113, 6, sy = 0.96, to = { nil, 0, "in", sy = 1 } },
{ 0.625, 0, 113, 0 },
{ 0.75, 1 },
{ 1, 2, 122, 0, to = { 370, nil, "in" } },
{ 1.975, 2, hide = true } },
}
-- by dancer index: Kanako (centre), Ayaka, Shiori and Momoka (who settle on a third pose), Reni
local MEMBERS = Dancer.define({ [0] = KANAKO, [1] = member(), [2] = member(2), [3] = member(2), [4] = RENI })
-- Fixed slots (far-left .. far-right), from Lumen's dancer positions; joins fill the centre,
-- left, right, far-left and far-right slots in index order
local DANCER_CENTERS = { 200, 420, 640, 860, 1080 }
local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- outer pairs first, Kanako on top
-- Chibis for good hits: one of the five members hops in growing from 70% size and runs
-- across the don band, six running frames a beat
local RUN = { { 0, 0 }, { 0.15, 1 }, { 0.325, 2 }, { 0.5, 3 }, { 0.675, 4 }, { 0.825, 5 } }
local CHIBIS = {}
for i = 1, 5 do CHIBIS[i] = { key = tostring(i), run = { y = 1 }, period = 1, poses = RUN } end
-- Drumroll: glowsticks fly up and to the right from below the screen, each over a glow
local RENDA = { ms = { 2000 }, dx = 2060, dy = -2060, x_range = 640, y_range = 100, glow = "renda_glow" }
-- Background
local MomocloBackground = {}
MomocloBackground.__index = MomocloBackground
function MomocloBackground.new(player_num, bpm)
local self = setmetatable({}, MomocloBackground)
self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DONBG)
self.stage = MomocloStage.new(bpm)
self.footer = MomocloFooter.new()
self.fever = Fever.new(bpm, COLLAB_PATH, FEVER)
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, MEMBERS)
table.sort(self.dancer.dancers, function(a, b) return a.index < b.index end)
self.renda = RendaController.new(COLLAB_PATH, 0, RENDA)
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1)
self.is_clear = false
self.is_rainbow = false
self.last_milestone = 1
return self
end
function MomocloBackground:update(current_ms, bpm)
self.donbg:update(current_ms, bpm)
self.stage: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 MomocloBackground:add_chibi(player_num)
BaseChibi.add(self.chibi, CHIBIS[math.random(#CHIBIS)], COLLAB_PATH, player_num)
end
function MomocloBackground:handle_good(player_num) self:add_chibi(player_num) end
function MomocloBackground:handle_ok(player_num) self:add_chibi(player_num) end
function MomocloBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
function MomocloBackground:handle_drumroll(player_num) self.renda:add_renda() end
function MomocloBackground:handle_balloon(player_num) self.renda:add_renda() end
function MomocloBackground:handle_gauge(player_num, progress, is_clear, is_rainbow)
self.donbg:set_clear(is_clear)
if is_clear ~= self.is_clear then self.stage:set_clear(is_clear) 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 MomocloBackground:draw_back()
self.stage: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 MomocloBackground:draw_fore() end
function MomocloBackground:destroy()
self.donbg:destroy()
self.stage:destroy()
self.fever:destroy()
self.dancer:destroy()
self.footer:destroy()
self.renda:destroy()
self.chibi:destroy()
end
return MomocloBackground