""" Character Sheet Export System - Generate D&D 5e Character Sheets Supports multiple formats: Markdown, JSON, PDF (future), PNG (future) """ from typing import Optional from pathlib import Path from datetime import datetime from src.models.character import Character, HIT_DICE_BY_CLASS class CharacterSheetExporter: """Export D&D characters to various formats""" def __init__(self): self.output_dir = Path("data/exports") self.output_dir.mkdir(parents=True, exist_ok=True) def export_to_markdown(self, character: Character, include_portrait: bool = True) -> str: """ Export character to detailed D&D 5e markdown format Args: character: Character to export include_portrait: Whether to include portrait reference Returns: Markdown formatted character sheet """ md = [] # Header with character name md.append(f"# {character.name}") md.append(f"*Level {character.level} {character.race.value} {character.character_class.value}*") md.append(f"**{character.alignment.value}**") if character.gender: md.append(f"*{character.gender}*") md.append("") md.append("---") md.append("") # Portrait reference if include_portrait and character.portrait_url: md.append(f"![Character Portrait]({character.portrait_url})") md.append("") # Core Combat Stats md.append("## ⚔️ Combat Statistics") md.append("") md.append(f"**Armor Class:** {character.armor_class} ") md.append(f"**Hit Points:** {character.current_hit_points} / {character.max_hit_points} ") md.append(f"**Hit Dice:** {character.level}d{HIT_DICE_BY_CLASS.get(character.character_class, 8)} ") md.append(f"**Proficiency Bonus:** +{character.proficiency_bonus} ") md.append(f"**Initiative:** {character.stats.dexterity_modifier:+d} (Dex) ") md.append(f"**Speed:** 30 ft. ") md.append(f"**Experience Points:** {character.experience_points} XP") md.append("") # Ability Scores - Clean vertical list format md.append("## 💪 Ability Scores") md.append("") str_mod = character.stats.strength_modifier dex_mod = character.stats.dexterity_modifier con_mod = character.stats.constitution_modifier int_mod = character.stats.intelligence_modifier wis_mod = character.stats.wisdom_modifier cha_mod = character.stats.charisma_modifier md.append(f"- **Strength:** {character.stats.strength} ({str_mod:+d})") md.append(f"- **Dexterity:** {character.stats.dexterity} ({dex_mod:+d})") md.append(f"- **Constitution:** {character.stats.constitution} ({con_mod:+d})") md.append(f"- **Intelligence:** {character.stats.intelligence} ({int_mod:+d})") md.append(f"- **Wisdom:** {character.stats.wisdom} ({wis_mod:+d})") md.append(f"- **Charisma:** {character.stats.charisma} ({cha_mod:+d})") md.append("") # Saving Throws - Clean vertical list proficient_saves = [p for p in character.proficiencies if p.startswith("Saving Throws:")] if proficient_saves: md.append("## 🛡️ Saving Throws") md.append("") # Determine which saves get proficiency bonus prof_str = "Strength" in proficient_saves[0] prof_dex = "Dexterity" in proficient_saves[0] prof_con = "Constitution" in proficient_saves[0] prof_int = "Intelligence" in proficient_saves[0] prof_wis = "Wisdom" in proficient_saves[0] prof_cha = "Charisma" in proficient_saves[0] prof_bonus = character.proficiency_bonus str_save = str_mod + (prof_bonus if prof_str else 0) dex_save = dex_mod + (prof_bonus if prof_dex else 0) con_save = con_mod + (prof_bonus if prof_con else 0) int_save = int_mod + (prof_bonus if prof_int else 0) wis_save = wis_mod + (prof_bonus if prof_wis else 0) cha_save = cha_mod + (prof_bonus if prof_cha else 0) md.append(f"- **Strength:** {str_save:+d}{' ✓' if prof_str else ''}") md.append(f"- **Dexterity:** {dex_save:+d}{' ✓' if prof_dex else ''}") md.append(f"- **Constitution:** {con_save:+d}{' ✓' if prof_con else ''}") md.append(f"- **Intelligence:** {int_save:+d}{' ✓' if prof_int else ''}") md.append(f"- **Wisdom:** {wis_save:+d}{' ✓' if prof_wis else ''}") md.append(f"- **Charisma:** {cha_save:+d}{' ✓' if prof_cha else ''}") md.append("") md.append("*✓ = Proficient (includes +{} proficiency bonus)*".format(prof_bonus)) md.append("") # Proficiencies - separate into categories for clarity md.append("## 🎯 Proficiencies") md.append("") if character.proficiencies: # Group proficiencies by type skills = [p for p in character.proficiencies if p.startswith("Choose") or p.startswith("Background")] armor_weapons = [p for p in character.proficiencies if any(x in p for x in ["armor", "Weapons:", "weapons", "shields", "Tools:"])] saves_list = [p for p in character.proficiencies if p.startswith("Saving Throws:")] if skills: md.append("### Skills") for skill in skills: md.append(f"- {skill}") md.append("") if armor_weapons: md.append("### Armor, Weapons & Tools") for item in armor_weapons: md.append(f"- {item}") md.append("") else: md.append("*No proficiencies listed*") md.append("") # Background md.append("## 📖 Background & Personality") md.append("") md.append(f"**Background**: {character.background.background_type}") md.append("") if character.background.personality_traits: md.append("**Personality Traits**:") for trait in character.background.personality_traits: md.append(f"- {trait}") md.append("") if character.background.ideals: md.append(f"**Ideals**: {character.background.ideals}") md.append("") if character.background.bonds: md.append(f"**Bonds**: {character.background.bonds}") md.append("") if character.background.flaws: md.append(f"**Flaws**: {character.background.flaws}") md.append("") if character.background.backstory: md.append("**Backstory**:") md.append("") md.append(character.background.backstory) md.append("") # Class Features md.append("## ⚔️ Class Features") md.append("") if character.features: for feature in character.features: md.append(f"- **{feature}**") else: md.append("*No class features listed*") md.append("") # Equipment md.append("## 🎒 Equipment") md.append("") if character.equipment: for item in character.equipment: md.append(f"- {item}") else: md.append("*No equipment listed*") md.append("") # Spells (if applicable) if character.spells: md.append("## ✨ Spells") md.append("") for spell in character.spells: md.append(f"- {spell}") md.append("") # Notes if character.notes: md.append("## 📝 Notes") md.append("") md.append(character.notes) md.append("") # Footer md.append("---") md.append(f"*Character ID: {character.id}*") md.append(f"*Created: {character.created_at.strftime('%Y-%m-%d %H:%M')}*") md.append(f"*Last Updated: {character.updated_at.strftime('%Y-%m-%d %H:%M')}*") return "\n".join(md) def export_to_json(self, character: Character) -> str: """ Export character to JSON format Args: character: Character to export Returns: JSON string """ import json char_dict = character.model_dump() # Convert enums to strings for JSON serialization char_dict['race'] = character.race.value char_dict['character_class'] = character.character_class.value char_dict['alignment'] = character.alignment.value # Convert datetime objects char_dict['created_at'] = character.created_at.isoformat() char_dict['updated_at'] = character.updated_at.isoformat() return json.dumps(char_dict, indent=2) def export_to_html(self, character: Character) -> str: """ Export character to styled HTML format (suitable for PDF conversion) Args: character: Character to export Returns: HTML string with embedded CSS """ html = f""" {character.name} - D&D 5e Character Sheet

{character.name}

Level {character.level} {character.race.value} {character.character_class.value}
{character.alignment.value}
{f'
{character.gender}
' if character.gender else ''}
""" # Portrait if character.portrait_url: html += f' Character Portrait\n\n' # Core Stats html += '
\n' html += '
Core Statistics
\n' html += '
\n' html += f'
Armor Class
{character.armor_class}
\n' html += f'
Hit Points
{character.current_hit_points}/{character.max_hit_points}
\n' html += f'
Hit Dice
{character.level}d{HIT_DICE_BY_CLASS.get(character.character_class, 8)}
\n' html += f'
Prof. Bonus
+{character.proficiency_bonus}
\n' html += f'
Speed
30 ft
\n' html += f'
Initiative
{character.stats.dexterity_modifier:+d}
\n' html += '
\n' html += '
\n\n' # Ability Scores html += '
\n' html += '
Ability Scores
\n' html += '
\n' abilities = [ ("STR", character.stats.strength, character.stats.strength_modifier), ("DEX", character.stats.dexterity, character.stats.dexterity_modifier), ("CON", character.stats.constitution, character.stats.constitution_modifier), ("INT", character.stats.intelligence, character.stats.intelligence_modifier), ("WIS", character.stats.wisdom, character.stats.wisdom_modifier), ("CHA", character.stats.charisma, character.stats.charisma_modifier), ] for name, score, mod in abilities: html += f'
\n' html += f'
{name}
\n' html += f'
{score}
\n' html += f'
({mod:+d})
\n' html += f'
\n' html += '
\n' html += '
\n\n' # Skills & Proficiencies if character.proficiencies: html += '
\n' html += '
Skills & Proficiencies
\n' for prof in character.proficiencies: html += f'
✓ {prof}
\n' html += '
\n\n' # Features if character.features: html += '
\n' html += '
Class Features
\n' for feature in character.features: html += f'
{feature}
\n' html += '
\n\n' # Equipment if character.equipment: html += '
\n' html += '
Equipment
\n' for item in character.equipment: html += f'
{item}
\n' html += '
\n\n' # Background html += '
\n' html += '
Background & Personality
\n' html += f'

Background: {character.background.background_type}

\n' if character.background.personality_traits: html += '

Personality Traits:

\n' if character.background.ideals: html += f'

Ideals: {character.background.ideals}

\n' if character.background.bonds: html += f'

Bonds: {character.background.bonds}

\n' if character.background.flaws: html += f'

Flaws: {character.background.flaws}

\n' if character.background.backstory: html += f'

Backstory:

\n' html += f'

{character.background.backstory}

\n' html += '
\n\n' # Spells if character.spells: html += '
\n' html += '
Spells
\n' for spell in character.spells: html += f'
{spell}
\n' html += '
\n\n' # Footer html += '
\n' html += f'

Character ID: {character.id}

\n' html += f'

Created: {character.created_at.strftime("%Y-%m-%d %H:%M")} | ' html += f'Last Updated: {character.updated_at.strftime("%Y-%m-%d %H:%M")}

\n' html += '

Generated by D\'n\'D Campaign Manager

\n' html += '
\n' html += '\n' return html def save_export(self, character: Character, format: str = "markdown") -> str: """ Save character sheet to file Args: character: Character to export format: Export format ('markdown', 'json', 'html') Returns: Path to saved file """ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_name = "".join(c if c.isalnum() or c in (' ', '_') else '_' for c in character.name) safe_name = safe_name.replace(' ', '_') if format == "markdown": content = self.export_to_markdown(character) filename = f"{safe_name}_{timestamp}.md" extension = ".md" elif format == "json": content = self.export_to_json(character) filename = f"{safe_name}_{timestamp}.json" extension = ".json" elif format == "html": content = self.export_to_html(character) filename = f"{safe_name}_{timestamp}.html" extension = ".html" else: raise ValueError(f"Unknown format: {format}") file_path = self.output_dir / filename with open(file_path, 'w', encoding='utf-8') as f: f.write(content) return str(file_path) # Global instance _exporter: Optional[CharacterSheetExporter] = None def get_exporter() -> CharacterSheetExporter: """Get or create global exporter instance""" global _exporter if _exporter is None: _exporter = CharacterSheetExporter() return _exporter