""" 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"") 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"""
Background: {character.background.background_type}
\n' if character.background.personality_traits: html += 'Personality Traits:
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 += '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 += '