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,116 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-only sender for <see cref="EquipRequest"/> / <see cref="UnequipRequest"/> RPCs. One-off actions, so
|
||||
/// RPCs (not per-tick input); the server applies them authoritatively in
|
||||
/// <see cref="ProjectM.Server.EquipSystem"/>. Number keys 1-9 equip the Nth EQUIPPABLE item in the local bag
|
||||
/// (resources are skipped via the catalog); U unequips the weapon. Managed SystemBase because it reads the
|
||||
/// managed Input System; Input System types are fully qualified and <c>using UnityEngine.InputSystem;</c> is
|
||||
/// omitted (it defines a colliding PlayerInput type). An <c>#if UNITY_EDITOR</c> static hook drives the same
|
||||
/// path from execute_code for headless validation. The HUD click-to-equip (HudSystem) is the primary UX;
|
||||
/// these keys are the reachable fallback. Wire types are unconditional; only this send SYSTEM is gated.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class EquipSendSystem : SystemBase
|
||||
{
|
||||
struct PendingEquip { public bool Unequip; public ushort ItemId; public byte Slot; }
|
||||
|
||||
static readonly System.Collections.Generic.Queue<PendingEquip> s_Pending =
|
||||
new System.Collections.Generic.Queue<PendingEquip>();
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue an equip of <paramref name="itemId"/> from the bag.</summary>
|
||||
public static void Equip(ushort itemId) =>
|
||||
s_Pending.Enqueue(new PendingEquip { Unequip = false, ItemId = itemId });
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue an unequip of <paramref name="slot"/> (an EquipSlotId).</summary>
|
||||
public static void Unequip(byte slot) =>
|
||||
s_Pending.Enqueue(new PendingEquip { Unequip = true, Slot = slot });
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if (!SystemAPI.TryGetSingletonEntity<NetworkId>(out var connection))
|
||||
return;
|
||||
|
||||
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
||||
if (keyboard != null)
|
||||
{
|
||||
int n = NumberKeyPressed(keyboard);
|
||||
if (n >= 0)
|
||||
{
|
||||
ushort itemId = NthEquippableBagItem(n);
|
||||
if (itemId != 0) SendEquip(connection, itemId);
|
||||
}
|
||||
if (keyboard.uKey.wasPressedThisFrame)
|
||||
SendUnequip(connection, EquipSlotId.Weapon);
|
||||
}
|
||||
|
||||
while (s_Pending.Count > 0)
|
||||
{
|
||||
var p = s_Pending.Dequeue();
|
||||
if (p.Unequip) SendUnequip(connection, p.Slot);
|
||||
else SendEquip(connection, p.ItemId);
|
||||
}
|
||||
}
|
||||
|
||||
static int NumberKeyPressed(UnityEngine.InputSystem.Keyboard kb)
|
||||
{
|
||||
if (kb.digit1Key.wasPressedThisFrame) return 0;
|
||||
if (kb.digit2Key.wasPressedThisFrame) return 1;
|
||||
if (kb.digit3Key.wasPressedThisFrame) return 2;
|
||||
if (kb.digit4Key.wasPressedThisFrame) return 3;
|
||||
if (kb.digit5Key.wasPressedThisFrame) return 4;
|
||||
if (kb.digit6Key.wasPressedThisFrame) return 5;
|
||||
if (kb.digit7Key.wasPressedThisFrame) return 6;
|
||||
if (kb.digit8Key.wasPressedThisFrame) return 7;
|
||||
if (kb.digit9Key.wasPressedThisFrame) return 8;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Resolve the Nth EQUIPPABLE distinct item in the local player's bag (skips resources via the catalog).</summary>
|
||||
ushort NthEquippableBagItem(int n)
|
||||
{
|
||||
bool haveDb = SystemAPI.TryGetSingleton<ItemDatabase>(out var db);
|
||||
foreach (var bag in SystemAPI.Query<DynamicBuffer<InventorySlot>>().WithAll<GhostOwnerIsLocal, PlayerTag>())
|
||||
{
|
||||
int idx = 0;
|
||||
for (int i = 0; i < bag.Length; i++)
|
||||
{
|
||||
ushort id = bag[i].ItemId;
|
||||
if (id == 0 || bag[i].Count <= 0) continue;
|
||||
if (haveDb && db.Value.IsCreated)
|
||||
{
|
||||
ref var b = ref db.Value.Value;
|
||||
if (!b.TryGetItem(id, out var def) || def.EquipSlot >= EquipSlotId.Count) continue;
|
||||
}
|
||||
if (idx == n) return id;
|
||||
idx++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SendEquip(Entity connection, ushort itemId)
|
||||
{
|
||||
var req = EntityManager.CreateEntity();
|
||||
EntityManager.AddComponentData(req, new EquipRequest { ItemId = itemId });
|
||||
EntityManager.AddComponentData(req, new SendRpcCommandRequest { TargetConnection = connection });
|
||||
}
|
||||
|
||||
void SendUnequip(Entity connection, byte slot)
|
||||
{
|
||||
var req = EntityManager.CreateEntity();
|
||||
EntityManager.AddComponentData(req, new UnequipRequest { Slot = slot });
|
||||
EntityManager.AddComponentData(req, new SendRpcCommandRequest { TargetConnection = connection });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user