Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

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;
}
}