""" Character Creator Agent - D&D character generation for D'n'D Campaign Manager """ import uuid from typing import Optional, Dict, Any from datetime import datetime from src.models.character import ( Character, CharacterStats, CharacterBackground, DnDRace, DnDClass, Alignment, HIT_DICE_BY_CLASS ) from src.utils.ai_client import get_ai_client from src.utils.dice import DiceRoller from src.utils.database import get_database from src.utils.validators import validate_character from src.utils.image_generator import get_image_generator class CharacterAgent: """Agent for creating D&D characters""" def __init__(self): self.ai_client = get_ai_client() self.dice_roller = DiceRoller() self.database = get_database() try: self.image_generator = get_image_generator() except Exception as e: print(f"Warning: Image generator not available: {e}") self.image_generator = None def create_character( self, name: Optional[str] = None, race: Optional[DnDRace] = None, character_class: Optional[DnDClass] = None, level: int = 1, background_type: Optional[str] = None, personality_prompt: Optional[str] = None, stats_method: str = "standard_array", # "roll", "standard_array", "point_buy" custom_stats: Optional[Dict[str, int]] = None, ) -> Character: """ Create a complete D&D character Args: name: Character name (auto-generated if None) race: Character race (random if None) character_class: Character class (random if None) level: Starting level (1-20) background_type: Background type personality_prompt: Prompt to generate personality stats_method: How to generate ability scores custom_stats: Pre-set ability scores Returns: Complete Character object """ # Generate character ID character_id = str(uuid.uuid4()) # Generate name if not provided if not name: name = self._generate_name(race, character_class) # Select race and class if not provided if not race: race = self._select_random_race() if not character_class: character_class = self._select_random_class() # Generate ability scores if custom_stats: stats = CharacterStats(**custom_stats) else: stats = self._generate_stats(stats_method) # Apply racial ability score bonuses (D&D 5e rule) stats = self._apply_racial_bonuses(stats, race) # Calculate HP max_hp = self._calculate_starting_hp(character_class, stats.constitution_modifier, level) # Generate background and personality background = self._generate_background( name, race, character_class, background_type, personality_prompt ) # Generate alignment based on personality alignment = self._determine_alignment(background) # Get starting equipment equipment = self._get_starting_equipment(character_class) # Get class features features = self._get_class_features(character_class, level) # Get proficiencies proficiencies = self._get_proficiencies(character_class, background.background_type) # Calculate AC (base 10 + dex modifier) armor_class = 10 + stats.dexterity_modifier # Create character character = Character( id=character_id, name=name, race=race, character_class=character_class, level=level, alignment=alignment, stats=stats, max_hit_points=max_hp, current_hit_points=max_hp, armor_class=armor_class, background=background, equipment=equipment, features=features, proficiencies=proficiencies, ) # Validate character is_valid, errors = validate_character(character) if not is_valid: raise ValueError(f"Character validation failed: {', '.join(errors)}") # Save to database self.save_character(character) return character def generate_name( self, race: Optional[DnDRace] = None, character_class: Optional[DnDClass] = None, gender: Optional[str] = None ) -> str: """ PUBLIC method to generate character name using AI Can be called independently of character creation Args: race: Character race (optional) character_class: Character class (optional) gender: Character gender (optional) Returns: Generated character name """ gender_text = f"\nGender: {gender}" if gender and gender != "Not specified" else "" prompt = f"""Generate a single fantasy character name for a D&D character. Race: {race.value if race else 'any race'} Class: {character_class.value if character_class else 'any class'}{gender_text} Requirements: - Just the name, nothing else - Make it sound appropriate for the race{' and gender' if gender_text else ''} - Make it memorable and fitting for an adventurer - 2-3 words maximum Example formats: - "Thorin Ironforge" (Male Dwarf) - "Elara Moonwhisper" (Female Elf) - "Grunk Bonecrusher" (Male Orc) - "Pip Thornberry" (Any Halfling) Generate only the name:""" try: name = self.ai_client.generate_creative(prompt).strip() # Clean up any extra text name = name.split('\n')[0].strip('"\'') return name except Exception as e: # Fallback to simple name generation import random prefixes = ["Brave", "Bold", "Swift", "Wise", "Dark", "Bright"] suffixes = ["blade", "heart", "forge", "walker", "runner", "seeker"] return f"{random.choice(prefixes)}{random.choice(suffixes)}" def _generate_name(self, race: Optional[DnDRace], character_class: Optional[DnDClass]) -> str: """ PRIVATE method - calls public generate_name Kept for backward compatibility """ return self.generate_name(race, character_class) def _select_random_race(self) -> DnDRace: """Select random race""" import random return random.choice(list(DnDRace)) def _select_random_class(self) -> DnDClass: """Select random class""" import random return random.choice(list(DnDClass)) def _generate_stats(self, method: str) -> CharacterStats: """Generate ability scores""" if method == "roll": # Roll 4d6 drop lowest stats_dict = self.dice_roller.roll_stats() return CharacterStats(**stats_dict) elif method == "standard_array": # Standard array: 15, 14, 13, 12, 10, 8 import random array = [15, 14, 13, 12, 10, 8] random.shuffle(array) return CharacterStats( strength=array[0], dexterity=array[1], constitution=array[2], intelligence=array[3], wisdom=array[4], charisma=array[5] ) elif method == "point_buy": # Balanced point buy (27 points) return CharacterStats( strength=13, dexterity=14, constitution=13, intelligence=12, wisdom=10, charisma=10 ) else: # Default to standard array return CharacterStats() def _apply_racial_bonuses(self, stats: CharacterStats, race: DnDRace) -> CharacterStats: """Apply racial ability score increases per D&D 5e PHB""" racial_bonuses = { DnDRace.HUMAN: {"strength": 1, "dexterity": 1, "constitution": 1, "intelligence": 1, "wisdom": 1, "charisma": 1}, DnDRace.ELF: {"dexterity": 2}, DnDRace.DWARF: {"constitution": 2}, DnDRace.HALFLING: {"dexterity": 2}, DnDRace.DRAGONBORN: {"strength": 2, "charisma": 1}, DnDRace.GNOME: {"intelligence": 2}, DnDRace.HALF_ELF: {"charisma": 2}, # +1 to two highest (auto-assigned) DnDRace.HALF_ORC: {"strength": 2, "constitution": 1}, DnDRace.TIEFLING: {"charisma": 2, "intelligence": 1}, DnDRace.DROW: {"dexterity": 2, "charisma": 1}, } bonuses = racial_bonuses.get(race, {}) stats_dict = stats.model_dump() # Apply bonuses, capping at 20 per D&D 5e standard rules for ability, bonus in bonuses.items(): stats_dict[ability] = min(20, stats_dict[ability] + bonus) # Half-Elf special case: +1 to two highest abilities after CHA if race == DnDRace.HALF_ELF: # Find two highest abilities (excluding charisma which already got +2) abilities_except_cha = [(k, v) for k, v in stats_dict.items() if k != "charisma"] abilities_except_cha.sort(key=lambda x: x[1], reverse=True) # Apply +1 to top two (capped at 20) for i in range(min(2, len(abilities_except_cha))): ability_name = abilities_except_cha[i][0] stats_dict[ability_name] = min(20, stats_dict[ability_name] + 1) return CharacterStats(**stats_dict) def _calculate_starting_hp(self, character_class: DnDClass, con_modifier: int, level: int) -> int: """Calculate starting hit points (D&D 5e rules)""" hit_die = HIT_DICE_BY_CLASS.get(character_class, 8) # First level: max hit die + con mod # Subsequent levels: average of hit die + con mod first_level_hp = hit_die + con_modifier subsequent_hp = ((hit_die // 2) + 1 + con_modifier) * (level - 1) return max(1, first_level_hp + subsequent_hp) def _generate_background( self, name: str, race: DnDRace, character_class: DnDClass, background_type: Optional[str], personality_prompt: Optional[str] ) -> CharacterBackground: """Generate character background and personality using AI""" system_prompt = """You are a D&D character background generator. Create compelling, detailed character backgrounds that feel authentic and provide hooks for roleplay. Be creative but grounded in D&D lore.""" prompt = f"""Generate a complete character background for: Name: {name} Race: {race.value} Class: {character_class.value} Background Type: {background_type or 'Adventurer'} {f'Additional guidance: {personality_prompt}' if personality_prompt else ''} Generate in this EXACT format: PERSONALITY TRAITS: - [trait 1] - [trait 2] IDEALS: [One core ideal that drives them] BONDS: [What/who they care about most] FLAWS: [A meaningful character flaw] BACKSTORY: [2-3 paragraphs of compelling backstory that explains how they became an adventurer] GOALS: - [goal 1] - [goal 2] Keep it concise but evocative. Focus on what makes this character interesting to play.""" response = self.ai_client.generate_creative(prompt, system_prompt=system_prompt) # Parse response background = self._parse_background_response(response, background_type or "Adventurer") return background def _parse_background_response(self, response: str, background_type: str) -> CharacterBackground: """Parse AI response into CharacterBackground""" lines = response.strip().split('\n') traits = [] ideals = "" bonds = "" flaws = "" backstory = "" goals = [] current_section = None backstory_lines = [] for line in lines: line = line.strip() if not line: continue if line.startswith('PERSONALITY TRAITS:'): current_section = 'traits' elif line.startswith('IDEALS:'): current_section = 'ideals' elif line.startswith('BONDS:'): current_section = 'bonds' elif line.startswith('FLAWS:'): current_section = 'flaws' elif line.startswith('BACKSTORY:'): current_section = 'backstory' elif line.startswith('GOALS:'): current_section = 'goals' elif line.startswith('-'): content = line[1:].strip() if current_section == 'traits': traits.append(content) elif current_section == 'goals': goals.append(content) else: if current_section == 'ideals': ideals += line + " " elif current_section == 'bonds': bonds += line + " " elif current_section == 'flaws': flaws += line + " " elif current_section == 'backstory': backstory_lines.append(line) backstory = '\n'.join(backstory_lines).strip() return CharacterBackground( background_type=background_type, personality_traits=traits[:3], # Max 3 traits ideals=ideals.strip(), bonds=bonds.strip(), flaws=flaws.strip(), backstory=backstory, goals=goals ) def _determine_alignment(self, background: CharacterBackground) -> Alignment: """Determine alignment based on personality""" # Simple heuristic based on ideals and traits ideals_lower = background.ideals.lower() if 'law' in ideals_lower or 'order' in ideals_lower or 'honor' in ideals_lower: if 'help' in ideals_lower or 'good' in ideals_lower or 'kind' in ideals_lower: return Alignment.LAWFUL_GOOD elif 'evil' in ideals_lower or 'power' in ideals_lower: return Alignment.LAWFUL_EVIL else: return Alignment.LAWFUL_NEUTRAL elif 'chaos' in ideals_lower or 'freedom' in ideals_lower: if 'help' in ideals_lower or 'good' in ideals_lower: return Alignment.CHAOTIC_GOOD elif 'evil' in ideals_lower or 'selfish' in ideals_lower: return Alignment.CHAOTIC_EVIL else: return Alignment.CHAOTIC_NEUTRAL else: if 'help' in ideals_lower or 'good' in ideals_lower: return Alignment.NEUTRAL_GOOD elif 'evil' in ideals_lower: return Alignment.NEUTRAL_EVIL else: return Alignment.TRUE_NEUTRAL def _get_starting_equipment(self, character_class: DnDClass) -> list: """Get starting equipment for class""" equipment_by_class = { DnDClass.FIGHTER: ["Longsword", "Shield", "Chain Mail", "Explorer's Pack"], DnDClass.WIZARD: ["Spellbook", "Quarterstaff", "Component Pouch", "Scholar's Pack"], DnDClass.ROGUE: ["Shortbow", "Arrows (20)", "Leather Armor", "Thieves' Tools", "Burglar's Pack"], DnDClass.CLERIC: ["Mace", "Scale Mail", "Holy Symbol", "Priest's Pack"], DnDClass.RANGER: ["Longbow", "Arrows (20)", "Leather Armor", "Explorer's Pack"], DnDClass.PALADIN: ["Longsword", "Shield", "Chain Mail", "Holy Symbol", "Priest's Pack"], DnDClass.BARD: ["Rapier", "Lute", "Leather Armor", "Entertainer's Pack"], DnDClass.BARBARIAN: ["Greataxe", "Javelin (4)", "Explorer's Pack"], DnDClass.DRUID: ["Quarterstaff", "Leather Armor", "Druidic Focus", "Explorer's Pack"], DnDClass.MONK: ["Shortsword", "Dart (10)", "Explorer's Pack"], DnDClass.SORCERER: ["Dagger (2)", "Component Pouch", "Dungeoneer's Pack"], DnDClass.WARLOCK: ["Crossbow", "Bolts (20)", "Leather Armor", "Component Pouch", "Scholar's Pack"], } return equipment_by_class.get(character_class, ["Basic Equipment"]) def _get_class_features(self, character_class: DnDClass, level: int) -> list: """Get class features for level (D&D 5e PHB)""" # Features by class and level features_by_level = { DnDClass.FIGHTER: { 1: ["Fighting Style", "Second Wind"], 2: ["Action Surge (1 use)"], 3: ["Martial Archetype"], 4: ["Ability Score Improvement"], 5: ["Extra Attack (1)"], 6: ["Ability Score Improvement"], 7: ["Martial Archetype Feature"], 8: ["Ability Score Improvement"], 9: ["Indomitable (1 use)"], 10: ["Martial Archetype Feature"], 11: ["Extra Attack (2)"], 12: ["Ability Score Improvement"], 13: ["Indomitable (2 uses)"], 14: ["Ability Score Improvement"], 15: ["Martial Archetype Feature"], 16: ["Ability Score Improvement"], 17: ["Action Surge (2 uses)", "Indomitable (3 uses)"], 18: ["Martial Archetype Feature"], 19: ["Ability Score Improvement"], 20: ["Extra Attack (3)"], }, DnDClass.WIZARD: { 1: ["Spellcasting", "Arcane Recovery"], 2: ["Arcane Tradition"], 3: [], 4: ["Ability Score Improvement"], 5: [], 6: ["Arcane Tradition Feature"], 7: [], 8: ["Ability Score Improvement"], 9: [], 10: ["Arcane Tradition Feature"], 11: [], 12: ["Ability Score Improvement"], 13: [], 14: ["Arcane Tradition Feature"], 15: [], 16: ["Ability Score Improvement"], 17: [], 18: ["Spell Mastery"], 19: ["Ability Score Improvement"], 20: ["Signature Spells"], }, DnDClass.ROGUE: { 1: ["Expertise", "Sneak Attack (1d6)", "Thieves' Cant"], 2: ["Cunning Action"], 3: ["Sneak Attack (2d6)", "Roguish Archetype"], 4: ["Ability Score Improvement"], 5: ["Sneak Attack (3d6)", "Uncanny Dodge"], 6: ["Expertise"], 7: ["Sneak Attack (4d6)", "Evasion"], 8: ["Ability Score Improvement"], 9: ["Sneak Attack (5d6)", "Roguish Archetype Feature"], 10: ["Ability Score Improvement"], 11: ["Sneak Attack (6d6)", "Reliable Talent"], 12: ["Ability Score Improvement"], 13: ["Sneak Attack (7d6)", "Roguish Archetype Feature"], 14: ["Blindsense"], 15: ["Sneak Attack (8d6)", "Slippery Mind"], 16: ["Ability Score Improvement"], 17: ["Sneak Attack (9d6)", "Roguish Archetype Feature"], 18: ["Elusive"], 19: ["Sneak Attack (10d6)", "Ability Score Improvement"], 20: ["Stroke of Luck"], }, DnDClass.CLERIC: { 1: ["Spellcasting", "Divine Domain"], 2: ["Channel Divinity (1/rest)", "Divine Domain Feature"], 3: [], 4: ["Ability Score Improvement"], 5: ["Destroy Undead (CR 1/2)"], 6: ["Channel Divinity (2/rest)", "Divine Domain Feature"], 7: [], 8: ["Ability Score Improvement", "Destroy Undead (CR 1)", "Divine Domain Feature"], 9: [], 10: ["Divine Intervention"], 11: ["Destroy Undead (CR 2)"], 12: ["Ability Score Improvement"], 13: [], 14: ["Destroy Undead (CR 3)"], 15: [], 16: ["Ability Score Improvement"], 17: ["Destroy Undead (CR 4)", "Divine Domain Feature"], 18: ["Channel Divinity (3/rest)"], 19: ["Ability Score Improvement"], 20: ["Divine Intervention Improvement"], }, DnDClass.RANGER: { 1: ["Favored Enemy", "Natural Explorer"], 2: ["Fighting Style", "Spellcasting"], 3: ["Ranger Archetype", "Primeval Awareness"], 4: ["Ability Score Improvement"], 5: ["Extra Attack"], 6: ["Favored Enemy Improvement", "Natural Explorer Improvement"], 7: ["Ranger Archetype Feature"], 8: ["Ability Score Improvement", "Land's Stride"], 9: [], 10: ["Natural Explorer Improvement", "Hide in Plain Sight"], 11: ["Ranger Archetype Feature"], 12: ["Ability Score Improvement"], 13: [], 14: ["Favored Enemy Improvement", "Vanish"], 15: ["Ranger Archetype Feature"], 16: ["Ability Score Improvement"], 17: [], 18: ["Feral Senses"], 19: ["Ability Score Improvement"], 20: ["Foe Slayer"], }, DnDClass.PALADIN: { 1: ["Divine Sense", "Lay on Hands"], 2: ["Fighting Style", "Spellcasting", "Divine Smite"], 3: ["Divine Health", "Sacred Oath"], 4: ["Ability Score Improvement"], 5: ["Extra Attack"], 6: ["Aura of Protection"], 7: ["Sacred Oath Feature"], 8: ["Ability Score Improvement"], 9: [], 10: ["Aura of Courage"], 11: ["Improved Divine Smite"], 12: ["Ability Score Improvement"], 13: [], 14: ["Cleansing Touch"], 15: ["Sacred Oath Feature"], 16: ["Ability Score Improvement"], 17: [], 18: ["Aura Improvements"], 19: ["Ability Score Improvement"], 20: ["Sacred Oath Feature"], }, DnDClass.BARD: { 1: ["Spellcasting", "Bardic Inspiration (d6)"], 2: ["Jack of All Trades", "Song of Rest (d6)"], 3: ["Bard College", "Expertise"], 4: ["Ability Score Improvement"], 5: ["Bardic Inspiration (d8)", "Font of Inspiration"], 6: ["Countercharm", "Bard College Feature"], 7: [], 8: ["Ability Score Improvement"], 9: ["Song of Rest (d8)"], 10: ["Bardic Inspiration (d10)", "Expertise", "Magical Secrets"], 11: [], 12: ["Ability Score Improvement"], 13: ["Song of Rest (d10)"], 14: ["Magical Secrets", "Bard College Feature"], 15: ["Bardic Inspiration (d12)"], 16: ["Ability Score Improvement"], 17: ["Song of Rest (d12)"], 18: ["Magical Secrets"], 19: ["Ability Score Improvement"], 20: ["Superior Inspiration"], }, DnDClass.BARBARIAN: { 1: ["Rage (2/day)", "Unarmored Defense"], 2: ["Reckless Attack", "Danger Sense"], 3: ["Primal Path", "Rage (3/day)"], 4: ["Ability Score Improvement"], 5: ["Extra Attack", "Fast Movement"], 6: ["Path Feature", "Rage (4/day)"], 7: ["Feral Instinct"], 8: ["Ability Score Improvement"], 9: ["Brutal Critical (1 die)"], 10: ["Path Feature", "Rage (5/day)"], 11: ["Relentless Rage"], 12: ["Ability Score Improvement", "Rage (6/day)"], 13: ["Brutal Critical (2 dice)"], 14: ["Path Feature"], 15: ["Persistent Rage"], 16: ["Ability Score Improvement"], 17: ["Brutal Critical (3 dice)", "Rage (Unlimited)"], 18: ["Indomitable Might"], 19: ["Ability Score Improvement"], 20: ["Primal Champion"], }, DnDClass.DRUID: { 1: ["Druidic", "Spellcasting"], 2: ["Wild Shape", "Druid Circle"], 3: [], 4: ["Wild Shape Improvement", "Ability Score Improvement"], 5: [], 6: ["Druid Circle Feature"], 7: [], 8: ["Wild Shape Improvement", "Ability Score Improvement"], 9: [], 10: ["Druid Circle Feature"], 11: [], 12: ["Ability Score Improvement"], 13: [], 14: ["Druid Circle Feature"], 15: [], 16: ["Ability Score Improvement"], 17: [], 18: ["Timeless Body", "Beast Spells"], 19: ["Ability Score Improvement"], 20: ["Archdruid"], }, DnDClass.MONK: { 1: ["Unarmored Defense", "Martial Arts (1d4)"], 2: ["Ki", "Unarmored Movement"], 3: ["Monastic Tradition", "Deflect Missiles"], 4: ["Ability Score Improvement", "Slow Fall"], 5: ["Extra Attack", "Stunning Strike", "Martial Arts (1d6)"], 6: ["Ki-Empowered Strikes", "Monastic Tradition Feature"], 7: ["Evasion", "Stillness of Mind"], 8: ["Ability Score Improvement"], 9: ["Unarmored Movement Improvement"], 10: ["Purity of Body"], 11: ["Monastic Tradition Feature", "Martial Arts (1d8)"], 12: ["Ability Score Improvement"], 13: ["Tongue of the Sun and Moon"], 14: ["Diamond Soul"], 15: ["Timeless Body"], 16: ["Ability Score Improvement"], 17: ["Monastic Tradition Feature", "Martial Arts (1d10)"], 18: ["Empty Body"], 19: ["Ability Score Improvement"], 20: ["Perfect Self"], }, DnDClass.SORCERER: { 1: ["Spellcasting", "Sorcerous Origin"], 2: ["Font of Magic"], 3: ["Metamagic (2 options)"], 4: ["Ability Score Improvement"], 5: [], 6: ["Sorcerous Origin Feature"], 7: [], 8: ["Ability Score Improvement"], 9: [], 10: ["Metamagic (3 options)"], 11: [], 12: ["Ability Score Improvement"], 13: [], 14: ["Sorcerous Origin Feature"], 15: [], 16: ["Ability Score Improvement"], 17: ["Metamagic (4 options)"], 18: ["Sorcerous Origin Feature"], 19: ["Ability Score Improvement"], 20: ["Sorcerous Restoration"], }, DnDClass.WARLOCK: { 1: ["Otherworldly Patron", "Pact Magic"], 2: ["Eldritch Invocations (2)"], 3: ["Pact Boon"], 4: ["Ability Score Improvement"], 5: ["Eldritch Invocations (3)"], 6: ["Otherworldly Patron Feature"], 7: ["Eldritch Invocations (4)"], 8: ["Ability Score Improvement"], 9: ["Eldritch Invocations (5)"], 10: ["Otherworldly Patron Feature"], 11: ["Mystic Arcanum (6th level)"], 12: ["Ability Score Improvement", "Eldritch Invocations (6)"], 13: ["Mystic Arcanum (7th level)"], 14: ["Otherworldly Patron Feature"], 15: ["Mystic Arcanum (8th level)", "Eldritch Invocations (7)"], 16: ["Ability Score Improvement"], 17: ["Mystic Arcanum (9th level)"], 18: ["Eldritch Invocations (8)"], 19: ["Ability Score Improvement"], 20: ["Eldritch Master"], }, } # Get features for this class up to the current level class_features_by_level = features_by_level.get(character_class, {}) all_features = [] for lvl in range(1, min(level + 1, 21)): all_features.extend(class_features_by_level.get(lvl, [])) return all_features if all_features else ["Class Features"] def _get_proficiencies(self, character_class: DnDClass, background: str) -> list: """ Get proficiencies - includes fixed proficiencies and notes about choices Players need to make skill choices in D&D 5e """ # Saving throw proficiencies (FIXED - no choice) saves = { DnDClass.FIGHTER: ["Saving Throws: Strength, Constitution"], DnDClass.WIZARD: ["Saving Throws: Intelligence, Wisdom"], DnDClass.ROGUE: ["Saving Throws: Dexterity, Intelligence"], DnDClass.CLERIC: ["Saving Throws: Wisdom, Charisma"], DnDClass.RANGER: ["Saving Throws: Strength, Dexterity"], DnDClass.PALADIN: ["Saving Throws: Wisdom, Charisma"], DnDClass.BARD: ["Saving Throws: Dexterity, Charisma"], DnDClass.BARBARIAN: ["Saving Throws: Strength, Constitution"], DnDClass.DRUID: ["Saving Throws: Intelligence, Wisdom"], DnDClass.MONK: ["Saving Throws: Strength, Dexterity"], DnDClass.SORCERER: ["Saving Throws: Constitution, Charisma"], DnDClass.WARLOCK: ["Saving Throws: Wisdom, Charisma"], } # Armor and weapon proficiencies (FIXED - no choice) armor_weapons = { DnDClass.FIGHTER: ["All armor", "All shields", "Simple weapons", "Martial weapons"], DnDClass.WIZARD: ["Weapons: Daggers, Darts, Slings, Quarterstaffs, Light crossbows"], DnDClass.ROGUE: ["Light armor", "Weapons: Simple weapons, Hand crossbows, Longswords, Rapiers, Shortswords", "Tools: Thieves' tools"], DnDClass.CLERIC: ["Light armor", "Medium armor", "Shields", "Simple weapons"], DnDClass.RANGER: ["Light armor", "Medium armor", "Shields", "Simple weapons", "Martial weapons"], DnDClass.PALADIN: ["All armor", "All shields", "Simple weapons", "Martial weapons"], DnDClass.BARD: ["Light armor", "Weapons: Simple weapons, Hand crossbows, Longswords, Rapiers, Shortswords", "Tools: Three musical instruments of your choice"], DnDClass.BARBARIAN: ["Light armor", "Medium armor", "Shields", "Simple weapons", "Martial weapons"], DnDClass.DRUID: ["Light armor (nonmetal)", "Medium armor (nonmetal)", "Shields (nonmetal)", "Weapons: Clubs, Daggers, Darts, Javelins, Maces, Quarterstaffs, Scimitars, Sickles, Slings, Spears", "Tools: Herbalism kit"], DnDClass.MONK: ["Weapons: Simple weapons, Shortswords", "Tools: Choose one artisan's tool or musical instrument"], DnDClass.SORCERER: ["Weapons: Daggers, Darts, Slings, Quarterstaffs, Light crossbows"], DnDClass.WARLOCK: ["Light armor", "Simple weapons"], } # Skill choices (PLAYER MUST CHOOSE - provide guidance) skill_choices = { DnDClass.FIGHTER: ["Choose 2 skills from: Acrobatics, Animal Handling, Athletics, History, Insight, Intimidation, Perception, Survival"], DnDClass.WIZARD: ["Choose 2 skills from: Arcana, History, Insight, Investigation, Medicine, Religion"], DnDClass.ROGUE: ["Choose 4 skills from: Acrobatics, Athletics, Deception, Insight, Intimidation, Investigation, Perception, Performance, Persuasion, Sleight of Hand, Stealth"], DnDClass.CLERIC: ["Choose 2 skills from: History, Insight, Medicine, Persuasion, Religion"], DnDClass.RANGER: ["Choose 3 skills from: Animal Handling, Athletics, Insight, Investigation, Nature, Perception, Stealth, Survival"], DnDClass.PALADIN: ["Choose 2 skills from: Athletics, Insight, Intimidation, Medicine, Persuasion, Religion"], DnDClass.BARD: ["Choose any 3 skills"], DnDClass.BARBARIAN: ["Choose 2 skills from: Animal Handling, Athletics, Intimidation, Nature, Perception, Survival"], DnDClass.DRUID: ["Choose 2 skills from: Arcana, Animal Handling, Insight, Medicine, Nature, Perception, Religion, Survival"], DnDClass.MONK: ["Choose 2 skills from: Acrobatics, Athletics, History, Insight, Religion, Stealth"], DnDClass.SORCERER: ["Choose 2 skills from: Arcana, Deception, Insight, Intimidation, Persuasion, Religion"], DnDClass.WARLOCK: ["Choose 2 skills from: Arcana, Deception, History, Intimidation, Investigation, Nature, Religion"], } # Background provides 2 additional skills (VARIES - typical examples provided) background_note = f"Background ({background}): Grants 2 additional skill proficiencies (varies by background)" # Combine all proficiencies proficiencies = [] proficiencies.extend(saves.get(character_class, [])) proficiencies.extend(armor_weapons.get(character_class, [])) proficiencies.extend(skill_choices.get(character_class, [])) proficiencies.append(background_note) return proficiencies def save_character(self, character: Character): """Save character to database""" self.database.save( entity_id=character.id, entity_type="character", data=character.to_dict() ) def load_character(self, character_id: str) -> Optional[Character]: """Load character from database""" data = self.database.load(character_id, "character") if data: return Character(**data) return None def list_characters(self) -> list[Character]: """List all saved characters""" characters_data = self.database.load_all("character") return [Character(**data) for data in characters_data] def delete_character(self, character_id: str): """Delete character from database""" self.database.delete(character_id) def generate_portrait( self, character: Character, style: str = "fantasy art", quality: str = "standard", provider: str = "auto" ) -> tuple[Optional[str], Optional[str]]: """ Generate character portrait using DALL-E 3 or HuggingFace Args: character: Character to generate portrait for style: Art style (e.g., "fantasy art", "digital painting", "anime") quality: Image quality ("standard" or "hd") - OpenAI only provider: "openai", "huggingface", or "auto" Returns: Tuple of (file_path, status_message) """ if not self.image_generator: return None, "❌ Image generation not available (API key required)" try: file_path, image_bytes = self.image_generator.generate_character_portrait( character=character, style=style, quality=quality, provider=provider ) if file_path: return file_path, f"✅ Portrait generated successfully!\nSaved to: {file_path}" else: return None, "❌ Failed to generate portrait" except Exception as e: return None, f"❌ Error generating portrait: {str(e)}" def get_portrait_path(self, character_id: str) -> Optional[str]: """Get saved portrait path for character""" if not self.image_generator: return None return self.image_generator.get_portrait_path(character_id)