155 lines
6.3 KiB
Python
155 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from engine.character import Character
|
|
from engine.defs import DefRegistry
|
|
|
|
# Fixed scan order populate_ability_bar fills slots in - matches HAND_TO_ARM_SLOT's own order
|
|
# in engine/character.py, so a two-armed character's slots land in the same left-to-right
|
|
# sense a player would expect (right hand first, then left, then the second pair if present).
|
|
ABILITY_BAR_HAND_SLOTS = ("right_hand", "left_hand", "right_hand_2", "left_hand_2")
|
|
|
|
|
|
@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]
|
|
|
|
|
|
def populate_ability_bar(bar: AbilityBar, character: Character, registry: DefRegistry) -> None:
|
|
"""Auto-fills `bar` from whatever `character` can currently actually cast: each wielded
|
|
hand holding an ability-granting item (see ABILITY_BAR_HAND_SLOTS), then each installed
|
|
ability-granting implant - in that fixed order, one per slot. Meant to be re-run whenever
|
|
the loadout might have changed (main.py calls it once per frame during play; cheap enough
|
|
not to bother caching), which also means it always leaves stale slots from a dropped/
|
|
unwielded item cleared rather than assigning past the end.
|
|
"""
|
|
bar.slots = [None] * len(bar.slots)
|
|
index = 0
|
|
for hand_slot in ABILITY_BAR_HAND_SLOTS:
|
|
if index >= len(bar.slots):
|
|
return
|
|
held = character.get_held_item(hand_slot)
|
|
if held is None:
|
|
continue
|
|
item_def = registry.get_item_def(held.item_def_id)
|
|
if item_def.grants_ability is None:
|
|
continue
|
|
bar.assign(index, AbilityBarSlot(kind="hand", source_id=hand_slot, ability_id=item_def.grants_ability))
|
|
index += 1
|
|
|
|
for implant_id in character.implants:
|
|
if index >= len(bar.slots):
|
|
return
|
|
item_def = registry.get_item_def(implant_id)
|
|
if item_def.grants_ability is None:
|
|
continue
|
|
bar.assign(index, AbilityBarSlot(kind="implant", source_id=implant_id, ability_id=item_def.grants_ability))
|
|
index += 1
|
|
|
|
|
|
@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), an "equipment" tab (worn
|
|
clothing + held items), and a "medical" tab (per-body-part integrity, ailments, and organ
|
|
health/removal - see engine/character.py's BodyPart/Organ/Ailment). 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,
|
|
equipment, and medical 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 (see engine/render/menu_draw.py::draw_character_card).
|
|
"""
|
|
|
|
TABS = ("bio", "equipment", "medical")
|
|
|
|
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
|