from __future__ import annotations from dataclasses import dataclass from enum import Enum from engine.character_creator import CharacterCreator MAX_NAME_LENGTH = 20 class Mode(Enum): """Which screen main.py's frame loop is currently driving - see main.py's on_event/ draw_frame dispatch. PLAYING is the only mode that touches World/InputState at all; every other mode is pure menu navigation over the pure-state classes below. """ MAIN_MENU = "main_menu" CHARACTER_CREATE = "character_create" WORLD_CREATE = "world_create" CONTROLS = "controls" PAUSED = "paused" PLAYING = "playing" NPC_SPAWN = "npc_spawn" # dev-mode-only: reuses CharacterCreationFlow to build and place an NPC @dataclass class MainMenu: """Pure interaction state for the title screen shown at launch instead of dropping straight into a world. "continue" is only offered when a save already exists (has_existing_save) - main.py decides that by checking whether WorldRepository.load_world() found anything. """ has_existing_save: bool selected_index: int = 0 def options(self) -> list[str]: # Only one save slot exists (engine/db.py::WorldRepository holds a single world) - once # a save exists, "new game" is left off rather than risking a silent overwrite; it only # appears before any save has ever been made. if self.has_existing_save: return ["continue", "controls"] return ["new_game", "controls"] def move(self, delta: int) -> None: opts = self.options() self.selected_index = (self.selected_index + delta) % len(opts) def selected(self) -> str: return self.options()[self.selected_index] @dataclass class PauseMenu: """Pure interaction state for the in-game Escape menu.""" OPTIONS = ("resume", "controls", "save_and_quit_to_menu") selected_index: int = 0 def move(self, delta: int) -> None: self.selected_index = (self.selected_index + delta) % len(self.OPTIONS) def selected(self) -> str: return self.OPTIONS[self.selected_index] @dataclass class CharacterCreationFlow: """Thin step-sequencing wrapper around CharacterCreator (engine/character_creator.py) for the launch-time "new game" flow: pick a species (left/right arrows), type a name, then an optional "secret items password" (see CharacterCreator.SECRET_ITEM_PASSWORD_GRANTS - typing the right one pre-installs a hidden implant on the finished character), then confirm. CharacterCreator itself already validates every choice - this only tracks which step is active and routes raw menu input to the right one. No account/CharacterLibrary here (see engine/character_library.py, engine/start_screen.py) - this engine only persists one world/player at a time via WorldRepository (engine/db.py), so "new game" just needs a single freshly-built Character, not a saved roster to pick among. """ creator: CharacterCreator step: str = "species" species_index: int = 0 def __post_init__(self) -> None: self._lock_in_species() def _lock_in_species(self) -> None: species = self.species_list() if species: self.creator.set_species(species[self.species_index % len(species)]) def species_list(self) -> list[str]: return self.creator.available_species() def cycle_species(self, delta: int) -> None: species = self.species_list() if not species: return self.species_index = (self.species_index + delta) % len(species) self._lock_in_species() def type_char(self, ch: str) -> None: if not ch.isprintable(): return if self.step == "name" and len(self.creator.name) < MAX_NAME_LENGTH: self.creator.set_name(self.creator.name + ch) elif self.step == "password" and len(self.creator.secret_password) < MAX_NAME_LENGTH: self.creator.set_secret_password(self.creator.secret_password + ch) def backspace(self) -> None: if self.step == "name": self.creator.set_name(self.creator.name[:-1]) elif self.step == "password": self.creator.set_secret_password(self.creator.secret_password[:-1]) def advance(self) -> bool: """Moves to the next step. Returns True once the flow is actually finished (password step confirmed - the password itself is optional, see CharacterCreator.build_character) - main.py builds the Character only then, via self.creator.build_character(). """ if self.step == "species": self.step = "name" return False if self.step == "name": if not self.creator.name.strip(): return False self.step = "password" return False return True def go_back(self) -> bool: """Steps back one stage; returns False if already at the first step (caller should leave character creation entirely, e.g. back to the main menu). """ if self.step == "password": self.step = "name" return True if self.step == "name": self.step = "species" return True return False