first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

290
api/app/models/skills.py Normal file
View 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())})"
)