using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Myra.Graphics2D; using Myra.Graphics2D.Brushes; using Myra.Graphics2D.UI; using Theriapolis.Core.Items; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; namespace Theriapolis.Game.Screens; /// /// Phase 5 M3 inventory screen. Pushed by when the /// player presses TAB. Two-column layout: equipped slots on the left, bagged /// items on the right. Click an equipped slot to unequip; click a bagged /// item to equip into its natural slot (weapon → main hand, armor → body, /// shield → off hand, enhancer → matching natural-weapon slot). /// /// All mutations are direct on the character's ; /// recomputes AC/Speed automatically on /// next read, so no signals or events are needed. /// public sealed class InventoryScreen : IScreen { private readonly Character _character; private Game1 _game = null!; private Desktop _desktop = null!; private Label? _statusLabel; private bool _tabWasDown = true; private bool _escWasDown = true; public InventoryScreen(Character character) { _character = character ?? throw new System.ArgumentNullException(nameof(character)); } public void Initialize(Game1 game) { _game = game; BuildUI(); } private void BuildUI() { var root = new VerticalStackPanel { Spacing = 6, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(20), Padding = new Thickness(20, 12, 20, 12), Background = new SolidBrush(new Color(0, 0, 0, 220)), }; // Header root.Widgets.Add(new Label { Text = $"INVENTORY — {_character.Species.Name} {_character.ClassDef.Name} (Lv{_character.Level})", HorizontalAlignment = HorizontalAlignment.Center, }); root.Widgets.Add(new Label { Text = FormatStatLine(), HorizontalAlignment = HorizontalAlignment.Center, }); root.Widgets.Add(new Label { Text = " " }); // Two columns var columns = new HorizontalStackPanel { Spacing = 24 }; // Equipped column var equippedCol = new VerticalStackPanel { Spacing = 4, Width = 320 }; equippedCol.Widgets.Add(new Label { Text = "EQUIPPED:" }); foreach (EquipSlot slot in EquipSlotsToShow()) equippedCol.Widgets.Add(BuildEquippedSlotRow(slot)); columns.Widgets.Add(equippedCol); // Inventory column var bagCol = new VerticalStackPanel { Spacing = 4, Width = 380 }; bagCol.Widgets.Add(new Label { Text = $"INVENTORY ({_character.Inventory.Items.Count} stacks, {_character.Inventory.TotalWeightLb:F1} lb):", }); bool any = false; foreach (var item in _character.Inventory.Items) { // Skip equipped items in the bag list — they're shown in the equipped column. if (item.EquippedAt is not null) continue; any = true; bagCol.Widgets.Add(BuildBagItemRow(item)); } if (!any) bagCol.Widgets.Add(new Label { Text = " (everything is equipped)" }); columns.Widgets.Add(bagCol); root.Widgets.Add(columns); // Status line root.Widgets.Add(new Label { Text = " " }); _statusLabel = new Label { Text = "TAB or ESC to close.", HorizontalAlignment = HorizontalAlignment.Center }; root.Widgets.Add(_statusLabel); _desktop = new Desktop { Root = root }; } private Widget BuildEquippedSlotRow(EquipSlot slot) { var inst = _character.Inventory.GetEquipped(slot); string text = inst is null ? $" {SlotLabel(slot),-18}—" : $" {SlotLabel(slot),-18}{inst.Def.Name}"; var btn = new TextButton { Text = text, Width = 320, Padding = new Thickness(4, 2, 4, 2), }; if (inst is not null) btn.Click += (_, _) => { _character.Inventory.TryUnequip(slot, out var err); if (!string.IsNullOrEmpty(err) && _statusLabel is not null) _statusLabel.Text = err; else SetStatus($"Unequipped {inst.Def.Name}."); BuildUI(); }; else btn.Enabled = false; return btn; } private Widget BuildBagItemRow(ItemInstance inst) { string suffix = inst.Qty > 1 ? $" ×{inst.Qty}" : ""; string weight = $" ({inst.TotalWeightLb:F1} lb)"; var btn = new TextButton { Text = $" {inst.Def.Name}{suffix}{weight}", Width = 380, Padding = new Thickness(4, 2, 4, 2), }; var auto = NaturalSlotFor(inst); if (auto is null) { btn.Enabled = false; // gear / consumables — no equip target in M3 } else { btn.Click += (_, _) => { if (_character.Inventory.TryEquip(inst, auto.Value, out var err)) SetStatus($"Equipped {inst.Def.Name} into {auto.Value}."); else SetStatus(err); BuildUI(); }; } return btn; } /// /// Default equip slot for an item based on kind. Returns null for items /// that have no obvious slot (consumables, adventuring gear). /// private static EquipSlot? NaturalSlotFor(ItemInstance inst) { switch (inst.Def.Kind) { case "weapon": return EquipSlot.MainHand; case "armor": return EquipSlot.Body; case "shield": return EquipSlot.OffHand; case "natural_weapon_enhancer": return EquipSlotExtensions.FromEnhancerSlot(inst.Def.EnhancerSlot); default: return null; } } private static IEnumerable EquipSlotsToShow() => new[] { EquipSlot.MainHand, EquipSlot.OffHand, EquipSlot.Body, EquipSlot.Helm, EquipSlot.Cloak, EquipSlot.Boots, EquipSlot.AdaptivePack, EquipSlot.NaturalWeaponFang, EquipSlot.NaturalWeaponClaw, EquipSlot.NaturalWeaponHoof, EquipSlot.NaturalWeaponAntler, EquipSlot.NaturalWeaponHorn, }; private static string SlotLabel(EquipSlot s) => s switch { EquipSlot.MainHand => "Main hand:", EquipSlot.OffHand => "Off hand:", EquipSlot.Body => "Body:", EquipSlot.Helm => "Helm:", EquipSlot.Cloak => "Cloak:", EquipSlot.Boots => "Boots:", EquipSlot.AdaptivePack => "Pack:", EquipSlot.NaturalWeaponFang => "Fang caps:", EquipSlot.NaturalWeaponClaw => "Claw sheaths:", EquipSlot.NaturalWeaponHoof => "Hoof plates:", EquipSlot.NaturalWeaponAntler => "Antler tips:", EquipSlot.NaturalWeaponHorn => "Horn rings:", _ => s.ToString(), }; private string FormatStatLine() { int ac = DerivedStats.ArmorClass(_character); int spd = DerivedStats.SpeedFt(_character); float cap = DerivedStats.CarryCapacityLb(_character); var enc = DerivedStats.Encumbrance(_character); return $"HP {_character.CurrentHp}/{_character.MaxHp} AC {ac} Speed {spd} ft. " + $"Carry {_character.Inventory.TotalWeightLb:F1}/{cap:F1} lb ({enc})"; } private void SetStatus(string text) { if (_statusLabel is not null) _statusLabel.Text = text; } public void Update(GameTime gt) { var ks = Keyboard.GetState(); bool tab = ks.IsKeyDown(Keys.Tab); bool esc = ks.IsKeyDown(Keys.Escape); bool tabPressed = tab && !_tabWasDown; bool escPressed = esc && !_escWasDown; _tabWasDown = tab; _escWasDown = esc; if (tabPressed || escPressed) _game.Screens.Pop(); } public void Draw(GameTime gt, SpriteBatch sb) { // Don't clear — let the play screen's last frame show through (semi-transparent overlay). _desktop.Render(); } public void Deactivate() { } public void Reactivate() { } }