2fc5ae9a10
Phase 1.5b ground bundle (operator: "the ground is very plain"): - Ground materials rebuilt: Grass/Sand base maps at real tiling (22x/26x, was a 2x smear across 90-130u) + Synty normal maps + a generated 0.5-centered macro noise detail texture (LINEAR import — sRGB detail mul2x darkens 2.3x) with baked rim vignette. Meadow ground moved OFF the shared Synty asset onto project-owned Mat_Ground_Meadow. - WorldAtmosphereSystem: per-room-biome ground tint on the expedition quads via MaterialPropertyBlock (Meadow green / Cavern blue / Blight purple; Arid sand default), lerped alongside the existing fog/ambient palettes. - RoomDressingSystem + RoomDressingConfig (new, client-only observe-only): 26 biome-flavoured cosmetic props per room, scattered inside the room's ACTUAL shape (same RoomLayoutMath authority as the server, distinct 0xD2E55 hash stream), deterministic from the replicated run seed, torn down with the room, colliders stripped. 47 curated prefab refs wired in Game.unity (build-safe). - Base: 357 curated flora props re-enabled (bushes/wildflowers/grass clumps; buildings/pond/FX stay parked) + generated worn-dirt paths storage->warpgate and storage->base gate. Live-verified: dressing 26/26 inside x[981,1023] z[-11,12] y=0, 11 varieties, 0 colliders; teardown on run end; Meadow tint converged to exact target; 470 EditMode green; console clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
5.1 KiB
C#
112 lines
5.1 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using UnityEngine;
|
|
|
|
namespace ProjectM.Client
|
|
{
|
|
/// <summary>
|
|
/// Phase 1.5b (ground bundle) — client-only, observe-only PER-ROOM DRESSING SCATTER. A managed
|
|
/// <see cref="SystemBase"/> in <see cref="PresentationSystemGroup"/> that OBSERVES replicated
|
|
/// <see cref="RunInfo"/> and instantiates biome-flavoured cosmetic ground props (pebbles, tufts, bones,
|
|
/// mushrooms) inside the room's ACTUAL shape — the same <see cref="RoomLayoutMath"/> 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.
|
|
/// </summary>
|
|
[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<RunInfo>(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<BaseAnchor>(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<Collider>(true)) Object.Destroy(col);
|
|
foreach (var rb in go.GetComponentsInChildren<Rigidbody>(true)) Object.Destroy(rb);
|
|
}
|
|
}
|
|
}
|
|
}
|