308 lines
12 KiB
Lua
308 lines
12 KiB
Lua
local DonBGBase = require("background/bg_objects/donbg/base")
|
|
local Fever = require("background/bg_objects/fever")
|
|
local PetalFever = require("background/bg_objects/petal_fever")
|
|
local Footer = require("background/bg_objects/footer")
|
|
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 COLLAB_PATH = "background/collab/a3"
|
|
local MAX_DANCERS = 4
|
|
|
|
local chain_moves, frame_change, sum, concat, Loop =
|
|
Timing.chain_moves, Timing.frame_change, Timing.sum, Timing.concat, Timing.Loop
|
|
|
|
-- Don band: one tile scrolls by every 21.3 seconds; clearing fades the festival set in
|
|
-- and back out over a quarter beat (the default)
|
|
local A3_DONBG = { layers = { { key = "background", ms = 21333 } } }
|
|
|
|
-- Normal background: the festival street under the stage curtain, with the lanterns' glow
|
|
-- flickering additively over it (ambient, in ms; opacity in thousandths from 699)
|
|
local FLICKER_MS = 333
|
|
local FLICKER_BASE = 699
|
|
local FLICKER = { { 83, -97 }, { 83, 46 }, { 84, -97 }, { 83, 148 } }
|
|
|
|
local A3BGNormal = {}
|
|
A3BGNormal.__index = A3BGNormal
|
|
|
|
function A3BGNormal.new()
|
|
local self = setmetatable({}, A3BGNormal)
|
|
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_overlay = id("overlay")
|
|
self._id_curtain = id("curtain")
|
|
self.flicker = Loop.new(FLICKER_MS, function() return { glow = chain_moves(FLICKER, 1, 1) } end)
|
|
return self
|
|
end
|
|
|
|
function A3BGNormal:update(current_time_ms)
|
|
self.flicker:update(current_time_ms)
|
|
end
|
|
|
|
function A3BGNormal:draw()
|
|
tex.draw_texture(self._id_bg)
|
|
if self.flicker.anims then
|
|
local glow = (FLICKER_BASE + sum(self.flicker.anims.glow)) / 1000
|
|
tex.draw_texture(self._id_overlay, { fade = glow })
|
|
end
|
|
tex.draw_texture(self._id_curtain)
|
|
end
|
|
|
|
function A3BGNormal:destroy()
|
|
tex.unload_folder(COLLAB_PATH, "bg_normal/" .. self.name)
|
|
end
|
|
|
|
-- Clear background: a golden haze and two streams of petals over the normal one. Petal
|
|
-- flights in ms.
|
|
local A3_FEVER = {
|
|
path = COLLAB_PATH, haze = { key = "overlay" }, haze_in = 1.5, haze_out = 1, petals_out = 0.25,
|
|
streams = {
|
|
-- falling petals: glow additively at 80%, fading out over the second half of the flight
|
|
{ key = "petals", flight = 6667, dx = -340, dy = 690, first = 0, alpha = 0.8, fade_out = true },
|
|
-- turned a quarter: drift across at full opacity (164 = half the petal texture)
|
|
{ key = "petals", flight = 5000, dx = -260, dy = 670, first = 3, rotation = 90, half = 164 },
|
|
},
|
|
}
|
|
|
|
-- Dancers: four fox-hooded dancers on fixed slots. They pop in and out with no
|
|
-- entrance, all following one 16-beat routine, so a dancer joining mid-song is in step.
|
|
-- Each dancer's N_loop holds its poses, then the same poses mirrored. Timed in beats.
|
|
-- Routine steps: { pose, beats, mirrored }; "x"/"after_x"/"y" are per-dancer poses
|
|
local DANCE = {
|
|
{ 0, 0.35 }, { 1, 0.25 }, { 2, 1.4 }, { 1, 0.1 }, { 0, 0.45 }, { 1, 0.05 }, { 2, 0.85 }, { 3, 0.55 },
|
|
{ 0, 0.35, true }, { 1, 0.25, true }, { 2, 1.4, true }, { 1, 0.1, true }, { 0, 0.25, true },
|
|
{ 1, 0.25, true }, { 2, 0.85, true }, { 3, 0.475, true }, { "x", 0.075, true },
|
|
{ "after_x", 0.6, true }, { 4, 0.2, true }, { 5, 0.2, true }, { 6, 0.85, true }, { 7, 0.6, true },
|
|
{ 8, 1.55, true },
|
|
{ "y", 0.6 }, { 4, 0.2 }, { 5, 0.2 }, { 6, 0.85 }, { 7, 0.6 }, { 8, 1.55 },
|
|
}
|
|
local DANCE_BEATS = 16
|
|
-- index = skin folder; centre = the slot's x. Dancer 1 has an extra pose (4) and shifts the
|
|
-- rest up by one.
|
|
local DANCERS = {
|
|
[0] = { center = 775, poses = 9, shift = 0, x = 1, after_x = 0, y = 1 },
|
|
[1] = { center = 505, poses = 10, shift = 1, x = 4, after_x = 4, y = 4 },
|
|
[2] = { center = 1035, poses = 9, shift = 0, x = 1, after_x = 0, y = 0 },
|
|
[3] = { center = 245, poses = 9, shift = 0, x = 1, after_x = 0, y = 0 },
|
|
}
|
|
local DRAW_ORDER = { 3, 2, 1, 0 } -- back to front
|
|
-- a hop every two beats for the first half (px), and the shadow shrinking under it
|
|
local HOP = { { 0.375, -16 }, { 0.5, 23 }, { 0.15, -7 } }
|
|
local HOPS = concat({ { 0.6, 0 } }, HOP, { { 0.975, 0 } }, HOP, { { 0.975, 0 } }, HOP,
|
|
{ { 0.975, 0 } }, HOP)
|
|
local SHADOW = { { 0.6, 0 }, { 0.35, -100 }, { 0.375, 100 }, { 0.675, 0 } } -- scale, per 2 beats
|
|
-- squash: px off the 312px height, feet planted. A dip on the pose changes, B a dip and a
|
|
-- stretch on the turn, C a slow sink on the last pose
|
|
local SQUASH_A = { { 0.2, 16 }, { 0.225, -16 } }
|
|
local SQUASH_B = { { 0.4, 16 }, { 0.6, -20 }, { 0.25, 4 } }
|
|
local SQUASH_C = { { 0, 6 }, { 0.525, 6 }, { 0.275, -12 } }
|
|
local SQUASH = concat({ { 0.125, 0 } }, SQUASH_A, { { 1.55, 0 } }, SQUASH_A, { { 1.6, 0 } },
|
|
SQUASH_A, { { 1.55, 0 } }, SQUASH_A, { { 1.475, 0 } }, SQUASH_B, { { 1.2, 0 } }, SQUASH_C,
|
|
{ { 0.75, 0 } }, SQUASH_B, { { 1.2, 0 } }, SQUASH_C)
|
|
-- dancer 0's music note pops by its head twice (texture.json "note" indices 0, 1)
|
|
local NOTES = {
|
|
{ { 3.45, 0 }, { 0.3, 1000 }, { 0.35, -1000 } },
|
|
{ { 7.45, 0 }, { 0.325, 1000 }, { 0.325, -1000 } },
|
|
}
|
|
local SHADOW_ALPHA = 0.45
|
|
|
|
local function dance_steps(spec)
|
|
local steps = {}
|
|
for _, step in ipairs(DANCE) do
|
|
local pose = step[1]
|
|
if type(pose) == "string" then
|
|
pose = spec[pose]
|
|
elseif pose >= 4 then
|
|
pose = pose + spec.shift
|
|
end
|
|
table.insert(steps, { pose + (step[3] and spec.poses or 0), step[2] })
|
|
end
|
|
return steps
|
|
end
|
|
|
|
local A3Dancers = {}
|
|
A3Dancers.__index = A3Dancers
|
|
|
|
function A3Dancers.new(bpm)
|
|
local self = setmetatable({}, A3Dancers)
|
|
self.name = "dancer_0"
|
|
tex.load_folder(COLLAB_PATH, "dancer/" .. self.name)
|
|
local function id(key) return tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. key) or 0 end
|
|
self._id_shadow = id("shadow")
|
|
self._id_note = id("note")
|
|
self._ids = {}
|
|
self.steps = {}
|
|
for i, spec in pairs(DANCERS) do
|
|
self._ids[i] = id(i .. "_loop")
|
|
self.steps[i] = dance_steps(spec)
|
|
end
|
|
self.width = tex.get_texture_info(self.name, "0_loop").width
|
|
self.shown = 1
|
|
self:set_bpm(bpm)
|
|
return self
|
|
end
|
|
|
|
function A3Dancers:set_bpm(bpm)
|
|
self.bpm = bpm
|
|
local beat = 60000 / bpm
|
|
local scale = tex.get_screen_scale()
|
|
self.routine = Loop.new(DANCE_BEATS * beat, function()
|
|
local frames = {}
|
|
for i in pairs(DANCERS) do
|
|
frames[i + 1] = frame_change(self.steps[i], beat)
|
|
frames[i + 1]:start()
|
|
end
|
|
local shadow = {}
|
|
for _ = 1, 4 do shadow = concat(shadow, SHADOW) end
|
|
return {
|
|
frames = frames,
|
|
hop = chain_moves(HOPS, beat, scale),
|
|
squash = chain_moves(SQUASH, beat, scale),
|
|
shadow = chain_moves(shadow, beat, 1),
|
|
note1 = chain_moves(NOTES[1], beat, 1),
|
|
note2 = chain_moves(NOTES[2], beat, 1),
|
|
}
|
|
end)
|
|
end
|
|
|
|
function A3Dancers:set_count(count) self.shown = count end
|
|
|
|
function A3Dancers:update(current_time_ms, bpm)
|
|
if bpm and bpm ~= self.bpm then self:set_bpm(bpm) end
|
|
self.routine:update(current_time_ms)
|
|
end
|
|
|
|
function A3Dancers:draw()
|
|
local a = self.routine.anims
|
|
if not a then return end
|
|
local scale = tex.get_screen_scale()
|
|
local squash = sum(a.squash)
|
|
local shadow = (1000 + sum(a.shadow)) / 1000
|
|
for _, i in ipairs(DRAW_ORDER) do
|
|
if i < self.shown then
|
|
local x = math.floor(DANCERS[i].center * scale - self.width / 2)
|
|
tex.draw_texture(self._id_shadow, { x = x, center = true, scale = shadow, fade = SHADOW_ALPHA * shadow })
|
|
tex.draw_texture(self._ids[i], {
|
|
frame = a.frames[i + 1].attribute,
|
|
x = x,
|
|
y = sum(a.hop) + squash,
|
|
y2 = -squash,
|
|
})
|
|
if i == 0 then
|
|
for n, note in ipairs({ a.note1, a.note2 }) do
|
|
local fade = sum(note) / 1000
|
|
if fade > 0 then tex.draw_texture(self._id_note, { index = n - 1, x = x, fade = fade }) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function A3Dancers:destroy()
|
|
tex.unload_folder(COLLAB_PATH, "dancer/" .. self.name)
|
|
end
|
|
|
|
-- Chibi for good hits: one of four festival tiles hops in growing from 70% size, then bobs
|
|
-- across the don band
|
|
local A3_CHIBI_MOTION = {
|
|
key = "1",
|
|
hop = { from = { -418, 0 }, peak = { -335, -47 }, up = 0.3 },
|
|
run = { y = -3 },
|
|
wave = { first = -12, height = 22, beats = 0.5 }, -- a steady bob
|
|
}
|
|
local A3_CHIBIS = {}
|
|
for frame = 0, 3 do -- one character per frame of chibi_1/1
|
|
A3_CHIBIS[frame + 1] = setmetatable({ first = frame }, { __index = A3_CHIBI_MOTION })
|
|
end
|
|
|
|
-- Background
|
|
|
|
local A3Background = {}
|
|
A3Background.__index = A3Background
|
|
|
|
function A3Background.new(player_num, bpm)
|
|
local self = setmetatable({}, A3Background)
|
|
self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, A3_DONBG)
|
|
self.bg_normal = A3BGNormal.new()
|
|
self.bg_fever = PetalFever.new(bpm, A3_FEVER)
|
|
-- A3 has no dodai or fever of its own; the game uses the regular ones
|
|
self.footer = Footer.new("background", math.random(0, 2))
|
|
self.fever = Fever.create(math.random(0, 3), bpm, "background")
|
|
self.dancer = A3Dancers.new(bpm)
|
|
self.renda = RendaController.new(COLLAB_PATH, 0) -- the default drumroll flight
|
|
self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1)
|
|
self.is_clear = false
|
|
self.is_rainbow = false
|
|
return self
|
|
end
|
|
|
|
function A3Background: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
|
|
|
|
local function add_chibi(self, player_num)
|
|
BaseChibi.add(self.chibi, A3_CHIBIS[math.random(#A3_CHIBIS)], COLLAB_PATH, player_num, ChibiWave)
|
|
end
|
|
|
|
function A3Background:handle_good(player_num) add_chibi(self, player_num) end
|
|
|
|
function A3Background:handle_ok(player_num) add_chibi(self, player_num) end
|
|
|
|
function A3Background:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
|
|
|
|
function A3Background:handle_drumroll(player_num) self.renda:add_renda() end
|
|
|
|
function A3Background:handle_balloon(player_num) self.renda:add_renda() end
|
|
|
|
function A3Background: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
|
|
-- one dancer to start, up to three with the gauge, all four once cleared
|
|
if is_clear then
|
|
self.dancer:set_count(MAX_DANCERS)
|
|
else
|
|
self.dancer:set_count(math.max(1, math.min(MAX_DANCERS - 1, math.floor(progress * MAX_DANCERS))))
|
|
end
|
|
end
|
|
|
|
function A3Background:draw_back()
|
|
self.bg_normal:draw()
|
|
self.bg_fever:draw()
|
|
self.footer:draw()
|
|
self.donbg:draw()
|
|
self.renda:draw()
|
|
self.dancer:draw()
|
|
self.fever:draw()
|
|
self.chibi:draw()
|
|
end
|
|
|
|
function A3Background:draw_fore() end
|
|
|
|
function A3Background:destroy()
|
|
self.donbg:destroy()
|
|
self.bg_normal:destroy()
|
|
self.bg_fever:destroy()
|
|
self.footer:destroy()
|
|
self.fever:destroy()
|
|
self.dancer:destroy()
|
|
self.renda:destroy()
|
|
self.chibi:destroy()
|
|
end
|
|
|
|
return A3Background
|