Files
YataiDONNijiiro/Scripts/background/background.lua
TaikoZhongandClaude Opus 5.5 f239bc156c background: cabinet backdrops for the collab presets
Collab songs (#SCENEPRESET) now get their own cabinet backdrop, replayed by
the same objects as the default backdrop instead of the parent's collab art:

  miku, toho, gumi, ia, lovelive, i7 (id7 / tri / rev / natsu), imas
  (765 / cg / ml / sidem), yo-kai (base / matsuri / hatsukoi / tokoroten /
  yougota), mario, a3

Each preset is a pack in bg_data/collab_packs.lua naming its parts: donbg
band (1P and 2P), normal and clear backdrops, troupe, stand under the
troupe, drumroll effect and soul-max crowd.  Shared parts (the i7, imas
and yo-kai families) are shared the way the cabinet shares them.  A pack is
used only when every part it names is present, otherwise the preset falls
back to the collab script as before.  Packs also apply in 2P (donbg bands).

The objects take a part name where they took a random index:
DonBG.create_named, BGNormal.create_named, BGFever.create_named,
ArcadeDancerGroup.new("<name>"), Fever.Anim.new("<name>").  New:
renda_clip.lua (each hit replays one of the effect's parts along its own
path), and a beat-synced BGAnim for the whole-troupe dancer movies (i7,
a3).  Collab troupes stand in fixed positions (lead in the centre) and
grow from 1 to the full troupe with the gauge.

Exported placements may now mirror: bg_anim, donbg and dancer data carry a
mirror flag (drawn with mirror = horizontal and the origin moved to the
other side of the png).  Existing data has no flag and draws as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:55:05 +08:00

507 lines
22 KiB
Lua

-- YataiDON-HSS child override of PyTaikoGreen/Scripts/background/background.lua:
-- byte-copy of the parent plus the ArcadeGauge overlay (3 hooks, marked below).
local DonBG = require("background/bg_objects/donbg")
local BGNormal = require("background/bg_objects/bg_normal")
local BGFever = require("background/bg_objects/bg_fever")
local Fever = require("background/bg_objects/fever")
local Footer = require("background/bg_objects/footer")
local Dancer = require("background/bg_objects/dancer")
local Ending = require("background/bg_objects/ending")
local RankBadge = require("background/bg_objects/rank_badge")
local RendaController = require("background/bg_objects/renda")
local ChibiController = require("background/bg_objects/chibi")
local ArcadeGauge = require("background/bg_objects/arcade_gauge") -- child-only
local EnsoSkip = require("background/bg_objects/enso_skip") -- child-only
local BGAnim = require("background/bg_objects/bg_anim")
local RendaClip = require("background/bg_objects/renda_clip")
local PACKS = require("background/bg_data/collab_packs")
-- A cabinet collab pack (bg_data/collab_packs.lua) for this #SCENEPRESET, when every part
-- it names was exported; nil sends the preset down the collab-script path.
local function module_exists(name)
local ok, data = pcall(require, name)
return ok and type(data) == "table"
end
local function pack_for(scene_preset)
local p = scene_preset and PACKS[scene_preset]
if not p then return nil end
for _, band in ipairs(p.donbg) do
if not module_exists("background/bg_data/donbg_anim/" .. band) then return nil end
end
if not (BGAnim.available(p.normal) and BGAnim.available(p.fever)) then return nil end
if p.dancer_movie then
if not BGAnim.available(p.dancer_movie) then return nil end
elseif not module_exists("background/dancer_data/" .. p.dancer) then
return nil
end
if p.dai and p.dai ~= "footer0" and not BGAnim.available(p.dai) then return nil end
if not RendaClip.available(p.renda) then return nil end
if not module_exists("background/bg_data/fever_anim/f_" .. p.crowd) then return nil end
return p
end
-- whole-troupe dancer movie (i7, A3): every member is always on stage, replayed on the beat
local MovieTroupe = {}
MovieTroupe.__index = MovieTroupe
function MovieTroupe.new(name)
local anim = BGAnim.new(name, "background")
if not anim then return nil end
anim:set_beat(40)
return setmetatable({ anim = anim }, MovieTroupe)
end
function MovieTroupe:update(current_ms, bpm) self.anim:update(current_ms, bpm) end
function MovieTroupe:draw() self.anim:draw(1.0) end
function MovieTroupe:set_dancer_count(n) end
function MovieTroupe:destroy() self.anim:destroy() end
-- the stand under a collab troupe, drawn where the default footer band is
local DaiAnim = {}
DaiAnim.__index = DaiAnim
function DaiAnim.new(name)
if name == "footer0" then return Footer.new("background", 0) end
local anim = name and BGAnim.new(name, "background")
if not anim then return Footer.new("background", -1) end
return setmetatable({ anim = anim }, DaiAnim)
end
function DaiAnim:draw() self.anim:draw(1.0) end
function DaiAnim:destroy() self.anim:destroy() end
local PracticeBackground = require("background/bg_collabs/practice")
local TutorialBackground = require("background/bg_collabs/tutorial")
local A3Background = require("background/bg_collabs/a3")
local AnimalBackground = require("background/bg_collabs/animal")
local ButtoBackground = require("background/bg_collabs/buttoburst")
local DanBackground = require("background/bg_collabs/dan")
local FunassyiBackground = require("background/bg_collabs/funassyi")
local ImasBackground = require("background/bg_collabs/imas")
local ImasSidemBackground = require("background/bg_collabs/imas_sidem")
local OshiriBackground = require("background/bg_collabs/oshiri")
-- the collabs PyTaikoGreen added after this file was forked (2026-09-07/09): Green's modules
local DQ10Background = require("background/bg_collabs/dq10")
local GbBackground = require("background/bg_collabs/gb")
local GmtBackground = require("background/bg_collabs/gmt")
local GumiBackground = require("background/bg_collabs/gumi")
local I7id7Background = require("background/bg_collabs/i7id7")
local I7natsuBackground = require("background/bg_collabs/i7natsu")
local I7revBackground = require("background/bg_collabs/i7rev")
local I7triBackground = require("background/bg_collabs/i7tri")
local IaBackground = require("background/bg_collabs/ia")
local KobayashiBackground = require("background/bg_collabs/kobayashi")
local KumamonBackground = require("background/bg_collabs/kumamon")
local LoveliveBackground = require("background/bg_collabs/lovelive")
local MarioBackground = require("background/bg_collabs/mario")
local Mh3GBackground = require("background/bg_collabs/mh3G")
local Mh3rdBackground = require("background/bg_collabs/mh3rd")
local MikuBackground = require("background/bg_collabs/miku")
local MomocloBackground = require("background/bg_collabs/momoclo")
local PzdBackground = require("background/bg_collabs/pzd")
local TohoBackground = require("background/bg_collabs/toho")
local TonkatsudjBackground = require("background/bg_collabs/tonkatsudj")
local ToukenBackground = require("background/bg_collabs/touken")
local YmckBackground = require("background/bg_collabs/ymck")
local YokaiBackground = require("background/bg_collabs/yokai")
local YokaiHatsukoiBackground = require("background/bg_collabs/yokai_hatsukoi")
local YokaiMatsuriBackground = require("background/bg_collabs/yokai_matsuri")
local YokaiTokorotenBackground = require("background/bg_collabs/yokai_tokoroten")
local YokaiYougotaBackground = require("background/bg_collabs/yokai_yougota")
local COLLABS = {
PRACTICE = { PracticeBackground.new, nil, 1 },
TUTORIAL = { TutorialBackground.new, nil, 1 },
A3 = { A3Background.new, "background/collab/a3", 4 },
ANIMAL = { AnimalBackground.new, "background/collab/animal", 5 },
BUTTOBURST = { ButtoBackground.new, "background/collab/buttoburst", 4 },
DAN = { DanBackground.new, "background/collab/dan", 1 },
FUNASSYI = { FunassyiBackground.new, "background/collab/funassyi", 5 },
IMAS = { ImasBackground.new, "background/collab/imas", 5 },
IMAS_CG = { ImasBackground.new, "background/collab/imas_cg", 3 },
IMAS_ML = { ImasBackground.new, "background/collab/imas_ml", 3 },
IMAS_SIDEM = { ImasSidemBackground.new, "background/collab/imas_sidem", 3 },
OSHIRI = { OshiriBackground.new, "background/collab/oshiri", 5 },
DQ10 = { DQ10Background.new, "background/collab/dq10", 5 },
GB = { GbBackground.new, "background/collab/gb", 5 },
GMT = { GmtBackground.new, "background/collab/gmt", 5 },
GUMI = { GumiBackground.new, "background/collab/gumi", 5 },
I7ID7 = { I7id7Background.new, "background/collab/i7id7", 5 },
I7NATSU = { I7natsuBackground.new, "background/collab/i7natsu", 5 },
I7REV = { I7revBackground.new, "background/collab/i7rev", 5 },
I7TRI = { I7triBackground.new, "background/collab/i7tri", 5 },
IA = { IaBackground.new, "background/collab/ia", 5 },
KOBAYASHI = { KobayashiBackground.new, "background/collab/kobayashi", 5 },
KUMAMON = { KumamonBackground.new, "background/collab/kumamon", 5 },
LOVELIVE = { LoveliveBackground.new, "background/collab/lovelive", 5 },
MARIO = { MarioBackground.new, "background/collab/mario", 5 },
MH3G = { Mh3GBackground.new, "background/collab/mh3G", 5 },
MH3RD = { Mh3rdBackground.new, "background/collab/mh3rd", 5 },
MIKU = { MikuBackground.new, "background/collab/miku", 5 },
MOMOCLO = { MomocloBackground.new, "background/collab/momoclo", 5 },
PZD = { PzdBackground.new, "background/collab/pzd", 5 },
TOHO = { TohoBackground.new, "background/collab/toho", 5 },
TONKATSUDJ = { TonkatsudjBackground.new, "background/collab/tonkatsudj", 1 },
TOUKEN = { ToukenBackground.new, "background/collab/touken", 5 },
YMCK = { YmckBackground.new, "background/collab/ymck", 5 },
YOKAI = { YokaiBackground.new, "background/collab/yokai", 5 },
YOKAI_HATSUKOI = { YokaiHatsukoiBackground.new, "background/collab/yokai_hatsukoi", 5 },
YOKAI_MATSURI = { YokaiMatsuriBackground.new, "background/collab/yokai_matsuri", 5 },
YOKAI_TOKOROTEN = { YokaiTokorotenBackground.new, "background/collab/yokai_tokoroten", 5 },
YOKAI_YOUGOTA = { YokaiYougotaBackground.new, "background/collab/yokai_yougota", 5 },
}
Background = {}
Background.__index = Background
function Background.new(player_num, bpm, scene_preset)
local self = setmetatable({}, Background)
self.player_num = player_num
self.bpm = bpm
self.scene_preset = scene_preset
self.is_2p = (player_num == 3) -- PlayerNum::TWO_PLAYER
-- child-only: arcade soul-gauge overlay, drawn in draw_fore (in front of the HUD)
self.arcade_gauge = ArcadeGauge.new()
-- child-only: 演奏スキップ countdown card / dim / banner.
self.enso_skip = EnsoSkip.new()
_G.g_active_enso_skip = self.enso_skip
-- a cabinet collab pack replaces the collab script (and works in 2P too)
local pack = pack_for(scene_preset)
self.pack = pack
local entry = (not pack) and (not self.is_2p) and scene_preset and COLLABS[scene_preset] or nil
if entry then
local ctor, path, max_dancers = entry[1], entry[2], entry[3]
if path then
self.collab = ctor(player_num, bpm, path, max_dancers)
else
self.collab = ctor(player_num, bpm)
end
else
-- Standard (non-collab) case
local max_dancers = 5
self.max_dancers = max_dancers
if self.is_2p then
if pack then
self.donbg_1 = DonBG.create_named(1, "background", pack.donbg[1])
self.donbg_2 = DonBG.create_named(2, "background", pack.donbg[2])
else
local donbg_index = math.random(1, 6)
self.donbg_1 = DonBG.create(1, "background", donbg_index)
self.donbg_2 = DonBG.create(2, "background", donbg_index)
end
local skin_cfg = tex.get_skin_config("game_2p_offset")
self.p2_y_offset = skin_cfg and skin_cfg.y or 0
self.is_clear_1 = false
self.is_clear_2 = false
elseif pack then
self.donbg = DonBG.create_named(player_num, "background", pack.donbg[player_num == 2 and 2 or 1])
self.bg_normal = BGNormal.create_named(pack.normal, "background")
self.bg_fever = BGFever.create_named(pack.fever, "background")
self.fever = Fever.Anim.new(pack.crowd, bpm, "background")
if pack.dancer_movie then
self.dancer = MovieTroupe.new(pack.dancer_movie)
else
self.dancer = Dancer.ArcadeDancerGroup.new(pack.dancer, bpm, nil, "background")
end
self.footer = DaiAnim.new(pack.dai)
else
self.donbg = DonBG.create(player_num, "background")
self.bg_normal = BGNormal.create(math.random(0, 4), "background")
self.bg_fever = BGFever.create(math.random(0, 3), "background")
self.fever = Fever.create(math.random(0, Fever.variant_count("background") - 1), bpm, "background")
self.dancer = Dancer.create(math.random(0, 20), bpm, "background", max_dancers)
self.footer = Footer.new("background", math.random(0, 2))
end
if pack then
self.renda = RendaClip.new("background", pack.renda)
if self.is_2p then
self.renda_2 = RendaClip.new("background", pack.renda, self.p2_y_offset)
end
else
local renda_index = math.random(0, 2)
self.renda = RendaController.new("background", renda_index)
if self.is_2p then
self.renda_2 = RendaController.new("background", renda_index, self.p2_y_offset)
end
end
self.chibi = ChibiController.new(bpm, "background")
self.is_clear = false
self.is_rainbow = false
self.clear_progress = nil
end
return self
end
-- a miss streak darkens the donbg band (each player's own band in 2P)
function Background:handle_miss_streak(player_num, on)
if self.collab then return end
local d = self.is_2p and (player_num == 2 and self.donbg_2 or self.donbg_1) or self.donbg
if d then d:set_dark(on) end
end
function Background:update(current_ms, bpm)
self.bpm = bpm
self.enso_skip:update(current_ms)
if self.collab then
self.collab:update(current_ms, bpm)
else
if self.is_2p then
self.donbg_1:update(current_ms)
self.donbg_2:update(current_ms)
else
self.donbg:update(current_ms)
self.bg_normal:update(current_ms)
self.bg_fever:update(current_ms)
self.fever:update(current_ms, bpm)
self.dancer:update(current_ms, bpm)
end
self.renda:update(current_ms)
if self.renda_2 then self.renda_2:update(current_ms) end
self.chibi:update(current_ms, bpm)
end
end
function Background:handle_good(player_num)
if self.collab then
self.collab:handle_good(player_num)
else
self.chibi:add_chibi(player_num, false)
end
end
function Background:handle_ok(player_num)
if self.collab then
self.collab:handle_ok(player_num)
else
self.chibi:add_chibi(player_num, false)
end
end
function Background:handle_bad(player_num)
if self.collab then
self.collab:handle_bad(player_num)
else
self.chibi:add_chibi(player_num, true)
end
end
function Background:handle_drumroll(player_num)
if self.collab then
self.collab:handle_drumroll(player_num)
else
if player_num == 2 and self.renda_2 then
self.renda_2:add_renda()
else
self.renda:add_renda()
end
end
end
function Background:handle_balloon(player_num)
if self.collab then
self.collab:handle_balloon(player_num)
end
end
function Background:handle_dan(player_num, state)
if self.collab and self.collab.handle_dan then
self.collab:handle_dan(player_num, state)
end
end
function Background:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress, flash)
-- child-only
self.arcade_gauge:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress, flash)
if self.collab then
self.collab:handle_gauge(player_num, progress, is_clear, is_rainbow, clear_progress)
elseif self.is_2p then
-- Update each player's donbg clear state independently
if player_num == 1 then
self.donbg_1:set_clear(is_clear)
self.is_clear_1 = is_clear
elseif player_num == 2 then
self.donbg_2:set_clear(is_clear)
self.is_clear_2 = is_clear
end
self.is_clear = self.is_clear_1 or self.is_clear_2
self.is_rainbow = is_rainbow
else
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
self:handle_dancer_count(progress, is_clear, clear_progress)
end
end
local GAUGE_MAX = 87.0
local CLEAR_STARTS = { 52, 60, 69 } -- easy / normal / hard+oni
local CLEAR_DEFAULT = 69 / GAUGE_MAX -- 0.79310
function Background:handle_dancer_count(progress, is_clear, clear_progress)
if not self.dancer or not self.dancer.set_dancer_count then return end
if clear_progress and clear_progress > 0 then
self.clear_progress = clear_progress
elseif is_clear and not self.clear_progress then
-- The engine flips is_clear at gauge_length > clear_start - 1, so the
-- progress at the flip is just under clear_start/87; pick the first
-- threshold at or above it.
local p = progress or 0
for _, cs in ipairs(CLEAR_STARTS) do
if p < cs / GAUGE_MAX + 0.0005 then
self.clear_progress = cs / GAUGE_MAX
break
end
end
self.clear_progress = self.clear_progress or CLEAR_DEFAULT
end
local cp = self.clear_progress or CLEAR_DEFAULT
local r = (progress or 0) / cp
local n = math.floor(2 * r + 3)
if n < 3 then n = 3 elseif n > 5 then n = 5 end
-- a collab troupe starts with its lead alone and grows to the full troupe at the clear
if self.pack then
n = math.max(1, math.min(5, math.floor(4 * r + 1)))
end
-- is_clear flips one gauge unit early; honour it so the fifth dancer and the
-- クリア state arrive on the same frame.
if is_clear then n = 5 end
self.dancer:set_dancer_count(n)
end
function Background:draw_back()
if self.collab then
self.collab:draw_back()
else
if not self.is_2p then
if self.bg_fever:is_visible() then
if not self.bg_fever.transitioned then
self.bg_normal:draw()
end
self.bg_fever:draw()
else
self.bg_normal:draw()
end
end
if self.is_2p then
self.donbg_1:draw(0)
self.donbg_2:draw(self.p2_y_offset)
else
self.donbg:draw()
end
self.renda:draw()
if self.renda_2 then self.renda_2:draw() end
if not self.is_2p then
self.dancer:draw()
-- footer (dai band) paints after the crowd so it rises from behind it.
self.footer:draw()
if self.fever:is_visible() then self.fever:draw() end
end
self.chibi:draw()
end
end
-- child-only. The engine only calls this when the skin declares it
-- (Background::wants_skip), and it then suppresses its own skip_counter text.
function Background:handle_skip(player_num, state)
self.enso_skip:handle(state)
end
-- Ending banner (engine hook): the cabinet clip for the result kind
-- ("clear" / "full_combo" / "donderful" / "fail") replaces the engine's drawing
-- when its exported data exists; otherwise the engine keeps drawing its own.
function Background:handle_ending(player_num, kind)
self.endings = self.endings or {}
if self.collab and self.collab.handle_ending then
self.collab:handle_ending(player_num, kind)
end
if Ending.available(kind) then
self.endings[player_num] = Ending.new(kind, player_num, "game")
end
end
function Background:draw_ending(player_num)
local e = self.endings and self.endings[player_num]
if e then e:draw(tex.get_current_ms()) end
end
-- Running score (engine hook): the 粋 / 雅 / 極 plate at the player panel's top-left
-- appears as each rank line is crossed (information/score `scorerank_Np`).
function Background:handle_score(player_num, score)
if self.collab and self.collab.handle_score then
self.collab:handle_score(player_num, score)
end
if not RankBadge.available() then return end
self.rank_badges = self.rank_badges or {}
local b = self.rank_badges[player_num]
if not b then
b = RankBadge.new(player_num, "game")
self.rank_badges[player_num] = b
end
b:set_score(score, tex.get_current_ms())
end
-- child-only: repaint the arcade gauge for the lane whose engine gauge just drew.
function Background:draw_gauge(player_num)
self.arcade_gauge:draw_one(player_num)
if self.collab and self.collab.draw_gauge then
self.collab:draw_gauge(player_num)
end
end
function Background:draw_fore()
if self.collab then
self.collab:draw_fore()
end
if self.rank_badges then
local now = tex.get_current_ms()
for _, b in pairs(self.rank_badges) do b:draw(now) end
end
end
function Background:destroy()
if _G.g_active_enso_skip == self.enso_skip then
_G.g_active_enso_skip = nil
end
if self.endings then
for _, e in pairs(self.endings) do e:destroy() end
self.endings = nil
end
if self.rank_badges then
for _, b in pairs(self.rank_badges) do b:destroy() end
self.rank_badges = nil
end
if self.collab then
self.collab:destroy()
else
if self.is_2p then
self.donbg_1:destroy()
self.donbg_2:destroy()
else
self.donbg:destroy()
self.bg_normal:destroy()
self.bg_fever:destroy()
self.fever:destroy()
self.dancer:destroy()
self.footer:destroy()
end
self.renda:destroy()
-- renda_2 shares the same texture folder; unload_folder is dedup-keyed
-- so the second destroy is a safe no-op.
if self.renda_2 then self.renda_2:destroy() end
self.chibi:destroy()
end
end