Files
2026-10-09 18:30:20 -04:00

359 lines
14 KiB
Lua

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 chain_moves, sum, pose_at = Timing.chain_moves, Timing.sum, Timing.pose_at
-- Monster Hunter 3G and Portable 3rd: the two ensos share every part's motion; only the art,
-- a few lengths and the fever differ. Each collab file gives a spec (see background below).
-- Don band: the crest pattern scrolling one tile left every `scroll` ms. The clear set swaps
-- in the bright tiles with the crest emblem riding on them, an additive copy of the emblem
-- glowing up and back down (`pulse`: { ms, thousandths } moves). Ambient, in ms.
local GLOW_BASE = 0.148
local function donbg_spec(d)
local period = 0
for _, m in ipairs(d.pulse) do period = period + m[1] end
return { layers = {
{ key = "background", ms = d.scroll },
{ key = "overlay", ms = d.scroll, clear_only = true },
{ key = "overlay", ms = d.scroll, clear_only = true, blend = "additive", alpha = GLOW_BASE,
period = period, pulse = d.pulse },
} }
end
-- Clear background: the panel drops in from above over the village, bounces once and
-- settles, with a strip of props scrolling left along it. Losing the clear stops the strip
-- and drops the panel out the bottom. Drop and fall in beats (gmt's), scroll in ms.
-- spec: tile, phase (the panel's tiles), strip = { tile, x } (one strip tile a 5.3 s scroll)
local DROP = { from = -378, beats = 1.225, bounce = 40, bounce_beats = 0.275 }
local FALL = { to = 408, beats = 0.975 }
local STRIP_MS = 5333
local BGFever = {}
BGFever.__index = BGFever
function BGFever.new(path, bpm, spec)
local self = setmetatable({}, BGFever)
self.path = path
self.name = "bg_fever_0"
self.spec = spec
tex.load_folder(path, "bg_fever/" .. self.name)
local function id(key) return tex.get_texture(path .. "/" .. self.name .. "/" .. key) or 0 end
self._id_bg = id("background")
self._id_strip = id("strip")
self.beat = 60000 / bpm
self.offset = 0
self.moves = {}
self.shown = false
self.scroll = anim.move(STRIP_MS, { total_distance = -math.floor(spec.strip.tile * tex.get_screen_scale()), loop = true })
self.scroll:start()
return self
end
function BGFever:y() return self.offset + sum(self.moves) end
-- drops in from wherever it is (from above when hidden)
function BGFever:start()
local scale = tex.get_screen_scale()
self.offset = self.shown and self:y() or DROP.from * scale
self.shown = true
self.leaving = false
self.moves = chain_moves({
{ DROP.beats, -self.offset / scale, "in" },
{ DROP.bounce_beats, -DROP.bounce, "out" },
{ DROP.bounce_beats, DROP.bounce, "in" },
}, self.beat, scale)
end
-- falls out the bottom from wherever it is
function BGFever:stop()
if not self.shown then return end
local scale = tex.get_screen_scale()
self.offset = self:y()
self.leaving = true
self.moves = chain_moves({ { FALL.beats, FALL.to - self.offset / scale } }, self.beat, scale)
end
function BGFever:update(current_time_ms, bpm)
if bpm then self.beat = 60000 / bpm end
for _, m in ipairs(self.moves) do m:update(current_time_ms) end
if not self.leaving then self.scroll:update(current_time_ms) end
if self.leaving and (not self.moves[1] or self.moves[1].is_finished) then
self.shown, self.leaving, self.moves, self.offset = false, false, {}, 0
end
end
function BGFever:draw()
if not self.shown then return end
local scale, spec = tex.get_screen_scale(), self.spec
local y = self:y()
local x = spec.phase
repeat
tex.draw_texture(self._id_bg, { x = x * scale, y = y })
x = x + spec.tile
until x >= 1280
for k = -1, 1 do
tex.draw_texture(self._id_strip, { x = (spec.strip.x + k * spec.strip.tile) * scale + self.scroll.attribute, y = y })
end
end
function BGFever:destroy() tex.unload_folder(self.path, "bg_fever/" .. self.name) end
-- Fever: a monster crossing the stage on a loop of `loop` beats, starting `delay` beats after
-- the rainbow; losing it fades everything out over `fade` beats (back in from wherever it is
-- if the rainbow returns first). The collab's class gives the folder's keys and
-- draw_loop(t, alpha), t in beats into the loop. Timed in beats.
local Fever = {}
Fever.__index = Fever
-- cls: the collab's fever class (keys, loop, delay, fade_beats, draw_loop)
function Fever.new(cls, path, bpm)
local self = setmetatable({}, cls)
self.path = path
self.name = "fever_0"
tex.load_folder(path, "fever/" .. self.name)
self.ids = {}
for _, key in ipairs(cls.keys) do
self.ids[key] = { id = tex.get_texture(path .. "/" .. self.name .. "/" .. key) or 0,
info = tex.get_texture_info(self.name, key) }
end
self.beat = 60000 / bpm
self.alpha = 0
return self
end
-- a fever class for spec = { keys, loop, delay, fade_beats }; give it draw_loop
function Fever.define(spec)
local cls = setmetatable(spec, { __index = Fever })
cls.__index = cls
return cls
end
function Fever:start()
if self.alpha > 0 then
self.fade = anim.fade(self.fade_beats * self.beat, { initial_opacity = self.alpha, final_opacity = 1 })
self.fade:start()
else
self.restart = true
end
end
function Fever:stop()
self.fade = anim.fade(self.fade_beats * self.beat, { initial_opacity = self.alpha, final_opacity = 0 })
self.fade:start()
end
function Fever:update(current_time_ms, bpm)
if bpm then self.beat = 60000 / bpm end
self.now = current_time_ms
if self.restart then
self.restart, self.fade = nil, nil
self.t0, self.alpha = current_time_ms + self.delay * self.beat, 1
end
if self.fade then
self.fade:update(current_time_ms)
self.alpha = self.fade.attribute
end
end
-- draws `key` with its centre at x, y (screen px, unscaled), posed by p (pose_at's fields)
function Fever:draw_part(key, p, x, y)
local t, s = self.ids[key], tex.get_screen_scale()
local w, h = t.info.width, t.info.height
local W, H = w * p.sx, h * p.sy
tex.draw_texture(t.id, {
frame = p.frame,
x = x * s,
y = y * s,
x2 = W - w,
y2 = H - h,
origin = { W / 2, H / 2 },
mirror = p.mirror and "horizontal" or nil,
fade = p.alpha * self.alpha,
})
end
function Fever:draw()
if not self.t0 or self.alpha <= 0 or self.now < self.t0 then return end
self:draw_loop(((self.now - self.t0) / self.beat) % self.loop, self.alpha)
end
function Fever:destroy() tex.unload_folder(self.path, "fever/" .. self.name) end
-- Dancers
-- Timed in beats. Each hunter jumps up from below, drops onto the stage and readies the
-- weapon (frames 1-4), then loops an 8-beat routine of three quick frames and a held pose a
-- beat, and leaves with a jump that drops away below. All five share it; only the art and
-- the stage height differ. Poses pivot about the feet (bottom-left).
local function hunter(height)
return {
origin = { 0, height },
enter = {
beats = 2, key = "loop",
{ 0, 0, 0, 338, to = { 0, -117, "out" } }, -- jumps up from below
{ 1, 0, 0, -117, to = { 0, 0, "in" } },
{ 1.4, 0, 0, 0 },
{ 1.425, 1 }, { 1.525, 2 }, { 1.625, 3 }, { 1.725, 4 },
},
dance = Timing.steps(
{ { 5, 0.6, x = 0, y = 0 } },
{ { 6, 0.125 }, { 7, 0.125 }, { 8, 0.125 }, { 9, 0.625 } },
{ { 8, 0.125 }, { 7, 0.125 }, { 6, 0.125 }, { 5, 0.625 } },
{ { 6, 0.125 }, { 7, 0.125 }, { 8, 0.125 }, { 9, 0.625 } },
{ { 8, 0.125 }, { 10, 0.125 }, { 11, 0.125 }, { 12, 0.625 } },
{ { 11, 0.125 }, { 10, 0.125 }, { 13, 0.125 }, { 14, 0.625 } },
{ { 13, 0.125 }, { 10, 0.125 }, { 11, 0.125 }, { 12, 0.625 } },
{ { 11, 0.125 }, { 10, 0.125 }, { 13, 0.125 }, { 14, 0.625 } },
{ { 13, 0.125 }, { 10, 0.125 }, { 11, 0.075 }, { 6, 0.05 } }),
leave = {
beats = 1.5, key = "loop",
{ 0, 15, 0, 0, to = { 0, -88 } }, -- jumps
{ 0.15, 16, to = { 0, -118 } },
{ 0.5, 16, to = { 0, 277, "in" } }, -- and drops away
},
}
end
local DANCER_DRAW_ORDER = { 5, 1, 4, 2, 3 } -- outer pairs first, the centre on top
-- dancer index by join order: joins fill the centre, left, right, far-left, far-right slots
local DANCER_JOIN_ORDER = { 4, 1, 0, 3, 2 }
local MAX_DANCERS = 5
-- Chibis for good hits: one of four Poogies hops in and runs across, each beat three steps
-- on its feet and a hop
local CHIBI_POSES = {
{ 0, 1, 0, 0 }, { 0.1, 2, 0, 0 }, { 0.2, 3, 0, 0 },
{ 0.3, 4, 0, -15 }, { 0.4, 0, 0, -20 }, { 0.5, 0, 0, -15 },
{ 0.6, 5, 0, -17 }, { 0.7, 5, 0, -11 }, { 0.8, 5, 0, -7 },
{ 0.9, 6, 0, 0 },
}
local CHIBIS = {}
for k = 1, 4 do
CHIBIS[k] = { key = tostring(k), run = { y = 1 }, period = 1, poses = CHIBI_POSES }
end
-- Drumroll: two cuts of meat (raw or well-done) a hit, on the default renda_a_03 flight up
-- and to the right
local RENDA = { ms = { 2000 }, dx = 1560, dy = -1648, x_range = 500, y_range = 20, per_hit = 2 }
-- Background
local Mh3Background = {}
Mh3Background.__index = Mh3Background
function Mh3Background:init(player_num, bpm)
local spec, path = self.spec, self.path
self.donbg = DonBGBase.new(1, player_num, path, spec.donbg)
self.bg_normal = BGNormal.Base.new(0, path)
self.bg_fever = BGFever.new(path, bpm, spec.bg_fever)
self.footer = Footer.new(path, 0)
self.fever = Fever.new(spec.fever, path, bpm)
self.dancer = Dancer.BaseDancerGroup.new(0, bpm, MAX_DANCERS, path, spec.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(path, 0, RENDA)
self.chibi = ChibiController.new(bpm, path, 1)
self.is_clear = false
self.is_rainbow = false
self.last_milestone = 1
end
function Mh3Background: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 Mh3Background:add_chibi(player_num)
BaseChibi.add(self.chibi, CHIBIS[math.random(#CHIBIS)], self.path, player_num)
end
function Mh3Background:handle_good(player_num) self:add_chibi(player_num) end
function Mh3Background:handle_ok(player_num) self:add_chibi(player_num) end
function Mh3Background:handle_bad(player_num) self.chibi:add_chibi(player_num, true) end
function Mh3Background:handle_drumroll(player_num) self.renda:add_renda() end
function Mh3Background:handle_balloon(player_num) self.renda:add_renda() end
function Mh3Background: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 Mh3Background:draw_back()
self.bg_normal:draw()
self.bg_fever:draw()
self.footer:draw()
self.donbg:draw()
self.renda:draw()
self.dancer:draw_slots(self.spec.centers, DANCER_DRAW_ORDER)
self.fever:draw()
self.chibi:draw()
end
function Mh3Background:draw_fore() end
function Mh3Background: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
-- spec: donbg = { scroll, pulse }, bg_fever = { tile, phase, strip = { tile, x } },
-- heights = { [dancer index] = image height }, centers = slot x's left to right,
-- fever = a class from Fever.define
local function background(path, spec)
spec.donbg = donbg_spec(spec.donbg)
local hunters = {}
for i, h in pairs(spec.heights) do hunters[i] = hunter(h) end
spec.dancers = Dancer.define(hunters)
local cls = setmetatable({ path = path, spec = spec }, { __index = Mh3Background })
cls.__index = cls
cls.new = function(player_num, bpm)
local self = setmetatable({}, cls)
self:init(player_num, bpm)
return self
end
return cls
end
return { background = background, Fever = Fever }