""" Skill tree and character class system. This module defines the progression system including skill nodes, skill trees, and player classes. Characters unlock skills by spending skill points earned through leveling. """ from dataclasses import dataclass, field, asdict from typing import Dict, Any, List, Optional from app.models.stats import Stats @dataclass class SkillNode: """ Represents a single skill in a skill tree. Skills can provide passive bonuses, unlock active abilities, or grant access to new features (like equipment types). Attributes: skill_id: Unique identifier name: Display name description: What this skill does tier: Skill tier (1-5, where 1 is basic and 5 is master) prerequisites: List of skill_ids that must be unlocked first effects: Dictionary of effects this skill provides Examples: - Passive bonuses: {"strength": 5, "defense": 10} - Ability unlocks: {"unlocks_ability": "shield_bash"} - Feature access: {"unlocks_equipment": "heavy_armor"} unlocked: Current unlock status (used during gameplay) """ skill_id: str name: str description: str tier: int # 1-5 prerequisites: List[str] = field(default_factory=list) effects: Dict[str, Any] = field(default_factory=dict) unlocked: bool = False def has_prerequisites_met(self, unlocked_skills: List[str]) -> bool: """ Check if all prerequisites for this skill are met. Args: unlocked_skills: List of skill_ids the character has unlocked Returns: True if all prerequisites are met, False otherwise """ return all(prereq in unlocked_skills for prereq in self.prerequisites) def get_stat_bonuses(self) -> Dict[str, int]: """ Extract stat bonuses from this skill's effects. Returns: Dictionary of stat bonuses (e.g., {"strength": 5, "defense": 3}) """ bonuses = {} for key, value in self.effects.items(): # Look for stat names in effects if key in ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"]: bonuses[key] = value elif key == "defense" or key == "resistance" or key == "hit_points" or key == "mana_points": bonuses[key] = value return bonuses def get_unlocked_abilities(self) -> List[str]: """ Extract ability IDs unlocked by this skill. Returns: List of ability_ids this skill unlocks """ abilities = [] if "unlocks_ability" in self.effects: ability = self.effects["unlocks_ability"] if isinstance(ability, list): abilities.extend(ability) else: abilities.append(ability) return abilities def to_dict(self) -> Dict[str, Any]: """Serialize skill node to dictionary.""" return asdict(self) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'SkillNode': """Deserialize skill node from dictionary.""" return cls( skill_id=data["skill_id"], name=data["name"], description=data["description"], tier=data["tier"], prerequisites=data.get("prerequisites", []), effects=data.get("effects", {}), unlocked=data.get("unlocked", False), ) @dataclass class SkillTree: """ Represents a complete skill tree for a character class. Each class has 2+ skill trees representing different specializations. Attributes: tree_id: Unique identifier name: Display name (e.g., "Shield Bearer", "Pyromancy") description: Theme and purpose of this tree nodes: All skill nodes in this tree (organized by tier) """ tree_id: str name: str description: str nodes: List[SkillNode] = field(default_factory=list) def can_unlock(self, skill_id: str, unlocked_skills: List[str]) -> bool: """ Check if a specific skill can be unlocked. Validates: 1. Skill exists in this tree 2. Prerequisites are met 3. Tier progression rules (must unlock tier N before tier N+1) Args: skill_id: The skill to check unlocked_skills: Currently unlocked skill_ids Returns: True if skill can be unlocked, False otherwise """ # Find the skill node skill_node = None for node in self.nodes: if node.skill_id == skill_id: skill_node = node break if not skill_node: return False # Skill not in this tree # Check if already unlocked if skill_id in unlocked_skills: return False # Check prerequisites if not skill_node.has_prerequisites_met(unlocked_skills): return False # Check tier progression # Must have at least one skill from previous tier unlocked # (except for tier 1 which is always available) if skill_node.tier > 1: has_previous_tier = False for node in self.nodes: if node.tier == skill_node.tier - 1 and node.skill_id in unlocked_skills: has_previous_tier = True break if not has_previous_tier: return False return True def get_nodes_by_tier(self, tier: int) -> List[SkillNode]: """ Get all skill nodes for a specific tier. Args: tier: Tier number (1-5) Returns: List of SkillNodes at that tier """ return [node for node in self.nodes if node.tier == tier] def to_dict(self) -> Dict[str, Any]: """Serialize skill tree to dictionary.""" data = asdict(self) data["nodes"] = [node.to_dict() for node in self.nodes] return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'SkillTree': """Deserialize skill tree from dictionary.""" nodes = [SkillNode.from_dict(n) for n in data.get("nodes", [])] return cls( tree_id=data["tree_id"], name=data["name"], description=data["description"], nodes=nodes, ) @dataclass class PlayerClass: """ Represents a character class (Vanguard, Assassin, Arcanist, etc.). Each class has unique base stats, multiple skill trees, and starting equipment. Attributes: class_id: Unique identifier name: Display name description: Class theme and playstyle base_stats: Starting stats for this class skill_trees: List of skill trees (2+ per class) starting_equipment: List of item_ids for initial equipment starting_abilities: List of ability_ids available from level 1 """ class_id: str name: str description: str base_stats: Stats skill_trees: List[SkillTree] = field(default_factory=list) starting_equipment: List[str] = field(default_factory=list) starting_abilities: List[str] = field(default_factory=list) def get_skill_tree(self, tree_id: str) -> Optional[SkillTree]: """ Get a specific skill tree by ID. Args: tree_id: Skill tree identifier Returns: SkillTree instance or None if not found """ for tree in self.skill_trees: if tree.tree_id == tree_id: return tree return None def get_all_skills(self) -> List[SkillNode]: """ Get all skill nodes from all trees. Returns: Flat list of all SkillNodes across all trees """ all_skills = [] for tree in self.skill_trees: all_skills.extend(tree.nodes) return all_skills def to_dict(self) -> Dict[str, Any]: """Serialize player class to dictionary.""" return { "class_id": self.class_id, "name": self.name, "description": self.description, "base_stats": self.base_stats.to_dict(), "skill_trees": [tree.to_dict() for tree in self.skill_trees], "starting_equipment": self.starting_equipment, "starting_abilities": self.starting_abilities, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'PlayerClass': """Deserialize player class from dictionary.""" base_stats = Stats.from_dict(data["base_stats"]) skill_trees = [SkillTree.from_dict(t) for t in data.get("skill_trees", [])] return cls( class_id=data["class_id"], name=data["name"], description=data["description"], base_stats=base_stats, skill_trees=skill_trees, starting_equipment=data.get("starting_equipment", []), starting_abilities=data.get("starting_abilities", []), ) def __repr__(self) -> str: """String representation of the player class.""" return ( f"PlayerClass({self.name}, " f"trees={len(self.skill_trees)}, " f"total_skills={len(self.get_all_skills())})" )