DnD_Campaign_Manager / src /agents /character_agent.py
official.ghost.logic
Deploy D&D Campaign Manager v2
71b378e
"""
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)