b34945c2d2
Deletes CyclePhaseSystem, GoalReachedSystem, CoreDamage/CoreRestore, ThreatDirector, CoreIntegrity/GoalProgress/RunPhase/RunOutcome/ThreatState components, CoreVisualFeedbackSystem, and the whole Client/Onboarding slice (+6 test files). Keepers reworked: RunDirectorSystem (UpdateBefore attr + launch guard + goal/threat bank removed; sole SaveRequest raiser now), CycleDirectorSpawnSystem (ledger/meta host only), WaveSystem UNGATED (waves run wherever a WaveDirector is baked), EnemyAISystem core-fallback stripped, AmbientAudioSystem reworked (bed + run cues; no CycleState gate), MusicSystem RunInfo-only, HudSystem big trim (goal meter, core bar, siege banner, terminal banner, outcome flash, onboarding hook all gone), MetaShop/ClassPrep/AimReticle siege gates dropped, DebugOverlay/ops re-meant (SpawnWave=force next wave, EndSiege=quiet arena; SetCalm/AdvanceGoal/SetHeat retired, bytes reserved), TuningConfig Core knobs retired (ids 20-23 reserved), StorageMath.DrainFraction deleted, HowToPlay copy rewritten. Save epoch v7 (fresh epoch, operator-approved): SaveData drops goal/core/outcome + conveyor/machine-IO fields; MinLoadableVersion=7; PendingSave/PendingStructure trimmed; RollTerminalCampaignForward deleted; SaveStructureScan signature slimmed. 390 tests green; Play world-creation clean (player + waves live, no exceptions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
134 lines
5.7 KiB
C#
134 lines
5.7 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
using UnityEngine;
|
|
|
|
namespace ProjectM.Client
|
|
{
|
|
/// <summary>
|
|
/// Client-only AMBIENT audio bed + run cues. A managed presentation <see cref="SystemBase"/>
|
|
/// (<see cref="PresentationSystemGroup"/>, main thread, no Burst) that plays a low, seamless-looping
|
|
/// procedural drone (asset-free, <c>AudioClip.Create</c> like <c>CombatFeedbackSystem.MakeClip</c>) plus
|
|
/// launch-countdown beeps and the boss-arrival roar — replicated-state observations only. Lives only in the
|
|
/// client world, so the server never creates audio and nothing here affects determinism. Volumes are
|
|
/// deliberately conservative. (The cycle-phase stingers + Core alarm retired with the siege loop — LANTERN purge.)
|
|
/// </summary>
|
|
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
|
[UpdateInGroup(typeof(PresentationSystemGroup))]
|
|
public partial class AmbientAudioSystem : SystemBase
|
|
{
|
|
AudioSource _ambient;
|
|
AudioClip _ambientClip;
|
|
GameObject _root;
|
|
|
|
AudioClip _stingBeep, _stingRoar;
|
|
int _lastCountdownSec = -1;
|
|
bool _bossRoared;
|
|
|
|
const float AmbientBaseVolume = 0.10f; // low ambient bed
|
|
|
|
protected override void OnCreate()
|
|
{
|
|
_ambientClip = MakeDrone();
|
|
_stingBeep = MakeSting(880f, 880f, 0.09f, 0.30f); // countdown tick
|
|
_stingRoar = MakeSting(90f, 38f, 0.90f, 0.60f); // boss-arrival roar (low falling growl)
|
|
}
|
|
|
|
protected override void OnStartRunning()
|
|
{
|
|
if (_root != null) return;
|
|
_root = new GameObject("~AmbientAudio");
|
|
_ambient = _root.AddComponent<AudioSource>();
|
|
_ambient.clip = _ambientClip;
|
|
_ambient.loop = true;
|
|
_ambient.playOnAwake = false;
|
|
_ambient.spatialBlend = 0f; // 2D bed
|
|
_ambient.volume = AmbientBaseVolume * GameVolume.Music;
|
|
_ambient.Play();
|
|
}
|
|
|
|
protected override void OnDestroy()
|
|
{
|
|
if (_root != null) Object.Destroy(_root);
|
|
}
|
|
|
|
protected override void OnUpdate()
|
|
{
|
|
if (_ambient == null) return;
|
|
|
|
_ambient.volume = Mathf.MoveTowards(_ambient.volume, AmbientBaseVolume * GameVolume.Music, SystemAPI.Time.DeltaTime * 0.25f);
|
|
|
|
// Launch countdown beeps (3-2-1) + the boss-arrival roar — replicated-state observations only.
|
|
if (SystemAPI.TryGetSingleton<RunInfo>(out var runAudio))
|
|
{
|
|
int sec = -1;
|
|
if (runAudio.Lifecycle == RunLifecycle.Launching && runAudio.LaunchTick != 0
|
|
&& SystemAPI.TryGetSingleton<NetworkTime>(out var antime) && antime.ServerTick.IsValid)
|
|
{
|
|
int tl = new NetworkTick(runAudio.LaunchTick).TicksSince(antime.ServerTick);
|
|
if (tl > 0) sec = tl / 60 + 1;
|
|
}
|
|
if (sec > 0 && sec != _lastCountdownSec && sec <= 3)
|
|
_ambient.PlayOneShot(_stingBeep, 0.5f * GameVolume.Sfx);
|
|
_lastCountdownSec = sec;
|
|
|
|
bool inBossRoom = runAudio.Lifecycle == RunLifecycle.InRoom
|
|
&& runAudio.CurrentRoomType == RoomTypeId.Boss;
|
|
if (!inBossRoom)
|
|
{
|
|
_bossRoared = false; // re-arm for the next boss room
|
|
}
|
|
else if (!_bossRoared)
|
|
{
|
|
foreach (var _ in SystemAPI.Query<RefRO<Health>>().WithAll<EnemyTag>())
|
|
{
|
|
_ambient.PlayOneShot(_stingRoar, 0.9f * GameVolume.Sfx);
|
|
_bossRoared = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Procedural audio (asset-free; mirrors CombatFeedbackSystem.MakeClip) ----
|
|
|
|
// A low, seamless-looping pad: each partial completes an integer number of cycles over the buffer
|
|
// (freq snapped to k/duration) so the loop point has no click. A slow tremolo adds motion.
|
|
static AudioClip MakeDrone()
|
|
{
|
|
const int rate = 44100;
|
|
const float dur = 4f;
|
|
int len = (int)(dur * rate);
|
|
var clip = AudioClip.Create("ambient_drone", len, 1, rate, false);
|
|
var data = new float[len];
|
|
float f0 = Snap(55f, dur); // sub
|
|
float f1 = Snap(110f, dur); // root
|
|
float f2 = Snap(164.81f, dur); // fifth-ish
|
|
float f3 = Snap(220f, dur);
|
|
float trem = Snap(0.5f, dur); // slow amplitude wobble
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
float t = i / (float)rate;
|
|
float s = 0.50f * Mathf.Sin(2f * Mathf.PI * f0 * t)
|
|
+ 0.35f * Mathf.Sin(2f * Mathf.PI * f1 * t)
|
|
+ 0.18f * Mathf.Sin(2f * Mathf.PI * f2 * t)
|
|
+ 0.10f * Mathf.Sin(2f * Mathf.PI * f3 * t);
|
|
float amp = 0.75f + 0.25f * Mathf.Sin(2f * Mathf.PI * trem * t);
|
|
data[i] = s * amp * 0.5f; // peak ~0.57, no clipping
|
|
}
|
|
clip.SetData(data, 0);
|
|
return clip;
|
|
}
|
|
|
|
// freq snapped so freq*dur is an integer -> the waveform closes seamlessly at the loop point.
|
|
static float Snap(float freq, float dur)
|
|
{
|
|
float cycles = Mathf.Max(1f, Mathf.Round(freq * dur));
|
|
return cycles / dur;
|
|
}
|
|
|
|
// Short one-shot tone sweeping f0->f1 with an exponential decay envelope.
|
|
static AudioClip MakeSting(float f0, float f1, float dur, float vol) => FeedbackFx.MakeClip("sting", f0, f1, dur, vol, decay: 3.5f);
|
|
}
|
|
}
|