""" Character data models for D&D 5e characters """ from datetime import datetime from typing import Optional, List, Dict, Any from enum import Enum from pydantic import BaseModel, Field, validator class DnDRace(str, Enum): """D&D 5e races""" HUMAN = "Human" ELF = "Elf" DWARF = "Dwarf" HALFLING = "Halfling" DRAGONBORN = "Dragonborn" GNOME = "Gnome" HALF_ELF = "Half-Elf" HALF_ORC = "Half-Orc" TIEFLING = "Tiefling" DROW = "Drow" class DnDClass(str, Enum): """D&D 5e classes""" BARBARIAN = "Barbarian" BARD = "Bard" CLERIC = "Cleric" DRUID = "Druid" FIGHTER = "Fighter" MONK = "Monk" PALADIN = "Paladin" RANGER = "Ranger" ROGUE = "Rogue" SORCERER = "Sorcerer" WARLOCK = "Warlock" WIZARD = "Wizard" class Alignment(str, Enum): """D&D alignments""" LAWFUL_GOOD = "Lawful Good" NEUTRAL_GOOD = "Neutral Good" CHAOTIC_GOOD = "Chaotic Good" LAWFUL_NEUTRAL = "Lawful Neutral" TRUE_NEUTRAL = "True Neutral" CHAOTIC_NEUTRAL = "Chaotic Neutral" LAWFUL_EVIL = "Lawful Evil" NEUTRAL_EVIL = "Neutral Evil" CHAOTIC_EVIL = "Chaotic Evil" # D&D 5e Hit Dice by Class (shared constant to avoid duplication) HIT_DICE_BY_CLASS = { DnDClass.BARBARIAN: 12, DnDClass.FIGHTER: 10, DnDClass.PALADIN: 10, DnDClass.RANGER: 10, DnDClass.BARD: 8, DnDClass.CLERIC: 8, DnDClass.DRUID: 8, DnDClass.MONK: 8, DnDClass.ROGUE: 8, DnDClass.WARLOCK: 8, DnDClass.SORCERER: 6, DnDClass.WIZARD: 6, } class CharacterStats(BaseModel): """D&D 5e ability scores and derived stats""" strength: int = Field(ge=1, le=20, default=10, description="Strength score (1-20 per D&D 5e standard rules)") dexterity: int = Field(ge=1, le=20, default=10, description="Dexterity score (1-20 per D&D 5e standard rules)") constitution: int = Field(ge=1, le=20, default=10, description="Constitution score (1-20 per D&D 5e standard rules)") intelligence: int = Field(ge=1, le=20, default=10, description="Intelligence score (1-20 per D&D 5e standard rules)") wisdom: int = Field(ge=1, le=20, default=10, description="Wisdom score (1-20 per D&D 5e standard rules)") charisma: int = Field(ge=1, le=20, default=10, description="Charisma score (1-20 per D&D 5e standard rules)") @property def strength_modifier(self) -> int: """Calculate strength modifier""" return (self.strength - 10) // 2 @property def dexterity_modifier(self) -> int: """Calculate dexterity modifier""" return (self.dexterity - 10) // 2 @property def constitution_modifier(self) -> int: """Calculate constitution modifier""" return (self.constitution - 10) // 2 @property def intelligence_modifier(self) -> int: """Calculate intelligence modifier""" return (self.intelligence - 10) // 2 @property def wisdom_modifier(self) -> int: """Calculate wisdom modifier""" return (self.wisdom - 10) // 2 @property def charisma_modifier(self) -> int: """Calculate charisma modifier""" return (self.charisma - 10) // 2 def get_all_modifiers(self) -> Dict[str, int]: """Get all ability modifiers""" return { "strength": self.strength_modifier, "dexterity": self.dexterity_modifier, "constitution": self.constitution_modifier, "intelligence": self.intelligence_modifier, "wisdom": self.wisdom_modifier, "charisma": self.charisma_modifier, } class CharacterBackground(BaseModel): """Character background and personality""" background_type: str = Field(default="Adventurer", description="Background archetype") personality_traits: List[str] = Field(default_factory=list, description="2-3 personality traits") ideals: str = Field(default="", description="Character's ideals") bonds: str = Field(default="", description="Character's bonds") flaws: str = Field(default="", description="Character's flaws") backstory: str = Field(default="", description="Full backstory") goals: List[str] = Field(default_factory=list, description="Character goals") class Character(BaseModel): """Complete D&D 5e character""" # Core identity id: Optional[str] = Field(default=None, description="Unique character ID") name: str = Field(min_length=1, max_length=100, description="Character name") race: DnDRace = Field(description="Character race") character_class: DnDClass = Field(description="Character class") level: int = Field(ge=1, le=20, default=1, description="Character level") alignment: Alignment = Field(default=Alignment.TRUE_NEUTRAL) gender: Optional[str] = Field(default=None, description="Character gender") skin_tone: Optional[str] = Field(default=None, description="Character skin tone/color") # Stats stats: CharacterStats = Field(default_factory=CharacterStats) max_hit_points: int = Field(ge=1, default=10) current_hit_points: int = Field(ge=0, default=10) armor_class: int = Field(ge=1, default=10) proficiency_bonus: int = Field(ge=2, default=2) # Background & personality background: CharacterBackground = Field(default_factory=CharacterBackground) # Equipment & abilities equipment: List[str] = Field(default_factory=list, description="Equipment list") spells: List[str] = Field(default_factory=list, description="Known spells") features: List[str] = Field(default_factory=list, description="Class features") proficiencies: List[str] = Field(default_factory=list, description="Skill proficiencies") # Portrait portrait_url: Optional[str] = Field(default=None, description="Character portrait URL") portrait_prompt: Optional[str] = Field(default=None, description="Prompt used for portrait") # Metadata campaign_id: Optional[str] = Field(default=None, description="Associated campaign") player_name: Optional[str] = Field(default=None, description="Player's name") created_at: datetime = Field(default_factory=datetime.now) updated_at: datetime = Field(default_factory=datetime.now) # Additional data notes: str = Field(default="", description="GM/player notes") experience_points: int = Field(ge=0, default=0) @validator("proficiency_bonus", always=True) def calculate_proficiency_bonus(cls, v, values): """Calculate proficiency bonus based on level""" level = values.get("level", 1) return 2 + ((level - 1) // 4) @validator("current_hit_points", always=True) def validate_hp(cls, v, values): """Ensure current HP doesn't exceed max HP""" max_hp = values.get("max_hit_points", 10) return min(v, max_hp) def calculate_max_hp(self) -> int: """Calculate max HP based on class and level (D&D 5e rules)""" hit_die = HIT_DICE_BY_CLASS.get(self.character_class, 8) con_mod = self.stats.constitution_modifier # First level: max hit die + con mod (minimum 1) first_level_hp = max(1, hit_die + con_mod) # Subsequent levels: average (rounded up) + con mod (minimum 1 per level per D&D 5e) hp_per_level = max(1, (hit_die // 2) + 1 + con_mod) subsequent_hp = hp_per_level * (self.level - 1) return first_level_hp + subsequent_hp def take_damage(self, damage: int) -> int: """Apply damage to character""" self.current_hit_points = max(0, self.current_hit_points - damage) self.updated_at = datetime.now() return self.current_hit_points def heal(self, healing: int) -> int: """Heal character""" self.current_hit_points = min(self.max_hit_points, self.current_hit_points + healing) self.updated_at = datetime.now() return self.current_hit_points def level_up(self): """Level up the character (D&D 5e rules)""" if self.level < 20: self.level += 1 # Recalculate proficiency bonus based on new level self.proficiency_bonus = 2 + ((self.level - 1) // 4) self.max_hit_points = self.calculate_max_hp() self.current_hit_points = self.max_hit_points self.updated_at = datetime.now() def to_dict(self) -> Dict[str, Any]: """Convert to dictionary""" return self.model_dump() def to_markdown(self) -> str: """Generate markdown character sheet""" return f"""# {self.name} **Level {self.level} {self.race.value} {self.character_class.value}** *{self.alignment.value}* ## Ability Scores - **STR:** {self.stats.strength} ({self.stats.strength_modifier:+d}) - **DEX:** {self.stats.dexterity} ({self.stats.dexterity_modifier:+d}) - **CON:** {self.stats.constitution} ({self.stats.constitution_modifier:+d}) - **INT:** {self.stats.intelligence} ({self.stats.intelligence_modifier:+d}) - **WIS:** {self.stats.wisdom} ({self.stats.wisdom_modifier:+d}) - **CHA:** {self.stats.charisma} ({self.stats.charisma_modifier:+d}) ## Combat Stats - **HP:** {self.current_hit_points}/{self.max_hit_points} - **AC:** {self.armor_class} - **Proficiency:** +{self.proficiency_bonus} ## Background **{self.background.background_type}** {self.background.backstory} ## Personality **Traits:** {', '.join(self.background.personality_traits)} **Ideals:** {self.background.ideals} **Bonds:** {self.background.bonds} **Flaws:** {self.background.flaws} ## Equipment {chr(10).join(f"- {item}" for item in self.equipment)} ## Notes {self.notes} """ class Config: json_schema_extra = { "example": { "name": "Thorin Ironforge", "race": "Dwarf", "character_class": "Fighter", "level": 3, "alignment": "Lawful Good", "stats": { "strength": 16, "dexterity": 12, "constitution": 15, "intelligence": 10, "wisdom": 13, "charisma": 8 }, "background": { "background_type": "Soldier", "backstory": "A veteran warrior seeking redemption." } } }