Inventory: per-player items backbone (DR-026 Phase 0)
Data-driven ItemDatabase catalog + per-player replicated InventorySlot ([GhostField] OwnerSendType.All, a StatModifier twin). Harvest reroutes to the firing player's personal inventory (optional ComponentLookup<GhostOwner> in ResourceHarvestSystem; remainder/un-owned -> ledger); the G-key InventoryDepositRequest RPC moves the bag into the shared ledger the build economy spends. Catalog asset (Aether/Ore/Biomass + Stone Pickaxe) wired into the Gameplay subscene; read-only HUD inventory panel. ushort ItemId subsumes ResourceId; byte Category/Tier baked for gear-tier progression. Session-only (no SaveData bump). Play-validated host+client: catalog baked into both worlds, the re-baked player ghost carries InventorySlot with a clean handshake, a server write replicates to the client owner, the deposit RPC round-trips, and the HUD renders catalog names. See DR-026. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,11 +33,16 @@ namespace ProjectM.Server
|
||||
{
|
||||
const float k_ProjectileRadius = Tuning.HarvestProjectileRadius;
|
||||
|
||||
ComponentLookup<GhostOwner> m_GhostOwnerLookup;
|
||||
BufferLookup<InventorySlot> m_InvLookup;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<Projectile>();
|
||||
state.RequireForUpdate<ResourceLedger>();
|
||||
m_GhostOwnerLookup = state.GetComponentLookup<GhostOwner>(true);
|
||||
m_InvLookup = state.GetBufferLookup<InventorySlot>(false);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
@@ -46,6 +51,18 @@ namespace ProjectM.Server
|
||||
var ledgerEntity = SystemAPI.GetSingletonEntity<ResourceLedger>();
|
||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
|
||||
|
||||
// Resolve the harvesting player from the projectile's GhostOwner so yield lands in their PERSONAL
|
||||
// inventory. Owner read via a cached lookup (optional); the owner->player map + item catalog are
|
||||
// hoisted out of the per-hit sweep (invariant for the tick).
|
||||
m_GhostOwnerLookup.Update(ref state);
|
||||
m_InvLookup.Update(ref state);
|
||||
bool haveDb = SystemAPI.TryGetSingleton<ItemDatabase>(out var itemDb);
|
||||
|
||||
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
||||
foreach (var (owner, playerEntity) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, InventorySlot>().WithEntityAccess())
|
||||
playerByConn[owner.ValueRO.NetworkId] = playerEntity;
|
||||
|
||||
// Snapshot all harvest/clear targets (nodes + clutter) once this tick into a UNIFIED set.
|
||||
var tgtEntity = new NativeList<Entity>(Allocator.Temp);
|
||||
var tgtPos = new NativeList<float2>(Allocator.Temp);
|
||||
@@ -121,7 +138,30 @@ namespace ProjectM.Server
|
||||
// A positive baked yield must always make progress: a raw (int) truncation of a sub-1.0 per-hit
|
||||
// value would deposit 0 AND never decrement Remaining -> an immortal target that silently eats shots.
|
||||
int amount = math.max(1, (int)tgtYieldPerHit[bestIdx]);
|
||||
StorageMath.Deposit(ledger, tgtYieldId[bestIdx], amount);
|
||||
byte yieldId = tgtYieldId[bestIdx];
|
||||
|
||||
// Route the yield into the HARVESTING player's PERSONAL inventory. The projectile carries the
|
||||
// firing player's GhostOwner (AbilityFireSystem); the owner is read OPTIONALLY (cached lookup) so
|
||||
// an un-owned projectile (or a test projectile with no GhostOwner) falls through to the ledger.
|
||||
int remainder = amount;
|
||||
if (m_GhostOwnerLookup.HasComponent(projEntity)
|
||||
&& playerByConn.TryGetValue(m_GhostOwnerLookup[projEntity].NetworkId, out var player)
|
||||
&& m_InvLookup.HasBuffer(player))
|
||||
{
|
||||
int stackMax = Tuning.DefaultStackMax;
|
||||
if (haveDb && itemDb.Value.IsCreated)
|
||||
{
|
||||
ref var itemBlob = ref itemDb.Value.Value;
|
||||
if (itemBlob.TryGetItem(yieldId, out var def) && def.StackMax > 0)
|
||||
stackMax = def.StackMax;
|
||||
}
|
||||
var inv = m_InvLookup[player];
|
||||
remainder = InventoryMath.Deposit(inv, yieldId, amount, stackMax, Tuning.InventoryMaxSlots);
|
||||
}
|
||||
|
||||
// Unresolvable owner or a full bag: the remainder credits the shared ledger (no-loss valve).
|
||||
if (remainder > 0)
|
||||
StorageMath.Deposit(ledger, yieldId, remainder);
|
||||
int rem = tgtRemaining[bestIdx] - amount;
|
||||
tgtRemaining[bestIdx] = rem;
|
||||
ecb.DestroyEntity(projEntity);
|
||||
@@ -158,6 +198,7 @@ namespace ProjectM.Server
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
playerByConn.Dispose();
|
||||
destroyed.Dispose();
|
||||
tgtEntity.Dispose();
|
||||
tgtPos.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user