Asset Dump

This commit is contained in:
2026-06-03 13:46:13 -07:00
parent e362aaeb43
commit 9091388bc2
20821 changed files with 26544125 additions and 58 deletions
@@ -16,8 +16,14 @@ namespace ProjectM.Client
/// spawns a death burst + death SFX; the local player crossing to 0 HP does the same. A local-player ability
/// fire (AbilityCooldown advancing) spawns a muzzle flash + zap. Everything derives from already-replicated
/// state, so it is correct without touching the prediction loop, and it lives only in the client world so the
/// server never instantiates GameObjects. SFX are generated procedurally; VFX use a small runtime pool — the
/// slice ships with NO binary audio/particle assets.
/// server never instantiates GameObjects.
/// <para>
/// VFX prefer authored GabrielAguiar Shuriken prefabs supplied by <see cref="VFXConfig"/> (muzzle / hit /
/// death + a projectile-following trail); each hook falls back to a procedural particle burst when no prefab
/// is assigned, so the slice still runs asset-free. Spawned VFX are stripped to particles only
/// (<see cref="StripCosmetic"/>) — GA "projectile" prefabs ship a Rigidbody + collider + mover that would
/// otherwise self-propel and spawn secondary effects. SFX remain procedural.
/// </para>
/// <para>
/// Per-entity last Health + position + isEnemy are cached in a managed dictionary (Entity is a stable client
/// key for a ghost's lifetime); stale keys are pruned each frame (a pruned Husk = a kill → death VFX at its
@@ -36,6 +42,12 @@ namespace ProjectM.Client
readonly List<Entity> _stale = new();
readonly List<FloatingNumber> _numbers = new();
// Authored-VFX lifetime tracking (GabrielAguiar prefabs spawned via VFXConfig).
readonly List<TimedVfx> _activeVfx = new();
readonly Dictionary<Entity, GameObject> _projTrails = new();
readonly HashSet<Entity> _projSeen = new();
readonly List<Entity> _projStale = new();
Transform _fxRoot;
ParticleSystem _hitFx;
ParticleSystem _deathFx;
@@ -49,6 +61,9 @@ namespace ProjectM.Client
bool _fireTickInit;
const int NumberPoolSize = 32;
const int MaxActiveVfx = 40; // bound one-shot VFX GameObject churn under sustained combat
struct TimedVfx { public GameObject Go; public double Kill; }
protected override void OnCreate()
{
@@ -81,6 +96,7 @@ namespace ProjectM.Client
{
float dt = SystemAPI.Time.DeltaTime;
var cam = Camera.main;
var cfg = VFXConfig.Instance;
// Make sure predicted/physics jobs writing these are done before this main-thread read.
EntityManager.CompleteDependencyBeforeRO<Health>();
@@ -112,7 +128,7 @@ namespace ProjectM.Client
if (cur < prev.Hp - 0.001f)
{
SpawnNumber(prev.Hp - cur, (Vector3)p, isLocalPlayer, cam);
EmitAt(_hitFx, (Vector3)p + Vector3.up * 0.8f, 10);
Burst(_hitFx, cfg != null ? cfg.Hit : null, (Vector3)p + Vector3.up * 0.8f, 10);
PlayClip(_hitClip, (Vector3)p, 0.7f);
PrototypeCameraRig.AddShake(isLocalPlayer ? 0.32f : 0.10f);
}
@@ -120,7 +136,7 @@ namespace ProjectM.Client
// Player death (players don't despawn — they respawn; Husk death is handled on prune).
if (!isEnemy && cur <= 0f && prev.Hp > 0f)
{
EmitAt(_deathFx, (Vector3)p + Vector3.up * 0.5f, 28);
Burst(_deathFx, PlayerDeathPrefab(cfg), (Vector3)p + Vector3.up * 0.5f, 28);
PlayClip(_deathClip, (Vector3)p, 0.7f);
PrototypeCameraRig.AddShake(isLocalPlayer ? 0.5f : 0.25f);
}
@@ -141,7 +157,7 @@ namespace ProjectM.Client
var c = _cache[_stale[i]];
if (c.IsEnemy)
{
EmitAt(_deathFx, (Vector3)c.Pos + Vector3.up * 0.5f, 28);
Burst(_deathFx, cfg != null ? cfg.EnemyDeath : null, (Vector3)c.Pos + Vector3.up * 0.5f, 28);
PlayClip(_deathClip, (Vector3)c.Pos, 0.65f);
PrototypeCameraRig.AddShake(0.16f);
}
@@ -150,21 +166,139 @@ namespace ProjectM.Client
}
// Local-player fire feedback: AbilityCooldown.NextFireTick advances on each shot.
// Raw uint inequality is intentional here: only the edge (a new shot) matters and the worst case
// of a tick wrap is a single dropped/duplicated muzzle flash — purely cosmetic, never the sim.
if (_localPlayer != Entity.Null && EntityManager.HasComponent<AbilityCooldown>(_localPlayer))
{
uint nextFire = EntityManager.GetComponentData<AbilityCooldown>(_localPlayer).NextFireTick;
if (_fireTickInit && nextFire != 0 && nextFire != _lastLocalFireTick)
{
EmitAt(_muzzleFx, (Vector3)localPos + Vector3.up * 0.9f, 8);
Burst(_muzzleFx, cfg != null ? cfg.Muzzle : null, (Vector3)localPos + Vector3.up * 0.9f, 8);
PlayClip(_fireClip, (Vector3)localPos, 0.5f);
}
_lastLocalFireTick = nextFire;
_fireTickInit = true;
}
UpdateProjectileTrails(cfg);
PruneVfx();
AnimateNumbers(dt, cam);
}
// ---- Authored VFX (GabrielAguiar prefabs via VFXConfig); fall back to the procedural burst ----
static GameObject PlayerDeathPrefab(VFXConfig cfg)
{
if (cfg == null) return null;
return cfg.PlayerDeath != null ? cfg.PlayerDeath : cfg.EnemyDeath;
}
void Burst(ParticleSystem fallback, GameObject prefab, Vector3 pos, int count)
{
if (prefab != null) SpawnVfx(prefab, pos, Quaternion.identity);
else EmitAt(fallback, pos, count);
}
void SpawnVfx(GameObject prefab, Vector3 pos, Quaternion rot)
{
if (prefab == null || _fxRoot == null) return;
if (_activeVfx.Count >= MaxActiveVfx) return; // saturated: drop (cheap) rather than thrash GC
var go = Object.Instantiate(prefab, pos, rot, _fxRoot);
go.transform.position = pos;
StripCosmetic(go);
var systems = go.GetComponentsInChildren<ParticleSystem>();
for (int i = 0; i < systems.Length; i++) systems[i].Play();
_activeVfx.Add(new TimedVfx { Go = go, Kill = SystemAPI.Time.ElapsedTime + VfxLifetime(go) });
}
void PruneVfx()
{
double now = SystemAPI.Time.ElapsedTime;
for (int i = _activeVfx.Count - 1; i >= 0; i--)
{
if (now < _activeVfx[i].Kill) continue;
if (_activeVfx[i].Go != null) Object.Destroy(_activeVfx[i].Go);
_activeVfx.RemoveAt(i);
}
}
// A looping trail prefab follows each in-flight projectile ghost; destroyed when it despawns.
void UpdateProjectileTrails(VFXConfig cfg)
{
if (cfg == null || cfg.ProjectileTrail == null || _fxRoot == null)
{
// Config cleared mid-run: drop any orphaned trails so they don't linger.
if (_projTrails.Count > 0)
{
foreach (var kv in _projTrails) if (kv.Value != null) Object.Destroy(kv.Value);
_projTrails.Clear();
}
return;
}
_projSeen.Clear();
foreach (var (xf, entity) in
SystemAPI.Query<RefRO<LocalTransform>>().WithAll<Projectile>().WithEntityAccess())
{
_projSeen.Add(entity);
Vector3 wp = (Vector3)xf.ValueRO.Position;
if (_projTrails.TryGetValue(entity, out var trail))
{
if (trail != null) trail.transform.position = wp;
}
else
{
var go = Object.Instantiate(cfg.ProjectileTrail, wp, Quaternion.identity, _fxRoot);
StripCosmetic(go); // GA "projectile" prefabs ship a Rigidbody + mover; keep particles only
var systems = go.GetComponentsInChildren<ParticleSystem>();
for (int i = 0; i < systems.Length; i++) systems[i].Play();
_projTrails[entity] = go;
}
}
if (_projTrails.Count == _projSeen.Count) return;
_projStale.Clear();
foreach (var kv in _projTrails)
if (!_projSeen.Contains(kv.Key)) _projStale.Add(kv.Key);
for (int i = 0; i < _projStale.Count; i++)
{
if (_projTrails[_projStale[i]] != null) Object.Destroy(_projTrails[_projStale[i]]);
_projTrails.Remove(_projStale[i]);
}
}
// Cosmetic VFX must be particles only. GA demo "projectile" prefabs ship a non-kinematic Rigidbody,
// a solid collider, and a mover (ProjectileMoveScript) that self-propels and spawns secondary muzzle/hit
// effects on contact — strip all of that so our per-frame reposition is authoritative and nothing leaks.
static void StripCosmetic(GameObject go)
{
foreach (var rb in go.GetComponentsInChildren<Rigidbody>(true)) Object.Destroy(rb);
foreach (var col in go.GetComponentsInChildren<Collider>(true)) Object.Destroy(col);
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
if (mb == null) continue;
string n = mb.GetType().Name;
// Disable (not destroy) BEFORE Start runs so the mover's Start-spawned muzzle never fires.
if (n.IndexOf("Projectile", System.StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Move", System.StringComparison.OrdinalIgnoreCase) >= 0)
mb.enabled = false;
}
}
// Real effect duration from the longest child ParticleSystem (clamped), so we don't force-kill early
// or hold a finished GameObject around on a blanket TTL.
static double VfxLifetime(GameObject go)
{
float longest = 0f;
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true))
{
var main = ps.main;
float d = main.duration + main.startLifetime.constantMax;
if (d > longest) longest = d;
}
return Mathf.Clamp(longest, 1f, 6f);
}
// ---- Floating damage numbers (pooled, billboarded TextMesh) ----
class FloatingNumber
@@ -236,7 +370,7 @@ namespace ProjectM.Client
}
}
// ---- Procedural SFX + pooled particle bursts ----
// ---- Procedural SFX + pooled particle bursts (fallback when no authored prefab) ----
static AudioClip MakeClip(string name, float f0, float f1, float dur, float vol, bool noise)
{