From eb7f572168d2aa0ef1142f4ea82dbc57d9685b07 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Sat, 11 Jul 2026 17:13:13 -0700 Subject: [PATCH] Fix: barrel fuse shows its blast radius + burns ~1.5s (was 0.6s) Operator feel report: the explosion radius was unreadable and the fuse too short to react to. Two changes: - Tuning.BarrelFuseTicks 36 -> 90 (~1.5s): long enough to READ the danger and walk out, and better for baiting enemies onto a lit barrel. - WorldFeedbackSystem draws a pulsing ground DISC at exactly Tuning.BarrelExplodeRadius under every lit fuse (keyed on the same replicated Remaining=0-on-a-live-barrel cue as the boom split; one shared pulsing material; removed via the existing prune path). Verified live: disc appeared 0.07s after the pop (first fuse snapshot), persisted to the boom at 1.54s, scale read exactly 3.25 = the true detonation radius; 470/470 EditMode; console clean. Co-Authored-By: Claude Fable 5 --- .../Presentation/WorldFeedbackSystem.cs | 70 ++++++++++++++++++- Assets/_Project/Scripts/Simulation/Tuning.cs | 7 +- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/Assets/_Project/Scripts/Client/Presentation/WorldFeedbackSystem.cs b/Assets/_Project/Scripts/Client/Presentation/WorldFeedbackSystem.cs index 54c899be6..1dc7a76aa 100644 --- a/Assets/_Project/Scripts/Client/Presentation/WorldFeedbackSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/WorldFeedbackSystem.cs @@ -37,6 +37,9 @@ namespace ProjectM.Client AudioClip _chipClip; AudioClip _clearClip; AudioClip _boomClip; + readonly Dictionary _fuseRings = new(); // blast-radius discs, one per lit fuse + Mesh _fuseDiscMesh; + Material _fuseMat; protected override void OnCreate() { @@ -52,11 +55,19 @@ namespace ProjectM.Client var mat = MakeParticleMaterial(); _chipFx = MakeBurst(_fxRoot, "HarvestChips", mat, new Color(2.6f, 1.9f, 0.7f), 0.10f, 5f, 0.30f, 256, 0.25f, 0.10f, 0.2f); _clearFx = MakeBurst(_fxRoot, "ClutterClear", mat, new Color(3.0f, 1.1f, 0.25f), 0.16f, 7f, 0.45f, 512, 0.25f, 0.10f, 0.2f); + // Blast-radius disc for lit barrel fuses (operator 07-10: the radius was unreadable) — a flat ground + // disc at EXACTLY Tuning.BarrelExplodeRadius, pulsing via the shared material's alpha. + _fuseMat = MakeParticleMaterial(); + _fuseMat.name = "BarrelFuseDisc"; + _fuseMat.color = new Color(2.8f, 0.55f, 0.18f, 0.35f); + _fuseDiscMesh = BuildDisc(32); } protected override void OnDestroy() { - if (_fxRoot != null) Object.Destroy(_fxRoot.gameObject); + if (_fxRoot != null) Object.Destroy(_fxRoot.gameObject); // fuse rings are children -> die with it + if (_fuseMat != null) Object.Destroy(_fuseMat); + if (_fuseDiscMesh != null) Object.Destroy(_fuseDiscMesh); } protected override void OnUpdate() @@ -93,6 +104,15 @@ namespace ProjectM.Client { _seen.Add(e); Observe(e, clutter.ValueRO.Remaining, xf.ValueRO.Position, true, WorldFeelConfig.WildTint, clutter.ValueRO.Variant, haveLocal && math.distancesq(xf.ValueRO.Position, localPos) <= WorldFeelConfig.ProximityRange * WorldFeelConfig.ProximityRange); + UpdateFuseRing(e, clutter.ValueRO, xf.ValueRO.Position); + } + + // blast-radius discs pulse in sync (one shared material) + if (_fuseRings.Count > 0 && _fuseMat != null) + { + float fa = 0.30f + 0.22f * Mathf.Abs(Mathf.Sin(UnityEngine.Time.time * 9f)); + var fc = _fuseMat.color; + _fuseMat.color = new Color(fc.r, fc.g, fc.b, fa); } // Prune: a despawn = the server destroyed it (node depleted / clutter shattered). Gate on proximity so @@ -131,6 +151,11 @@ namespace ProjectM.Client } } } + if (_fuseRings.TryGetValue(_stale[i], out var ring)) + { + if (ring != null) Object.Destroy(ring); + _fuseRings.Remove(_stale[i]); + } _cache.Remove(_stale[i]); } } @@ -151,6 +176,49 @@ void Observe(Entity e, int remaining, float3 pos, bool isClutter, Color tint, by _cache[e] = new Cache { Remaining = remaining, Pos = pos, IsClutter = isClutter, Tint = tint, Variant = variant }; } + // Lit fuse (Variant 3, Remaining 0 on a live barrel) -> a flat ground disc at EXACTLY the detonation + // radius so the player reads WHERE not to stand. Removed via the prune path (a fused barrel can only + // despawn — detonation or teardown). + void UpdateFuseRing(Entity e, BlightClutter clutter, float3 pos) + { + if (clutter.Variant != 3 || clutter.Remaining > 0) return; + if (!_fuseRings.TryGetValue(e, out var go) || go == null) + { + go = new GameObject("BarrelBlastRadius"); + go.transform.SetParent(_fxRoot, false); + var mf = go.AddComponent(); + mf.sharedMesh = _fuseDiscMesh; + var mr = go.AddComponent(); + mr.sharedMaterial = _fuseMat; + mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + mr.receiveShadows = false; + go.transform.localScale = new Vector3(Tuning.BarrelExplodeRadius, 1f, Tuning.BarrelExplodeRadius); + _fuseRings[e] = go; + } + go.transform.position = new Vector3(pos.x, 0.06f, pos.z); + } + + // Unit-radius upward-facing disc fan; scaled per ring to the blast radius. + static Mesh BuildDisc(int segments) + { + var m = new Mesh { name = "FuseDisc" }; + var v = new Vector3[segments + 1]; + var tris = new int[segments * 3]; + v[0] = Vector3.zero; + for (int i = 0; i < segments; i++) + { + float a = i / (float)segments * Mathf.PI * 2f; + v[i + 1] = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)); + int n = (i + 1) % segments; + tris[i * 3] = 0; tris[i * 3 + 1] = n + 1; tris[i * 3 + 2] = i + 1; + } + m.vertices = v; + m.triangles = tris; + m.RecalculateBounds(); + return m; + } + + static Color TintForResource(byte resourceId) { if (resourceId == ResourceId.Ore) return WorldFeelConfig.OreTint; diff --git a/Assets/_Project/Scripts/Simulation/Tuning.cs b/Assets/_Project/Scripts/Simulation/Tuning.cs index 3614ca2c8..3eed64daf 100644 --- a/Assets/_Project/Scripts/Simulation/Tuning.cs +++ b/Assets/_Project/Scripts/Simulation/Tuning.cs @@ -109,9 +109,10 @@ namespace ProjectM.Simulation // ---- Exploding barrels (Phase 1.5 hazard; server-only detonation consts — see Exploding_Barrels_Build_Spec) ---- - /// Fuse ticks between the pop (Remaining hits 0) and detonation (~0.6 s @60): long enough for the - /// popper to step out + for the replicated Remaining=0 to straddle snapshots (the client's fuse cue). - public const uint BarrelFuseTicks = 36; + /// Fuse ticks between the pop (Remaining hits 0) and detonation (~1.5 s @60; operator feel call + /// 07-10 — the 0.6 s fuse popped before the danger could be read): long enough to READ the blast-radius + /// disc + walk out, and for the replicated Remaining=0 to straddle snapshots (the client's fuse cue). + public const uint BarrelFuseTicks = 90; /// Detonation radius (world units, XZ). Above melee reach so point-blank pops are punished, but a /// fuse-length walk clears it comfortably.