local DonBGBase = require("background/bg_objects/donbg/base") local BGNormal = require("background/bg_objects/bg_normal") local PetalFever = require("background/bg_objects/petal_fever") 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") -- Touken Ranbu local COLLAB_PATH = "background/collab/touken" local MAX_DANCERS = 3 -- Don band: the base scrolls two tiles left every 5.33s; the clear set scrolls with it under -- a pink glow strip that pulses every second (IMAS's band with half-width tiles). Ambient, in ms. local TOUKEN_DONBG = { fade = 0.2, layers = { { key = "background", ms = 5333, dx = -328 }, { key = "glow", clear_only = true, tile = 0, alpha = 0.148, blend = "additive", period = 1000, pulse = { { 500, -1000 }, { 500, 1000, "out" } } }, }, } -- Clear background: the shrine courtyard (bg_normal) stays underneath; a pink glow fades in -- over it and two streams of cherry petals fall across, a new pass every 1.25s. Lumen screens -- the glow onto the courtyard; glow.png is that screen baked as an additive layer. local TOUKEN_FEVER = { path = COLLAB_PATH, haze = { key = "glow", blend = "additive" }, haze_in = 1.5, haze_out = 1, petals_out = 0.225, streams = { { key = "petals", flight = 2500, dx = -332, dy = 622, warm = true }, { key = "petals_2", flight = 2500, dx = -259, dy = 650, warm = true }, }, } -- Dancers: three swordsmen jump up from below, land with a squash, dance an 8-beat routine and -- hop up and drop away when removed. Timed in beats; poses are offsets of their feet (texture.json -- y 686), the pivot for the squash. Dancer 2 (left) has an extra landing frame, so its routine -- frames are one higher. local SQUASH = 0.978 -- one 2-beat phrase: two poses with a dip, a hop on the third, then two quarter-beat poses local function phrase(a, b, c, d, e) return { { a, 0.225, y = 0 }, { a, 0.25, sy = SQUASH }, { b, 0.25, sy = SQUASH }, { b, 0.275, y = 0 }, { c, 0.2, y = -11 }, { c, 0.15, y = -6 }, { c, 0.15, y = 0, sy = SQUASH }, { d, 0.25 }, { e, 0.25 }, } end local function touken_dancer(shift) local function routine(...) local poses = Timing.steps(...) for _, p in ipairs(poses) do p[2] = p[2] + shift end return poses end local dance = routine(phrase(1, 2, 3, 4, 5), phrase(6, 7, 8, 9, 10), phrase(1, 2, 3, 4, 5), phrase(6, 7, 11, 12, 13)) dance[1][3] = 0 return { origin = { 128, 312 }, enter = { beats = 2, key = "loop", { 0.2, 0, 0, 394, to = { 0, -40, "out" } }, -- jumps up from below { 0.875, 0, 0, -40, to = { 0, -4, "in" } }, { 1.35, 1, 0, 0, to = { nil, nil, "out", sy = 0.94 } }, -- lands with a squash { 1.575, 1, 0, 0, sy = 0.94, to = { nil, nil, "in", sy = 1 } }, { 1.825, 1, 0, 0 }, }, dance = dance, leave = { beats = 1.125, key = "loop", { 0, 14 + shift, 0, 0, to = { 0, -40, "out" } }, -- hops up and drops away { 0.45, 14 + shift, 0, -40, to = { 0, 394, "in" } }, }, } end local TOUKEN_DANCERS = Dancer.define({ default = touken_dancer(0), [1] = touken_dancer(1) }) -- Slots (left, centre, right) from Lumen's dancer positions; centre on top local DANCER_CENTERS = { 427, 640, 853 } local DANCER_DRAW_ORDER = { 3, 1, 2 } local DANCER_SPAWN = { 1, 0, 2 } -- 0-indexed slots in join order: centre, left, right -- dancer index by join order: dancer1 (centre), dancer2 (left), dancer3 (right) local DANCER_JOIN_ORDER = { 2, 1, 0 } -- Chibis for good hits: a crest flower fades in growing from half size as it rises onto the -- don band, zigzags 33px down and up seven times at a steady pace, then rolls on level. -- Offsets from texture.json (Lumen's attach point), in px; timed in beats. local CHIBI_LIFE = 5 local CHIBI_HOP = { length = 0.5, { 0, 0, -424, 13, scale = 0.5, alpha = 0, to = { -355, 17, scale = 0.65, alpha = 0.3 } }, { 0.15, 0, -355, 17, scale = 0.65, alpha = 0.3, to = { -206, -24, scale = 1, alpha = 1 } }, } local CHIBI_RUN = { from = -206, to = 938 } local CHIBI_ZIG = { top = -24, bottom = 9, half = 0.51, halves = 7 } local CHIBI_KEYS = { "1", "2", "3" } -- red, blue, purple local ToukenChibi = setmetatable({}, { __index = BaseChibi }) ToukenChibi.__index = ToukenChibi function ToukenChibi.new(spec, path, folder, bpm, is_2p) local self = setmetatable({}, ToukenChibi) self:init(spec, path, folder, bpm, is_2p) return self end function ToukenChibi:update(current_time_ms) self.t0 = self.t0 or current_time_ms self.t = (current_time_ms - self.t0) / self.beat self.done = self.t >= CHIBI_LIFE end function ToukenChibi:draw() if not self.t then return end local t = self.t if t < CHIBI_HOP.length then local p = Timing.pose_at(CHIBI_HOP, t) self:draw_image({ x = p.x, y = p.y, sx = p.sx, sy = p.sy, fade = p.alpha }) return end local run, z = t - CHIBI_HOP.length, CHIBI_ZIG local leg, y = run / z.half, z.bottom if leg < z.halves then local half = leg - math.floor(leg) if math.floor(leg) % 2 == 1 then half = 1 - half end -- even legs fall, odd ones rise y = z.top + (z.bottom - z.top) * half end local x = CHIBI_RUN.from + (CHIBI_RUN.to - CHIBI_RUN.from) * run / (CHIBI_LIFE - CHIBI_HOP.length) self:draw_image({ x = x, y = y }) end -- Drumroll: notes fly up and to the right, two turns over the 2s flight, centred on Lumen's -- spawn area (x 0-640, y 720-820) local RENDA = { ms = { 2000 }, spin = 720, x = -55, x_range = 640, y = -55, y_range = 100 } -- Background local ToukenBackground = {} ToukenBackground.__index = ToukenBackground function ToukenBackground.new(player_num, bpm) local self = setmetatable({}, ToukenBackground) self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, TOUKEN_DONBG) self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH) self.bg_fever = PetalFever.new(bpm, TOUKEN_FEVER) -- no dodai: the courtyard is the stage. No fever of its own either: the game uses a default self.footer = Footer.new(COLLAB_PATH, -1) self.fever = Fever.create(math.random(0, 3), bpm, "background") self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, TOUKEN_DANCERS) self.dancer.spawn_positions = DANCER_SPAWN local by_index = {} for _, d in ipairs(self.dancer.dancers) do by_index[d.index] = d end for i, index in ipairs(DANCER_JOIN_ORDER) do self.dancer.dancers[i] = by_index[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 ToukenBackground: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 function ToukenBackground:add_chibi(player_num) BaseChibi.add(self.chibi, { key = CHIBI_KEYS[math.random(#CHIBI_KEYS)] }, COLLAB_PATH, player_num, ToukenChibi) end function ToukenBackground:handle_good(player_num) self:add_chibi(player_num) end function ToukenBackground:handle_ok(player_num) self:add_chibi(player_num) end function ToukenBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function ToukenBackground:handle_drumroll(player_num) self.renda:add_renda() end function ToukenBackground:handle_balloon(player_num) self.renda:add_renda() end function ToukenBackground: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 ToukenBackground: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 ToukenBackground:draw_fore() end function ToukenBackground: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 ToukenBackground