143 lines
4.8 KiB
Python
143 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate `runtime/assets/audio/audio_manifest.json`.
|
|
|
|
Maps every contract SFX event (docs/04_ART_ANIMATION_AUDIO.md section 8) to its
|
|
generated candidate file plus bus, priority class and cooldown, and maps the
|
|
non-battle music families to their base beds.
|
|
|
|
Bus and priority assignment follows the contract's section 3 mix tree and 3.1
|
|
priority classes; per-event overrides cover the P0/P1/P2 semantics that cannot
|
|
be inferred from the family alone.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
CONTRACT = ROOT / "docs/04_ART_ANIMATION_AUDIO.md"
|
|
SFX_DIR = ROOT / "runtime/assets/audio/sfx"
|
|
MUSIC_DIR = ROOT / "runtime/assets/audio/music"
|
|
TARGET = ROOT / "runtime/assets/audio/audio_manifest.json"
|
|
|
|
FAMILY_BUS = {
|
|
"UI": "SFXUI",
|
|
"SHP": "SFXPlayerCommit",
|
|
"EQP": "SFXPlayerCommit",
|
|
"ORD": "SFXPlayerCommit",
|
|
"RES": "SFXCritical",
|
|
"RUN": "SFXWorld",
|
|
"BAT": "SFXCombat",
|
|
}
|
|
FAMILY_PRIORITY = {
|
|
"UI": "P4",
|
|
"SHP": "P3",
|
|
"EQP": "P3",
|
|
"ORD": "P3",
|
|
"RES": "P2",
|
|
"RUN": "P3",
|
|
"BAT": "P4",
|
|
}
|
|
FAMILY_COOLDOWN_MS = {
|
|
"UI": 60,
|
|
"SHP": 250,
|
|
"EQP": 150,
|
|
"ORD": 80,
|
|
"RES": 400,
|
|
"RUN": 150,
|
|
"BAT": 60,
|
|
}
|
|
# P0 never suppressed; P1 tactical; P2 player commits.
|
|
OVERRIDES: dict[str, dict] = {
|
|
"SFX-UI-018": {"priority": "P2", "cooldown_ms": 120},
|
|
"SFX-UI-021": {"priority": "P0", "cooldown_ms": 400},
|
|
"SFX-SHP-008": {"priority": "P2"},
|
|
"SFX-SHP-015": {"priority": "P2"},
|
|
"SFX-SHP-020": {"priority": "P0"},
|
|
"SFX-EQP-007": {"priority": "P2"},
|
|
"SFX-EQP-012": {"priority": "P2"},
|
|
"SFX-ORD-006": {"priority": "P2"},
|
|
"SFX-ORD-020": {"priority": "P2"},
|
|
"SFX-RUN-003": {"priority": "P2"},
|
|
"SFX-RUN-016": {"priority": "P2"},
|
|
"SFX-RUN-018": {"priority": "P1"},
|
|
"SFX-RUN-023": {"priority": "P1"},
|
|
"SFX-RUN-025": {"priority": "P0"},
|
|
"SFX-RES-002": {"priority": "P1"},
|
|
"SFX-RES-004": {"priority": "P1"},
|
|
"SFX-RES-011": {"priority": "P2"},
|
|
"SFX-RES-012": {"priority": "P1"},
|
|
"SFX-RES-013": {"priority": "P1"},
|
|
"SFX-RES-019": {"priority": "P2"},
|
|
}
|
|
MUSIC_FAMILIES = {
|
|
"title": ("MUS-G00-TITLE-base.mp3", "return title layer", "G00"),
|
|
"guild": ("MUS-G01-GUILD-base.mp3", "guild base", "G01"),
|
|
"market": ("MUS-G02-SHOP-base.mp3", "browse base", "G02"),
|
|
"prep": ("MUS-G03-PREP-base.mp3", "recruit/equipment/order base", "G03"),
|
|
"run": ("MUS-G04-RUN-base.mp3", "run overview base", "G04"),
|
|
"result_win": ("MUS-G05-RESULT-WIN-base.mp3", "survival statement", "G05"),
|
|
"result_loss": ("MUS-G06-RESULT-LOSS-base.mp3", "consequence statement", "G06"),
|
|
}
|
|
|
|
|
|
def contract_events() -> dict[str, str]:
|
|
names: dict[str, str] = {}
|
|
for line in CONTRACT.read_text().splitlines():
|
|
match = re.match(r"^(SFX-([A-Z]+)-\d{3})\s+(\S+)\s*$", line.strip())
|
|
if match and match.group(2) in FAMILY_BUS:
|
|
names[match.group(1)] = match.group(3)
|
|
return names
|
|
|
|
|
|
def find_file(event_id: str, event_name: str) -> str | None:
|
|
preferred = SFX_DIR / f"{event_id}-{event_name}.v01.mp3"
|
|
if preferred.exists():
|
|
return preferred.name
|
|
matches = sorted(SFX_DIR.glob(f"{event_id}-*.v*.mp3"))
|
|
return matches[0].name if matches else None
|
|
|
|
|
|
def main() -> int:
|
|
events = contract_events()
|
|
items: dict[str, dict] = {}
|
|
missing: list[str] = []
|
|
resolved_ids: set[str] = set()
|
|
for event_id, event_name in sorted(events.items()):
|
|
family = event_id.split("-")[1]
|
|
filename = find_file(event_id, event_name)
|
|
if filename is None:
|
|
missing.append(event_id)
|
|
continue
|
|
entry = {
|
|
"event": event_id,
|
|
"name": event_name,
|
|
"file": f"sfx/{filename}",
|
|
"bus": FAMILY_BUS[family],
|
|
"priority": FAMILY_PRIORITY[family],
|
|
"cooldown_ms": FAMILY_COOLDOWN_MS[family],
|
|
}
|
|
entry.update(OVERRIDES.get(event_id, {}))
|
|
items[event_name] = entry
|
|
resolved_ids.add(event_id)
|
|
music = {}
|
|
for family, (filename, role, suite) in MUSIC_FAMILIES.items():
|
|
path = MUSIC_DIR / filename
|
|
if path.exists():
|
|
music[family] = {"file": f"music/{filename}", "bus": "MusicBed", "role": role, "suite": suite}
|
|
battle_deferred = sorted(set(events) - resolved_ids)
|
|
manifest = {
|
|
"version": 1,
|
|
"music": {"crossfade_seconds": 0.9, "families": music},
|
|
"sfx": {"max_simultaneous": 12, "deferred_family": "BAT", "deferred_events": battle_deferred},
|
|
"events": items,
|
|
}
|
|
TARGET.write_text(json.dumps(manifest, ensure_ascii=False, indent=1) + "\n")
|
|
print(f"events={len(items)} music_families={len(music)} battle_deferred={len(battle_deferred)} unresolved={missing}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|