local DonBG = require("background/bg_objects/donbg") local DonBGBase = require("background/bg_objects/donbg/base") local BGNormal = require("background/bg_objects/bg_normal") 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 ChibiWave = require("background/bg_objects/chibi/wave") local Timing = require("background/bg_objects/timing") local COLLAB_PATH = "background/collab/kumamon" local MAX_DANCERS = 5 local chain_moves, sum = Timing.chain_moves, Timing.sum -- Clear background: Kumamoto Castle swaps in under a sunburst that fades in over 1.75 beats -- and swaps panels every 0.75 beat. The prefecture map pops in from double size, overshoots -- small and settles; the produce strip (three copies, 1216 px apart) drops in from above, -- bounces once and scrolls 1212 px left every 14 beats. Leaving, the burst fades, the map -- shrinks away and the strip falls off over 0.85 beat; the castle cuts back 1.35 beats in. -- Timed in beats. local BG_FEVER_IN = 1.75 local BG_FEVER_OUT = 0.85 local BG_FEVER_CUT = 1.35 local BG_FEVER_SWAP = 0.75 local PRODUCE = { beats = 14, distance = -1212, tile = 1216, from = -362, drop = 364 } local MAP_FROM = 2000 -- scale in thousandths local KumamonBGFever = {} KumamonBGFever.__index = KumamonBGFever -- a value made of an offset plus chained moves, so a new move starts from wherever it is local function track(value) return { offset = value, moves = {} } end local function value(t) return t.offset + sum(t.moves) end local function retarget(t, moves, unit, scale, from) t.offset = from or value(t) t.moves = chain_moves(moves, unit, scale) end function KumamonBGFever.new(bpm) local self = setmetatable({}, KumamonBGFever) 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_rays = id("rays") self._id_map = id("map") self._id_produce = id("produce") self.map_info = tex.get_texture_info(self.name, "map") self.beat = 60000 / bpm self.rays = track(0) self.map_scale = track(MAP_FROM) self.map_alpha = track(0) self.produce_y = track(0) self.cut = {} self.shown = false return self end function KumamonBGFever:start() local scale, beat = tex.get_screen_scale(), self.beat local swap = BG_FEVER_SWAP * beat -- after a full exit everything starts over; mid-exit it comes back from where it is local fresh = not self.shown or sum(self.cut) > 0 self.shown, self.cut = true, {} retarget(self.rays, { { BG_FEVER_IN, 1000 - value(self.rays) } }, beat, 1, fresh and 0 or nil) local s = fresh and MAP_FROM or value(self.map_scale) retarget(self.map_scale, { { 0.8, 900 - s, "out" }, { 0.25, 150, "in" }, { 0.425, -50, "out" } }, beat, 1, s) local a = fresh and 0 or value(self.map_alpha) retarget(self.map_alpha, { { 0.8, 1000 - a } }, beat, 1, a) local y = fresh and math.floor(PRODUCE.from * scale) or value(self.produce_y) retarget(self.produce_y, { { 0.825, -y / scale, "in" }, { 0.325, -58, "out" }, { 0.3, 58, "in" } }, beat, scale, y) if fresh then self.panels = anim.texture_change(2 * swap, { { 0, swap, 0 }, { swap, 2 * swap, 1 } }, { loop = true }) self.scroll = anim.move(PRODUCE.beats * beat, { total_distance = math.floor(PRODUCE.distance * scale), loop = true }) self.panels:start() self.scroll:start() end end function KumamonBGFever:stop() local scale, beat = tex.get_screen_scale(), self.beat retarget(self.rays, { { BG_FEVER_OUT, -value(self.rays) } }, beat, 1) retarget(self.map_scale, { { BG_FEVER_OUT, 400 - value(self.map_scale), "in" } }, beat, 1) retarget(self.map_alpha, { { BG_FEVER_OUT, -value(self.map_alpha) } }, beat, 1) local y = value(self.produce_y) retarget(self.produce_y, { { BG_FEVER_OUT, PRODUCE.drop - y / scale, "in" } }, beat, scale) self.cut = chain_moves({ { BG_FEVER_CUT, 0 }, { 0, 1 } }, beat, 1) end function KumamonBGFever:update(current_time_ms, bpm) if bpm then self.beat = 60000 / bpm end for _, t in ipairs({ self.rays, self.map_scale, self.map_alpha, self.produce_y }) do for _, m in ipairs(t.moves) do m:update(current_time_ms) end end for _, m in ipairs(self.cut) do m:update(current_time_ms) end if self.panels then self.panels:update(current_time_ms) self.scroll:update(current_time_ms) end end function KumamonBGFever:draw() if not self.shown or sum(self.cut) > 0 then return end tex.draw_texture(self._id_bg) local rays = value(self.rays) / 1000 if rays > 0 then tex.draw_texture(self._id_rays, { frame = self.panels.attribute, fade = rays }) end local alpha = value(self.map_alpha) / 1000 if alpha > 0 then local k = value(self.map_scale) / 1000 - 1 local dw, dh = self.map_info.width * k, self.map_info.height * k tex.draw_texture(self._id_map, { x = -dw / 2, y = -dh / 2, x2 = dw, y2 = dh, fade = alpha }) end local y = value(self.produce_y) local tile = math.floor(PRODUCE.tile * tex.get_screen_scale()) for k = 0, 2 do tex.draw_texture(self._id_produce, { x = self.scroll.attribute + k * tile, y = y }) end end function KumamonBGFever:destroy() tex.unload_folder(COLLAB_PATH, "bg_fever/" .. self.name) end -- Fever: the local mascots in two halves jump up 340 px, bounce 40 px a beat swapping poses, -- and drop 340 px when the rainbow is lost local FEVER_SPEC = { keys = { "left", "right" }, rise = 300, peak = 40, bounce = 40, drop = 340, poses = true } -- Dancers -- Timed in beats; poses are offsets from Lumen's dancer origin (texture.json y 670), moved -- to the dancers' feet so squashes keep them planted. Every part uses the loop frames. -- The four small dancers shoot up from below, land with a squash, then loop a 16-beat -- routine of poses with a stretch on each change; leaving, they squash, spring up and drop. local SMALL_Y = 28 local SMALL_ENTER = Timing.steps({ { 0, 0.625, x = 0, y = 394, to = { 0, -15, "out" } }, -- shoots up from below { 0, 0.25, to = { nil, SMALL_Y, "in" } }, { 0, 0.25, to = { nil, nil, "out", sy = 0.94 } }, -- lands { 0, 0.25, sy = 0.94, to = { nil, nil, "in", sy = 1 } }, { 0, 0.35 }, { 1, 0.15, to = { nil, nil, "out", sy = 1.04 } }, { 1, 0.125, sy = 1.04, to = { nil, nil, "in", sy = 1 } }, }) SMALL_ENTER.beats, SMALL_ENTER.key = 2, "loop" -- a pose change with a stretch: up to 1.04 and back local function stretch(frame, up, back) return { frame, up, to = { nil, nil, "out", sy = 1.04 } }, { frame, back, sy = 1.04, to = { nil, nil, "in", sy = 1 } } end -- a, b: the first phrase's pose pair (the far-right dancer has them the other way round) local function small_dance(a, b) local s1, s2 = stretch(b, 0.15, 0.125) local s3, s4 = stretch(3, 0.15, 0.1) local s5, s6 = stretch(b, 0.15, 0.125) return Timing.steps({ { a, 0.725, x = 0, y = SMALL_Y }, s1, s2, { a, 0.75 }, { b, 0.25 }, { 0, 1.75 }, s3, s4, { 4, 0.75 }, s3, s4, { 4, 0.75 }, { 3, 0.25 }, { 0, 2.025 }, { 0, 0.325, to = { nil, nil, "out", sy = 0.96 } }, { 0, 0.325, sy = 0.96 }, -- crouch { 5, 0.675 }, { 0, 0.675 }, { 0, 0.925, to = { nil, nil, "out", sy = 1.04 } }, { 0, 0.4, sy = 1.04 }, -- stretch up { 0, 0.65, sy = 1.04, to = { nil, nil, "in", sy = 1 } }, { 6, 1.025 }, { 6, 0.25, to = { nil, nil, "out", sy = 0.94 } }, { 6, 0.25, sy = 0.94, to = { nil, nil, "in", sy = 1 } }, { 7, 0.25, to = { nil, nil, "out", sy = 1.08 } }, { 7, 0.25, sy = 1.08, to = { nil, nil, "in", sy = 1 } }, { 7, 1.7 }, s5, s6, }) end local SMALL_LEAVE = Timing.steps({ { 8, 0.15, x = 0, y = SMALL_Y, to = { nil, nil, "out", sy = 0.93 } }, { 8, 0.125, sy = 0.93, to = { nil, nil, "in", sy = 1 } }, { 8, 0.225, to = { nil, -72, "out" } }, -- springs up { 8, 0.85, to = { nil, 242, "in" } }, -- and drops off }) SMALL_LEAVE.beats, SMALL_LEAVE.key = 1.35, "loop" -- Kumamon in the middle shoots up the same way, then dances its own 16 beats: four poses, -- the same four turned round, a run of quick poses, a long stretch and a sink; it never leaves. local BIG_Y = 25 local BIG_ENTER = Timing.steps({ { 0, 0.625, x = 0, y = 392, to = { 0, -1, "out" } }, -- shoots up from below { 0, 0.25, to = { nil, BIG_Y - 2, "in" } }, { 1, 0.225, to = { nil, nil, "out", sy = 0.96 } }, -- lands { 1, 0.05, sy = 0.96 }, { 1, 0.225, sy = 0.96, to = { nil, nil, "in", sy = 1 } }, { 1, 0.35 }, { 2, 0.15, y = BIG_Y, to = { nil, nil, "out", sy = 1.04 } }, { 2, 0.125, sy = 1.04, to = { nil, nil, "in", sy = 1 } }, }) BIG_ENTER.beats, BIG_ENTER.key = 2, "loop" -- turned round: mirror every step local function turned(list) for _, step in ipairs(list) do step.mirror = true end return list end local BIG_DANCE = Timing.steps( { { 3, 0.725, x = 0, y = BIG_Y } }, { stretch(4, 0.15, 0.125) }, { { 5, 0.75 } }, { stretch(6, 0.15, 0.1) }, { { 7, 1.75 } }, -- the same four poses, turned round and 20 px over turned({ { 2, 0.15, x = 20, to = { nil, nil, "out", sy = 1.04 } }, { 2, 0.1, sy = 1.04, to = { nil, nil, "in", sy = 1 } } }), turned({ { 3, 0.75 } }), turned({ stretch(4, 0.15, 0.1) }), turned({ { 5, 0.75 } }), turned({ stretch(6, 0.15, 0.1) }), turned({ { 7, 1 } }), turned({ -- quick poses, a long stretch and a sink { 0, 1, x = 0 }, { 8, 0.35 }, { 9, 0.325 }, { 10, 0.325 }, { 11, 0.35 }, { 0, 0.325 }, { 8, 0.325 }, { 9, 0.95, to = { nil, nil, "out", sy = 1.04 } }, { 9, 0.25, sy = 1.04 }, { 9, 0.8, sy = 1.04, to = { nil, nil, "in", sy = 0.96 } }, { 12, 1.025, y = BIG_Y + 1 }, { 12, 0.225, to = { nil, nil, "out", sy = 0.96 } }, { 12, 0.05, sy = 0.96 }, { 12, 0.225, sy = 0.96, to = { nil, nil, "in", sy = 1 } }, }), { { 1, 0.25, x = -4, y = BIG_Y, to = { nil, nil, "out", sy = 1.04 } }, { 1, 0.25, sy = 1.04, to = { nil, nil, "in", sy = 1 } }, { 1, 1.7 }, }, { stretch(2, 0.15, 0.125) }) local SMALL = { origin = { 136, 200 }, enter = SMALL_ENTER, dance = small_dance(2, 1), leave = SMALL_LEAVE } local KUMAMON_DANCERS = Dancer.define({ [0] = { origin = { 136, 200 }, enter = SMALL_ENTER, dance = small_dance(1, 2), leave = SMALL_LEAVE }, [4] = { origin = { 140, 304 }, enter = BIG_ENTER, dance = BIG_DANCE }, default = SMALL, }) -- Lumen's slots, left to right; the group fills centre, left, right, far-left, far-right, -- which is Lumen's dancer1..5 (skin dancers 4, 3, 2, 1, 0) local DANCER_CENTERS = { 200, 420, 640, 860, 1080 } local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } local DANCER_JOIN_ORDER = { 4, 3, 2, 1, 0 } -- Chibi for good hits: a tomato, a dekopon or a pomelo hops in growing from 70% size, then -- zigzags up and down across the don band local KUMAMON_CHIBI_MOTION = { key = "1", hop = { from = { -418, 21 }, peak = { -345, -31 }, up = 0.3 }, run = { y = -8 }, wave = { first = 24, height = 39, beats = 0.8 }, } local KUMAMON_CHIBIS = {} for frame = 0, 2 do -- one fruit per frame of chibi_1/1 KUMAMON_CHIBIS[frame + 1] = setmetatable({ first = frame }, { __index = KUMAMON_CHIBI_MOTION }) end -- Background local KumamonBackground = {} KumamonBackground.__index = KumamonBackground function KumamonBackground.new(player_num, bpm) local self = setmetatable({}, KumamonBackground) -- the don band is donbg_a_01's motion with new art self.donbg = DonBGBase.new(1, player_num, COLLAB_PATH, DonBG.SPECS[1]) self.bg_normal = BGNormal.Base.new(0, COLLAB_PATH) self.bg_fever = KumamonBGFever.new(bpm) -- the footer and renda are the default dodai_a_03 and renda_a_02 self.footer = Footer.new("background", 2) self.fever = Fever.new(bpm, COLLAB_PATH, FEVER_SPEC) self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, COLLAB_PATH, KUMAMON_DANCERS) 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("background", 1) self.chibi = ChibiController.new(bpm, COLLAB_PATH, 1) self.is_clear = false self.is_rainbow = false self.last_milestone = 1 return self end function KumamonBackground: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, KUMAMON_CHIBIS[math.random(#KUMAMON_CHIBIS)], COLLAB_PATH, player_num, ChibiWave) end function KumamonBackground:handle_good(player_num) add_chibi(self, player_num) end function KumamonBackground:handle_ok(player_num) add_chibi(self, player_num) end function KumamonBackground:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end function KumamonBackground:handle_drumroll(player_num) self.renda:add_renda() end function KumamonBackground:handle_balloon(player_num) self.renda:add_renda() end function KumamonBackground: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 KumamonBackground: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 KumamonBackground:draw_fore() end function KumamonBackground: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 KumamonBackground