134 lines
5.3 KiB
Python
134 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
|
|
from engine.character import Character
|
|
from engine.defs import DefRegistry
|
|
from engine.item import ItemInstance
|
|
|
|
|
|
@dataclass
|
|
class DevSpawnMenu:
|
|
"""Dev-mode-only cheat tool: cycle through every loaded item def and materialize one
|
|
straight into the player's inventory (first free-fitting slot). Gated off in normal play by
|
|
engine/dev_config.py::DevConfig.enabled - see main.py's dev_spawn_object handling.
|
|
|
|
No world floor-item system exists in this engine yet (items only ever live in an Inventory,
|
|
or worn/held on a Character), so "spawning" here means "into inventory", not placing a loose
|
|
item on the ground - that's a bigger feature this doesn't attempt.
|
|
"""
|
|
|
|
registry: DefRegistry
|
|
item_ids: list[str] = field(default_factory=list)
|
|
selected_index: int = 0
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.item_ids:
|
|
# secret items (see ItemDef.secret, e.g. a one-off vanity implant) are deliberately
|
|
# left out of the cycle - they still work fine if granted some other way, they just
|
|
# aren't discoverable through this menu.
|
|
self.item_ids = sorted(def_id for def_id, item_def in self.registry.item_defs.items() if not item_def.secret)
|
|
|
|
def cycle(self, delta: int) -> None:
|
|
if self.item_ids:
|
|
self.selected_index = (self.selected_index + delta) % len(self.item_ids)
|
|
|
|
def selected_item_id(self) -> str | None:
|
|
if not self.item_ids:
|
|
return None
|
|
return self.item_ids[self.selected_index]
|
|
|
|
def spawn_into(self, character: Character) -> bool:
|
|
"""Returns whether it actually fit - a no-op (False) if the character has no inventory
|
|
or the selected item has nowhere left to go.
|
|
"""
|
|
item_id = self.selected_item_id()
|
|
if item_id is None or character.inventory is None:
|
|
return False
|
|
item_def = self.registry.get_item_def(item_id)
|
|
slot = character.inventory.find_first_fit(item_def)
|
|
if slot is None:
|
|
return False
|
|
instance = ItemInstance(f"dev-spawn-{uuid.uuid4().hex[:8]}", item_id)
|
|
return character.inventory.place(instance, item_def, *slot)
|
|
|
|
|
|
@dataclass
|
|
class DevTurretMenu:
|
|
"""Dev-mode-only cheat tool: cycle through every turret entity template (see entities.json's
|
|
turret_pistol/turret_autocannon - any template with turret_weapon_item_id set, so a new
|
|
variant only needs adding to entities.json, not here) and place the selected one into the
|
|
world - see main.py's dev_place_turret handling.
|
|
"""
|
|
|
|
registry: DefRegistry
|
|
entity_def_ids: list[str] = field(default_factory=list)
|
|
selected_index: int = 0
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.entity_def_ids:
|
|
self.entity_def_ids = sorted(
|
|
def_id for def_id, entity_def in self.registry.entity_defs.items()
|
|
if entity_def.turret_weapon_item_id is not None
|
|
)
|
|
|
|
def cycle(self, delta: int) -> None:
|
|
if self.entity_def_ids:
|
|
self.selected_index = (self.selected_index + delta) % len(self.entity_def_ids)
|
|
|
|
def selected_entity_def_id(self) -> str | None:
|
|
if not self.entity_def_ids:
|
|
return None
|
|
return self.entity_def_ids[self.selected_index]
|
|
|
|
|
|
MAX_FACTION_TAG_LENGTH = 24
|
|
|
|
|
|
@dataclass
|
|
class DevFactionMenu:
|
|
"""Dev-mode-only cheat tool: view and edit a Character's faction tags (see
|
|
Character.factions - the general tag system resources/logic/turrets.py::is_hostile reads to
|
|
tell allies from enemies). Not itself aware of which Character it's editing - main.py
|
|
resolves that fresh each time it's opened (the player, or whichever Character is directly
|
|
ahead - see main.py's dev_edit_player_factions/dev_edit_target_factions) and passes it into
|
|
every method here, the same way the backpack popup doesn't own the Character it displays.
|
|
"""
|
|
|
|
input_text: str = ""
|
|
selected_index: int = 0
|
|
|
|
def type_char(self, ch: str) -> None:
|
|
if ch.isprintable() and len(self.input_text) < MAX_FACTION_TAG_LENGTH:
|
|
self.input_text += ch
|
|
|
|
def backspace(self) -> None:
|
|
self.input_text = self.input_text[:-1]
|
|
|
|
def add_tag(self, character: Character) -> bool:
|
|
"""Adds the currently-typed text as a new faction tag and clears the input field.
|
|
No-op (returns False) for a blank tag or one the character already has.
|
|
"""
|
|
tag = self.input_text.strip()
|
|
if not tag or tag in character.factions:
|
|
return False
|
|
character.factions.append(tag)
|
|
self.input_text = ""
|
|
return True
|
|
|
|
def cycle_selected(self, character: Character, delta: int) -> None:
|
|
if character.factions:
|
|
self.selected_index = (self.selected_index + delta) % len(character.factions)
|
|
|
|
def remove_selected(self, character: Character) -> bool:
|
|
"""Removes whichever existing tag is currently selected. No-op (returns False) if the
|
|
character has no tags to remove.
|
|
"""
|
|
if not character.factions:
|
|
return False
|
|
index = self.selected_index % len(character.factions)
|
|
character.factions.pop(index)
|
|
self.selected_index = max(0, min(self.selected_index, len(character.factions) - 1))
|
|
return True
|