using System; using System.Collections.Generic; namespace AcDream.Core.Items; // ───────────────────────────────────────────────────────────────────── // Scaffold for R6 — items + inventory data model. // Full research: docs/research/deepdives/r06-items-inventory.md // ───────────────────────────────────────────────────────────────────── /// /// AC's ItemType is a 32-bit flags enum — a single dat weenie can /// assert multiple type bits. From ACE.Entity.Enum.ItemType /// cross-checked against the decompile paperdoll tooltip dispatcher. /// Full bit list in the research doc §1. /// [Flags] public enum ItemType : uint { None = 0, MeleeWeapon = 0x00000001, Armor = 0x00000002, Clothing = 0x00000004, Jewelry = 0x00000008, Creature = 0x00000010, Food = 0x00000020, Money = 0x00000040, Misc = 0x00000080, MissileWeapon = 0x00000100, Container = 0x00000200, Useless = 0x00000400, Gem = 0x00000800, SpellComponents = 0x00001000, Writable = 0x00002000, Key = 0x00004000, Caster = 0x00008000, Portal = 0x00010000, Lockable = 0x00020000, PromissoryNote = 0x00040000, ManaStone = 0x00080000, Service = 0x00100000, MagicWieldable = 0x00200000, CraftCookingBase = 0x00400000, CraftAlchemyBase = 0x00800000, CraftFletchingBase = 0x01000000, CraftAlchemyIntermediate= 0x02000000, CraftCookingIntermediate= 0x04000000, CraftFletchingIntermediate = 0x08000000, LifeStone = 0x10000000, TinkeringTool = 0x20000000, TinkeringMaterial = 0x40000000, Gameboard = 0x80000000u, Vestements = Armor | Clothing, Weapon = MeleeWeapon | MissileWeapon | Caster, WeaponOrCaster = Weapon, Item = Weapon | Armor | Clothing | Jewelry | Container, } /// /// Equipment slot bitmask. 31 slots from head to Aetheria. Paperdoll /// widget offsets +0x604..+0x660 in the retail panel correspond /// to these bits 1:1 (see r06 §2 and UI slice 05 paperdoll section). /// [Flags] public enum EquipMask : uint { None = 0, HeadWear = 0x00000001, ChestWear = 0x00000002, AbdomenWear = 0x00000004, UpperArmWear = 0x00000008, LowerArmWear = 0x00000010, HandWear = 0x00000020, UpperLegWear = 0x00000040, LowerLegWear = 0x00000080, FootWear = 0x00000100, ChestArmor = 0x00000200, AbdomenArmor = 0x00000400, UpperArmArmor = 0x00000800, LowerArmArmor = 0x00001000, HandArmor = 0x00002000, UpperLegArmor = 0x00004000, LowerLegArmor = 0x00008000, FootArmor = 0x00010000, Necklace = 0x00020000, LeftBracelet = 0x00040000, RightBracelet = 0x00080000, LeftRing = 0x00100000, RightRing = 0x00200000, MeleeWeapon = 0x00400000, Shield = 0x00800000, MissileWeapon = 0x01000000, Held = 0x02000000, // lit torch, book in hand MissileAmmo = 0x04000000, Cloak = 0x08000000, TrinketOne = 0x10000000, AetheriaRed = 0x20000000, AetheriaYellow= 0x40000000, AetheriaBlue = 0x80000000u, } /// /// AC's property model is split across 7 typed tables. See r06 §3 for /// the full property enumeration. This struct is a thin wrapper; real /// bundles come over the wire in ObjectDesc / IdentifyResponse. /// public sealed class PropertyBundle { public Dictionary Ints { get; } = new(); public Dictionary Int64s { get; } = new(); public Dictionary Bools { get; } = new(); public Dictionary Floats { get; } = new(); public Dictionary Strings { get; } = new(); public Dictionary DataIds { get; } = new(); public Dictionary InstanceIds { get; } = new(); public int GetInt (uint k, int def = 0) => Ints.TryGetValue(k, out var v) ? v : def; public bool GetBool (uint k, bool def = false) => Bools.TryGetValue(k, out var v) ? v : def; public double GetFloat (uint k, double def = 0) => Floats.TryGetValue(k, out var v) ? v : def; public string GetString(uint k, string def = "") => Strings.TryGetValue(k, out var v) ? v : def; } /// /// Per-item live state. The server owns item identity (ObjectId); /// acdream mirrors properties here on CreateObject and updates /// via UpdateProperty* messages. /// public sealed class ItemInstance { public uint ObjectId { get; init; } public uint WeenieClassId { get; init; } // "blueprint" public string Name { get; set; } = ""; public ItemType Type { get; set; } public EquipMask ValidLocations { get; set; } public EquipMask CurrentlyEquippedLocation { get; set; } public uint IconId { get; set; } // 0x06xxxxxx public uint IconUnderlayId{ get; set; } // "magic" underlay public uint IconOverlayId { get; set; } // "enchanted" overlay public int StackSize { get; set; } = 1; public int StackSizeMax { get; set; } = 1; public int Burden { get; set; } // per-stack total public int Value { get; set; } // pyreals public uint ContainerId { get; set; } // parent container ObjectId, or 0 public int ContainerSlot { get; set; } = -1; public bool Attuned { get; set; } public bool Bonded { get; set; } public PropertyBundle Properties { get; } = new(); } /// /// Container = inventory pack. Hierarchy is strictly 2-deep: character /// → side packs; a side pack cannot hold another side pack (r06 §7). /// public sealed class Container { public uint ObjectId { get; init; } public int Capacity { get; set; } = 102; // main inv default public int SideCapacity { get; set; } = 0; // 0 for side-pack public int BurdenLimit { get; set; } public List Items { get; } = new(); public List SidePacks { get; } = new(); // empty for side-pack public bool IsSidePack => SideCapacity == 0; } /// /// Burden math — r06 §6. maxBurden = 150 × Strength + Strength × bonusBurden; /// carry limit is 3 × maxBurden before you can't pick up at all. /// public static class BurdenMath { public const int BurdenPerStrength = 150; public static int ComputeMax(int strength, int bonusBurden) => BurdenPerStrength * strength + strength * bonusBurden; public static int ComputeCarryLimit(int strength, int bonusBurden) => 3 * ComputeMax(strength, bonusBurden); /// /// Retail's "encumbered" multiplier interpolates between 1.0 at /// zero burden and a low value at max. See r06 §6 for the curve. /// public static float ComputeEncumbranceMod(int currentBurden, int maxBurden) { if (maxBurden <= 0) return 1f; float ratio = (float)currentBurden / maxBurden; // Roughly 1.0 until 50%, then linear decay to ~0.7 at 100%, 0.1 at 300%. if (ratio <= 0.5f) return 1f; if (ratio <= 1.0f) return 1f - (ratio - 0.5f) * 0.6f; // 1.0 → 0.7 if (ratio <= 3.0f) return 0.7f - (ratio - 1.0f) * 0.3f; // 0.7 → 0.1 return 0.1f; } }