137 lines
4.6 KiB
C#
137 lines
4.6 KiB
C#
|
|
using Theriapolis.Core.Data;
|
||
|
|
|
||
|
|
namespace Theriapolis.Core.Items;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// A character's items: everything they're carrying plus the per-slot equipped
|
||
|
|
/// references. Equipped items remain in <see cref="Items"/> — equipping does
|
||
|
|
/// not move the instance, it just sets <see cref="ItemInstance.EquippedAt"/>
|
||
|
|
/// and registers it in <see cref="Equipped"/> 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.
|
||
|
|
/// </summary>
|
||
|
|
public sealed class Inventory
|
||
|
|
{
|
||
|
|
public List<ItemInstance> Items { get; } = new();
|
||
|
|
|
||
|
|
/// <summary>Slot → currently-equipped instance. Missing key = empty slot.</summary>
|
||
|
|
public Dictionary<EquipSlot, ItemInstance> 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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Equip <paramref name="item"/> into <paramref name="slot"/>. 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.
|
||
|
|
/// </summary>
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|