""" Character Create Tab for D'n'D Campaign Manager """ import gradio as gr from typing import Tuple import traceback from src.agents.character_agent import CharacterAgent from src.models.character import DnDRace, DnDClass, Alignment from src.utils.validators import get_available_races, get_available_classes from src.utils.image_generator import RACE_SKIN_TONES class CharacterCreateTab: """Create Character tab for D&D character creation""" def __init__(self, character_agent: CharacterAgent): self.character_agent = character_agent def _get_alignment_description(self, alignment: str) -> str: """Get personality guidance based on alignment""" descriptions = { "Lawful Good": "a strong sense of justice, honor, and desire to help others within the rules", "Neutral Good": "genuine kindness and desire to help, but flexibility in how they achieve good", "Chaotic Good": "rebellious goodness, fighting for freedom and helping others by breaking unjust rules", "Lawful Neutral": "strict adherence to law, order, and tradition above good or evil", "True Neutral": "balance and pragmatism, avoiding extreme positions", "Chaotic Neutral": "unpredictability, freedom-loving nature, and self-interest", "Lawful Evil": "tyrannical control, following their own code while causing harm", "Neutral Evil": "pure self-interest and willingness to harm others for personal gain", "Chaotic Evil": "destructive chaos, cruelty, and disregard for any rules or others' wellbeing" } return descriptions.get(alignment, "their moral compass") def generate_name_ui( self, race: str, character_class: str, gender: str, alignment: str, ) -> str: """ Generate a character name using AI Alignment can influence name generation (e.g., darker names for evil characters) """ try: race_enum = DnDRace(race) class_enum = DnDClass(character_class) # Add alignment hint to the generation prompt alignment_hint = None if "Evil" in alignment: alignment_hint = "with a darker, more menacing tone" elif "Good" in alignment: alignment_hint = "with a heroic, noble tone" elif alignment == "Chaotic Neutral": alignment_hint = "with a wild, unpredictable feel" # Build custom prompt if we have alignment influence if alignment_hint: prompt = f"""Generate a single fantasy character name for a D&D character. Race: {race_enum.value} Class: {class_enum.value} Gender: {gender if gender != "Not specified" else "any"} Alignment: {alignment} - name should reflect this {alignment_hint} Requirements: - Just the name, nothing else - Make it sound appropriate for the race, gender, and alignment - {alignment_hint} - Make it memorable and fitting for an adventurer - 2-3 words maximum Examples: - Evil: "Malakai Shadowbane", "Drusilla Nightwhisper" - Good: "Elara Lightbringer", "Theron Brightheart" - Chaotic: "Raven Wildfire", "Zephyr Stormblade" Generate only the name:""" name = self.character_agent.ai_client.generate_creative(prompt).strip() name = name.split('\n')[0].strip('"\'') return name else: # Use standard generation name = self.character_agent.generate_name( race=race_enum, character_class=class_enum, gender=gender if gender != "Not specified" else None ) return name except Exception as e: return f"Error: {str(e)}" def create_character_ui( self, name: str, race: str, character_class: str, level: int, gender: str, skin_tone: str, alignment: str, background_dropdown: str, custom_background: str, personality_prompt: str, stats_method: str, use_ai_background: bool, ) -> Tuple[str, str]: """ Create character with UI inputs Returns: Tuple of (character_sheet_markdown, status_message) """ try: # Validate inputs if not name.strip(): return "", "❌ Error: Please provide a character name (use 'Generate Name' button or type one)" if level < 1 or level > 20: return "", "❌ Error: Level must be between 1 and 20" # Convert race, class, and alignment try: race_enum = DnDRace(race) class_enum = DnDClass(character_class) alignment_enum = Alignment(alignment) except ValueError as e: return "", f"❌ Error: Invalid race, class, or alignment - {e}" # Determine final background type if background_dropdown == "Custom (enter below)": final_background = custom_background.strip() if custom_background.strip() else "Adventurer" else: final_background = background_dropdown # Create character with gender AND alignment in personality prompt gender_hint = f"Character is {gender}. " if gender != "Not specified" else "" alignment_hint = f"Character's alignment is {alignment}, so their personality and backstory should reflect {self._get_alignment_description(alignment)}. " full_personality_prompt = gender_hint + alignment_hint + (personality_prompt if personality_prompt else "") character = self.character_agent.create_character( name=name, race=race_enum, character_class=class_enum, level=level, background_type=final_background, personality_prompt=full_personality_prompt if use_ai_background else None, stats_method=stats_method, ) # Override alignment, gender, and skin tone if user specified character.alignment = alignment_enum character.gender = gender if gender != "Not specified" else None character.skin_tone = skin_tone if skin_tone else None # Generate markdown markdown = character.to_markdown() status = f"""✅ Character Created Successfully! **ID:** {character.id} **Name:** {character.name} **Race:** {character.race.value} **Class:** {character.character_class.value} **Level:** {character.level} Character has been saved to database.""" return markdown, status except Exception as e: error_msg = f"❌ Error creating character:\n\n{str(e)}\n\n{traceback.format_exc()}" return "", error_msg def create(self) -> None: """Create and return the Create Character tab component""" with gr.Tab("Create Character"): gr.Markdown("### Character Creation") with gr.Row(): with gr.Column(): gr.Markdown("#### Basic Information") with gr.Row(): name_input = gr.Textbox( label="Character Name", placeholder="Thorin Ironforge", info="Type a name or generate one below", scale=3 ) race_dropdown = gr.Dropdown( choices=get_available_races(), label="Race", value="Human", info="Character's race" ) class_dropdown = gr.Dropdown( choices=get_available_classes(), label="Class", value="Fighter", info="Character's class" ) gender_dropdown = gr.Dropdown( choices=["Male", "Female", "Non-binary", "Not specified"], label="Gender", value="Not specified", info="Character's gender" ) skin_tone_dropdown = gr.Dropdown( choices=RACE_SKIN_TONES[DnDRace.HUMAN], # Default to Human label="Skin Tone / Color", value=None, info="Select appropriate color for the race" ) generate_name_btn = gr.Button("🎲 Generate Name", variant="secondary", size="sm") level_slider = gr.Slider( minimum=1, maximum=20, value=1, step=1, label="Level", info="Character level (1-20)" ) alignment_dropdown = gr.Dropdown( choices=[ "Lawful Good", "Neutral Good", "Chaotic Good", "Lawful Neutral", "True Neutral", "Chaotic Neutral", "Lawful Evil", "Neutral Evil", "Chaotic Evil" ], label="Alignment", value="True Neutral", info="Character's moral alignment" ) with gr.Column(): gr.Markdown("#### Background & Personality") background_dropdown = gr.Dropdown( choices=[ "Acolyte", "Charlatan", "Criminal", "Entertainer", "Folk Hero", "Guild Artisan", "Hermit", "Noble", "Outlander", "Sage", "Sailor", "Soldier", "Urchin", "Custom (enter below)" ], label="Background Type", value="Soldier", info="Select from D&D 5e backgrounds or choose Custom" ) custom_background_input = gr.Textbox( label="Custom Background", placeholder="Enter your custom background...", value="", visible=False, info="Only used if 'Custom' is selected above" ) use_ai_background = gr.Checkbox( label="Generate detailed backstory", value=True, info="Create a unique backstory for this character" ) personality_input = gr.Textbox( label="Personality Guidance (Optional)", placeholder="A mysterious ranger who protects the forest...", lines=3, info="Guide AI in creating personality (if enabled)" ) stats_method = gr.Radio( choices=["standard_array", "roll", "point_buy"], label="Ability Score Method", value="standard_array", info="How to generate ability scores" ) create_btn = gr.Button("⚔️ Create Character", variant="primary", size="lg") gr.Markdown("---") with gr.Row(): character_output = gr.Markdown(label="Character Sheet") status_output = gr.Textbox(label="Status", lines=8) # Toggle custom background visibility def toggle_custom_background(background_choice): return gr.update(visible=background_choice == "Custom (enter below)") # Update skin tone options when race changes def update_skin_tone_choices(race: str): try: race_enum = DnDRace(race) skin_tones = RACE_SKIN_TONES.get(race_enum, RACE_SKIN_TONES[DnDRace.HUMAN]) return gr.update(choices=skin_tones, value=skin_tones[0] if skin_tones else None) except: return gr.update(choices=RACE_SKIN_TONES[DnDRace.HUMAN], value=RACE_SKIN_TONES[DnDRace.HUMAN][0]) background_dropdown.change( fn=toggle_custom_background, inputs=[background_dropdown], outputs=[custom_background_input] ) race_dropdown.change( fn=update_skin_tone_choices, inputs=[race_dropdown], outputs=[skin_tone_dropdown] ) # Generate name action - includes alignment for more thematic names generate_name_btn.click( fn=self.generate_name_ui, inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown], outputs=[name_input] ) # Create character action create_btn.click( fn=self.create_character_ui, inputs=[ name_input, race_dropdown, class_dropdown, level_slider, gender_dropdown, skin_tone_dropdown, alignment_dropdown, background_dropdown, custom_background_input, personality_input, stats_method, use_ai_background, ], outputs=[character_output, status_output] )