File size: 13,926 Bytes
71b378e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
"""
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]
            )