local DonBGBase = require("background/bg_objects/donbg/base") local BGNormal = require("background/bg_objects/bg_normal") 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/dq10" local MAX_DANCERS = 5 local chain_moves, sum, pose_at, steps = Timing.chain_moves, Timing.sum, Timing.pose_at, Timing.steps -- Don band: slimes scrolling one tile every 5.3s; clearing fades in coloured slimes over a -- yellow glow. Ambient, in ms. local DONBG_SPEC = { layers = { { key = "backdrop", tile = 0, clear_only = true }, { key = "background", ms = 5333 }, } } -- Clear background: the view tilts up the stage into the sky (the day panel pans down 52px -- over 1.5 beats), then night falls: the day fades off the night panel and sparkles burst in -- the night sky behind the arches (the front panel has the sky cut out). Losing clear fades -- the day back in over the night as the view tilts back down onto the normal stage, which -- the day panel matches at the bottom of its pan. Timed in beats; sparkles in ms. local TILT, TILT_BEATS = 52, 1.5 local NIGHT_BEATS = 0.45 -- a burst every 150ms somewhere in the sky, 16 frames of 33ms; Lumen loops a fixed 4.9s -- pattern of them, spread like this local SPARKLE_EVERY_MS, SPARKLE_FRAME_MS, SPARKLE_FRAMES = 150, 33, 16 local SPARKLE_X, SPARKLE_Y = { 0, 1280 }, { 330, 600 } local SPARKLE_SCALE, SPARKLE_BIG, SPARKLE_BIG_CHANCE = { 0.3, 0.8 }, 1.1, 0.15 local DQ10BGFever = {} DQ10BGFever.__index = DQ10BGFever function DQ10BGFever.new(bpm) local self = setmetatable({}, DQ10BGFever) 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_day = id("day") self._id_night = id("night") self._id_front = id("front") self._id_sparkle = id("sparkle") self.sparkle_half = (tex.get_texture_info(self.name, "sparkle") or {}).width or 256 self.sparkle_half = self.sparkle_half / 2 self.beat = 60000 / bpm self.state = nil -- nil (hidden), "in", "out" self.tilt = { from = 0, moves = {} } self.day = { from = 1000, moves = {} } self.sparkles = {} return self end local function track(t) return t.from + sum(t.moves) end local function retrack(t, moves, beat) t.from, t.moves = track(t), chain_moves(moves, beat, 1) end -- both directions start from wherever the tilt and the day fade are function DQ10BGFever:start() local tilt, day = self.state and track(self.tilt) or 0, self.state and track(self.day) or 1000 self.tilt.from, self.day.from = tilt, day local up = TILT_BEATS * (TILT - tilt) / TILT self.tilt.moves = chain_moves({ { up, TILT - tilt } }, self.beat, 1) self.day.moves = chain_moves({ { up, 0 }, { NIGHT_BEATS * day / 1000, -day } }, self.beat, 1) self.state = "in" end function DQ10BGFever:stop() if self.state ~= "in" then return end local tilt, day = track(self.tilt), track(self.day) retrack(self.tilt, { { TILT_BEATS * tilt / TILT, -tilt } }, self.beat) retrack(self.day, { { NIGHT_BEATS * (1000 - day) / 1000, 1000 - day } }, self.beat) self.state = "out" end function DQ10BGFever:is_night() return self.state == "in" and track(self.day) <= 0 end function DQ10BGFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end if not self.state then return end for _, t in ipairs({ self.tilt, self.day }) do for _, m in ipairs(t.moves) do m:update(current_time_ms) end end if self.state == "out" and track(self.tilt) <= 0 then self.state = nil return end local now = current_time_ms for i = #self.sparkles, 1, -1 do if now - self.sparkles[i].t0 >= SPARKLE_FRAMES * SPARKLE_FRAME_MS then table.remove(self.sparkles, i) end end if not self:is_night() then self.next_sparkle = nil elseif not self.next_sparkle or now >= self.next_sparkle then local s = math.random() < SPARKLE_BIG_CHANCE and SPARKLE_BIG or SPARKLE_SCALE[1] + math.random() * (SPARKLE_SCALE[2] - SPARKLE_SCALE[1]) table.insert(self.sparkles, { t0 = now, scale = s, x = math.random(SPARKLE_X[1], SPARKLE_X[2]), y = math.random(SPARKLE_Y[1], SPARKLE_Y[2]), }) self.next_sparkle = now + SPARKLE_EVERY_MS end self.now = now end function DQ10BGFever:draw() if not self.state then return end local scale = tex.get_screen_scale() local y = math.floor((track(self.tilt) - TILT) * scale) tex.draw_texture(self._id_night, { y = y }) if self:is_night() then for _, s in ipairs(self.sparkles) do tex.draw_texture(self._id_sparkle, { frame = math.min(SPARKLE_FRAMES - 1, math.floor((self.now - s.t0) / SPARKLE_FRAME_MS)), center = true, scale = s.scale, blend = "additive", x = s.x * scale - self.sparkle_half, y = s.y * scale - self.sparkle_half, }) end tex.draw_texture(self._id_front, { y = y }) end local day = track(self.day) / 1000 if day > 0 then tex.draw_texture(self._id_day, { y = y, fade = day }) end end function DQ10BGFever:destroy() tex.unload_folder(COLLAB_PATH, "bg_fever/" .. self.name) end -- Fever: the song's closing notes call up the five races, a pair from both edges each: -- the first balloon, then five dons near the end of the chart. The last pair brings the -- DRAGON QUEST X logo: the art fades in, the title shrinks into place, the X zooms down -- onto it, and a glow and the subtitle fade in. Fades in beats. Each character is an -- outline (frame 0) and its colours (frame 1); a side's outlines all sit under its colours. local CAST_FADE = 0.45 local CAST_DEPTH = { 1, 3, 6, 2, 4, 5 } -- draw order of right_N / left_N (N = entrance order) local CAST_PAIRS = #CAST_DEPTH -- chart times (ms from the song's first update, shifted by the offset) of the dons that bring -- pairs 2-6; pair 1 comes with the first balloon local FEVER_DON_TIMES_MS = { 70966, 71941, 72917, 77343, 79326 } local CHART_OFFSET_MS = -3715 - 975 local function fade_in(beat, length) return { { beat, 0, 0, 0, alpha = 0, to = { nil, nil, nil, alpha = 1 } }, { beat + length, 0, 0, 0 } } end -- logo layers in draw order, in beats from the last pair local LOGO = { { "art_glow", fade_in(6.05, 1.5) }, { "art", fade_in(0, 1.05) }, { "subtitle", fade_in(8.4, 1.5) }, { "logo", { { 1.9, 0, 0, 0, alpha = 0, to = { nil, nil, "in", alpha = 1, scale = 0.605 } }, { 4.65, 0, 0, 0, scale = 0.605 } } }, { "x", { { 3.225, 0, 0, 0, alpha = 0, scale = 1.78, to = { nil, nil, "in", alpha = 1, scale = 0.605 } }, { 5.425, 0, 0, 0, scale = 0.605 } } }, } local CAST_POSES = fade_in(0, CAST_FADE) local DQ10Fever = {} DQ10Fever.__index = DQ10Fever function DQ10Fever.new(bpm) local self = setmetatable({}, DQ10Fever) self.name = "fever_0" tex.load_folder(COLLAB_PATH, "fever/" .. self.name) local function id(key) return tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/" .. key) or 0 end self.cast = {} for _, side in ipairs({ "right", "left" }) do for _, n in ipairs(CAST_DEPTH) do table.insert(self.cast, { pair = n, id = id(side .. "_" .. n) }) end end self.logo = {} for _, l in ipairs(LOGO) do table.insert(self.logo, { id = id(l[1]), poses = l[2] }) end self.beat = 60000 / bpm self.shown = {} -- ms each pair was called up self.pending = 0 self.next_trigger = 1 return self end -- calls up the next pair function DQ10Fever:pulse() self.pending = self.pending + 1 end function DQ10Fever:update(current_time_ms, bpm) local now = current_time_ms if bpm then self.beat = 60000 / bpm end if not self.epoch_zero then self.epoch_zero = now end local ms_from_start = (now - self.epoch_zero) + CHART_OFFSET_MS while self.next_trigger <= #FEVER_DON_TIMES_MS and ms_from_start >= FEVER_DON_TIMES_MS[self.next_trigger] do self:pulse() self.next_trigger = self.next_trigger + 1 end for _ = 1, self.pending do if #self.shown < CAST_PAIRS then table.insert(self.shown, now) end end self.pending, self.now = 0, now end function DQ10Fever:beats_since(ms) return (self.now - ms) / self.beat end function DQ10Fever:draw() if #self.shown == 0 then return end for i = 1, #self.cast, CAST_PAIRS do -- per side: outlines, then colours for frame = 0, 1 do for k = i, i + CAST_PAIRS - 1 do local c = self.cast[k] local p = self.shown[c.pair] and pose_at(CAST_POSES, self:beats_since(self.shown[c.pair])) if p then tex.draw_texture(c.id, { frame = frame, fade = p.alpha }) end end end end local last = self.shown[CAST_PAIRS] if not last then return end for _, l in ipairs(self.logo) do local p = pose_at(l.poses, self:beats_since(last)) if p then tex.draw_texture(l.id, { center = true, scale = p.sx, fade = p.alpha }) end end end function DQ10Fever:destroy() tex.unload_folder(COLLAB_PATH, "fever/" .. self.name) end -- Dancers: one of two sets per play, five of the same character. Timed in beats; poses are -- Lumen's offsets from the dancer's origin (the centre of its images' Lumen box). -- set 0: spins round as it hops in, then a 24-step routine, a third of a beat a step; -- leaving, it springs up and drops away local SPIN_DANCER = { origin = { 112, 128 }, enter = { beats = 1.975, move = { { 0, 0, 0, 296, to = { 0, -52, "out" } }, { 0.85, 0, 0, -52, to = { 0, 10, "in" } }, { 1.275, 0, 0, 10, to = { 0, 0, "out" } }, { 1.625, 0, 0, 0 }, }, steps({ { 0, 0.125, x = 0, y = -125, key = "loop" }, { 1, 0.125, key = "loop" }, { 2, 0.125, key = "loop" }, { 3, 0.125, key = "loop" }, { 4, 0.125, key = "loop" }, { 5, 0.125, key = "loop" }, { 6, 0.125, key = "loop" }, { 7, 0.125, key = "loop" }, { 8, 0.125, key = "loop" }, { 9, 0.35, key = "loop" }, { 10, 0.125, key = "loop" }, { 11, 0.125, key = "loop" }, { 12, 0.125, key = "loop" }, { 13, 0.125, key = "loop" } }), }, dance = steps({ { 14, 1 / 3, x = 0, y = -125 }, { 15, 1 / 3 }, { 16, 1 / 3 }, { 17, 1 / 3 }, { 18, 1 / 3 }, { 19, 1 / 3 }, { 14, 1 / 3 }, { 15, 1 / 3 }, { 20, 1 / 3 }, { 21, 1 / 3 }, { 0, 1 / 3 }, { 1, 1 / 3 }, { 2, 1 / 3 }, { 3, 1 / 3 }, { 4, 1 / 3 }, { 5, 1 / 3 }, { 6, 1 / 3 }, { 7, 1 / 3 }, { 8, 1 / 3 }, { 9, 1 / 3 }, { 10, 1 / 3 }, { 11, 1 / 3 }, { 12, 1 / 3 }, { 13, 1 / 3 }, }), leave = { beats = 1.2, { 0, 11, 0, -125, key = "loop" }, { 0.15, 13, 0, -125, key = "loop", to = { 0, -176, "out" } }, { 0.425, 12, 0, -181, key = "loop" }, { 0.5, 11, 0, -181, key = "loop", to = { 0, 203, "in" } }, }, } -- set 1: shoots up from below and settles, then an 8-beat routine of beat-long poses with -- quick in-betweens and a hop; leaving, it bobs up and drops away local HOP_DANCER = { origin = { 148, 148 }, enter = { beats = 2, { 0.25, 0, 0, 226, to = { 0, -139, "out" } }, { 1.375, 0, 0, -135, to = { 0, -99 } }, { 1.625, 1, 0, -99, key = "loop" }, { 1.75, 2, 0, -99, key = "loop" }, }, dance = steps({ { 3, 0.75, x = 0, y = -99 }, { 4, 0.25 }, { 2, 0.75 }, { 5, 0.25 }, { 6, 0.75 }, { 7, 0.25 }, { 8, 0.85 }, { 1, 0.15 }, { 9, 0.4, y = -79, to = { 0, -99, "out" } }, { 9, 0.375, to = { 0, -79, "in" } }, { 9, 0.1 }, { 1, 0.125, y = -99 }, { 10, 0.75 }, { 11, 0.25 }, { 12, 0.75 }, { 13, 0.125 }, { 14, 0.125 }, { 2, 0.75 }, { 15, 0.25 }, }), leave = { beats = 1.525, { 0, 16, 0, -99, key = "loop", to = { 0, -109, "out" } }, { 0.4, 16, 0, -109, key = "loop", to = { 0, 226, "in" } }, }, } -- slot centres, far-left .. far-right; the outer pairs are drawn first, the centre on top local DANCER_SETS = { [0] = { class = Dancer.define({ default = SPIN_DANCER }), centers = { 183, 417, 640, 863, 1097 } }, [1] = { class = Dancer.define({ default = HOP_DANCER }), centers = { 172, 406, 640, 874, 1108 } }, } local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- Chibis for good hits: one of three characters hops in and runs across, each step a -- shuffle back on its walking frames and a skip forward local CHIBI_POSES = { { 0, 1, 0, 0 }, { 0.025, 2, -2, 0 }, { 0.05, 3, -4, 0 }, { 0.075, 2, -6, 0 }, { 0.1, 1, -8, 0, to = { -40, 0 } }, { 0.25, 2, -40, 0 }, { 0.275, 3, -40, 0 }, { 0.35, 3, -41, 0, to = { -38, -20 } }, { 0.45, 4, -38, -20, to = { -32, -25 } }, { 0.525, 0, -32, -25, to = { -30, -28 } }, { 0.6, 5, -30, -28, to = { -14, -35, "in" } }, { 0.775, 0, -14, -35, to = { -8, -20, "in" } }, { 0.9, 0, -8, -20, to = { 0, 0 } }, } local CHIBIS = {} for k = 0, 2 do CHIBIS[k + 1] = { key = tostring(k), hop = { from = { -418, 19 }, peak = { -326, -36 }, up = 0.3 }, run = { y = 0 }, period = 1, poses = CHIBI_POSES } end -- Drumroll: the default renda_a_03 flight, up and to the right local RENDA_SPEC = { ms = { 2000 }, dx = 1560, dy = -1648, x_range = 500, y_range = 20 } -- Background local DQ10Background = {} DQ10Background.__index = DQ10Background function DQ10Background.new(player_num, bpm) local self = setmetatable({}, DQ10Background) self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DONBG_SPEC) self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH) self.bg_fever = DQ10BGFever.new(bpm) self.footer = Footer.new(COLLAB_PATH, 0) self.fever = DQ10Fever.new(bpm) local set = math.random(0, 1) self.dancer_centers = DANCER_SETS[set].centers self.dancer = Dancer.BaseDancerGroup.new(set, bpm, MAX_DANCERS, COLLAB_PATH, DANCER_SETS[set].class) self.renda = RendaController.new(COLLAB_PATH, 0, RENDA_SPEC) self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1) self.is_clear = false self.is_rainbow = false self.last_milestone = 1 self.balloon_pulsed = false return self end function DQ10Background:update(current_ms, bpm) self.donbg: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 local function add_chibi(self, player_num) BaseChibi.add(self.chibi, CHIBIS[math.random(#CHIBIS)], COLLAB_PATH, player_num) end function DQ10Background:handle_good(player_num) add_chibi(self, player_num) end function DQ10Background:handle_ok(player_num) add_chibi(self, player_num) end function DQ10Background:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function DQ10Background:handle_drumroll(player_num) self.renda:add_renda() end function DQ10Background:handle_balloon(player_num) self.renda:add_renda() if not self.balloon_pulsed then self.balloon_pulsed = true self.fever:pulse() end end function DQ10Background: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 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 DQ10Background:draw_back() self.bg_normal:draw() self.bg_fever:draw() self.footer:draw() self.donbg:draw() self.renda:draw() self.dancer:draw_slots(self.dancer_centers, DANCER_DRAW_ORDER) self.fever:draw() self.chibi:draw() end function DQ10Background:draw_fore() end function DQ10Background: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 DQ10Background