Init Homebase

This commit is contained in:
2026-06-02 18:28:23 -07:00
parent 2ee30c01fd
commit dd0064c377
48 changed files with 1934 additions and 12 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d26b9ac48ea390e4fa1310800701eb52
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Singleton baked into the gameplay subscene describing the shared home base: a fixed anchor
/// position and the planar build-grid coordinate space (origin + cell size + extent) that M6 build
/// placement snaps structures into and M7 production chains tick inside. Flat and blittable (no
/// entity refs) so it stays deterministic across both worlds and serialization-friendly for later
/// persistence. Present identically on client and server (baked, not replicated).
/// </summary>
public struct BaseAnchor : IComponentData
{
/// <summary>World-space center of the plot; also the player spawn-ring center. Equals BaseGridMath.PlotCenter(this).</summary>
public float3 AnchorPos;
/// <summary>World-space min-XZ CORNER of cell (0,0). Y is the base plane. Baked = AnchorPos - (GridDims*CellSize)/2 on XZ.</summary>
public float3 GridOrigin;
/// <summary>Size of one square grid cell in world units.</summary>
public float CellSize;
/// <summary>Grid extent in cells along X and Z; valid cell indices are [0, GridDims).</summary>
public int2 GridDims;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9434d000149a08e438c5a7b876f7e10c
@@ -0,0 +1,54 @@
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Deterministic, side-effect-free math for the home-base build grid. Pure (no RNG, no wall-clock)
/// so server and predicting client agree on cell occupancy. Convention: <see cref="BaseAnchor.GridOrigin"/>
/// is the min-XZ CORNER of cell (0,0); <see cref="CellToWorld"/> returns cell CENTERS; cell bounds are
/// half-open so a point on a cell's lower edge belongs to that cell and the far plot edge is out of plot.
/// World-&gt;cell uses math.floor (not truncation) so negative coordinates snap correctly. Unit-tested in
/// EditMode (no netcode world required); M6's server placement handler calls these directly.
/// </summary>
public static class BaseGridMath
{
/// <summary>Planar (XZ) world position -&gt; integer grid cell. Uses floor, so values below GridOrigin go negative (out of plot).</summary>
public static int2 WorldToCell(in BaseAnchor a, float3 worldPos)
{
float2 local = (worldPos.xz - a.GridOrigin.xz) / a.CellSize;
return (int2)math.floor(local);
}
/// <summary>Grid cell -&gt; world position of the cell CENTER, on the base plane (Y = GridOrigin.y).</summary>
public static float3 CellToWorld(in BaseAnchor a, int2 cell)
{
float2 centerXZ = a.GridOrigin.xz + ((float2)cell + 0.5f) * a.CellSize;
return new float3(centerXZ.x, a.GridOrigin.y, centerXZ.y);
}
/// <summary>True when the cell is inside the plot. Half-open: [0, GridDims), so the far edge is out.</summary>
public static bool IsCellInPlot(in BaseAnchor a, int2 cell)
{
return math.all(cell >= 0) && math.all(cell < a.GridDims);
}
/// <summary>True when the world point falls inside the plot (its cell is in-plot).</summary>
public static bool IsPointInPlot(in BaseAnchor a, float3 worldPos)
{
return IsCellInPlot(a, WorldToCell(a, worldPos));
}
/// <summary>Clamp a cell into the valid [0, GridDims-1] range.</summary>
public static int2 ClampCell(in BaseAnchor a, int2 cell)
{
return math.clamp(cell, new int2(0, 0), a.GridDims - 1);
}
/// <summary>World-space center of the whole plot (== AnchorPos when baked correctly).</summary>
public static float3 PlotCenter(in BaseAnchor a)
{
float2 centerXZ = a.GridOrigin.xz + (float2)a.GridDims * a.CellSize * 0.5f;
return new float3(centerXZ.x, a.GridOrigin.y, centerXZ.y);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 30b3681e3ce98b241a7ceb7af7e62962
@@ -0,0 +1,12 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Tag marking the shared home-base storage container. All state lives in the entity's
/// <see cref="StorageEntry"/> buffer. In M5 there is exactly one (server-spawned at a fixed base
/// cell), so server systems resolve it as a singleton. Server-authoritative and world-resident, so
/// its contents survive a player disconnect (no disk persistence yet).
/// </summary>
public struct SharedStorageContainer : IComponentData { }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8de8d91f5d8f0a64c87b0847ae85c564
@@ -0,0 +1,23 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// One (item, count) row in a shared storage container's contents. The container's DynamicBuffer of
/// these is the server-authoritative shared inventory; it is a GhostField buffer so the server's
/// mutations replicate to every client. The container is an ownerless INTERPOLATED ghost, so no
/// OwnerSendType / GhostOwner applies (those are for owner-predicted ghosts like the player).
/// ItemId is an opaque id; a richer item model (and the M7 machine input/output generalisation)
/// layers on top without changing replication.
/// </summary>
[InternalBufferCapacity(16)]
public struct StorageEntry : IBufferElementData
{
/// <summary>Opaque item identifier (0 = empty/unused).</summary>
[GhostField] public ushort ItemId;
/// <summary>Quantity of this item in the container.</summary>
[GhostField] public int Count;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f017cb14ca58bc54191ad3f356541ca6
@@ -0,0 +1,62 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Pure, deterministic merge logic for a shared storage container's <see cref="StorageEntry"/> buffer.
/// No RNG / wall-clock, so server and (future) prediction agree. Deposit merges into an existing row
/// for the same item or appends a new row; Withdraw decrements and drops a row that hits zero, clamping
/// to available. DynamicBuffer is a handle, so mutations apply to the underlying entity buffer.
/// Unit-tested in EditMode via a plain Entities world.
/// </summary>
public static class StorageMath
{
/// <summary>Add <paramref name="count"/> of <paramref name="itemId"/>, merging into an existing row if present. No-op for count &lt;= 0 or itemId 0.</summary>
public static void Deposit(DynamicBuffer<StorageEntry> buffer, ushort itemId, int count)
{
if (count <= 0 || itemId == 0)
return;
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i].ItemId == itemId)
{
var entry = buffer[i];
entry.Count += count;
buffer[i] = entry;
return;
}
}
buffer.Add(new StorageEntry { ItemId = itemId, Count = count });
}
/// <summary>
/// Remove up to <paramref name="count"/> of <paramref name="itemId"/>, clamped to what is available.
/// Drops the row when it reaches zero. Returns the amount actually withdrawn (0 if none). No-op for
/// count &lt;= 0 or itemId 0.
/// </summary>
public static int Withdraw(DynamicBuffer<StorageEntry> buffer, ushort itemId, int count)
{
if (count <= 0 || itemId == 0)
return 0;
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i].ItemId == itemId)
{
var entry = buffer[i];
int taken = entry.Count < count ? entry.Count : count;
entry.Count -= taken;
if (entry.Count <= 0)
buffer.RemoveAt(i);
else
buffer[i] = entry;
return taken;
}
}
return 0;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c20f98fb70e66c94193c16e4bfd7f1b4
@@ -0,0 +1,33 @@
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Client -&gt; server request to deposit into or withdraw from the shared storage container. A one-off
/// action, so it is an RPC (not a per-tick predicted input). Op is stored as a byte (see
/// <see cref="StorageOp"/>) rather than an enum to keep the generated serializer trivial and avoid the
/// cross-assembly enum-codegen hazard. No target entity is carried: M5 has a single shared container,
/// which the server resolves as a singleton (entity refs are not stable across worlds).
/// </summary>
public struct StorageOpRequest : IRpcCommand
{
/// <summary>Operation code (see <see cref="StorageOp"/>): 0 = deposit, 1 = withdraw.</summary>
public byte Op;
/// <summary>Item to deposit/withdraw.</summary>
public ushort ItemId;
/// <summary>Quantity to deposit/withdraw (server clamps withdraw to available).</summary>
public int Count;
}
/// <summary>Operation codes for <see cref="StorageOpRequest.Op"/> (byte to keep RPC serialization trivial).</summary>
public static class StorageOp
{
/// <summary>Add items to the shared container.</summary>
public const byte Deposit = 0;
/// <summary>Remove items from the shared container.</summary>
public const byte Withdraw = 1;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dc9ec88867d746e45b9204331b5bab51
@@ -0,0 +1,20 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Singleton baked into the gameplay subscene, holding the baked storage-container ghost prefab and
/// the base-grid cell to spawn it at. A one-shot server system instantiates the prefab at
/// BaseGridMath.CellToWorld(anchor, Cell) and then destroys this singleton. Mirrors the
/// UpgradePickupSpawner / PlayerSpawner pattern.
/// </summary>
public struct StorageSpawner : IComponentData
{
/// <summary>Baked storage-container ghost prefab to instantiate.</summary>
public Entity Prefab;
/// <summary>Base-grid cell at which to place the container (cell center, on the base plane).</summary>
public int2 Cell;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6a2e4fad83fa03b4890d736b388a9917