first commit
This commit is contained in:
290
api/app/models/skills.py
Normal file
290
api/app/models/skills.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
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())})"
|
||||
)
|
||||
Reference in New Issue
Block a user