official.ghost.logic
Deploy D&D Campaign Manager v2
71b378e
"""
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."
}
}
}