Mi2dRPGamEng/engine/game_modes.py

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