c58439d33a
- PostFX_Lantern.asset: the ONE shared grade (ACES, bloom 0.6 cool-tinted for the
HDR gloam, vignette 0.28, faint teal filter) applied to Game, DevSandbox, and
ArtStaging; PostFX_DarkSciFi / PostFX_Daylight / Sky_DaytimeProcedural deleted.
- Env_SeabedKit.prefab: ArtStaging's environment (seabed, flora, rocks, marine
snow, caustics, warm pool, gloam fill, key light, StagingAmbiance animator)
prefab-ized and placed in Game + DevSandbox; ArtStaging connected to it.
- Scene surgery: old DevSandbox.unity deleted; Gym.unity RENAMED DevSandbox.unity
(GUID preserved -> GymSub wiring + the F1/F2 dev scripts that gate on the
'DevSandbox' name come back to life). SyntyWorld root deleted from DevSandbox;
BaseBiome/ExpeditionBiome/Slot1 deleted from Game.
- RenderSettings unified: no skybox, Exp2 teal fog {0.02,0.10,0.12} @ 0.035, flat
near-black ambient {0.03,0.055,0.08}; camera clearFlags -> solid deep-water.
- ScenePolicy.IsGameplayScene() replaces the six scene.name=="Game" string gates
(Game + DevSandbox share the dynamic look; menu/ArtStaging untouched).
- WorldAtmosphereSystem rewritten as water-column murk (LANTERN defaults; biome
variants re-meant to kelp/trench/gloam-bloom; Ground_Arid tint block deleted).
390 green; Play-verified in Game: murk values live, warm-vs-cold reads, no errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
178 lines
8.4 KiB
C#
178 lines
8.4 KiB
C#
using System.Collections.Generic;
|
|
using ProjectM.Simulation;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.NetCode;
|
|
using Unity.Transforms;
|
|
using UnityEngine;
|
|
using static ProjectM.Client.FeedbackFx;
|
|
|
|
namespace ProjectM.Client
|
|
{
|
|
/// <summary>
|
|
/// Client-only COVER DAMAGE decals (Phase 1.5b bundle 2 — decals). Destructible cover = a
|
|
/// <see cref="BlightClutter"/> ghost (Variant 4, Remaining 8 → 0; see the Destructible_Cover_Build_Spec). This
|
|
/// observe-only <see cref="PresentationSystemGroup"/> <see cref="SystemBase"/> edge-detects the replicated
|
|
/// <see cref="BlightClutter.Remaining"/> on cover ghosts and accretes jagged CRACK decals on the ground around
|
|
/// the rock as it is carved (<see cref="WorldFeedbackSystem"/> already fires the chip puff / SFX / camera-punch
|
|
/// on the same edge — we only add the persistent cracks). On despawn (the rock shattered NEAR the local player)
|
|
/// it drops a small ground scorch via <see cref="ScorchDecalSystem.RequestScorch"/> and releases the piece's
|
|
/// cracks. The despawn scorch is PROXIMITY-GATED (mirrors WorldFeedbackSystem) so a room-teardown / region-transit
|
|
/// despawn storm — every cover ghost dropped at once, far off-camera — can't spray scorches or exhaust the pool.
|
|
/// <para>
|
|
/// Pure procedural decal quads — NO material/shader dependency. This is deliberate: cover's Synty prop shader
|
|
/// declares <c>_BaseColor</c> as Unity-Per-Material (NOT Hybrid-Per-Instance), so the
|
|
/// <c>URPMaterialPropertyBaseColor</c> darken that <see cref="EnemyHitFlashSystem"/> uses on the DOTS-authored
|
|
/// AnimatedLitShader would silently no-op here. Decals sidestep that entirely and read as literal damage.
|
|
/// </para>
|
|
/// No new component / [GhostField], no server work. Knobs live in <see cref="DecalConfig"/>.
|
|
/// </summary>
|
|
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
|
[UpdateInGroup(typeof(PresentationSystemGroup))]
|
|
public partial class CoverDamageSystem : SystemBase
|
|
{
|
|
const byte CoverVariant = 4;
|
|
|
|
class Piece
|
|
{
|
|
public int LastRemaining;
|
|
public int MaxRemaining;
|
|
public float3 Pos;
|
|
public readonly List<GameObject> Cracks = new();
|
|
}
|
|
|
|
readonly Dictionary<Entity, Piece> _tracked = new();
|
|
readonly HashSet<Entity> _seen = new();
|
|
readonly List<Entity> _stale = new();
|
|
|
|
GameObject _root;
|
|
Material _mat;
|
|
Mesh[] _crackMeshes; // pre-built variants, reused across all cracks (no per-crack Mesh alloc/leak)
|
|
MaterialPropertyBlock _mpb;
|
|
static readonly int ColorId = Shader.PropertyToID("_Color");
|
|
|
|
protected override void OnCreate()
|
|
{
|
|
_crackMeshes = new Mesh[6];
|
|
for (int i = 0; i < _crackMeshes.Length; i++) _crackMeshes[i] = BuildCrackMesh(2 + (i % 2), i + 1);
|
|
_mpb = new MaterialPropertyBlock();
|
|
}
|
|
|
|
protected override void OnDestroy()
|
|
{
|
|
if (_root != null) Object.Destroy(_root);
|
|
if (_mat != null) Object.Destroy(_mat);
|
|
if (_crackMeshes != null)
|
|
for (int i = 0; i < _crackMeshes.Length; i++)
|
|
if (_crackMeshes[i] != null) Object.Destroy(_crackMeshes[i]);
|
|
}
|
|
|
|
protected override void OnUpdate()
|
|
{
|
|
if (!ScenePolicy.IsGameplayScene()) return;
|
|
if (!DecalConfig.Enabled) { if (_tracked.Count > 0) ClearAll(); return; }
|
|
|
|
EntityManager.CompleteDependencyBeforeRO<BlightClutter>();
|
|
EntityManager.CompleteDependencyBeforeRO<LocalTransform>();
|
|
|
|
if (_root == null)
|
|
{
|
|
_root = new GameObject("~CoverDecals");
|
|
Object.DontDestroyOnLoad(_root);
|
|
_mat = MakeDecalMaterial("CoverCrackDecal");
|
|
}
|
|
|
|
// Local player position for the shatter-scorch proximity gate (mirrors WorldFeedbackSystem).
|
|
bool haveLocal = false;
|
|
float3 localPos = default;
|
|
foreach (var xf in SystemAPI.Query<RefRO<LocalTransform>>().WithAll<GhostOwnerIsLocal, PlayerTag>())
|
|
{
|
|
localPos = xf.ValueRO.Position;
|
|
haveLocal = true;
|
|
}
|
|
|
|
_seen.Clear();
|
|
foreach (var (clutter, xf, e) in
|
|
SystemAPI.Query<RefRO<BlightClutter>, RefRO<LocalTransform>>().WithEntityAccess())
|
|
{
|
|
if (clutter.ValueRO.Variant != CoverVariant) continue; // cover only
|
|
_seen.Add(e);
|
|
int remaining = clutter.ValueRO.Remaining;
|
|
float3 pos = xf.ValueRO.Position;
|
|
if (!_tracked.TryGetValue(e, out var piece))
|
|
{
|
|
// First sight: cache the baseline; a joiner mid-carve keeps its already-damaged look implicitly.
|
|
_tracked[e] = new Piece { LastRemaining = remaining, MaxRemaining = math.max(1, remaining), Pos = pos };
|
|
continue;
|
|
}
|
|
piece.Pos = pos;
|
|
if (remaining < piece.LastRemaining)
|
|
{
|
|
// accrete cracks proportional to damage taken; cap at CoverCrackMaxCount.
|
|
float dmgFrac = 1f - remaining / (float)piece.MaxRemaining;
|
|
int want = math.clamp((int)math.round(DecalConfig.CoverCrackMaxCount * dmgFrac), 0, DecalConfig.CoverCrackMaxCount);
|
|
while (piece.Cracks.Count < want) SpawnCrack(piece, piece.Cracks.Count);
|
|
piece.LastRemaining = remaining;
|
|
}
|
|
}
|
|
|
|
// ---- prune shattered cover ----
|
|
if (_tracked.Count != _seen.Count)
|
|
{
|
|
float rangeSq = WorldFeelConfig.ProximityRange * WorldFeelConfig.ProximityRange;
|
|
_stale.Clear();
|
|
foreach (var kv in _tracked) if (!_seen.Contains(kv.Key)) _stale.Add(kv.Key);
|
|
for (int i = 0; i < _stale.Count; i++)
|
|
{
|
|
var piece = _tracked[_stale[i]];
|
|
// Only a real shatter NEAR the local player leaves a scorch; a teardown / region-transit despawn
|
|
// storm (all cover dropped at once, off-camera) stays silent — no spray, no pool exhaustion.
|
|
if (DecalConfig.CoverShatterScorch && haveLocal && math.distancesq(piece.Pos, localPos) <= rangeSq)
|
|
ScorchDecalSystem.RequestScorch((Vector3)piece.Pos, 1.6f);
|
|
DestroyCracks(piece);
|
|
_tracked.Remove(_stale[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SpawnCrack(Piece piece, int index)
|
|
{
|
|
uint seed = (uint)math.max(1, index * 131 + (int)(math.abs(piece.Pos.x) * 7f + math.abs(piece.Pos.z) * 13f));
|
|
var rng = new Unity.Mathematics.Random(seed);
|
|
var go = new GameObject("Crack");
|
|
go.transform.SetParent(_root.transform, false);
|
|
go.AddComponent<MeshFilter>().sharedMesh = _crackMeshes[(index + (int)(seed % 3u)) % _crackMeshes.Length];
|
|
var mr = go.AddComponent<MeshRenderer>();
|
|
mr.sharedMaterial = _mat;
|
|
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
|
mr.receiveShadows = false;
|
|
mr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
|
_mpb.SetColor(ColorId, DecalConfig.CoverCrackColor);
|
|
mr.SetPropertyBlock(_mpb);
|
|
|
|
// scatter as GROUND cracks radiating from the rock's footprint (avoids z-fighting the rock mesh)
|
|
float ang = rng.NextFloat(0f, math.PI * 2f);
|
|
float rad = rng.NextFloat(0.8f, 1.8f);
|
|
float scale = rng.NextFloat(0.9f, 1.6f);
|
|
go.transform.SetPositionAndRotation(
|
|
new Vector3(piece.Pos.x + math.cos(ang) * rad, 0.06f + 0.003f * index, piece.Pos.z + math.sin(ang) * rad),
|
|
Quaternion.Euler(0f, rng.NextFloat(0f, 360f), 0f));
|
|
go.transform.localScale = new Vector3(scale, 1f, scale);
|
|
piece.Cracks.Add(go);
|
|
}
|
|
|
|
void DestroyCracks(Piece piece)
|
|
{
|
|
for (int c = 0; c < piece.Cracks.Count; c++)
|
|
if (piece.Cracks[c] != null) Object.Destroy(piece.Cracks[c]);
|
|
piece.Cracks.Clear();
|
|
}
|
|
|
|
void ClearAll()
|
|
{
|
|
foreach (var kv in _tracked) DestroyCracks(kv.Value);
|
|
_tracked.Clear();
|
|
}
|
|
}
|
|
}
|