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:
@@ -71,7 +71,7 @@ namespace ProjectM.Client
|
||||
float _prevHp, _flash;
|
||||
bool _haveHp;
|
||||
// personal inventory panel (read-only; toggled with I)
|
||||
VisualElement _invPanel, _invList;
|
||||
VisualElement _invPanel, _invList, _equipList;
|
||||
bool _invOpen;
|
||||
|
||||
EntityQuery _huskQuery;
|
||||
@@ -299,8 +299,10 @@ namespace ProjectM.Client
|
||||
if (_invOpen && found)
|
||||
{
|
||||
EntityManager.CompleteDependencyBeforeRO<InventorySlot>();
|
||||
EntityManager.CompleteDependencyBeforeRO<EquipmentSlot>();
|
||||
bool haveItemDb = SystemAPI.TryGetSingleton<ItemDatabase>(out var itemDb);
|
||||
_invPanel.style.display = DisplayStyle.Flex;
|
||||
|
||||
_invList.Clear();
|
||||
int shown = 0;
|
||||
foreach (var bag in SystemAPI.Query<DynamicBuffer<InventorySlot>>()
|
||||
@@ -310,13 +312,27 @@ namespace ProjectM.Client
|
||||
{
|
||||
var slot = bag[i];
|
||||
if (slot.ItemId == 0 || slot.Count <= 0) continue;
|
||||
AddInvRow(ItemName(haveItemDb, itemDb, slot.ItemId), ItemTint(slot.ItemId), slot.Count);
|
||||
AddInvRow(slot.ItemId, ItemName(haveItemDb, itemDb, slot.ItemId), ItemTint(slot.ItemId), slot.Count,
|
||||
IsEquippable(haveItemDb, itemDb, slot.ItemId));
|
||||
shown++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (shown == 0)
|
||||
_invList.Add(HudUi.Text("(empty)", 13, MenuUi.SubCol, TextAnchor.MiddleLeft));
|
||||
|
||||
_equipList.Clear();
|
||||
foreach (var slots in SystemAPI.Query<DynamicBuffer<EquipmentSlot>>()
|
||||
.WithAll<GhostOwnerIsLocal, PlayerTag>())
|
||||
{
|
||||
for (byte s = 0; s < EquipSlotId.Count && s < slots.Length; s++)
|
||||
{
|
||||
ushort id = slots[s].ItemId;
|
||||
string label = SlotName(s) + ": " + (id == 0 ? "-" : ItemName(haveItemDb, itemDb, id));
|
||||
AddEquipRow(s, label, id != 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -776,7 +792,15 @@ namespace ProjectM.Client
|
||||
_invList.pickingMode = PickingMode.Ignore;
|
||||
_invPanel.Add(_invList);
|
||||
|
||||
var hint = HudUi.Text("I close - G deposit all at base", 11, MenuUi.SubCol, TextAnchor.MiddleLeft);
|
||||
var equipHeader = HudUi.Display("EQUIPMENT", 14, AetherCyan, TextAnchor.MiddleLeft);
|
||||
equipHeader.style.marginTop = 8; equipHeader.style.marginBottom = 4;
|
||||
_invPanel.Add(equipHeader);
|
||||
|
||||
_equipList = new VisualElement();
|
||||
_equipList.pickingMode = PickingMode.Ignore;
|
||||
_invPanel.Add(_equipList);
|
||||
|
||||
var hint = HudUi.Text("I close - click item=equip / slot=unequip - G deposit", 11, MenuUi.SubCol, TextAnchor.MiddleLeft);
|
||||
hint.style.marginTop = 8;
|
||||
_invPanel.Add(hint);
|
||||
|
||||
@@ -784,16 +808,22 @@ namespace ProjectM.Client
|
||||
root.Add(_invPanel);
|
||||
}
|
||||
|
||||
void AddInvRow(string name, Color tint, int count)
|
||||
void AddInvRow(ushort itemId, string name, Color tint, int count, bool equippable)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.justifyContent = Justify.SpaceBetween;
|
||||
row.style.minWidth = 196;
|
||||
row.style.marginTop = 2;
|
||||
row.pickingMode = PickingMode.Ignore;
|
||||
row.Add(HudUi.Text(name, 13, tint, TextAnchor.MiddleLeft));
|
||||
row.Add(HudUi.Text(name + (equippable ? " (equip)" : ""), 13, tint, TextAnchor.MiddleLeft));
|
||||
row.Add(HudUi.Display("x" + count, 13, Color.white, TextAnchor.MiddleRight));
|
||||
if (equippable)
|
||||
{
|
||||
row.pickingMode = PickingMode.Position;
|
||||
ushort id = itemId;
|
||||
row.RegisterCallback<ClickEvent>(_ => EquipSendSystem.Equip(id));
|
||||
}
|
||||
else row.pickingMode = PickingMode.Ignore;
|
||||
_invList.Add(row);
|
||||
}
|
||||
|
||||
@@ -817,6 +847,44 @@ namespace ProjectM.Client
|
||||
if (id == ResourceId.Biomass) return BioGreen;
|
||||
return new Color(0.85f, 0.85f, 0.9f);
|
||||
}
|
||||
static bool IsEquippable(bool haveDb, ItemDatabase db, ushort id)
|
||||
{
|
||||
if (!haveDb || !db.Value.IsCreated) return false;
|
||||
ref var b = ref db.Value.Value;
|
||||
return b.TryGetItem(id, out var def) && def.EquipSlot < EquipSlotId.Count;
|
||||
}
|
||||
|
||||
static string SlotName(byte slot)
|
||||
{
|
||||
switch (slot)
|
||||
{
|
||||
case EquipSlotId.Weapon: return "Weapon";
|
||||
case EquipSlotId.Armor: return "Armor";
|
||||
case EquipSlotId.Trinket: return "Trinket";
|
||||
case EquipSlotId.Tool: return "Tool";
|
||||
default: return "Slot " + slot;
|
||||
}
|
||||
}
|
||||
|
||||
void AddEquipRow(byte slot, string label, bool occupied)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.justifyContent = Justify.SpaceBetween;
|
||||
row.style.minWidth = 196;
|
||||
row.style.marginTop = 2;
|
||||
row.Add(HudUi.Text(label, 13, occupied ? AetherCyan : MenuUi.SubCol, TextAnchor.MiddleLeft));
|
||||
if (occupied)
|
||||
{
|
||||
row.pickingMode = PickingMode.Position;
|
||||
byte s = slot;
|
||||
row.RegisterCallback<ClickEvent>(_ => EquipSendSystem.Unequip(s));
|
||||
row.Add(HudUi.Text("unequip", 11, new Color(1f, 0.6f, 0.5f), TextAnchor.MiddleRight));
|
||||
}
|
||||
else row.pickingMode = PickingMode.Ignore;
|
||||
_equipList.Add(row);
|
||||
}
|
||||
|
||||
|
||||
static Color ResourceTint(byte resId)
|
||||
=> resId == ResourceId.Aether ? AetherCyan : resId == ResourceId.Biomass ? BioGreen : OreAmber;
|
||||
|
||||
Reference in New Issue
Block a user