363 lines
15 KiB
Lua
363 lines
15 KiB
Lua
local DonBGBase = require("background/bg_objects/donbg/base")
|
|
local DonBG = require("background/bg_objects/donbg")
|
|
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")
|
|
|
|
local COLLAB_PATH = "background/collab/gumi"
|
|
-- fever line-up: two poses, 70px bounce
|
|
local FEVER_SPEC = { bounce = 70, poses = true }
|
|
local MAX_DANCERS = 5
|
|
|
|
local chain_moves, frame_change, sum = Timing.chain_moves, Timing.frame_change, Timing.sum
|
|
|
|
-- Normal background: the stage screens spell out G, U, M, I, one letter every 1.5 beats,
|
|
-- the next letter fading in over the last half beat. Timed in beats.
|
|
local BG_NORMAL_PANEL = { { 1, 0 }, { 0.5, 1000 } } -- next panel's opacity in thousandths
|
|
|
|
local GumiBGNormal = {}
|
|
GumiBGNormal.__index = GumiBGNormal
|
|
|
|
function GumiBGNormal.new(bpm)
|
|
local self = setmetatable({}, GumiBGNormal)
|
|
self.name = "bg_0"
|
|
tex.load_folder(COLLAB_PATH, "bg_normal/" .. self.name)
|
|
self.frames = tex.get_texture_info(self.name, "background").frame_count
|
|
self._id_bg = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. "background") or 0
|
|
self.beat = 60000 / bpm
|
|
self.panel = 0
|
|
self.fade = chain_moves(BG_NORMAL_PANEL, self.beat, 1)
|
|
return self
|
|
end
|
|
|
|
-- rebuilt per panel so a tempo change takes effect on the next letter
|
|
function GumiBGNormal:update(current_time_ms, bpm)
|
|
if bpm then self.beat = 60000 / bpm end
|
|
for _, m in ipairs(self.fade) do m:update(current_time_ms) end
|
|
if self.fade[#self.fade].is_finished then
|
|
self.panel = (self.panel + 1) % self.frames
|
|
self.fade = chain_moves(BG_NORMAL_PANEL, self.beat, 1)
|
|
end
|
|
end
|
|
|
|
function GumiBGNormal:draw()
|
|
tex.draw_texture(self._id_bg, { frame = self.panel })
|
|
tex.draw_texture(self._id_bg, { frame = (self.panel + 1) % self.frames, fade = sum(self.fade) / 1000 })
|
|
end
|
|
|
|
function GumiBGNormal:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name) end
|
|
|
|
-- Clear background: a white flash over the stage, then the screens cycle through eight
|
|
-- panels, one a beat, with a white pulse and pulsing spotlights on every beat. Losing clear
|
|
-- cuts to the dark stage, flickers the last panel and fades back to the normal stage.
|
|
-- Timed in beats; drawn over bg_normal, which shows through before and after.
|
|
local BG_FEVER_PANELS = 8 -- frame_order in texture.json
|
|
local BG_FEVER_FLASH = { { 0.375, 1000, "out" }, { 0.75, -1000, "in" } } -- entry flash
|
|
local BG_FEVER_LEAD = 0.375 -- panels start under it
|
|
local BG_FEVER_SPOTS = { { 0.475, 0 }, { 1.025, 1000 } } -- spotlights fade in
|
|
-- per beat: the next panel fades in at the end; additive white pulse; spotlight pulse
|
|
local BG_FEVER_FADE = { { 0.625, 0 }, { 0.375, 1000 } }
|
|
-- a full white pulse is too strong in game; toned down to 40% (peak in thousandths) so the stage stays visible
|
|
local BG_FEVER_PULSE = { { 0.375, 400, "in" }, { 0, -55 }, { 0.325, -345, "out" } }
|
|
local BG_FEVER_SPOT = { { 0.375, 0 }, { 0, -43 }, { 0.575, -957 } } -- from 1000
|
|
-- exit: the first panel flickers over the dark stage, then fades out
|
|
local BG_FEVER_FLICKER = {
|
|
{ 0.025, 0 }, { 0, 1000 }, { 0.15, 0 }, { 0, -1000 }, { 0.025, 0 }, { 0, 1000 }, { 0.1, 0 },
|
|
{ 0, -1000 }, { 0.025, 0 }, { 0, 1000 }, { 0.1, 0 }, { 0, -1000 }, { 0.025, 0 }, { 0, 1000 },
|
|
{ 0.525, -1000 },
|
|
}
|
|
local BG_FEVER_FLICKER_FRAMES = { { 0, 0.2 }, { 2, 0.125 }, { 0, 0.65 } }
|
|
local BG_FEVER_SPOTS_OUT = 0.55
|
|
local LIGHT_FRAMES = { 0, 1, 1, 2, 2, 3, 3 } -- texture.json "light" indices
|
|
local LIGHT_MIRROR = { false, false, true, false, true, false, true }
|
|
|
|
local GumiBGFever = {}
|
|
GumiBGFever.__index = GumiBGFever
|
|
|
|
function GumiBGFever.new(bpm)
|
|
local self = setmetatable({}, GumiBGFever)
|
|
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._id_bg = id("background")
|
|
self._id_flash = id("flash")
|
|
self._id_dark = id("dark")
|
|
self._id_light = id("light")
|
|
self.beat = 60000 / bpm
|
|
self.state = nil -- nil (hidden), "in", "out"
|
|
return self
|
|
end
|
|
|
|
local function update_all(moves, current_time_ms)
|
|
for _, m in ipairs(moves) do m:update(current_time_ms) end
|
|
end
|
|
|
|
function GumiBGFever:spots() return (self.spots_offset + sum(self.spot_env)) / 1000 end
|
|
|
|
function GumiBGFever:next_beat()
|
|
self.fade = chain_moves(BG_FEVER_FADE, self.beat, 1)
|
|
self.pulse = chain_moves(BG_FEVER_PULSE, self.beat, 1)
|
|
self.spot = chain_moves(BG_FEVER_SPOT, self.beat, 1)
|
|
end
|
|
|
|
function GumiBGFever:start()
|
|
self.state = "in"
|
|
self.flash = chain_moves(BG_FEVER_FLASH, self.beat, 1)
|
|
self.spots_offset = 0
|
|
self.spot_env = chain_moves(BG_FEVER_SPOTS, self.beat, 1)
|
|
self.lead = anim.move(BG_FEVER_LEAD * self.beat, { total_distance = 1 })
|
|
self.lead:start()
|
|
self.panel = 0
|
|
self.fade = nil -- the beat loop starts once the lead is over
|
|
end
|
|
|
|
-- spotlights fade from wherever they are
|
|
function GumiBGFever:stop()
|
|
if self.state ~= "in" then return end
|
|
local spots = math.floor(self:spots() * 1000)
|
|
self.state = "out"
|
|
self.spots_offset = spots
|
|
self.spot_env = chain_moves({ { BG_FEVER_SPOTS_OUT, -spots } }, self.beat, 1)
|
|
self.flicker = chain_moves(BG_FEVER_FLICKER, self.beat, 1)
|
|
self.flicker_frame = frame_change(BG_FEVER_FLICKER_FRAMES, self.beat)
|
|
self.flicker_frame:start()
|
|
end
|
|
|
|
function GumiBGFever:update(current_time_ms, bpm)
|
|
if bpm then self.beat = 60000 / bpm end
|
|
if not self.state then return end
|
|
update_all(self.spot_env, current_time_ms)
|
|
update_all(self.flash, current_time_ms)
|
|
self.lead:update(current_time_ms)
|
|
if self.state == "out" then
|
|
update_all(self.flicker, current_time_ms)
|
|
self.flicker_frame:update(current_time_ms)
|
|
if self.flicker[#self.flicker].is_finished then self.state = nil end
|
|
end
|
|
if not self.lead.is_finished then return end
|
|
if not self.fade then self:next_beat() end
|
|
update_all(self.fade, current_time_ms)
|
|
update_all(self.pulse, current_time_ms)
|
|
update_all(self.spot, current_time_ms)
|
|
if self.fade[#self.fade].is_finished then
|
|
self.panel = (self.panel + 1) % BG_FEVER_PANELS
|
|
self:next_beat()
|
|
end
|
|
end
|
|
|
|
function GumiBGFever:draw_spots()
|
|
if not self.fade then return end
|
|
local fade = self:spots() * (1000 + sum(self.spot)) / 1000
|
|
for i, frame in ipairs(LIGHT_FRAMES) do
|
|
tex.draw_texture(self._id_light, {
|
|
index = i - 1, frame = frame, fade = fade, blend = "additive",
|
|
mirror = LIGHT_MIRROR[i] and "horizontal" or nil,
|
|
})
|
|
end
|
|
end
|
|
|
|
function GumiBGFever:draw()
|
|
if not self.state then return end
|
|
if self.state == "in" then
|
|
if self.fade then
|
|
tex.draw_texture(self._id_bg, { frame = self.panel })
|
|
tex.draw_texture(self._id_bg, { frame = (self.panel + 1) % BG_FEVER_PANELS, fade = sum(self.fade) / 1000 })
|
|
tex.draw_texture(self._id_flash, { fade = sum(self.pulse) / 1000, blend = "additive" })
|
|
end
|
|
tex.draw_texture(self._id_flash, { fade = sum(self.flash) / 1000 })
|
|
else
|
|
tex.draw_texture(self._id_dark)
|
|
tex.draw_texture(self._id_bg, { frame = self.flicker_frame.attribute, fade = sum(self.flicker) / 1000 })
|
|
end
|
|
self:draw_spots()
|
|
end
|
|
|
|
function GumiBGFever:destroy() tex.unload_folder(COLLAB_PATH, "bg_fever/" .. self.name) end
|
|
|
|
-- Dancers
|
|
-- All timings are in beats so the dancers follow the song's BPM. Each jumps up from below,
|
|
-- lands with a squash, then loops a 16-beat routine; the small ones hop off when removed.
|
|
|
|
-- Small dancers: pose 0.725 beat, in-between 0.275 beat. Dancer 3 ends its routine on frame 18.
|
|
local function small_dance(last)
|
|
return {
|
|
{ 1, 0.725 }, { 2, 0.275 }, { 3, 0.725 }, { 4, 0.275 },
|
|
{ 1, 0.725 }, { 2, 0.275 }, { 3, 0.725 }, { 4, 0.275 },
|
|
{ 0, 0.725 }, { 5, 0.275 }, { 6, 0.725 }, { 7, 0.275 },
|
|
{ 0, 0.725 }, { 8, 0.275 }, { 9, 0.45 }, { 10, 0.275 }, { 0, 0.275 },
|
|
{ 11, 0.725 }, { 12, 0.275 }, { 13, 0.725 }, { 14, 0.275 },
|
|
{ 11, 0.725 }, { 12, 0.275 }, { 13, 0.725 }, { 15, 0.275 },
|
|
{ 16, 1 }, { 17, 0.475 }, { 16, 0.525 }, { 17, 1.725 }, { last, 0.275 },
|
|
}
|
|
end
|
|
|
|
-- poses are offsets from where each dancer stands, pivoting about its feet (bottom-left)
|
|
local function small_dancer(last)
|
|
return {
|
|
origin = { 0, 200 },
|
|
enter = {
|
|
beats = 2,
|
|
{ 0, 0, 0, 213 },
|
|
{ 0.35, 0, 0, 213, to = { 0, -43, "out" } }, -- jumps up from below
|
|
{ 0.925, 0, 0, -43, to = { 0, 0, "in" } },
|
|
{ 1.2, 0, 0, 0, to = { nil, nil, "out", sy = 0.94 } }, -- lands with a squash
|
|
{ 1.3, 0, 0, 0, sy = 0.94, to = { nil, nil, "in", sy = 1 } },
|
|
{ 1.425, 0, 0, 0 },
|
|
},
|
|
dance = Timing.steps(small_dance(last)),
|
|
leave = {
|
|
beats = 1.325,
|
|
{ 0, 0, 0, 0, to = { nil, nil, "out", sy = 0.93 } }, -- crouches
|
|
{ 0.125, 0, 0, 0, sy = 0.93, to = { nil, nil, "in", sy = 1 } },
|
|
{ 0.25, 0, 0, -21, to = { 0, -100, "out" } }, -- hops up and drops away
|
|
{ 0.475, 0, 0, -100, to = { 0, 214, "in" } },
|
|
},
|
|
}
|
|
end
|
|
|
|
-- GUMI in the centre: lands on her second entry frame, which sits 13px higher
|
|
local GUMI_DANCER = {
|
|
origin = { 0, 296 },
|
|
enter = {
|
|
beats = 2,
|
|
{ 0, 0, 0, 366 },
|
|
{ 0.35, 0, 0, 366, to = { 0, -40, "out" } },
|
|
{ 0.925, 0, 0, -40, to = { 0, 13, "in" } },
|
|
{ 1.175, 0, 0, 0 },
|
|
{ 1.2, 1, 0, 0, to = { nil, nil, "out", sy = 0.95 } },
|
|
{ 1.325, 1, 0, 0, sy = 0.95, to = { nil, nil, "in", sy = 1 } },
|
|
{ 1.45, 1, 0, 0 },
|
|
},
|
|
dance = Timing.steps({
|
|
{ 2, 0.725 }, { 3, 0.275 }, { 4, 0.725 }, { 5, 0.275 },
|
|
{ 2, 0.725 }, { 3, 0.275 }, { 6, 0.725 }, { 7, 0.275 },
|
|
{ 8, 0.725 }, { 9, 0.275 }, { 10, 0.725 }, { 11, 0.275 },
|
|
{ 12, 0.275 }, { 13, 0.45 }, { 14, 0.275 },
|
|
{ 15, 0.725 }, { 16, 0.275 }, { 17, 0.725 }, { 18, 0.275 },
|
|
{ 19, 0.725 }, { 20, 0.275 }, { 17, 0.725 }, { 18, 0.275 }, { 19, 0.725 }, { 20, 0.275 },
|
|
{ 21, 1 }, { 22, 0.5 }, { 23, 0.5 }, { 24, 1.725 }, { 11, 0.275 },
|
|
}),
|
|
}
|
|
local GUMI_INDEX = 4
|
|
local GUMI_DANCERS = Dancer.define({
|
|
[0] = small_dancer(0), [1] = small_dancer(0), [2] = small_dancer(0),
|
|
[3] = small_dancer(18), [GUMI_INDEX] = GUMI_DANCER,
|
|
})
|
|
|
|
-- Dancers stand on fixed slots (far-left .. far-right) instead of the group's even
|
|
-- spacing, since the centre dancer is wider than the others
|
|
local DANCER_CENTERS = { 183, 417, 620, 863, 1097 }
|
|
local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- outer pairs first, centre on top
|
|
|
|
-- Chibi GUMI for good hits: hops in on her carrot growing from 70% size, then rides it
|
|
-- straight across the don band
|
|
local GUMI_CHIBI = {
|
|
key = "1",
|
|
hop = { from = { -418, 21 }, peak = { -334, -36 }, up = 0.3 },
|
|
hop_frames = { { 0, 0.3 }, { 1, 0.175 }, { 0, 0.025 } },
|
|
period = 2,
|
|
poses = { { 0, 0 }, { 0.3, 1 }, { 0.65, 2 }, { 1, 3 }, { 1.325, 4 }, { 1.675, 5 } },
|
|
}
|
|
|
|
-- Background
|
|
|
|
local GumiBackground = {}
|
|
GumiBackground.__index = GumiBackground
|
|
|
|
function GumiBackground.new(player_num, bpm)
|
|
local self = setmetatable({}, GumiBackground)
|
|
self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DonBG.parallax())
|
|
self.bg_normal = GumiBGNormal.new(bpm)
|
|
self.bg_fever = GumiBGFever.new(bpm)
|
|
self.footer = Footer.new(COLLAB_PATH, 0, { false, true })
|
|
self.fever = Fever.new(bpm, COLLAB_PATH, FEVER_SPEC)
|
|
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, GUMI_DANCERS)
|
|
for i, d in ipairs(self.dancer.dancers) do
|
|
if d.index == GUMI_INDEX then
|
|
self.dancer.dancers[i], self.dancer.dancers[1] = self.dancer.dancers[1], self.dancer.dancers[i]
|
|
break
|
|
end
|
|
end
|
|
self.renda = RendaController.new(COLLAB_PATH, 0, RendaController.ACROSS)
|
|
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1)
|
|
self.is_clear = false
|
|
self.is_rainbow = false
|
|
self.last_milestone = 1
|
|
|
|
return self
|
|
end
|
|
|
|
function GumiBackground: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 GumiBackground:handle_good(player_num) BaseChibi.add(self.chibi, GUMI_CHIBI, COLLAB_PATH, player_num) end
|
|
|
|
function GumiBackground:handle_ok(player_num) BaseChibi.add(self.chibi, GUMI_CHIBI, COLLAB_PATH, player_num) end
|
|
|
|
function GumiBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
|
|
|
|
function GumiBackground:handle_drumroll(player_num) self.renda:add_renda() end
|
|
|
|
function GumiBackground:handle_balloon(player_num) self.renda:add_renda() end
|
|
|
|
function GumiBackground: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 GumiBackground:draw_back()
|
|
self.bg_normal:draw()
|
|
self.bg_fever: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 GumiBackground:draw_fore() end
|
|
|
|
function GumiBackground: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
|
|
|
|
return GumiBackground
|