Mi2dRPGamEng/engine/dev_tools.py

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