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