Mi2dRPGamEng/engine/character_creator.py

114 lines
4.3 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from engine.character import BodyPart, Character
from engine.character_library import CharacterLibrary
from engine.defs import DefRegistry
from engine.skills import default_bio
DEFAULT_SKILL_POINTS = 10
DEFAULT_MAX_SKILL_LEVEL = 5
@dataclass
class CharacterCreator:
"""Pure state/logic for building a new persistent character: pick a species, a gender
valid for that species (species define their own valid genders - see SpeciesDef.genders),
a name, and spend a fixed point budget across the classic D&D-style bio skills
(engine/skills.py) - then confirm() builds an actual Character and saves it into the
CharacterLibrary under a given account, ready to appear on the StartScreen.
No rendering here - same boundary as StartScreen/CharacterMenu/LayerPickerMenu/AbilityBar
(engine/ui.py, engine/start_screen.py): drawing species swatches, a name field, and skill
+/- steppers is a follow-up UI-layer task. This is the state/logic side: valid choices,
point-budget bookkeeping, and producing the finished Character.
"""
registry: DefRegistry
skill_points_budget: int = DEFAULT_SKILL_POINTS
max_skill_level: int = DEFAULT_MAX_SKILL_LEVEL
name: str = ""
species_id: str | None = None
gender: str | None = None
bio: dict[str, int] = field(default_factory=default_bio)
def set_name(self, name: str) -> None:
self.name = name.strip()
def set_species(self, species_id: str) -> bool:
"""Picks a species, resetting gender to that species' first valid option if the
current gender (from a previous species pick) isn't valid for it.
"""
if species_id not in self.registry.species_defs:
return False
self.species_id = species_id
species = self.registry.get_species_def(species_id)
if self.gender not in species.genders:
self.gender = species.genders[0] if species.genders else None
return True
def set_gender(self, gender: str) -> bool:
if self.species_id is None:
return False
species = self.registry.get_species_def(self.species_id)
if gender not in species.genders:
return False
self.gender = gender
return True
def spent_skill_points(self) -> int:
return sum(self.bio.values())
def remaining_skill_points(self) -> int:
return self.skill_points_budget - self.spent_skill_points()
def increase_skill(self, skill_id: str) -> bool:
if skill_id not in self.bio:
return False
if self.remaining_skill_points() <= 0 or self.bio[skill_id] >= self.max_skill_level:
return False
self.bio[skill_id] += 1
return True
def decrease_skill(self, skill_id: str) -> bool:
if skill_id not in self.bio or self.bio[skill_id] <= 0:
return False
self.bio[skill_id] -= 1
return True
def preview_body_parts(self) -> list[BodyPart]:
"""Read-only preview of the anatomy this character will start with, for a UI to render
without needing to build a whole Character first.
"""
if self.species_id is None:
return []
return self.registry.new_default_body_parts(self.species_id)
def is_ready(self) -> bool:
return bool(self.name) and self.species_id is not None and self.gender is not None
def build_character(self) -> Character | None:
if not self.is_ready():
return None
assert self.species_id is not None
return Character(
name=self.name,
species_id=self.species_id,
gender=self.gender,
default_body_parts=self.registry.new_default_body_parts(self.species_id),
bio=dict(self.bio),
)
def confirm(self, library: CharacterLibrary, account_id: str, character_id: str) -> Character | None:
"""Builds the character and saves it into the library under (account_id, character_id) -
the "create" action once name/species/gender/skills are all set. No-op (returns None,
nothing saved) if the creator isn't ready yet.
"""
character = self.build_character()
if character is None:
return None
library.save_character(account_id, character_id, character)
return character