142 lines
5.2 KiB
Python
142 lines
5.2 KiB
Python
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
|