114 lines
4.3 KiB
Python
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
|