Equipment: weapon-granted abilities + gear mods (DR-027 Phase 1)
Equipment slots reusing the AbilityRef/StatModifier machinery: EquipmentSlot [GhostField] buffer (index=slot), server-only event-driven EquipSystem (RPC). Weapon -> AbilityRef.Id swaps the attack (prefab + base stats, prediction-correct); gear -> StatModifiers tagged a reserved per-slot EquipSourceId, stripped target-agnostically via RemoveBySourceId. Item mods are INLINE on ItemDefBlob (a nested BlobArray reads empty under the by-value TryGetItem copy). Atomic equip-over swap (no item loss); DefaultAbility restores the unarmed ability on weapon-unequip. Client keys + build-safe hooks; HUD equipment panel + click-to-equip. 4 catalog weapon/gear items wired + re-baked. Play-validated host+client: weapon equip swaps AbilityRef on both worlds, gear folds into EffectiveCharacterStats, unequip reverses + restores DefaultAbility, all replicated to the owner. See DR-027. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// The player's "unarmed" / base ability id, baked from PlayerAuthoring.PrimaryAbility. Restored into
|
||||
/// <see cref="AbilityRef"/>.Id by EquipSystem when a weapon is unequipped. NOT replicated (it never changes,
|
||||
/// so a [GhostField] would waste snapshot bytes and there is no client consumer). AbilityRef itself cannot
|
||||
/// serve double duty because EquipSystem overwrites AbilityRef.Id when a weapon is equipped — this preserves
|
||||
/// the immutable default to fall back to. Server-read only.
|
||||
/// </summary>
|
||||
public struct DefaultAbility : IComponentData
|
||||
{
|
||||
/// <summary>The <see cref="AbilityId"/> (as a byte) the player fires with no weapon equipped.</summary>
|
||||
public byte Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c6831e7f8bb98d448917f88dcbe12db
|
||||
@@ -0,0 +1,27 @@
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Client -> server request to equip an item from the sender's personal inventory into the slot the
|
||||
/// catalog assigns it (<see cref="ItemDefBlob.EquipSlot"/>). A one-off action, so it is an RPC (not a
|
||||
/// per-tick predicted input); applied exactly once server-only in the plain SimulationSystemGroup. Carries
|
||||
/// only the ItemId — the server derives the target slot from the catalog, so a client can't force a weapon
|
||||
/// into the armor slot. Unconditional wire type (no #if); the server resolves the sender via SourceConnection.
|
||||
/// </summary>
|
||||
public struct EquipRequest : IRpcCommand
|
||||
{
|
||||
/// <summary>The inventory item to equip; the server resolves its slot + effects from the catalog.</summary>
|
||||
public ushort ItemId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client -> server request to unequip whatever occupies <see cref="Slot"/> (an <see cref="EquipSlotId"/>),
|
||||
/// returning the item to the personal inventory and stripping its effects. Unconditional wire type.
|
||||
/// </summary>
|
||||
public struct UnequipRequest : IRpcCommand
|
||||
{
|
||||
/// <summary>The <see cref="EquipSlotId"/> to clear.</summary>
|
||||
public byte Slot;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad5475188aa05de45a231f937b19f069
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Equipment-slot ids (a byte, not an enum, per the cross-assembly enum-in-Burst hazard). The player's
|
||||
/// <see cref="EquipmentSlot"/> buffer holds one row PER slot in this fixed order (the buffer index IS the
|
||||
/// slot), so these double as both the catalog's <see cref="ItemDefBlob.EquipSlot"/> value and the buffer
|
||||
/// index. The Weapon slot grants its item's <see cref="ItemDefBlob.GrantedAbilityId"/> into AbilityRef;
|
||||
/// every slot grants the item's inline stat mods. <see cref="Tool"/> is reserved for Phase 2 (tool-gated
|
||||
/// harvesting). 255 = not equippable.
|
||||
/// </summary>
|
||||
public static class EquipSlotId
|
||||
{
|
||||
/// <summary>Weapon: grants the item's ability (AbilityRef.Id) + its stat mods.</summary>
|
||||
public const byte Weapon = 0;
|
||||
|
||||
/// <summary>Armor: grants the item's stat mods.</summary>
|
||||
public const byte Armor = 1;
|
||||
|
||||
/// <summary>Trinket: grants the item's stat mods.</summary>
|
||||
public const byte Trinket = 2;
|
||||
|
||||
/// <summary>Tool (axe/pickaxe) — reserved for Phase 2 tool-gated harvesting.</summary>
|
||||
public const byte Tool = 3;
|
||||
|
||||
/// <summary>Number of equipment slots (the baked <see cref="EquipmentSlot"/> buffer length).</summary>
|
||||
public const byte Count = 4;
|
||||
|
||||
/// <summary>Sentinel: this item is not equippable.</summary>
|
||||
public const byte None = 255;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce8addc5ae6832a449d1b6ad459ab21a
|
||||
@@ -0,0 +1,26 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// One equipment slot on the player. The per-player buffer holds exactly <see cref="EquipSlotId.Count"/>
|
||||
/// rows in fixed slot order (the buffer INDEX is the slot — Weapon=0/Armor=1/Trinket=2/Tool=3), so only the
|
||||
/// equipped item id needs to replicate; there is no separate Slot field to desync. A [GhostField]
|
||||
/// <see cref="SendToOwnerType.All"/> buffer (a <see cref="StatModifier"/>/<see cref="InventorySlot"/> twin)
|
||||
/// so the owning client's HUD can show its loadout; the server is the SOLE writer (EquipSystem).
|
||||
///
|
||||
/// The actual effects — AbilityRef.Id from the Weapon slot + StatModifiers per slot — are applied
|
||||
/// EVENT-DRIVEN by EquipSystem (once per equip/unequip), NOT re-derived from this buffer each tick; this
|
||||
/// buffer is the replicated record of WHAT is equipped (HUD-facing + persistence-ready), not the effect.
|
||||
/// NOTE: adding this [GhostField] buffer changes the player ghost serialization hash → the player
|
||||
/// prefab/subscene MUST be re-baked consistently in both worlds (see <see cref="InventorySlot"/>).
|
||||
/// </summary>
|
||||
[GhostComponent(OwnerSendType = SendToOwnerType.All)]
|
||||
[InternalBufferCapacity(4)]
|
||||
public struct EquipmentSlot : IBufferElementData
|
||||
{
|
||||
/// <summary>Item equipped in this slot (0 = empty). The buffer INDEX is the <see cref="EquipSlotId"/>.</summary>
|
||||
[GhostField] public ushort ItemId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac0f8812307d3ef43bbed63d1e3fb737
|
||||
@@ -76,6 +76,27 @@ namespace ProjectM.Simulation
|
||||
return taken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-mutating check: would depositing <paramref name="count"/> of <paramref name="itemId"/> FULLY fit
|
||||
/// (top-up existing stacks + new stacks within <paramref name="maxSlots"/>)? Used by the equip swap to
|
||||
/// guarantee the swapped-out item has room BEFORE any withdrawal (no item loss). Mirrors Deposit's space math.
|
||||
/// </summary>
|
||||
public static bool CanDeposit(DynamicBuffer<InventorySlot> buffer, ushort itemId, int count, int stackMax, int maxSlots)
|
||||
{
|
||||
if (count <= 0) return true;
|
||||
if (itemId == 0) return false;
|
||||
if (stackMax < 1) stackMax = int.MaxValue;
|
||||
|
||||
long space = 0;
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
if (buffer[i].ItemId == itemId)
|
||||
space += stackMax - buffer[i].Count;
|
||||
int freeSlots = maxSlots - buffer.Length;
|
||||
if (freeSlots > 0)
|
||||
space += (long)freeSlots * stackMax;
|
||||
return space >= count;
|
||||
}
|
||||
|
||||
/// <summary>Total quantity of <paramref name="itemId"/> across all stacks (0 if absent).</summary>
|
||||
public static int CountOf(DynamicBuffer<InventorySlot> buffer, ushort itemId)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,20 @@ namespace ProjectM.Simulation
|
||||
/// is baked NOW because the project's progression axis is gear tiers, so Phase 2/3 tier gating is a
|
||||
/// content-only edit.
|
||||
/// </summary>
|
||||
/// <summary>One stat-modifier grant on an equippable item, stored INLINE (NOT a nested BlobArray: a nested
|
||||
/// BlobArray is a relative-offset pointer that corrupts the moment <see cref="ItemDatabaseBlob.TryGetItem"/>
|
||||
/// returns the containing <see cref="ItemDefBlob"/> BY VALUE — the same copy hazard the class note warns about).
|
||||
/// Target 255 = unused.</summary>
|
||||
public struct ItemModSpec
|
||||
{
|
||||
/// <summary><see cref="StatTarget"/> as a byte; 255 = unused slot.</summary>
|
||||
public byte Target;
|
||||
/// <summary><see cref="ModOp"/> as a byte.</summary>
|
||||
public byte Op;
|
||||
/// <summary>Magnitude (flat amount or fractional percent).</summary>
|
||||
public float Value;
|
||||
}
|
||||
|
||||
public struct ItemDefBlob
|
||||
{
|
||||
/// <summary>Stable item id (ushort; 1-3 reserved for the existing resources, keep stable for saves).</summary>
|
||||
@@ -33,8 +47,32 @@ namespace ProjectM.Simulation
|
||||
/// <summary>Max units that stack in a single inventory slot (1 for non-stacking equipment).</summary>
|
||||
public int StackMax;
|
||||
|
||||
/// <summary>Equip slot (see <see cref="EquipSlotId"/>); 255 = not equippable.</summary>
|
||||
public byte EquipSlot;
|
||||
|
||||
/// <summary>AbilityId granted when equipped in the Weapon slot (0 = none); the equip handler writes it into AbilityRef.Id.</summary>
|
||||
public byte GrantedAbilityId;
|
||||
|
||||
/// <summary>Up to <see cref="MaxMods"/> INLINE stat-mod grants applied while equipped (Target 255 = unused). Inline, not a nested BlobArray.</summary>
|
||||
public ItemModSpec Mod0, Mod1, Mod2, Mod3;
|
||||
|
||||
/// <summary>Designer-facing display name (shown in the HUD inventory panel).</summary>
|
||||
public FixedString64Bytes Name;
|
||||
|
||||
/// <summary>Number of inline mod slots.</summary>
|
||||
public const int MaxMods = 4;
|
||||
|
||||
/// <summary>Indexed access to the inline mod slots (returns a copy — safe, ItemModSpec holds no BlobArray).</summary>
|
||||
public ItemModSpec GetMod(int i)
|
||||
{
|
||||
switch (i)
|
||||
{
|
||||
case 0: return Mod0;
|
||||
case 1: return Mod1;
|
||||
case 2: return Mod2;
|
||||
default: return Mod3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user