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