291 lines
9.2 KiB
Python
291 lines
9.2 KiB
Python
"""
|
|
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())})"
|
|
)
|