Add formal enemies, remaining formal partners and synthesized UI SFX

- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to
  buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting.
- Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn):
  Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type
  in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes.
- Formal partners for all ten pets in the arena and portraits, including new
  VineSprite, ThunderLeopard and TideWhale.
- Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands,
  configurable Unity run_script timeout, companion static stage.
- Also includes concurrent work from another tool in this working tree
  (FlameLion, enemy combat FX, challenge mode, home explorer, server updates).
- Resources registered in Doc/ResDesc_*.md; verified on isolated :18081.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
udx
2026-10-21 12:23:55 +08:00
co-authored by Claude Opus 5.5
parent 6c64210331
commit 89e0fa246f
780 changed files with 7117051 additions and 15906 deletions
@@ -0,0 +1,84 @@
using System;
using UnityEngine;
namespace HotUpdate.Gameplay
{
/// <summary>Authored bounded VFX/audio pools; consumes resolved facts without touching battle state.</summary>
public sealed class EnemyCombatPresentation : MonoBehaviour
{
[Serializable] public sealed class EffectPool
{
public ParticleSystem[] instances;
[NonSerialized] public int next;
}
[SerializeField] EffectPool[] _effects;
[SerializeField] AudioClip[] _clips;
[SerializeField] AudioSource[] _voices;
[SerializeField, Range(0, 1)] float _mixBudget = .7f;
[SerializeField, Range(0, 1)] float _voiceVolume = .4f;
readonly float[] _lastSound = new float[8];
int _voice;
bool _paused;
float _speed = 1;
public int PlayedEffects { get; private set; }
public int PlayedSounds { get; private set; }
public int EffectCount => _effects?.Length ?? 0;
public AudioClip Clip(int index) => _clips[index];
public int VoiceCount => _voices?.Length ?? 0;
void OnEnable() { PrototypeClientPrefs.AudioPreferencesChanged += ApplyAudio; ResetPlayback(); }
void OnDisable() { PrototypeClientPrefs.AudioPreferencesChanged -= ApplyAudio; ResetPlayback(); }
void ApplyAudio() { if (!PrototypeClientPrefs.Sfx) foreach (var v in _voices) v.Stop(); }
public void ResetPlayback()
{
if (_effects != null) foreach (var pool in _effects)
foreach (var ps in pool.instances) ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
if (_voices != null) foreach (var v in _voices) v.Stop();
for (int i = 0; i < _lastSound.Length; i++) _lastSound[i] = -100;
_paused = false; _speed = 1; PlayedEffects = PlayedSounds = 0;
}
public void SetPlayback(bool paused, float speed)
{
if (_paused == paused && Mathf.Approximately(_speed, speed)) return;
bool speedChanged = !Mathf.Approximately(_speed, speed);
_speed = speed;
foreach (var pool in _effects) foreach (var ps in pool.instances)
{
if (speedChanged) foreach (var child in ps.GetComponentsInChildren<ParticleSystem>()) { var m = child.main; m.simulationSpeed = speed; }
if (paused && !_paused) ps.Pause(true);
else if (!paused && _paused && ps.IsAlive(true)) ps.Play(true);
}
if (paused != _paused) foreach (var v in _voices) { if (paused) v.Pause(); else v.UnPause(); }
_paused = paused;
}
public void Play(PrototypeBattle.CombatCue cue)
{
if (_paused || _effects == null) return;
int index = cue.Kind == PrototypeBattle.CueKind.EnemyAttack ? Mathf.Clamp(cue.Type, 0, 4) : (cue.Type >= 2 ? 6 : 5);
Burst(index, cue.Kind == PrototypeBattle.CueKind.EnemyAttack ? cue.AttackPosition : cue.Position, cue.Direction);
if (cue.Kind == PrototypeBattle.CueKind.EnemyAttack)
Burst(cue.Shield ? 7 : 5, cue.Position, -cue.Direction);
}
void Burst(int index, Vector3 position, Vector3 direction)
{
var pool = _effects[index];
var ps = pool.instances[pool.next++ % pool.instances.Length];
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
ps.transform.position = transform.TransformPoint(position);
ps.transform.rotation = Quaternion.LookRotation(direction);
foreach (var child in ps.GetComponentsInChildren<ParticleSystem>()) { var m = child.main; m.simulationSpeed = _speed; }
ps.Play(true); PlayedEffects++;
// Mix budget and same-cue guard prevent a wave of hits becoming a wall of sound.
if (!PrototypeClientPrefs.Sfx || Time.unscaledTime - _lastSound[index] < .075f) return;
_lastSound[index] = Time.unscaledTime;
var voice = _voices[_voice++ % _voices.Length]; voice.Stop(); voice.clip = _clips[index]; voice.Play(); PlayedSounds++;
int active = 0; foreach (var v in _voices) if (v.isPlaying) active++;
float gain = Mathf.Min(_voiceVolume, _mixBudget / Mathf.Max(1, active));
foreach (var v in _voices) if (v.isPlaying) v.volume = gain;
}
}
}