using Theriapolis.Core.Data; namespace Theriapolis.Core.Items; /// /// A character's items: everything they're carrying plus the per-slot equipped /// references. Equipped items remain in — equipping does /// not move the instance, it just sets /// and registers it in for fast slot lookup. /// /// Phase 5 M2 ships the basic plumbing (add/remove/equip/unequip with size /// checks). Encumbrance speed effects, proficiency-driven attack disadvantage, /// and the equip UI itself land in M3. /// public sealed class Inventory { public List Items { get; } = new(); /// Slot → currently-equipped instance. Missing key = empty slot. public Dictionary Equipped { get; } = new(); public float TotalWeightLb { get { float w = 0f; foreach (var i in Items) w += i.TotalWeightLb; return w; } } public ItemInstance Add(ItemDef def, int qty = 1) { var inst = new ItemInstance(def, qty); Items.Add(inst); return inst; } public bool Remove(ItemInstance inst) { if (inst.EquippedAt is { } slot) Equipped.Remove(slot); return Items.Remove(inst); } /// /// Equip into . Returns false /// with an error message if the slot is occupied, the item isn't in this /// inventory, or there's a basic structural mismatch (two-handed weapon /// when OffHand is taken, etc.). /// /// Note: this method does NOT enforce proficiency or size disadvantage — /// those are computed at attack-resolution time so the player can equip a /// wrong-size weapon and accept the penalty. Hard structural blocks only. /// public bool TryEquip(ItemInstance item, EquipSlot slot, out string error) { error = ""; if (!Items.Contains(item)) { error = "Item is not in this inventory."; return false; } if (Equipped.TryGetValue(slot, out var existing) && existing != item) { error = $"Slot {slot} is already occupied by {existing.Def.Name}."; return false; } // Two-handed weapon must clear OffHand first. if (slot == EquipSlot.MainHand && HasProperty(item.Def, "two_handed") && Equipped.TryGetValue(EquipSlot.OffHand, out var offHand)) { error = $"Cannot wield two-handed weapon: OffHand holds {offHand.Def.Name}."; return false; } // Equipping into OffHand while MainHand has a two-handed weapon is invalid. if (slot == EquipSlot.OffHand && Equipped.TryGetValue(EquipSlot.MainHand, out var mainHand) && HasProperty(mainHand.Def, "two_handed")) { error = $"Cannot use OffHand: MainHand holds two-handed {mainHand.Def.Name}."; return false; } // Natural-weapon enhancer must go into a NaturalWeapon* slot matching its declared anatomy. if (item.Def.Kind == "natural_weapon_enhancer") { var declared = EquipSlotExtensions.FromEnhancerSlot(item.Def.EnhancerSlot); if (declared is null) { error = $"Item '{item.Def.Id}' has invalid enhancer_slot '{item.Def.EnhancerSlot}'."; return false; } if (slot != declared.Value) { error = $"Enhancer '{item.Def.Name}' fits {declared.Value}, not {slot}."; return false; } } // Unequip the item from any prior slot first. if (item.EquippedAt is { } prior && prior != slot) Equipped.Remove(prior); Equipped[slot] = item; item.EquippedAt = slot; return true; } public bool TryUnequip(EquipSlot slot, out string error) { error = ""; if (!Equipped.TryGetValue(slot, out var inst)) { error = $"Slot {slot} is empty."; return false; } Equipped.Remove(slot); inst.EquippedAt = null; return true; } public ItemInstance? GetEquipped(EquipSlot slot) => Equipped.TryGetValue(slot, out var i) ? i : null; private static bool HasProperty(ItemDef def, string prop) { foreach (var p in def.Properties) if (string.Equals(p, prop, StringComparison.OrdinalIgnoreCase)) return true; return false; } }