Spaces:
Sleeping
Sleeping
| """ | |
| 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] | |
| ) | |