using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
namespace ProjectM.Client
{
///
/// Phase 1.5b (ground bundle) — client-only, observe-only PER-ROOM DRESSING SCATTER. A managed
/// in that OBSERVES replicated
/// and instantiates biome-flavoured cosmetic ground props (pebbles, tufts, bones,
/// mushrooms) inside the room's ACTUAL shape — the same authority the server
/// scatter uses, on a DISTINCT hash stream so dressing never co-locates with nodes (0x0DE) or clutter
/// (0xC17). Deterministic from the replicated run seed: every co-op client (and a late-joiner) sees the
/// same floor. Torn down when the room changes or the run ends. Colliders + rigidbodies are stripped on
/// spawn — the aim-reticle raycast and the physics world must never see dressing. Never mutates the sim.
///
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class RoomDressingSystem : SystemBase
{
Transform _root;
uint _activeKey;
protected override void OnStartRunning()
{
if (_root == null) _root = new GameObject("~RoomDressing").transform;
}
protected override void OnDestroy()
{
if (_root != null) Object.Destroy(_root.gameObject);
}
protected override void OnUpdate()
{
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name != "Game") return;
if (_root == null) return;
var cfg = RoomDressingConfig.Instance;
bool inRoom = SystemAPI.TryGetSingleton(out var ri)
&& (ri.Lifecycle == RunLifecycle.InRoom
|| ri.Lifecycle == RunLifecycle.RoomReward
|| ri.Lifecycle == RunLifecycle.RoomExplore);
bool on = cfg != null && cfg.Enabled && cfg.CountPerRoom > 0;
if (!inRoom || !on || !SystemAPI.TryGetSingleton(out var anchor))
{
Clear();
return;
}
uint key = RunMapMath.Hash(ri.RunSeed, (uint)(ri.CurrentRoom + 1), 0xD2E5u);
if (key == _activeKey) return;
Clear();
Build(in ri, in anchor, cfg);
_activeKey = key;
}
void Clear()
{
if (_activeKey == 0 || _root == null) { _activeKey = 0; return; }
for (int i = _root.childCount - 1; i >= 0; i--)
Object.Destroy(_root.GetChild(i).gameObject);
_activeKey = 0;
}
void Build(in RunInfo ri, in BaseAnchor anchor, RoomDressingConfig cfg)
{
GameObject[] set = ri.CurrentBiome switch
{
RoomBiomeId.Meadow => cfg.Meadow,
RoomBiomeId.Cavern => cfg.Cavern,
RoomBiomeId.Blight => cfg.Blight,
_ => cfg.Arid,
};
if (set == null || set.Length == 0) return;
float3 baseCenter = BaseGridMath.PlotCenter(anchor);
byte subSlot = (byte)(ri.CurrentRoom & 1);
float3 origin = RegionMath.ExpeditionRoomOrigin(baseCenter, subSlot);
float3 portal = RegionMath.ExpeditionPortalPos(baseCenter, subSlot);
var map = RunMapMath.Generate(ri.RunSeed);
var node = map.NodeAt(RunMap.NodeId(ri.CurrentRoom, ri.CurrentCol));
var plan = RoomLayoutMath.Plan(node, ri.CurrentRoom, ri.RoomCount);
var rng = new Unity.Mathematics.Random(RunMapMath.Hash(ri.RunSeed, (uint)(ri.CurrentRoom + 1), 0xD2E55u) | 1u);
int count = cfg.CountPerRoom;
for (int i = 0; i < count; i++)
{
float3 pos = RoomLayoutMath.ScatterInShape(plan.ShapeId, origin, i, count, ref rng);
for (int attempt = 0; attempt < 4; attempt++)
{
bool nearLanding = math.distancesq(pos.xz, origin.xz) < 9f; // party lands at the origin
bool nearPortal = math.distancesq(pos.xz, portal.xz) < 6.25f; // keep the exit beacon clear
if (!nearLanding && !nearPortal) break;
pos = RoomLayoutMath.ScatterInShape(plan.ShapeId, origin, i, count, ref rng);
}
var prefab = set[rng.NextInt(0, set.Length)];
if (prefab == null) continue;
var go = Object.Instantiate(prefab, _root, false);
go.transform.SetPositionAndRotation(
new Vector3(pos.x, 0f, pos.z), // terrain y=0 (origin.y is the CC capsule plane, 1 u up)
Quaternion.Euler(0f, rng.NextFloat(0f, 360f), 0f));
go.transform.localScale = Vector3.one * rng.NextFloat(cfg.ScaleMin, cfg.ScaleMax);
foreach (var col in go.GetComponentsInChildren(true)) Object.Destroy(col);
foreach (var rb in go.GetComponentsInChildren(true)) Object.Destroy(rb);
}
}
}
}