379 lines
15 KiB
Lua
379 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 ChibiController = require("background/bg_objects/chibi")
|
|
local BaseChibi = require("background/bg_objects/chibi/base")
|
|
local RendaController = require("background/bg_objects/renda")
|
|
local Timing = require("background/bg_objects/timing")
|
|
|
|
local COLLAB_PATH = "background/collab/miku"
|
|
-- fever line-up: two poses, 70px bounce
|
|
local FEVER_SPEC = { bounce = 70, poses = true }
|
|
local MAX_DANCERS = 5
|
|
|
|
local chain_moves, frame_change, sum, Loop =
|
|
Timing.chain_moves, Timing.frame_change, Timing.sum, Timing.Loop
|
|
|
|
-- Normal background: a DJ booth whose three monitors spell out MIKU, a flickering
|
|
-- glow over the stage and thumping speakers on both sides. Plain ms timings: the
|
|
-- monitors run on their own loops rather than the song's beat.
|
|
|
|
-- Right monitor: powers on, glitches into M, I, K, U, glitches out and powers off again
|
|
local SCREEN_R = {
|
|
{ 0, 333 }, { 1, 117 }, { 2, 67 }, { 1, 117 }, { 2, 67 }, { 1, 133 }, { 2, 167 },
|
|
{ 3, 667 }, { 4, 667 }, { 5, 667 }, { 6, 683 },
|
|
{ 1, 117 }, { 2, 67 }, { 1, 117 }, { 2, 67 }, { 1, 133 }, { 2, 200 }, { 0, 350 },
|
|
}
|
|
-- its opacity, in thousandths: fades in when the loop starts and again when it powers off
|
|
local SCREEN_R_FADE = { { 100, 1000, "out" }, { 4283, 0 }, { 0, -1000 }, { 333, 1000 } }
|
|
local SCREEN_C = {
|
|
{ 0, 917 }, { 1, 800 }, { 2, 883 },
|
|
{ 3, 117 }, { 4, 50 }, { 3, 117 }, { 4, 83 }, { 3, 117 }, { 4, 67 },
|
|
{ 3, 117 }, { 4, 50 }, { 3, 117 }, { 4, 83 }, { 3, 117 }, { 5, 700 },
|
|
}
|
|
local SCREEN_L = {
|
|
{ 0, 567 }, { 1, 350 },
|
|
{ 2, 67 }, { 3, 67 }, { 2, 67 }, { 3, 117 }, { 2, 67 }, { 3, 33 }, { 2, 67 },
|
|
{ 3, 200 }, { 2, 67 }, { 3, 33 }, { 2, 67 }, { 3, 383 },
|
|
{ 4, 833 }, { 5, 783 },
|
|
}
|
|
local BAND_MOVE = { { 3250, -56 } } -- scanline sweeping up the centre monitor, then gone
|
|
local BAND_START = 28
|
|
-- glow opacity in thousandths: three quick dips and a slow one
|
|
local GLOW_FLICKER = {
|
|
{ 67, -200 }, { 67, 200 }, { 67, -200 }, { 67, 200 }, { 67, -200 }, { 117, 200 },
|
|
{ 250, -290 }, { 83, 290 },
|
|
}
|
|
local GLOW_OPACITY = 0.84
|
|
local SPEAKER_PULSE = { { 150, -50, "out" }, { 183, 50, "in" } } -- scale in thousandths
|
|
|
|
local MikuBGNormal = {}
|
|
MikuBGNormal.__index = MikuBGNormal
|
|
|
|
function MikuBGNormal.new()
|
|
local self = setmetatable({}, MikuBGNormal)
|
|
self.name = "bg_0"
|
|
tex.load_folder(COLLAB_PATH, "bg_normal/" .. 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_screen_l = id("screen_l")
|
|
self._id_screen_c = id("screen_c")
|
|
self._id_screen_r = id("screen_r")
|
|
self._id_band = id("band")
|
|
self._id_glow = id("glow")
|
|
self._id_speaker = id("speaker")
|
|
self.speaker_width = tex.get_texture_info(self.name, "speaker").width
|
|
|
|
local scale = tex.get_screen_scale()
|
|
self.screen_l = Loop.new(3767, function()
|
|
local f = frame_change(SCREEN_L, 1)
|
|
f:start()
|
|
return { frames = f }
|
|
end)
|
|
self.screen_c = Loop.new(4333, function()
|
|
local f = frame_change(SCREEN_C, 1)
|
|
f:start()
|
|
return { frames = f, band = chain_moves(BAND_MOVE, 1, scale) }
|
|
end)
|
|
self.screen_r = Loop.new(4733, function()
|
|
local f = frame_change(SCREEN_R, 1)
|
|
f:start()
|
|
return { frames = f, fade = chain_moves(SCREEN_R_FADE, 1, 1) }
|
|
end)
|
|
self.glow = Loop.new(850, function() return { flicker = chain_moves(GLOW_FLICKER, 1, 1) } end)
|
|
self.speaker = Loop.new(333, function() return { pulse = chain_moves(SPEAKER_PULSE, 1, 1) } end)
|
|
self.band_start = math.floor(BAND_START * scale)
|
|
return self
|
|
end
|
|
|
|
function MikuBGNormal:update(current_time_ms)
|
|
self.screen_l:update(current_time_ms)
|
|
self.screen_c:update(current_time_ms)
|
|
self.screen_r:update(current_time_ms)
|
|
self.glow:update(current_time_ms)
|
|
self.speaker:update(current_time_ms)
|
|
end
|
|
|
|
function MikuBGNormal:draw()
|
|
if not self.glow.anims then return end -- not updated yet
|
|
tex.draw_texture(self._id_bg)
|
|
-- the right monitor is off (opacity 0) at the start of its loop
|
|
tex.draw_texture(self._id_screen_r, {
|
|
frame = self.screen_r.anims.frames.attribute,
|
|
fade = sum(self.screen_r.anims.fade) / 1000,
|
|
})
|
|
tex.draw_texture(self._id_screen_c, { frame = self.screen_c.anims.frames.attribute })
|
|
local band = self.screen_c.anims.band[1]
|
|
if not band.is_finished then
|
|
tex.draw_texture(self._id_band, { y = self.band_start + band.attribute })
|
|
end
|
|
tex.draw_texture(self._id_screen_l, { frame = self.screen_l.anims.frames.attribute })
|
|
tex.draw_texture(self._id_glow, {
|
|
fade = GLOW_OPACITY * (1 + sum(self.glow.anims.flicker) / 1000),
|
|
})
|
|
-- speakers shrink towards their outer edge
|
|
local s = 1 + sum(self.speaker.anims.pulse) / 1000
|
|
local shift = self.speaker_width * (1 - s) / 2
|
|
tex.draw_texture(self._id_speaker, { index = 0, scale = s, center = true, x = -shift })
|
|
tex.draw_texture(self._id_speaker, { index = 1, scale = s, center = true, x = shift, mirror = "horizontal" })
|
|
end
|
|
|
|
function MikuBGNormal:destroy() tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name) end
|
|
|
|
local MIKU_DONBG_FAST_X = -164 -- don band fast layer's tile phase
|
|
|
|
-- Chibi Miku for good hits: hops in growing from 70% size, then runs straight across
|
|
-- the don band (no bounce). Bad hits play the shared tamashii.
|
|
local MIKU_CHIBI = {
|
|
key = "1",
|
|
period = 1,
|
|
poses = { { 0, 0 }, { 0.2, 1 }, { 0.4, 2 }, { 0.6, 3 }, { 0.8, 4 } },
|
|
}
|
|
|
|
-- Clear background: the panel drops in from above and bounces, then the light rays fade
|
|
-- in; losing clear drops it away again. Transitions are timed in beats, the tile scroll
|
|
-- in plain ms.
|
|
local BG_FEVER_SCROLL_MS = 1333 -- one tile
|
|
local BG_FEVER_DROP_Y0 = -360
|
|
local BG_FEVER_DROP = { { 1, 360, "in" }, { 0.425, -50, "out" }, { 0.325, 50, "in" } }
|
|
local BG_FEVER_RAYS_IN = { { 1, 0 }, { 0.75, 1000 } } -- opacity in thousandths
|
|
local BG_FEVER_FALL = { { 1, 390 } }
|
|
local BG_FEVER_RAYS_OUT = 0.65
|
|
|
|
local MikuBGFever = {}
|
|
MikuBGFever.__index = MikuBGFever
|
|
|
|
function MikuBGFever.new(bpm)
|
|
local self = setmetatable({}, MikuBGFever)
|
|
self.name = "bg_fever_0"
|
|
tex.load_folder(COLLAB_PATH, "bg_fever/" .. self.name)
|
|
self.tile = tex.get_texture_info(self.name, "background").width
|
|
self._id_bg = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. "background") or 0
|
|
self._id_ray = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. "ray") or 0
|
|
self.beat = 60000 / bpm
|
|
self.scroll = anim.move(BG_FEVER_SCROLL_MS, { total_distance = -self.tile, loop = true })
|
|
self.scroll:start()
|
|
self.state = nil -- nil (hidden), "in", "out"
|
|
return self
|
|
end
|
|
|
|
function MikuBGFever:y() return self.offset + sum(self.drop) end
|
|
function MikuBGFever:ray_opacity() return (self.ray_offset + sum(self.rays)) / 1000 end
|
|
|
|
function MikuBGFever:start()
|
|
local scale = tex.get_screen_scale()
|
|
self.state = "in"
|
|
self.offset = math.floor(BG_FEVER_DROP_Y0 * scale)
|
|
self.drop = chain_moves(BG_FEVER_DROP, self.beat, scale)
|
|
self.ray_offset = 0
|
|
self.rays = chain_moves(BG_FEVER_RAYS_IN, self.beat, 1)
|
|
end
|
|
|
|
-- falls away from wherever it is, rays fading from their current opacity
|
|
function MikuBGFever:stop()
|
|
if not self.state then return end
|
|
local opacity = math.floor(self:ray_opacity() * 1000)
|
|
self.state = "out"
|
|
self.offset = self:y()
|
|
self.drop = chain_moves(BG_FEVER_FALL, self.beat, tex.get_screen_scale())
|
|
self.ray_offset = opacity
|
|
self.rays = chain_moves({ { BG_FEVER_RAYS_OUT, -opacity } }, self.beat, 1)
|
|
end
|
|
|
|
function MikuBGFever:update(current_time_ms, bpm)
|
|
if bpm then self.beat = 60000 / bpm end
|
|
self.scroll:update(current_time_ms)
|
|
if not self.state then return end
|
|
for _, m in ipairs(self.drop) do m:update(current_time_ms) end
|
|
for _, m in ipairs(self.rays) do m:update(current_time_ms) end
|
|
if self.state == "out" and self.drop[#self.drop].is_finished then self.state = nil end
|
|
end
|
|
|
|
function MikuBGFever:draw()
|
|
if not self.state then return end
|
|
local y = self:y()
|
|
for i = 0, 4 do
|
|
tex.draw_texture(self._id_bg, { x = i * self.tile + self.scroll.attribute, y = y })
|
|
end
|
|
local fade = self:ray_opacity()
|
|
tex.draw_texture(self._id_ray, { index = 0, fade = fade })
|
|
tex.draw_texture(self._id_ray, { index = 1, fade = fade, mirror = "horizontal" })
|
|
end
|
|
|
|
function MikuBGFever: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.
|
|
|
|
-- One beat of the small dancers: a pose held most of the beat, then a short in-between
|
|
local function small_dance(s)
|
|
return {
|
|
{ s, 0.175 }, { s + 1, 0.55 }, { s + 2, 0.275 },
|
|
{ s + 3, 0.725 }, { s + 4, 0.275 },
|
|
{ s + 1, 0.725 }, { s + 2, 0.275 },
|
|
{ s + 3, 0.725 }, { s + 4, 0.275 },
|
|
}
|
|
end
|
|
|
|
-- poses are offsets from where each dancer stands, pivoting about its feet (bottom-left)
|
|
local SMALL_DANCER = {
|
|
origin = { 0, 200 },
|
|
enter = {
|
|
beats = 2,
|
|
{ 0, 0, 0, 213 },
|
|
{ 0.35, 0, 0, 213, to = { 0, -43, "out" } }, -- jumps up from below, hangs
|
|
{ 0.825, 0, 0, -43 },
|
|
{ 1.125, 0, 0, -43, to = { 0, 0, "in" } },
|
|
{ 1.275, 0, 0, 0, to = { sy = 0.95 } }, -- lands with a squash
|
|
{ 1.35, 0, 0, 0, sy = 0.95, to = { sy = 1 } },
|
|
{ 1.45, 0, 0, 0 },
|
|
},
|
|
dance = Timing.steps(small_dance(0), small_dance(5)),
|
|
leave = {
|
|
beats = 1.425,
|
|
{ 0, 0, 0, 0 },
|
|
{ 0.1, 0, 0, 0, to = { 0, -100, "out" } }, -- hops up and drops away
|
|
{ 0.475, 0, 0, -100, to = { 0, 214, "in" } },
|
|
},
|
|
}
|
|
|
|
local MIKU_A = {
|
|
{ 2, 0.1 }, { 3, 0.075 }, { 4, 0.55 }, { 5, 0.075 }, { 6, 0.2 },
|
|
{ 7, 0.725 }, { 8, 0.075 }, { 9, 0.2 },
|
|
{ 4, 0.725 }, { 5, 0.075 }, { 6, 0.2 },
|
|
{ 7, 0.725 }, { 8, 0.075 }, { 9, 0.2 },
|
|
}
|
|
local MIKU_B = {
|
|
{ 10, 0.1 }, { 11, 0.075 }, { 12, 0.55 }, { 13, 0.075 }, { 14, 0.2 },
|
|
{ 15, 0.725 }, { 16, 0.075 }, { 17, 0.2 },
|
|
{ 12, 0.725 }, { 13, 0.075 }, { 14, 0.2 },
|
|
{ 18, 0.725 }, { 16, 0.075 }, { 17, 0.2 },
|
|
}
|
|
local MIKU_FINALE = {
|
|
{ 19, 0.35 }, { 20, 0.325 }, { 21, 0.325 }, { 22, 0.3 },
|
|
{ 23, 0.35 }, { 24, 0.35 }, { 25, 0.35 }, { 26, 0.375 }, { 27, 1.275 },
|
|
}
|
|
|
|
local MIKU_DANCER = {
|
|
origin = { 0, 304 },
|
|
enter = {
|
|
beats = 2,
|
|
{ 0, 0, 0, 343 },
|
|
{ 0.35, 0, 0, 343, to = { 0, -69, "out" } },
|
|
{ 0.925, 0, 0, -69, to = { 0, 0, "in" } },
|
|
{ 1.2, 1, 0, 0 },
|
|
{ 1.225, 1, 0, 0, to = { sy = 0.95 } },
|
|
{ 1.25, 1, 0, 0, sy = 0.95, to = { sy = 1 } },
|
|
{ 1.325, 1, 0, 0 },
|
|
},
|
|
dance = Timing.steps(MIKU_A, MIKU_B, MIKU_A, MIKU_FINALE),
|
|
}
|
|
local MIKU_INDEX = 4
|
|
local MIKU_DANCERS = Dancer.define({ [MIKU_INDEX] = MIKU_DANCER, default = SMALL_DANCER })
|
|
|
|
-- Dancers stand on fixed slots (far-left .. far-right) instead of the group's even spacing,
|
|
-- since Miku's frames are wider than the others
|
|
local DANCER_CENTERS = { 173, 387, 640, 893, 1107 }
|
|
local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- outer pairs first, centre on top
|
|
|
|
|
|
-- Background
|
|
|
|
local MikuBackground = {}
|
|
MikuBackground.__index = MikuBackground
|
|
|
|
function MikuBackground.new(player_num, bpm)
|
|
local self = setmetatable({}, MikuBackground)
|
|
self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DonBG.parallax(MIKU_DONBG_FAST_X))
|
|
self.bg_normal = MikuBGNormal.new()
|
|
self.bg_fever = MikuBGFever.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, MIKU_DANCERS)
|
|
for i, d in ipairs(self.dancer.dancers) do
|
|
if d.index == MIKU_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 MikuBackground: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 MikuBackground:handle_good(player_num) BaseChibi.add(self.chibi, MIKU_CHIBI, COLLAB_PATH, player_num) end
|
|
|
|
function MikuBackground:handle_ok(player_num) BaseChibi.add(self.chibi, MIKU_CHIBI, COLLAB_PATH, player_num) end
|
|
|
|
function MikuBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
|
|
|
|
function MikuBackground:handle_drumroll(player_num) self.renda:add_renda() end
|
|
|
|
function MikuBackground:handle_balloon(player_num) self.renda:add_renda() end
|
|
|
|
function MikuBackground: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 MikuBackground: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 MikuBackground:draw_fore() end
|
|
|
|
function MikuBackground: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 MikuBackground
|