File size: 10,329 Bytes
71b378e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
"""
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."
                }
            }
        }