from __future__ import annotations from dataclasses import dataclass, field @dataclass class LayerPickerMenu: """Pure interaction state for choosing which of a tile's occupied layers a tool targets. No rendering here - this project has no text-rendering system yet, so drawing an actual on-screen popup box is a follow-up UI-layer task. This is the game-logic side of it: which option is highlighted, and what layer name comes back once the player confirms. """ map_id: str x: int y: int z: int options: list[str] = field(default_factory=list) selected_index: int = 0 def cycle(self, delta: int = 1) -> None: if self.options: self.selected_index = (self.selected_index + delta) % len(self.options) def confirm(self) -> str | None: if not self.options: return None return self.options[self.selected_index] @dataclass class AbilityBarSlot: """One assigned slot on the ability bar: where to find the ability-granting thing (a wielded item in a hand, or an installed implant) when this slot gets triggered - see resources/logic/actions.py::activate_ability_bar_slot for the dispatch on `kind`. """ kind: str # "hand" or "implant" source_id: str # hand_slot name (e.g. "right_hand") or implant item_def_id ability_id: str # the ability granted, for display/bookkeeping @dataclass class AbilityBar: """Pure interaction state for the bottom-left ability bar: up to 10 assignable slots (numkeys 1-9 and 0), selectable by mouse click or hotkey - both just call select(). No rendering here - same boundary as LayerPickerMenu above: this project has no text/icon-rendering system yet, so drawing the actual bar and highlighting the selected slot on-screen is a follow-up UI-layer task. """ slots: list[AbilityBarSlot | None] = field(default_factory=lambda: [None] * 10) selected_index: int | None = None def assign(self, index: int, slot: AbilityBarSlot | None) -> None: self.slots[index] = slot def select(self, index: int) -> None: if 0 <= index < len(self.slots): self.selected_index = index def selected_slot(self) -> AbilityBarSlot | None: if self.selected_index is None: return None return self.slots[self.selected_index] @dataclass class CharacterMenu: """Pure interaction state for the bottom-left tabbed character menu: a "bio" tab (skills/ mental stats/health - all already on the Character model) and an "equipment" tab (worn clothing + held items). Right-clicking an equipped slot that grants an inventory (e.g. a worn backpack) opens a popup submenu above the menu showing that slot's contents. No rendering here - same boundary as LayerPickerMenu/AbilityBar above: the actual bio and equipment data already lives on the Character/DefRegistry models this engine built over the whole session, so this class only tracks which tab is active and which backpack popup (if any) is open - drawing the tabs, labels, and popup box is a follow-up UI-layer task. """ TABS = ("bio", "equipment") active_tab: str = "bio" open_backpack_slot: str | None = None # clothing slot whose granted-inventory popup is open def select_tab(self, tab: str) -> None: if tab in self.TABS: self.active_tab = tab self.open_backpack_slot = None # leaving equipment closes any open popup def right_click_equipment_slot(self, slot: str, character, registry) -> bool: """Toggles the backpack popup for a worn item that grants an inventory (e.g. a backpack). No-op (returns False, nothing opens) for an empty slot or a worn item with no granted inventory - e.g. a plain jacket has nothing to pop open. """ piece = character.get_clothing(slot) if piece is None: return False item_def = registry.get_item_def(piece.item_def_id) if item_def.grants_inventory_size is None: return False if self.open_backpack_slot == slot: self.open_backpack_slot = None return False self.open_backpack_slot = slot return True def close_backpack_popup(self) -> None: self.open_backpack_slot = None