367 lines
15 KiB
Lua
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
|