MelkortheCorrupt commited on
Commit
b197a8c
·
1 Parent(s): 237e002

D&D Character Creator and Campaign Assistant

Browse files
app.py CHANGED
@@ -1,14 +1,14 @@
1
- # app.py - Fixed D&D Campaign and Character Creator with AI Agents
2
-
3
  import gradio as gr
4
  import logging
5
- from typing import Dict, List, Tuple, Optional
6
- import json
7
  import os
8
  from dotenv import load_dotenv
 
9
  from dataclasses import dataclass
10
  from enum import Enum
 
 
11
  import random
 
12
 
13
  # Load environment variables
14
  load_dotenv()
@@ -17,22 +17,10 @@ load_dotenv()
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger(__name__)
19
 
20
- # Load OpenAI API key
21
- try:
22
- import openai
23
- api_key = os.getenv("OPENAI_API_KEY")
24
- if api_key:
25
- openai.api_key = api_key
26
- logger.info("✅ OpenAI API key loaded")
27
- else:
28
- logger.warning("⚠️ No OpenAI API key found")
29
- except ImportError:
30
- logger.warning("⚠️ OpenAI package not installed")
31
-
32
  # ===== DATA MODELS =====
33
  class Alignment(Enum):
34
  LAWFUL_GOOD = "Lawful Good"
35
- NEUTRAL_GOOD = "Neutral Good"
36
  CHAOTIC_GOOD = "Chaotic Good"
37
  LAWFUL_NEUTRAL = "Lawful Neutral"
38
  TRUE_NEUTRAL = "True Neutral"
@@ -41,1353 +29,61 @@ class Alignment(Enum):
41
  NEUTRAL_EVIL = "Neutral Evil"
42
  CHAOTIC_EVIL = "Chaotic Evil"
43
 
44
- @dataclass
45
- class CharacterClass:
46
- name: str
47
- hit_die: int
48
- primary_ability: List[str]
49
- saving_throws: List[str]
50
- skills: List[str]
51
-
52
- @dataclass
53
- class Race:
54
- name: str
55
- ability_modifiers: Dict[str, int]
56
- traits: List[str]
57
- languages: List[str]
58
-
59
  @dataclass
60
  class Character:
61
  name: str
62
  race: str
63
  character_class: str
64
  level: int
65
- gender: str
66
  alignment: Alignment
67
- abilities: Dict[str, int]
68
- hit_points: int
69
- skills: List[str]
70
- background: str
71
- backstory: str
72
  portrait_url: Optional[str] = None
73
 
74
- @dataclass
75
- class Campaign:
76
- name: str
77
- theme: str
78
- level_range: str
79
- description: str
80
- locations: List[str]
81
- npcs: List[str]
82
- plot_hooks: List[str]
83
-
84
- @dataclass
85
- class NPC:
86
- name: str
87
- race: str
88
- occupation: str
89
- personality: str
90
- secret: str
91
- voice_description: str
92
- relationship_to_party: str
93
-
94
- # ===== AI AGENT CLASSES =====
95
- class DungeonMasterAgent:
96
- """AI agent that acts as a Dungeon Master"""
97
-
98
- def __init__(self):
99
- self.personality = "Creative, fair, and engaging storyteller"
100
- self.knowledge_areas = ["D&D rules", "storytelling", "world-building", "character development"]
101
-
102
- def generate_campaign_concept(self, theme: str, level: int, player_count: int) -> Dict:
103
- """Generate a complete campaign concept"""
104
- try:
105
- from openai import OpenAI
106
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
107
-
108
- prompt = f"""As an experienced D&D Dungeon Master, create a campaign concept for:
109
- Theme: {theme}
110
- Player Level: {level}
111
- Number of Players: {player_count}
112
-
113
- Provide:
114
- 1. Campaign Name
115
- 2. Core Plot Hook (2-3 sentences)
116
- 3. Main Antagonist
117
- 4. 3 Key Locations
118
- 5. Central Conflict
119
- 6. Estimated Campaign Length
120
- 7. Unique Elements/Mechanics
121
-
122
- Make it engaging and ready to play!"""
123
-
124
- response = client.chat.completions.create(
125
- model="gpt-4",
126
- messages=[{"role": "system", "content": "You are an expert D&D Dungeon Master with 20 years of experience creating memorable campaigns."},
127
- {"role": "user", "content": prompt}],
128
- max_tokens=500,
129
- temperature=0.8
130
- )
131
-
132
- content = response.choices[0].message.content
133
- return {"success": True, "content": content}
134
-
135
- except Exception as e:
136
- logger.error(f"Campaign generation failed: {e}")
137
- return {"success": False, "error": str(e), "content": f"Mock Campaign: {theme} adventure for {player_count} level {level} characters"}
138
-
139
- def generate_session_content(self, campaign_context: str, session_number: int) -> Dict:
140
- """Generate content for a specific session"""
141
- try:
142
- from openai import OpenAI
143
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
144
-
145
- prompt = f"""Create session {session_number} content for this campaign:
146
- {campaign_context}
147
-
148
- Generate:
149
- 1. Session Opening (scene description)
150
- 2. 3 Potential Encounters (combat, social, exploration)
151
- 3. Key NPCs for this session
152
- 4. Skill challenges or puzzles
153
- 5. Potential plot developments
154
- 6. Cliffhanger ending options
155
-
156
- Make it detailed enough for a DM to run immediately."""
157
-
158
- response = client.chat.completions.create(
159
- model="gpt-4",
160
- messages=[{"role": "system", "content": "You are a D&D Dungeon Master preparing detailed session content."},
161
- {"role": "user", "content": prompt}],
162
- max_tokens=600,
163
- temperature=0.7
164
- )
165
-
166
- return {"success": True, "content": response.choices[0].message.content}
167
-
168
- except Exception as e:
169
- logger.error(f"Session generation failed: {e}")
170
- return {"success": False, "error": str(e), "content": f"Mock Session {session_number}: Adventure continues..."}
171
-
172
- class NPCAgent:
173
- """AI agent specialized in creating and roleplaying NPCs"""
174
-
175
- def __init__(self):
176
- self.personality = "Versatile character actor with deep understanding of motivations"
177
- self.specializations = ["Character creation", "Dialogue", "Motivations", "Voice acting"]
178
-
179
- def generate_npc(self, context: str, role: str, importance: str) -> Dict:
180
- """Generate a detailed NPC"""
181
- try:
182
- from openai import OpenAI
183
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
184
-
185
- prompt = f"""Create a detailed NPC for:
186
- Context: {context}
187
- Role: {role}
188
- Importance: {importance}
189
-
190
- Generate:
191
- 1. Name and basic demographics
192
- 2. Personality traits (3-4 key traits)
193
- 3. Background and motivation
194
- 4. Speech patterns/accent description
195
- 5. Physical description
196
- 6. Secret or hidden agenda
197
- 7. How they react to different party approaches
198
- 8. Potential quest hooks they could provide
199
-
200
- Make them memorable and three-dimensional!"""
201
-
202
- response = client.chat.completions.create(
203
- model="gpt-4",
204
- messages=[{"role": "system", "content": "You are an expert at creating memorable, three-dimensional NPCs for D&D campaigns."},
205
- {"role": "user", "content": prompt}],
206
- max_tokens=400,
207
- temperature=0.8
208
- )
209
-
210
- return {"success": True, "content": response.choices[0].message.content}
211
-
212
- except Exception as e:
213
- logger.error(f"NPC generation failed: {e}")
214
- return {"success": False, "error": str(e), "content": f"Mock NPC: {role} character for {context}"}
215
-
216
- def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict:
217
- """Roleplay as an NPC in response to player actions"""
218
- try:
219
- from openai import OpenAI
220
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
221
-
222
- prompt = f"""You are roleplaying as this NPC:
223
- {npc_description}
224
-
225
- Context: {context}
226
- Player says/does: {player_input}
227
-
228
- Respond in character with:
229
- 1. Dialogue (in quotes)
230
- 2. Actions/body language (in italics)
231
- 3. Internal thoughts/motivations (in parentheses)
232
-
233
- Stay true to the character's personality and motivations!"""
234
-
235
- response = client.chat.completions.create(
236
- model="gpt-4",
237
- messages=[{"role": "system", "content": "You are a skilled voice actor and D&D player, staying in character as NPCs."},
238
- {"role": "user", "content": prompt}],
239
- max_tokens=200,
240
- temperature=0.9
241
- )
242
-
243
- return {"success": True, "content": response.choices[0].message.content}
244
-
245
- except Exception as e:
246
- logger.error(f"NPC roleplay failed: {e}")
247
- return {"success": False, "error": str(e), "content": "Mock NPC Response: The character responds appropriately to your action."}
248
-
249
- class WorldBuilderAgent:
250
- """AI agent focused on creating consistent world elements"""
251
-
252
- def __init__(self):
253
- self.personality = "Detail-oriented architect of fictional worlds"
254
- self.specializations = ["Geography", "Politics", "Culture", "History", "Economics"]
255
-
256
- def generate_location(self, location_type: str, theme: str, purpose: str) -> Dict:
257
- """Generate a detailed location"""
258
- try:
259
- from openai import OpenAI
260
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
261
-
262
- prompt = f"""Create a detailed {location_type} with:
263
- Theme: {theme}
264
- Purpose in campaign: {purpose}
265
-
266
- Generate:
267
- 1. Name and general description
268
- 2. Key areas/rooms (at least 5)
269
- 3. Notable inhabitants
270
- 4. Hidden secrets or mysteries
271
- 5. Potential dangers or challenges
272
- 6. Valuable resources or rewards
273
- 7. Connections to broader world/campaign
274
- 8. Sensory details (sights, sounds, smells)
275
-
276
- Make it feel lived-in and realistic!"""
277
-
278
- response = client.chat.completions.create(
279
- model="gpt-4",
280
- messages=[{"role": "system", "content": "You are a master world-builder creating immersive D&D locations."},
281
- {"role": "user", "content": prompt}],
282
- max_tokens=500,
283
- temperature=0.7
284
- )
285
-
286
- return {"success": True, "content": response.choices[0].message.content}
287
-
288
- except Exception as e:
289
- logger.error(f"Location generation failed: {e}")
290
- return {"success": False, "error": str(e), "content": f"Mock Location: {theme} {location_type} for {purpose}"}
291
 
292
- class LootMasterAgent:
293
- """AI agent specialized in creating balanced loot and magic items"""
294
-
295
- def __init__(self):
296
- self.personality = "Meticulous curator of magical treasures"
297
- self.specializations = ["Game balance", "Magic item design", "Treasure distribution"]
298
-
299
- def generate_loot_table(self, level: int, encounter_type: str, rarity: str) -> Dict:
300
- """Generate a balanced loot table"""
301
- try:
302
- from openai import OpenAI
303
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
304
-
305
- prompt = f"""Create a balanced loot table for:
306
- Party Level: {level}
307
- Encounter Type: {encounter_type}
308
- Rarity Level: {rarity}
309
-
310
- Generate:
311
- 1. Gold/Currency amounts
312
- 2. Common items (consumables, gear)
313
- 3. Uncommon magical items (if appropriate)
314
- 4. Rare items (if high level)
315
- 5. Unique/plot-relevant items
316
- 6. Alternative treasures (information, allies, etc.)
317
-
318
- Ensure balance and appropriateness for the level!"""
319
-
320
- response = client.chat.completions.create(
321
- model="gpt-4",
322
- messages=[{"role": "system", "content": "You are an expert at D&D game balance and treasure design."},
323
- {"role": "user", "content": prompt}],
324
- max_tokens=300,
325
- temperature=0.6
326
- )
327
-
328
- return {"success": True, "content": response.choices[0].message.content}
329
-
330
- except Exception as e:
331
- logger.error(f"Loot generation failed: {e}")
332
- return {"success": False, "error": str(e), "content": f"Mock Loot: Level {level} {encounter_type} {rarity} treasures"}
333
 
334
- def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict:
335
- """Create a custom magic item"""
336
- try:
337
- from openai import OpenAI
338
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
339
-
340
- prompt = f"""Design a custom magic item:
341
- Concept: {item_concept}
342
- Power Level: {power_level}
343
- Campaign Theme: {campaign_theme}
344
-
345
- Provide:
346
- 1. Item name and basic description
347
- 2. Mechanical effects (stats, abilities)
348
- 3. Activation requirements
349
- 4. Rarity and attunement needs
350
- 5. Physical appearance
351
- 6. Historical background/lore
352
- 7. Potential drawbacks or limitations
353
- 8. How it fits the campaign theme
354
-
355
- Make it balanced and interesting!"""
356
-
357
- response = client.chat.completions.create(
358
- model="gpt-4",
359
- messages=[{"role": "system", "content": "You are a master craftsperson of magical items for D&D, balancing power with narrative interest."},
360
- {"role": "user", "content": prompt}],
361
- max_tokens=400,
362
- temperature=0.7
363
- )
364
-
365
- return {"success": True, "content": response.choices[0].message.content}
366
-
367
- except Exception as e:
368
- logger.error(f"Magic item creation failed: {e}")
369
- return {"success": False, "error": str(e), "content": f"Mock Magic Item: {power_level} {item_concept} with {campaign_theme} theme"}
370
 
371
- # ===== CHARACTER CREATOR CLASS =====
372
- class CharacterCreator:
373
- """Enhanced character creator with AI integration"""
 
 
 
374
 
375
- def __init__(self):
376
- self.classes = self._get_default_classes()
377
- self.races = self._get_default_races()
378
- self.backgrounds = self._get_backgrounds()
379
-
380
- def _get_default_classes(self) -> Dict[str, CharacterClass]:
381
- return {
382
- "Fighter": CharacterClass(
383
- name="Fighter", hit_die=10,
384
- primary_ability=["Strength", "Dexterity"],
385
- saving_throws=["Strength", "Constitution"],
386
- skills=["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Perception", "Survival"]
387
- ),
388
- "Wizard": CharacterClass(
389
- name="Wizard", hit_die=6,
390
- primary_ability=["Intelligence"],
391
- saving_throws=["Intelligence", "Wisdom"],
392
- skills=["Arcana", "History", "Insight", "Investigation", "Medicine", "Religion"]
393
- ),
394
- "Rogue": CharacterClass(
395
- name="Rogue", hit_die=8,
396
- primary_ability=["Dexterity"],
397
- saving_throws=["Dexterity", "Intelligence"],
398
- skills=["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Performance", "Persuasion", "Sleight of Hand", "Stealth"]
399
- ),
400
- "Cleric": CharacterClass(
401
- name="Cleric", hit_die=8,
402
- primary_ability=["Wisdom"],
403
- saving_throws=["Wisdom", "Charisma"],
404
- skills=["History", "Insight", "Medicine", "Persuasion", "Religion"]
405
- ),
406
- "Barbarian": CharacterClass(
407
- name="Barbarian", hit_die=12,
408
- primary_ability=["Strength"],
409
- saving_throws=["Strength", "Constitution"],
410
- skills=["Animal Handling", "Athletics", "Intimidation", "Nature", "Perception", "Survival"]
411
- ),
412
- "Bard": CharacterClass(
413
- name="Bard", hit_die=8,
414
- primary_ability=["Charisma"],
415
- saving_throws=["Dexterity", "Charisma"],
416
- skills=["Any three of your choice"]
417
- ),
418
- "Druid": CharacterClass(
419
- name="Druid", hit_die=8,
420
- primary_ability=["Wisdom"],
421
- saving_throws=["Intelligence", "Wisdom"],
422
- skills=["Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival"]
423
- ),
424
- "Monk": CharacterClass(
425
- name="Monk", hit_die=8,
426
- primary_ability=["Dexterity", "Wisdom"],
427
- saving_throws=["Strength", "Dexterity"],
428
- skills=["Acrobatics", "Athletics", "History", "Insight", "Religion", "Stealth"]
429
- ),
430
- "Paladin": CharacterClass(
431
- name="Paladin", hit_die=10,
432
- primary_ability=["Strength", "Charisma"],
433
- saving_throws=["Wisdom", "Charisma"],
434
- skills=["Athletics", "Insight", "Intimidation", "Medicine", "Persuasion", "Religion"]
435
- ),
436
- "Ranger": CharacterClass(
437
- name="Ranger", hit_die=10,
438
- primary_ability=["Dexterity", "Wisdom"],
439
- saving_throws=["Strength", "Dexterity"],
440
- skills=["Animal Handling", "Athletics", "Insight", "Investigation", "Nature", "Perception", "Stealth", "Survival"]
441
- ),
442
- "Sorcerer": CharacterClass(
443
- name="Sorcerer", hit_die=6,
444
- primary_ability=["Charisma"],
445
- saving_throws=["Constitution", "Charisma"],
446
- skills=["Arcana", "Deception", "Insight", "Intimidation", "Persuasion", "Religion"]
447
- ),
448
- "Warlock": CharacterClass(
449
- name="Warlock", hit_die=8,
450
- primary_ability=["Charisma"],
451
- saving_throws=["Wisdom", "Charisma"],
452
- skills=["Arcana", "Deception", "History", "Intimidation", "Investigation", "Nature", "Religion"]
453
- )
454
- }
455
-
456
- def _get_default_races(self) -> Dict[str, Race]:
457
- return {
458
- "Human": Race(
459
- name="Human",
460
- ability_modifiers={"All": 1},
461
- traits=["Extra Language", "Extra Skill", "Versatile"],
462
- languages=["Common", "One other"]
463
- ),
464
- "Elf": Race(
465
- name="Elf",
466
- ability_modifiers={"Dexterity": 2},
467
- traits=["Darkvision", "Keen Senses", "Fey Ancestry", "Trance"],
468
- languages=["Common", "Elvish"]
469
- ),
470
- "Dwarf": Race(
471
- name="Dwarf",
472
- ability_modifiers={"Constitution": 2},
473
- traits=["Darkvision", "Dwarven Resilience", "Stonecunning"],
474
- languages=["Common", "Dwarvish"]
475
- ),
476
- "Halfling": Race(
477
- name="Halfling",
478
- ability_modifiers={"Dexterity": 2},
479
- traits=["Lucky", "Brave", "Halfling Nimbleness"],
480
- languages=["Common", "Halfling"]
481
- ),
482
- "Dragonborn": Race(
483
- name="Dragonborn",
484
- ability_modifiers={"Strength": 2, "Charisma": 1},
485
- traits=["Draconic Ancestry", "Breath Weapon", "Damage Resistance"],
486
- languages=["Common", "Draconic"]
487
- ),
488
- "Gnome": Race(
489
- name="Gnome",
490
- ability_modifiers={"Intelligence": 2},
491
- traits=["Darkvision", "Gnome Cunning"],
492
- languages=["Common", "Gnomish"]
493
- ),
494
- "Half-Elf": Race(
495
- name="Half-Elf",
496
- ability_modifiers={"Charisma": 2, "Choice": 1},
497
- traits=["Darkvision", "Fey Ancestry", "Two Skills"],
498
- languages=["Common", "Elvish", "One other"]
499
- ),
500
- "Half-Orc": Race(
501
- name="Half-Orc",
502
- ability_modifiers={"Strength": 2, "Constitution": 1},
503
- traits=["Darkvision", "Relentless Endurance", "Savage Attacks"],
504
- languages=["Common", "Orc"]
505
- ),
506
- "Tiefling": Race(
507
- name="Tiefling",
508
- ability_modifiers={"Intelligence": 1, "Charisma": 2},
509
- traits=["Darkvision", "Hellish Resistance", "Infernal Legacy"],
510
- languages=["Common", "Infernal"]
511
- )
512
- }
513
-
514
- def _get_backgrounds(self) -> List[str]:
515
- return [
516
- "Acolyte", "Criminal", "Folk Hero", "Noble", "Sage", "Soldier",
517
- "Charlatan", "Entertainer", "Guild Artisan", "Hermit", "Outlander", "Sailor"
518
- ]
519
-
520
- def roll_ability_scores(self) -> Dict[str, int]:
521
- """Roll 4d6, drop lowest, for each ability score"""
522
- abilities = {}
523
- for ability in ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"]:
524
- rolls = [random.randint(1, 6) for _ in range(4)]
525
- rolls.sort(reverse=True)
526
- abilities[ability] = sum(rolls[:3]) # Take top 3
527
- return abilities
528
-
529
- def calculate_ability_modifier(self, score: int) -> int:
530
- """Calculate ability modifier from score"""
531
- return (score - 10) // 2
532
-
533
- def calculate_hit_points(self, char_class: str, level: int, constitution_modifier: int) -> int:
534
- """Calculate hit points based on class, level, and CON modifier"""
535
- hit_die = self.classes[char_class].hit_die
536
- base_hp = hit_die + constitution_modifier # Max HP at level 1
537
-
538
- # Add average HP for additional levels
539
- for _ in range(level - 1):
540
- base_hp += (hit_die // 2 + 1) + constitution_modifier
541
-
542
- return max(1, base_hp) # Minimum 1 HP
543
-
544
- def apply_racial_modifiers(self, base_abilities: Dict[str, int], race: str) -> Dict[str, int]:
545
- """Apply racial ability score modifiers"""
546
- modified_abilities = base_abilities.copy()
547
- race_data = self.races[race]
548
-
549
- for ability, modifier in race_data.ability_modifiers.items():
550
- if ability == "All":
551
- for ability_name in modified_abilities:
552
- modified_abilities[ability_name] += modifier
553
- elif ability == "Choice":
554
- # For simplicity, add to lowest score
555
- lowest_ability = min(modified_abilities, key=modified_abilities.get)
556
- modified_abilities[lowest_ability] += modifier
557
- else:
558
- modified_abilities[ability] += modifier
559
-
560
- return modified_abilities
561
-
562
- # ===== IMAGE GENERATION =====
563
- def generate_image(prompt: str) -> str:
564
- """Generate image using OpenAI DALL-E"""
565
- try:
566
- from openai import OpenAI
567
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
568
-
569
- response = client.images.generate(
570
- model="dall-e-3",
571
- prompt=prompt,
572
- size="1024x1024",
573
- quality="standard",
574
- n=1,
575
- )
576
-
577
- return response.data[0].url
578
-
579
- except Exception as e:
580
- logger.error(f"Image generation failed: {e}")
581
- return "https://via.placeholder.com/512x512/dc2626/ffffff?text=Image+Generation+Failed"
582
-
583
- # ===== MAIN INTERFACE =====
584
- def create_main_interface():
585
- """Create the main D&D Campaign Manager interface"""
586
-
587
- # Initialize agents
588
- dm_agent = DungeonMasterAgent()
589
- npc_agent = NPCAgent()
590
- world_agent = WorldBuilderAgent()
591
- loot_agent = LootMasterAgent()
592
- character_creator = CharacterCreator()
593
-
594
- custom_css = """
595
- .agent-card {
596
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
597
- padding: 20px;
598
- border-radius: 15px;
599
- margin: 10px 0;
600
- color: white;
601
- }
602
-
603
- .output-box {
604
- background: #f8f9fa;
605
- border: 1px solid #dee2e6;
606
- border-radius: 10px;
607
- padding: 15px;
608
- margin: 10px 0;
609
- }
610
- """
611
-
612
- with gr.Blocks(css=custom_css, title="D&D Campaign Manager") as demo:
613
  gr.Markdown("""
614
- # 🏰 Advanced D&D Campaign Manager
615
-
616
- *Your AI-powered toolkit for epic adventures*
617
-
618
- **Features:**
619
- - 🎭 AI Dungeon Master Assistant
620
- - 👥 Intelligent NPC Creator & Roleplay
621
- - 🗺️ World Builder Agent
622
- - 💰 Loot Master & Magic Item Designer
623
- - 🐉 Enhanced Character Creator
624
  """)
625
-
626
- with gr.Tabs():
627
- # ===== CHARACTER CREATOR TAB =====
628
- with gr.TabItem("🐉 Character Creator"):
629
- with gr.Row():
630
- with gr.Column(scale=2):
631
- gr.Markdown("## 📝 Character Details")
632
-
633
- character_name = gr.Textbox(label="Character Name", placeholder="Enter name...")
634
-
635
- with gr.Row():
636
- race_dropdown = gr.Dropdown(
637
- choices=list(character_creator.races.keys()),
638
- label="Race", value="Human"
639
- )
640
- class_dropdown = gr.Dropdown(
641
- choices=list(character_creator.classes.keys()),
642
- label="Class", value="Fighter"
643
- )
644
- gender_dropdown = gr.Dropdown(
645
- choices=["Male", "Female", "Non-binary", "Transgender Male", "Transgender Female", "Genderfluid", "Agender", "Other"],
646
- label="Gender", value="Male"
647
- )
648
-
649
- with gr.Row():
650
- level_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Level")
651
- alignment_dropdown = gr.Dropdown(
652
- choices=[alignment.value for alignment in Alignment],
653
- label="Alignment", value=Alignment.LAWFUL_GOOD.value
654
- )
655
-
656
- background_dropdown = gr.Dropdown(
657
- choices=character_creator._get_backgrounds(),
658
- label="Background", value="Folk Hero"
659
- )
660
-
661
- # AI Enhancement Buttons
662
- with gr.Row():
663
- generate_name_btn = gr.Button("✨ AI Generate Name", variant="secondary")
664
- generate_backstory_btn = gr.Button("📚 AI Generate Backstory", variant="secondary")
665
-
666
- backstory = gr.Textbox(label="Backstory", lines=4, placeholder="Character background...")
667
-
668
- # Ability Scores
669
- gr.Markdown("## 🎲 Ability Scores")
670
- roll_btn = gr.Button("🎲 Roll Ability Scores", variant="primary")
671
-
672
- with gr.Row():
673
- str_score = gr.Number(label="Strength", value=10, precision=0)
674
- dex_score = gr.Number(label="Dexterity", value=10, precision=0)
675
- con_score = gr.Number(label="Constitution", value=10, precision=0)
676
-
677
- with gr.Row():
678
- int_score = gr.Number(label="Intelligence", value=10, precision=0)
679
- wis_score = gr.Number(label="Wisdom", value=10, precision=0)
680
- cha_score = gr.Number(label="Charisma", value=10, precision=0)
681
-
682
- with gr.Column(scale=1):
683
- gr.Markdown("## 📊 Character Summary")
684
- character_summary = gr.Markdown("*Create character to see summary*")
685
-
686
- gr.Markdown("## 🎨 Character Portrait")
687
- portrait_btn = gr.Button("🎨 Generate AI Portrait", variant="primary")
688
- character_portrait = gr.Image(label="Portrait", height=300)
689
-
690
- gr.Markdown("## 💾 Export")
691
- export_btn = gr.Button("📥 Export Character")
692
- export_file = gr.File(label="Character JSON")
693
-
694
- # Hidden state
695
- character_data = gr.State()
696
-
697
- # ===== CAMPAIGN CREATOR TAB =====
698
- with gr.TabItem("🎭 AI Dungeon Master"):
699
- gr.Markdown("## 🎯 Campaign Generation", elem_classes=["agent-card"])
700
-
701
- with gr.Row():
702
- with gr.Column():
703
- campaign_theme = gr.Dropdown(
704
- choices=["High Fantasy", "Dark Fantasy", "Urban Fantasy", "Steampunk", "Horror", "Comedy", "Political Intrigue", "Exploration"],
705
- label="Campaign Theme", value="High Fantasy"
706
- )
707
- campaign_level = gr.Slider(minimum=1, maximum=20, value=5, label="Starting Level")
708
- player_count = gr.Slider(minimum=1, maximum=8, value=4, label="Number of Players")
709
-
710
- generate_campaign_btn = gr.Button("🎲 Generate Campaign Concept", variant="primary", size="lg")
711
-
712
- with gr.Column():
713
- campaign_visual_btn = gr.Button("🖼️ Generate Campaign Art")
714
- campaign_image = gr.Image(label="Campaign Visual", height=300)
715
-
716
- campaign_output = gr.Textbox(label="Campaign Concept", lines=10, elem_classes=["output-box"])
717
-
718
- gr.Markdown("## 📅 Session Planning")
719
- with gr.Row():
720
- session_number = gr.Number(label="Session Number", value=1, precision=0)
721
- generate_session_btn = gr.Button("📝 Generate Session Content", variant="secondary")
722
-
723
- session_output = gr.Textbox(label="Session Content", lines=8, elem_classes=["output-box"])
724
-
725
- # ===== NPC CREATOR TAB =====
726
- with gr.TabItem("👥 NPC Agent"):
727
- gr.Markdown("## 🎭 NPC Creator & Roleplay Assistant", elem_classes=["agent-card"])
728
-
729
- with gr.Row():
730
- with gr.Column():
731
- gr.Markdown("### Create New NPC")
732
- npc_context = gr.Textbox(label="Campaign/Scene Context", placeholder="Describe the setting or situation...")
733
- npc_role = gr.Dropdown(
734
- choices=["Ally", "Neutral", "Antagonist", "Quest Giver", "Merchant", "Authority Figure", "Mysterious Stranger"],
735
- label="NPC Role", value="Neutral"
736
- )
737
- npc_importance = gr.Dropdown(
738
- choices=["Minor", "Moderate", "Major", "Recurring"],
739
- label="Importance Level", value="Moderate"
740
- )
741
-
742
- create_npc_btn = gr.Button("🎭 Generate NPC", variant="primary")
743
-
744
- npc_portrait_btn = gr.Button("🎨 Generate NPC Portrait")
745
- npc_portrait = gr.Image(label="NPC Portrait", height=300)
746
-
747
- with gr.Column():
748
- npc_output = gr.Textbox(label="Generated NPC", lines=12, elem_classes=["output-box"])
749
-
750
- gr.Markdown("### 💬 NPC Roleplay")
751
- with gr.Row():
752
- with gr.Column(scale=2):
753
- player_input = gr.Textbox(label="Player Action/Dialogue", placeholder="What does the player say or do?")
754
- roleplay_context = gr.Textbox(label="Scene Context", placeholder="Describe the current situation...")
755
-
756
- with gr.Column(scale=1):
757
- roleplay_btn = gr.Button("🎭 NPC Response", variant="secondary", size="lg")
758
-
759
- npc_response = gr.Textbox(label="NPC Response", lines=4, elem_classes=["output-box"])
760
-
761
- # Hidden states for NPC
762
- current_npc_data = gr.State()
763
-
764
- # ===== WORLD BUILDER TAB =====
765
- with gr.TabItem("🗺️ World Builder"):
766
- gr.Markdown("## 🏗️ Location & World Generator", elem_classes=["agent-card"])
767
-
768
- with gr.Row():
769
- with gr.Column():
770
- location_type = gr.Dropdown(
771
- choices=["Tavern", "Dungeon", "City", "Forest", "Castle", "Temple", "Shop", "Wilderness", "Mansion", "Cave System"],
772
- label="Location Type", value="Tavern"
773
- )
774
- location_theme = gr.Dropdown(
775
- choices=["Standard Fantasy", "Dark/Gothic", "Mystical", "Abandoned/Ruined", "Luxurious", "Dangerous", "Peaceful", "Mysterious"],
776
- label="Theme", value="Standard Fantasy"
777
- )
778
- location_purpose = gr.Textbox(label="Purpose in Campaign", placeholder="How will this location be used?")
779
-
780
- generate_location_btn = gr.Button("🏗️ Generate Location", variant="primary")
781
- location_art_btn = gr.Button("🖼️ Generate Location Art")
782
-
783
- with gr.Column():
784
- location_image = gr.Image(label="Location Visual", height=300)
785
-
786
- location_output = gr.Textbox(label="Location Details", lines=12, elem_classes=["output-box"])
787
-
788
- # ===== LOOT MASTER TAB =====
789
- with gr.TabItem("💰 Loot Master"):
790
- gr.Markdown("## 💎 Treasure & Magic Item Generator", elem_classes=["agent-card"])
791
-
792
- with gr.Row():
793
- with gr.Column():
794
- gr.Markdown("### 🎲 Loot Table Generator")
795
- loot_level = gr.Slider(minimum=1, maximum=20, value=5, label="Party Level")
796
- encounter_type = gr.Dropdown(
797
- choices=["Boss Fight", "Mini-boss", "Standard Combat", "Exploration Reward", "Quest Completion", "Treasure Hoard"],
798
- label="Encounter Type", value="Standard Combat"
799
- )
800
- loot_rarity = gr.Dropdown(
801
- choices=["Poor", "Standard", "Rich", "Legendary"],
802
- label="Treasure Quality", value="Standard"
803
- )
804
-
805
- generate_loot_btn = gr.Button("💰 Generate Loot Table", variant="primary")
806
-
807
- with gr.Column():
808
- gr.Markdown("### ✨ Custom Magic Item")
809
- item_concept = gr.Textbox(label="Item Concept", placeholder="What kind of item? (sword, ring, staff, etc.)")
810
- power_level = gr.Dropdown(
811
- choices=["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"],
812
- label="Power Level", value="Uncommon"
813
- )
814
- campaign_theme_item = gr.Textbox(label="Campaign Theme", placeholder="How should it fit your campaign?")
815
-
816
- create_item_btn = gr.Button("✨ Create Magic Item", variant="secondary")
817
- item_art_btn = gr.Button("🎨 Generate Item Art")
818
-
819
- with gr.Row():
820
- loot_output = gr.Textbox(label="Loot Table", lines=8, elem_classes=["output-box"])
821
- magic_item_output = gr.Textbox(label="Magic Item Details", lines=8, elem_classes=["output-box"])
822
-
823
- item_image = gr.Image(label="Magic Item Visual", height=250)
824
-
825
- # ===== CAMPAIGN TOOLS TAB =====
826
- with gr.TabItem("🛠️ Campaign Tools"):
827
- gr.Markdown("## 🎯 Advanced Campaign Management")
828
-
829
- with gr.Tabs():
830
- with gr.TabItem("📊 Initiative Tracker"):
831
- gr.Markdown("### ⚔️ Combat Management")
832
-
833
- with gr.Row():
834
- add_character = gr.Textbox(label="Character Name", placeholder="Add to initiative...")
835
- add_initiative = gr.Number(label="Initiative Roll", value=10)
836
- add_btn = gr.Button("➕ Add to Initiative")
837
-
838
- initiative_list = gr.Dataframe(
839
- headers=["Name", "Initiative", "HP", "AC", "Status"],
840
- label="Initiative Order",
841
- interactive=True
842
- )
843
-
844
- with gr.Row():
845
- next_turn_btn = gr.Button("⏭️ Next Turn", variant="primary")
846
- reset_combat_btn = gr.Button("🔄 Reset Combat")
847
-
848
- with gr.TabItem("📝 Session Notes"):
849
- gr.Markdown("### 📖 Session Management")
850
-
851
- session_date = gr.Textbox(label="Session Date", value="Session 1")
852
-
853
- with gr.Row():
854
- with gr.Column():
855
- key_events = gr.Textbox(label="Key Events", lines=4, placeholder="What happened this session?")
856
- npc_interactions = gr.Textbox(label="NPC Interactions", lines=3, placeholder="Who did they meet?")
857
-
858
- with gr.Column():
859
- player_actions = gr.Textbox(label="Notable Player Actions", lines=4, placeholder="What did the players do?")
860
- next_session_prep = gr.Textbox(label="Next Session Prep", lines=3, placeholder="What to prepare for next time?")
861
-
862
- save_notes_btn = gr.Button("💾 Save Session Notes", variant="primary")
863
- notes_output = gr.File(label="Session Notes File")
864
-
865
- with gr.TabItem("🎲 Random Generators"):
866
- gr.Markdown("### 🎯 Quick Generators")
867
-
868
- with gr.Row():
869
- with gr.Column():
870
- gr.Markdown("**Name Generators**")
871
- name_type = gr.Dropdown(
872
- choices=["Human Male", "Human Female", "Elven", "Dwarven", "Orcish", "Fantasy Place", "Tavern", "Shop"],
873
- value="Human Male"
874
- )
875
- gen_name_btn = gr.Button("🎲 Generate Name")
876
- random_name = gr.Textbox(label="Generated Name")
877
-
878
- with gr.Column():
879
- gr.Markdown("**Quick Encounters**")
880
- encounter_level = gr.Slider(1, 20, value=5, label="Party Level")
881
- encounter_difficulty = gr.Dropdown(
882
- choices=["Easy", "Medium", "Hard", "Deadly"],
883
- value="Medium"
884
- )
885
- gen_encounter_btn = gr.Button("⚔️ Generate Encounter")
886
- random_encounter = gr.Textbox(label="Encounter", lines=3)
887
-
888
- with gr.Row():
889
- with gr.Column():
890
- gr.Markdown("**Plot Hooks**")
891
- hook_theme = gr.Dropdown(
892
- choices=["Mystery", "Adventure", "Political", "Personal", "Rescue", "Exploration"],
893
- value="Adventure"
894
- )
895
- gen_hook_btn = gr.Button("🎣 Generate Plot Hook")
896
- plot_hook = gr.Textbox(label="Plot Hook", lines=2)
897
-
898
- with gr.Column():
899
- gr.Markdown("**Weather & Atmosphere**")
900
- climate = gr.Dropdown(
901
- choices=["Temperate", "Tropical", "Arctic", "Desert", "Mountainous"],
902
- value="Temperate"
903
- )
904
- gen_weather_btn = gr.Button("🌤️ Generate Weather")
905
- weather = gr.Textbox(label="Weather & Mood", lines=2)
906
 
907
- # ===== EVENT HANDLERS =====
908
-
909
- # Character Creator Events
910
- def roll_abilities():
911
- abilities = character_creator.roll_ability_scores()
912
- return (
913
- abilities["Strength"], abilities["Dexterity"], abilities["Constitution"],
914
- abilities["Intelligence"], abilities["Wisdom"], abilities["Charisma"]
915
- )
916
-
917
- def generate_character_name(race, char_class, gender, alignment):
918
- try:
919
- from openai import OpenAI
920
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
921
-
922
- prompt = f"Generate a {gender} {race} {char_class} name appropriate for D&D. Return only the name."
923
-
924
- response = client.chat.completions.create(
925
- model="gpt-4",
926
- messages=[{"role": "user", "content": prompt}],
927
- max_tokens=30
928
- )
929
-
930
- return response.choices[0].message.content.strip()
931
- except Exception as e:
932
- # Fallback to simple name generation
933
- names = {
934
- "Human": {
935
- "Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
936
- "Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
937
- "Non-binary": ["Alex", "Jordan", "Riley", "Casey", "Taylor"],
938
- "Other": ["River", "Sage", "Phoenix", "Ash", "Rowan"]
939
- },
940
- "Elf": {
941
- "Male": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
942
- "Female": ["Adrie", "Althaea", "Anastrianna", "Andraste", "Antinua"],
943
- "Non-binary": ["Aerdyl", "Ahvir", "Aramil", "Aranea", "Berris"],
944
- "Other": ["Dayereth", "Enna", "Galinndan", "Hadarai", "Halimath"]
945
- },
946
- "Dwarf": {
947
- "Male": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
948
- "Female": ["Amber", "Bardryn", "Diesa", "Eldeth", "Gunnloda"],
949
- "Non-binary": ["Alberich", "Balin", "Dain", "Fundin", "Gloín"],
950
- "Other": ["Hávar", "Kíli", "Náli", "Ori", "Thorek"]
951
- },
952
- "Halfling": {
953
- "Male": ["Alton", "Ander", "Cade", "Corrin", "Eldon"],
954
- "Female": ["Andry", "Bree", "Callie", "Cora", "Euphemia"],
955
- "Non-binary": ["Finnan", "Garret", "Lindal", "Lyle", "Merric"],
956
- "Other": ["Nedda", "Paela", "Portia", "Seraphina", "Shaena"]
957
- }
958
- }
959
-
960
- # Handle various gender identities
961
- gender_key = gender
962
- if gender in ["Transgender Male", "Male"]:
963
- gender_key = "Male"
964
- elif gender in ["Transgender Female", "Female"]:
965
- gender_key = "Female"
966
- elif gender in ["Non-binary", "Genderfluid", "Agender"]:
967
- gender_key = "Non-binary"
968
- else:
969
- gender_key = "Other"
970
-
971
- race_names = names.get(race, names["Human"])
972
- return random.choice(race_names.get(gender_key, race_names["Other"]))
973
-
974
- def generate_character_backstory(name, race, char_class, gender, alignment, background):
975
- try:
976
- from openai import OpenAI
977
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
978
-
979
- prompt = f"""Create a compelling backstory for {name}, a {gender} {race} {char_class} with {alignment} alignment and {background} background.
980
- Write 2-3 paragraphs about their origins, motivations, and key life events. Be respectful and inclusive in your portrayal."""
981
-
982
- response = client.chat.completions.create(
983
- model="gpt-4",
984
- messages=[{"role": "user", "content": prompt}],
985
- max_tokens=300
986
- )
987
-
988
- return response.choices[0].message.content.strip()
989
- except Exception as e:
990
- return f"{name} is a {gender} {race} {char_class} with a {background} background. Their journey began in their homeland, where they learned the skills that would define their path as an adventurer, embracing their identity and forging their own destiny."
991
-
992
- def generate_character_portrait(character_data):
993
- if not character_data:
994
- return "Please create a character first"
995
-
996
- prompt = f"Fantasy portrait of a {character_data.gender.lower()} {character_data.race.lower()} {character_data.character_class.lower()}, professional D&D character art, respectful and inclusive representation"
997
- return generate_image(prompt)
998
-
999
- def update_character_summary(name, race, char_class, level, gender, alignment,
1000
- str_val, dex_val, con_val, int_val, wis_val, cha_val, background, backstory):
1001
- if not all([str_val, dex_val, con_val, int_val, wis_val, cha_val]):
1002
- return "*Roll ability scores to see summary*", None
1003
-
1004
- base_abilities = {
1005
- "Strength": int(str_val), "Dexterity": int(dex_val), "Constitution": int(con_val),
1006
- "Intelligence": int(int_val), "Wisdom": int(wis_val), "Charisma": int(cha_val)
1007
- }
1008
-
1009
- final_abilities = character_creator.apply_racial_modifiers(base_abilities, race)
1010
- con_modifier = character_creator.calculate_ability_modifier(final_abilities["Constitution"])
1011
- hit_points = character_creator.calculate_hit_points(char_class, int(level), con_modifier)
1012
-
1013
- character = Character(
1014
- name=name or "Unnamed Character", race=race, character_class=char_class,
1015
- level=int(level), gender=gender, alignment=Alignment(alignment), abilities=final_abilities,
1016
- hit_points=hit_points, skills=character_creator.classes[char_class].skills[:2],
1017
- background=background, backstory=backstory
1018
- )
1019
-
1020
- summary = f"""**{character.name}**
1021
- *Level {level} {gender} {race} {char_class} ({alignment})*
1022
-
1023
- **Ability Scores:**
1024
- - STR: {final_abilities['Strength']} ({character_creator.calculate_ability_modifier(final_abilities['Strength']):+d})
1025
- - DEX: {final_abilities['Dexterity']} ({character_creator.calculate_ability_modifier(final_abilities['Dexterity']):+d})
1026
- - CON: {final_abilities['Constitution']} ({character_creator.calculate_ability_modifier(final_abilities['Constitution']):+d})
1027
- - INT: {final_abilities['Intelligence']} ({character_creator.calculate_ability_modifier(final_abilities['Intelligence']):+d})
1028
- - WIS: {final_abilities['Wisdom']} ({character_creator.calculate_ability_modifier(final_abilities['Wisdom']):+d})
1029
- - CHA: {final_abilities['Charisma']} ({character_creator.calculate_ability_modifier(final_abilities['Charisma']):+d})
1030
-
1031
- **Combat Stats:**
1032
- - Hit Points: {hit_points}
1033
- - Hit Die: d{character_creator.classes[char_class].hit_die}
1034
-
1035
- **Background:** {background}"""
1036
-
1037
- return summary, character
1038
-
1039
- # Campaign Events
1040
- def generate_campaign_concept(theme, level, players):
1041
- result = dm_agent.generate_campaign_concept(theme, level, players)
1042
- return result.get("content", "Error generating campaign")
1043
-
1044
- def generate_campaign_art(theme, level):
1045
- prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration"
1046
- return generate_image(prompt)
1047
-
1048
- def generate_session_content(campaign_output, session_num):
1049
- if not campaign_output:
1050
- return "Please generate a campaign concept first"
1051
- result = dm_agent.generate_session_content(campaign_output, session_num)
1052
- return result.get("content", "Error generating session")
1053
-
1054
- # NPC Events
1055
- def create_npc(context, role, importance):
1056
- result = npc_agent.generate_npc(context, role, importance)
1057
- return result.get("content", "Error creating NPC")
1058
-
1059
- def generate_npc_portrait(npc_data):
1060
- if not npc_data:
1061
- prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art"
1062
- else:
1063
- # Extract key details from NPC description for better portraits
1064
- prompt = f"Fantasy portrait of a D&D NPC character based on: {npc_data[:200]}..., detailed face, professional RPG art"
1065
- return generate_image(prompt)
1066
-
1067
- def npc_roleplay_response(npc_data, player_input, context):
1068
- if not npc_data or not player_input:
1069
- return "Please create an NPC and enter player input"
1070
- result = npc_agent.roleplay_npc(npc_data, player_input, context)
1071
- return result.get("content", "Error in roleplay")
1072
-
1073
- # World Builder Events
1074
- def create_location(loc_type, theme, purpose):
1075
- result = world_agent.generate_location(loc_type, theme, purpose)
1076
- return result.get("content", "Error creating location")
1077
-
1078
- def generate_location_art(loc_type, theme):
1079
- prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting"
1080
- return generate_image(prompt)
1081
-
1082
- # Loot Events
1083
- def create_loot_table(level, encounter, rarity):
1084
- result = loot_agent.generate_loot_table(level, encounter, rarity)
1085
- return result.get("content", "Error creating loot")
1086
-
1087
- def create_magic_item(concept, power, theme):
1088
- result = loot_agent.create_custom_magic_item(concept, power, theme)
1089
- return result.get("content", "Error creating item")
1090
-
1091
- def generate_item_art(concept, power):
1092
- prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects"
1093
- return generate_image(prompt)
1094
-
1095
- # Random Generator Events
1096
- def generate_random_name(name_type):
1097
- names = {
1098
- "Human Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
1099
- "Human Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
1100
- "Elven": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
1101
- "Dwarven": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
1102
- "Orcish": ["Grul", "Thark", "Dench", "Feng", "Gell"],
1103
- "Fantasy Place": ["Ravenshollow", "Goldbrook", "Thornfield", "Mistral Keep"],
1104
- "Tavern": ["The Prancing Pony", "Dragon's Rest", "The Silver Tankard"],
1105
- "Shop": ["Magical Mysteries", "Fine Blades & More", "Potions & Remedies"]
1106
- }
1107
- return random.choice(names.get(name_type, ["Unknown"]))
1108
-
1109
- def generate_random_encounter(level, difficulty):
1110
- encounters = [
1111
- f"Bandit ambush (adapted for level {level})",
1112
- f"Wild animal encounter (CR {max(1, level//4)})",
1113
- f"Mysterious traveler with a quest",
1114
- f"Ancient ruins with {difficulty.lower()} traps",
1115
- f"Rival adventuring party",
1116
- f"Magical phenomenon requiring investigation"
1117
- ]
1118
- return random.choice(encounters)
1119
-
1120
- def generate_plot_hook(theme):
1121
- hooks = {
1122
- "Mystery": "A beloved local figure has vanished without a trace, leaving behind only cryptic clues.",
1123
- "Adventure": "Ancient maps surface pointing to a legendary treasure thought lost forever.",
1124
- "Political": "A diplomatic envoy requests secret protection during dangerous negotiations.",
1125
- "Personal": "A character's past catches up with them in an unexpected way.",
1126
- "Rescue": "Innocent people are trapped in a dangerous situation and need immediate help.",
1127
- "Exploration": "Uncharted territories beckon with promises of discovery and danger."
1128
- }
1129
- return hooks.get(theme, "A mysterious stranger approaches with an urgent request.")
1130
-
1131
- def generate_weather(climate):
1132
- weather_options = {
1133
- "Temperate": ["Sunny and mild", "Light rain showers", "Overcast skies", "Gentle breeze"],
1134
- "Tropical": ["Hot and humid", "Sudden thunderstorm", "Sweltering heat", "Monsoon rains"],
1135
- "Arctic": ["Bitter cold winds", "Heavy snowfall", "Blizzard conditions", "Icy fog"],
1136
- "Desert": ["Scorching sun", "Sandstorm approaching", "Cool desert night", "Rare rainfall"],
1137
- "Mountainous": ["Mountain mist", "Alpine winds", "Rocky terrain", "Sudden weather change"]
1138
- }
1139
- return random.choice(weather_options.get(climate, ["Pleasant weather"]))
1140
-
1141
- # Wire up all events
1142
- roll_btn.click(roll_abilities, outputs=[str_score, dex_score, con_score, int_score, wis_score, cha_score])
1143
-
1144
- generate_name_btn.click(
1145
- generate_character_name,
1146
- inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown],
1147
- outputs=[character_name]
1148
- )
1149
-
1150
- generate_backstory_btn.click(
1151
- generate_character_backstory,
1152
- inputs=[character_name, race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown, background_dropdown],
1153
- outputs=[backstory]
1154
- )
1155
-
1156
- portrait_btn.click(
1157
- generate_character_portrait,
1158
- inputs=[character_data],
1159
- outputs=[character_portrait]
1160
- )
1161
-
1162
- # Update character summary when inputs change
1163
- for component in [character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1164
- str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]:
1165
- component.change(
1166
- update_character_summary,
1167
- inputs=[character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1168
- str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory],
1169
- outputs=[character_summary, character_data]
1170
- )
1171
-
1172
- # Campaign events
1173
- generate_campaign_btn.click(
1174
- generate_campaign_concept,
1175
- inputs=[campaign_theme, campaign_level, player_count],
1176
- outputs=[campaign_output]
1177
- )
1178
-
1179
- campaign_visual_btn.click(
1180
- generate_campaign_art,
1181
- inputs=[campaign_theme, campaign_level],
1182
- outputs=[campaign_image]
1183
- )
1184
-
1185
- generate_session_btn.click(
1186
- generate_session_content,
1187
- inputs=[campaign_output, session_number],
1188
- outputs=[session_output]
1189
- )
1190
-
1191
- # NPC events
1192
- create_npc_btn.click(
1193
- create_npc,
1194
- inputs=[npc_context, npc_role, npc_importance],
1195
- outputs=[npc_output]
1196
- )
1197
-
1198
- npc_portrait_btn.click(
1199
- generate_npc_portrait,
1200
- inputs=[npc_output], # Changed from current_npc_data to npc_output
1201
- outputs=[npc_portrait]
1202
- )
1203
-
1204
- roleplay_btn.click(
1205
- npc_roleplay_response,
1206
- inputs=[npc_output, player_input, roleplay_context],
1207
- outputs=[npc_response]
1208
- )
1209
-
1210
- # World builder events
1211
- generate_location_btn.click(
1212
- create_location,
1213
- inputs=[location_type, location_theme, location_purpose],
1214
- outputs=[location_output]
1215
- )
1216
-
1217
- location_art_btn.click(
1218
- generate_location_art,
1219
- inputs=[location_type, location_theme],
1220
- outputs=[location_image]
1221
- )
1222
-
1223
- # Loot events
1224
- generate_loot_btn.click(
1225
- create_loot_table,
1226
- inputs=[loot_level, encounter_type, loot_rarity],
1227
- outputs=[loot_output]
1228
- )
1229
-
1230
- create_item_btn.click(
1231
- create_magic_item,
1232
- inputs=[item_concept, power_level, campaign_theme_item],
1233
- outputs=[magic_item_output]
1234
- )
1235
-
1236
- item_art_btn.click(
1237
- generate_item_art,
1238
- inputs=[item_concept, power_level],
1239
- outputs=[item_image]
1240
- )
1241
-
1242
- # Random generator events
1243
- gen_name_btn.click(generate_random_name, inputs=[name_type], outputs=[random_name])
1244
- gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter])
1245
- gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook])
1246
- gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather])
1247
-
1248
- # Additional missing event handlers
1249
-
1250
- # Export character functionality
1251
- def export_character_json(character_data):
1252
- if not character_data:
1253
- return None
1254
-
1255
- try:
1256
- # Convert character to dict for JSON export
1257
- char_dict = {
1258
- "name": character_data.name,
1259
- "race": character_data.race,
1260
- "class": character_data.character_class,
1261
- "level": character_data.level,
1262
- "gender": character_data.gender,
1263
- "alignment": character_data.alignment.value,
1264
- "abilities": character_data.abilities,
1265
- "hit_points": character_data.hit_points,
1266
- "skills": character_data.skills,
1267
- "background": character_data.background,
1268
- "backstory": character_data.backstory
1269
- }
1270
-
1271
- # Create temporary file
1272
- import tempfile
1273
- import json
1274
-
1275
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
1276
- json.dump(char_dict, f, indent=2)
1277
- return f.name
1278
-
1279
- except Exception as e:
1280
- logger.error(f"Character export failed: {e}")
1281
- return None
1282
-
1283
- export_btn.click(
1284
- export_character_json,
1285
- inputs=[character_data],
1286
- outputs=[export_file]
1287
- )
1288
-
1289
- # Session notes functionality
1290
- def save_session_notes(session_date, key_events, npc_interactions, player_actions, next_session_prep):
1291
- try:
1292
- import tempfile
1293
-
1294
- notes_content = f"""# {session_date}
1295
-
1296
- ## Key Events
1297
- {key_events}
1298
-
1299
- ## NPC Interactions
1300
- {npc_interactions}
1301
-
1302
- ## Notable Player Actions
1303
- {player_actions}
1304
-
1305
- ## Next Session Preparation
1306
- {next_session_prep}
1307
 
1308
- ---
1309
- *Generated by D&D Campaign Manager*
1310
- """
1311
-
1312
- with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
1313
- f.write(notes_content)
1314
- return f.name
1315
-
1316
- except Exception as e:
1317
- logger.error(f"Session notes save failed: {e}")
1318
- return None
1319
-
1320
- save_notes_btn.click(
1321
- save_session_notes,
1322
- inputs=[session_date, key_events, npc_interactions, player_actions, next_session_prep],
1323
- outputs=[notes_output]
1324
- )
1325
-
1326
- # Initiative tracker functionality
1327
- initiative_data = gr.State([])
1328
-
1329
- def add_to_initiative(current_data, char_name, initiative_roll):
1330
- if not char_name:
1331
- return current_data
1332
-
1333
- if current_data is None:
1334
- current_data = []
1335
-
1336
- # Add new character
1337
- new_entry = [char_name, int(initiative_roll), "Full", "10", "Active"]
1338
- current_data.append(new_entry)
1339
-
1340
- # Sort by initiative (descending)
1341
- current_data.sort(key=lambda x: x[1], reverse=True)
1342
-
1343
- return current_data
1344
-
1345
- def reset_initiative():
1346
- return []
1347
-
1348
- add_btn.click(
1349
- add_to_initiative,
1350
- inputs=[initiative_data, add_character, add_initiative],
1351
- outputs=[initiative_data]
1352
- )
1353
-
1354
- # Update the dataframe when initiative_data changes
1355
- initiative_data.change(
1356
- lambda data: data,
1357
- inputs=[initiative_data],
1358
- outputs=[initiative_list]
1359
- )
1360
-
1361
- reset_combat_btn.click(
1362
- reset_initiative,
1363
- outputs=[initiative_data]
1364
- )
1365
-
1366
  return demo
1367
 
1368
- # Main entry point
1369
  if __name__ == "__main__":
1370
- logger.info("🏰 Starting Advanced D&D Campaign Manager...")
1371
-
1372
- # Check for OpenAI API key
1373
- api_key = os.getenv("OPENAI_API_KEY")
1374
- if api_key:
1375
- logger.info("✅ OpenAI API key found - All AI features enabled!")
1376
- else:
1377
- logger.warning("⚠️ No OpenAI API key found - AI features will be limited")
1378
- logger.info("💡 Add OPENAI_API_KEY as a repository secret in HF Spaces settings")
1379
-
1380
- demo = create_main_interface()
1381
-
1382
- try:
1383
- # HF Spaces compatible launch
1384
- demo.launch(
1385
- share=False, # HF Spaces handles sharing
1386
- inbrowser=False # Don't try to open browser in cloud environment
1387
- )
1388
- logger.info("🌐 App launched successfully!")
1389
-
1390
- except Exception as e:
1391
- logger.error(f"❌ Launch failed: {e}")
1392
- # Fallback launch for HF Spaces
1393
- demo.launch()
 
 
 
1
  import gradio as gr
2
  import logging
 
 
3
  import os
4
  from dotenv import load_dotenv
5
+ from image_utils import generate_character_image, generate_npc_image, generate_item_image, generate_location_image, generate_faction_image, generate_deity_image, generate_scenario_image
6
  from dataclasses import dataclass
7
  from enum import Enum
8
+ from typing import Optional, Dict
9
+ import json
10
  import random
11
+ from datetime import datetime
12
 
13
  # Load environment variables
14
  load_dotenv()
 
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger(__name__)
19
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # ===== DATA MODELS =====
21
  class Alignment(Enum):
22
  LAWFUL_GOOD = "Lawful Good"
23
+ NEUTRAL_GOOD = "Neutral Good"
24
  CHAOTIC_GOOD = "Chaotic Good"
25
  LAWFUL_NEUTRAL = "Lawful Neutral"
26
  TRUE_NEUTRAL = "True Neutral"
 
29
  NEUTRAL_EVIL = "Neutral Evil"
30
  CHAOTIC_EVIL = "Chaotic Evil"
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  @dataclass
33
  class Character:
34
  name: str
35
  race: str
36
  character_class: str
37
  level: int
 
38
  alignment: Alignment
39
+ sex: str
 
 
 
 
40
  portrait_url: Optional[str] = None
41
 
42
+ # ===== MOCKUP PLACEHOLDERS =====
43
+ def generate_campaign_concept(theme: str, level: int, player_count: int) -> str:
44
+ return f"Campaign Theme: {theme}\nLevel: {level}\nPlayers: {player_count}\nPlot: A dark force rises..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ def generate_session_content(campaign_summary: str, session_number: int) -> str:
47
+ return f"Session {session_number}: The heroes explore forgotten ruins beneath the city."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ def dummy_generate(data):
50
+ return f"Generated description for: {json.dumps(data, indent=2)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ def save_to_gallery(content_type: str, data: Dict, image_path: str):
53
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
54
+ filename = f"gallery/{content_type}_{timestamp}.json"
55
+ os.makedirs("gallery", exist_ok=True)
56
+ with open(filename, "w") as f:
57
+ json.dump({"type": content_type, "data": data, "image": image_path}, f, indent=2)
58
 
59
+ # ===== GRADIO INTERFACE =====
60
+ def create_dm_toolkit():
61
+ with gr.Blocks(title="🎲 D&D Dungeon Master Toolkit") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  gr.Markdown("""
63
+ # 🎲 D&D Dungeon Master Toolkit
64
+ Create characters, generate visuals, and build epic campaigns.
 
 
 
 
 
 
 
 
65
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ with gr.Tabs():
68
+ from components.characters import character_tab
69
+ from components.npcs import npc_tab
70
+ from components.items import item_tab
71
+ from components.locations import location_tab
72
+ from components.factions_deities import faction_deity_tab
73
+ from components.scenarios import scenario_tab
74
+ from components.campaign import campaign_tab
75
+
76
+ character_tab()
77
+ npc_tab()
78
+ item_tab()
79
+ location_tab()
80
+ faction_deity_tab()
81
+ scenario_tab()
82
+ campaign_tab()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  return demo
85
 
86
+ # ===== MAIN ENTRY =====
87
  if __name__ == "__main__":
88
+ app = create_dm_toolkit()
89
+ app.launch(share=True, server_name="0.0.0.0", server_port=7860, inbrowser=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py.save ADDED
@@ -0,0 +1,1337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ languages: List[str]
2
+
3
+ @dataclass
4
+ class Character:
5
+ name: str
6
+ race: str
7
+ character_class: str
8
+ level: int
9
+ gender: str
10
+ alignment: Alignment
11
+ abilities: Dict[str, int]
12
+ hit_points: int
13
+ skills: List[str]
14
+ background: str
15
+ backstory: str
16
+ portrait_url: Optional[str] = None
17
+
18
+ @dataclass
19
+ class Campaign:
20
+ name: str
21
+ theme: str
22
+ level_range: str
23
+ description: str
24
+ locations: List[str]
25
+ npcs: List[str]
26
+ plot_hooks: List[str]
27
+
28
+ @dataclass
29
+ class NPC:
30
+ name: str
31
+ race: str
32
+ occupation: str
33
+ personality: str
34
+ secret: str
35
+ voice_description: str
36
+ relationship_to_party: str
37
+
38
+ # ===== AI AGENT CLASSES =====
39
+ class DungeonMasterAgent:
40
+ """AI agent that acts as a Dungeon Master"""
41
+
42
+ def __init__(self):
43
+ self.personality = "Creative, fair, and engaging storyteller"
44
+ self.knowledge_areas = ["D&D rules", "storytelling", "world-building", "character development"]
45
+
46
+ def generate_campaign_concept(self, theme: str, level: int, player_count: int) -> Dict:
47
+ """Generate a complete campaign concept"""
48
+ try:
49
+ from openai import OpenAI
50
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
51
+
52
+ prompt = f"""As an experienced D&D Dungeon Master, create a campaign concept for:
53
+ Theme: {theme}
54
+ Player Level: {level}
55
+ Number of Players: {player_count}
56
+
57
+ Provide:
58
+ 1. Campaign Name
59
+ 2. Core Plot Hook (2-3 sentences)
60
+ 3. Main Antagonist
61
+ 4. 3 Key Locations
62
+ 5. Central Conflict
63
+ 6. Estimated Campaign Length
64
+ 7. Unique Elements/Mechanics
65
+
66
+ Make it engaging and ready to play!"""
67
+
68
+ response = client.chat.completions.create(
69
+ model="gpt-4",
70
+ messages=[{"role": "system", "content": "You are an expert D&D Dungeon Master with 20 years of experience creating memorable campaigns."},
71
+ {"role": "user", "content": prompt}],
72
+ max_tokens=500,
73
+ temperature=0.8
74
+ )
75
+
76
+ content = response.choices[0].message.content
77
+ return {"success": True, "content": content}
78
+
79
+ except Exception as e:
80
+ logger.error(f"Campaign generation failed: {e}")
81
+ return {"success": False, "error": str(e), "content": f"Mock Campaign: {theme} adventure for {player_count} level {level} characters"}
82
+
83
+ def generate_session_content(self, campaign_context: str, session_number: int) -> Dict:
84
+ """Generate content for a specific session"""
85
+ try:
86
+ from openai import OpenAI
87
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
88
+
89
+ prompt = f"""Create session {session_number} content for this campaign:
90
+ {campaign_context}
91
+
92
+ Generate:
93
+ 1. Session Opening (scene description)
94
+ 2. 3 Potential Encounters (combat, social, exploration)
95
+ 3. Key NPCs for this session
96
+ 4. Skill challenges or puzzles
97
+ 5. Potential plot developments
98
+ 6. Cliffhanger ending options
99
+
100
+ Make it detailed enough for a DM to run immediately."""
101
+
102
+ response = client.chat.completions.create(
103
+ model="gpt-4",
104
+ messages=[{"role": "system", "content": "You are a D&D Dungeon Master preparing detailed session content."},
105
+ {"role": "user", "content": prompt}],
106
+ max_tokens=600,
107
+ temperature=0.7
108
+ )
109
+
110
+ return {"success": True, "content": response.choices[0].message.content}
111
+
112
+ except Exception as e:
113
+ logger.error(f"Session generation failed: {e}")
114
+ return {"success": False, "error": str(e), "content": f"Mock Session {session_number}: Adventure continues..."}
115
+
116
+ class NPCAgent:
117
+ """AI agent specialized in creating and roleplaying NPCs"""
118
+
119
+ def __init__(self):
120
+ self.personality = "Versatile character actor with deep understanding of motivations"
121
+ self.specializations = ["Character creation", "Dialogue", "Motivations", "Voice acting"]
122
+
123
+ def generate_npc(self, context: str, role: str, importance: str) -> Dict:
124
+ """Generate a detailed NPC"""
125
+ try:
126
+ from openai import OpenAI
127
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
128
+
129
+ prompt = f"""Create a detailed NPC for:
130
+ Context: {context}
131
+ Role: {role}
132
+ Importance: {importance}
133
+
134
+ Generate:
135
+ 1. Name and basic demographics
136
+ 2. Personality traits (3-4 key traits)
137
+ 3. Background and motivation
138
+ 4. Speech patterns/accent description
139
+ 5. Physical description
140
+ 6. Secret or hidden agenda
141
+ 7. How they react to different party approaches
142
+ 8. Potential quest hooks they could provide
143
+
144
+ Make them memorable and three-dimensional!"""
145
+
146
+ response = client.chat.completions.create(
147
+ model="gpt-4",
148
+ messages=[{"role": "system", "content": "You are an expert at creating memorable, three-dimensional NPCs for D&D campaigns."},
149
+ {"role": "user", "content": prompt}],
150
+ max_tokens=400,
151
+ temperature=0.8
152
+ )
153
+
154
+ return {"success": True, "content": response.choices[0].message.content}
155
+
156
+ except Exception as e:
157
+ logger.error(f"NPC generation failed: {e}")
158
+ return {"success": False, "error": str(e), "content": f"Mock NPC: {role} character for {context}"}
159
+
160
+ def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict:
161
+ """Roleplay as an NPC in response to player actions"""
162
+ try:
163
+ from openai import OpenAI
164
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
165
+
166
+ prompt = f"""You are roleplaying as this NPC:
167
+ {npc_description}
168
+
169
+ Context: {context}
170
+ Player says/does: {player_input}
171
+
172
+ Respond in character with:
173
+ 1. Dialogue (in quotes)
174
+ 2. Actions/body language (in italics)
175
+ 3. Internal thoughts/motivations (in parentheses)
176
+
177
+ Stay true to the character's personality and motivations!"""
178
+
179
+ response = client.chat.completions.create(
180
+ model="gpt-4",
181
+ messages=[{"role": "system", "content": "You are a skilled voice actor and D&D player, staying in character as NPCs."},
182
+ {"role": "user", "content": prompt}],
183
+ max_tokens=200,
184
+ temperature=0.9
185
+ )
186
+
187
+ return {"success": True, "content": response.choices[0].message.content}
188
+
189
+ except Exception as e:
190
+ logger.error(f"NPC roleplay failed: {e}")
191
+ return {"success": False, "error": str(e), "content": "Mock NPC Response: The character responds appropriately to your action."}
192
+
193
+ class WorldBuilderAgent:
194
+ """AI agent focused on creating consistent world elements"""
195
+
196
+ def __init__(self):
197
+ self.personality = "Detail-oriented architect of fictional worlds"
198
+ self.specializations = ["Geography", "Politics", "Culture", "History", "Economics"]
199
+
200
+ def generate_location(self, location_type: str, theme: str, purpose: str) -> Dict:
201
+ """Generate a detailed location"""
202
+ try:
203
+ from openai import OpenAI
204
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
205
+
206
+ prompt = f"""Create a detailed {location_type} with:
207
+ Theme: {theme}
208
+ Purpose in campaign: {purpose}
209
+
210
+ Generate:
211
+ 1. Name and general description
212
+ 2. Key areas/rooms (at least 5)
213
+ 3. Notable inhabitants
214
+ 4. Hidden secrets or mysteries
215
+ 5. Potential dangers or challenges
216
+ 6. Valuable resources or rewards
217
+ 7. Connections to broader world/campaign
218
+ 8. Sensory details (sights, sounds, smells)
219
+
220
+ Make it feel lived-in and realistic!"""
221
+
222
+ response = client.chat.completions.create(
223
+ model="gpt-4",
224
+ messages=[{"role": "system", "content": "You are a master world-builder creating immersive D&D locations."},
225
+ {"role": "user", "content": prompt}],
226
+ max_tokens=500,
227
+ temperature=0.7
228
+ )
229
+
230
+ return {"success": True, "content": response.choices[0].message.content}
231
+
232
+ except Exception as e:
233
+ logger.error(f"Location generation failed: {e}")
234
+ return {"success": False, "error": str(e), "content": f"Mock Location: {theme} {location_type} for {purpose}"}
235
+
236
+ class LootMasterAgent:
237
+ """AI agent specialized in creating balanced loot and magic items"""
238
+
239
+ def __init__(self):
240
+ self.personality = "Meticulous curator of magical treasures"
241
+ self.specializations = ["Game balance", "Magic item design", "Treasure distribution"]
242
+
243
+ def generate_loot_table(self, level: int, encounter_type: str, rarity: str) -> Dict:
244
+ """Generate a balanced loot table"""
245
+ try:
246
+ from openai import OpenAI
247
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
248
+
249
+ prompt = f"""Create a balanced loot table for:
250
+ Party Level: {level}
251
+ Encounter Type: {encounter_type}
252
+ Rarity Level: {rarity}
253
+
254
+ Generate:
255
+ 1. Gold/Currency amounts
256
+ 2. Common items (consumables, gear)
257
+ 3. Uncommon magical items (if appropriate)
258
+ 4. Rare items (if high level)
259
+ 5. Unique/plot-relevant items
260
+ 6. Alternative treasures (information, allies, etc.)
261
+
262
+ Ensure balance and appropriateness for the level!"""
263
+
264
+ response = client.chat.completions.create(
265
+ model="gpt-4",
266
+ messages=[{"role": "system", "content": "You are an expert at D&D game balance and treasure design."},
267
+ {"role": "user", "content": prompt}],
268
+ max_tokens=300,
269
+ temperature=0.6
270
+ )
271
+
272
+ return {"success": True, "content": response.choices[0].message.content}
273
+
274
+ except Exception as e:
275
+ logger.error(f"Loot generation failed: {e}")
276
+ return {"success": False, "error": str(e), "content": f"Mock Loot: Level {level} {encounter_type} {rarity} treasures"}
277
+
278
+ def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict:
279
+ """Create a custom magic item"""
280
+ try:
281
+ from openai import OpenAI
282
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
283
+
284
+ prompt = f"""Design a custom magic item:
285
+ Concept: {item_concept}
286
+ Power Level: {power_level}
287
+ Campaign Theme: {campaign_theme}
288
+
289
+ Provide:
290
+ 1. Item name and basic description
291
+ 2. Mechanical effects (stats, abilities)
292
+ 3. Activation requirements
293
+ 4. Rarity and attunement needs
294
+ 5. Physical appearance
295
+ 6. Historical background/lore
296
+ 7. Potential drawbacks or limitations
297
+ 8. How it fits the campaign theme
298
+
299
+ Make it balanced and interesting!"""
300
+
301
+ response = client.chat.completions.create(
302
+ model="gpt-4",
303
+ messages=[{"role": "system", "content": "You are a master craftsperson of magical items for D&D, balancing power with narrative interest."},
304
+ {"role": "user", "content": prompt}],
305
+ max_tokens=400,
306
+ temperature=0.7
307
+ )
308
+
309
+ return {"success": True, "content": response.choices[0].message.content}
310
+
311
+ except Exception as e:
312
+ logger.error(f"Magic item creation failed: {e}")
313
+ return {"success": False, "error": str(e), "content": f"Mock Magic Item: {power_level} {item_concept} with {campaign_theme} theme"}
314
+
315
+ # ===== CHARACTER CREATOR CLASS =====
316
+ class CharacterCreator:
317
+ """Enhanced character creator with AI integration"""
318
+
319
+ def __init__(self):
320
+ self.classes = self._get_default_classes()
321
+ self.races = self._get_default_races()
322
+ self.backgrounds = self._get_backgrounds()
323
+
324
+ def _get_default_classes(self) -> Dict[str, CharacterClass]:
325
+ return {
326
+ "Fighter": CharacterClass(
327
+ name="Fighter", hit_die=10,
328
+ primary_ability=["Strength", "Dexterity"],
329
+ saving_throws=["Strength", "Constitution"],
330
+ skills=["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Perception", "Survival"]
331
+ ),
332
+ "Wizard": CharacterClass(
333
+ name="Wizard", hit_die=6,
334
+ primary_ability=["Intelligence"],
335
+ saving_throws=["Intelligence", "Wisdom"],
336
+ skills=["Arcana", "History", "Insight", "Investigation", "Medicine", "Religion"]
337
+ ),
338
+ "Rogue": CharacterClass(
339
+ name="Rogue", hit_die=8,
340
+ primary_ability=["Dexterity"],
341
+ saving_throws=["Dexterity", "Intelligence"],
342
+ skills=["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Performance", "Persuasion", "Sleight of Hand", "Stealth"]
343
+ ),
344
+ "Cleric": CharacterClass(
345
+ name="Cleric", hit_die=8,
346
+ primary_ability=["Wisdom"],
347
+ saving_throws=["Wisdom", "Charisma"],
348
+ skills=["History", "Insight", "Medicine", "Persuasion", "Religion"]
349
+ ),
350
+ "Barbarian": CharacterClass(
351
+ name="Barbarian", hit_die=12,
352
+ primary_ability=["Strength"],
353
+ saving_throws=["Strength", "Constitution"],
354
+ skills=["Animal Handling", "Athletics", "Intimidation", "Nature", "Perception", "Survival"]
355
+ ),
356
+ "Bard": CharacterClass(
357
+ name="Bard", hit_die=8,
358
+ primary_ability=["Charisma"],
359
+ saving_throws=["Dexterity", "Charisma"],
360
+ skills=["Any three of your choice"]
361
+ ),
362
+ "Druid": CharacterClass(
363
+ name="Druid", hit_die=8,
364
+ primary_ability=["Wisdom"],
365
+ saving_throws=["Intelligence", "Wisdom"],
366
+ skills=["Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival"]
367
+ ),
368
+ "Monk": CharacterClass(
369
+ name="Monk", hit_die=8,
370
+ primary_ability=["Dexterity", "Wisdom"],
371
+ saving_throws=["Strength", "Dexterity"],
372
+ skills=["Acrobatics", "Athletics", "History", "Insight", "Religion", "Stealth"]
373
+ ),
374
+ "Paladin": CharacterClass(
375
+ name="Paladin", hit_die=10,
376
+ primary_ability=["Strength", "Charisma"],
377
+ saving_throws=["Wisdom", "Charisma"],
378
+ skills=["Athletics", "Insight", "Intimidation", "Medicine", "Persuasion", "Religion"]
379
+ ),
380
+ "Ranger": CharacterClass(
381
+ name="Ranger", hit_die=10,
382
+ primary_ability=["Dexterity", "Wisdom"],
383
+ saving_throws=["Strength", "Dexterity"],
384
+ skills=["Animal Handling", "Athletics", "Insight", "Investigation", "Nature", "Perception", "Stealth", "Survival"]
385
+ ),
386
+ "Sorcerer": CharacterClass(
387
+ name="Sorcerer", hit_die=6,
388
+ primary_ability=["Charisma"],
389
+ saving_throws=["Constitution", "Charisma"],
390
+ skills=["Arcana", "Deception", "Insight", "Intimidation", "Persuasion", "Religion"]
391
+ ),
392
+ "Warlock": CharacterClass(
393
+ name="Warlock", hit_die=8,
394
+ primary_ability=["Charisma"],
395
+ saving_throws=["Wisdom", "Charisma"],
396
+ skills=["Arcana", "Deception", "History", "Intimidation", "Investigation", "Nature", "Religion"]
397
+ )
398
+ }
399
+
400
+ def _get_default_races(self) -> Dict[str, Race]:
401
+ return {
402
+ "Human": Race(
403
+ name="Human",
404
+ ability_modifiers={"All": 1},
405
+ traits=["Extra Language", "Extra Skill", "Versatile"],
406
+ languages=["Common", "One other"]
407
+ ),
408
+ "Elf": Race(
409
+ name="Elf",
410
+ ability_modifiers={"Dexterity": 2},
411
+ traits=["Darkvision", "Keen Senses", "Fey Ancestry", "Trance"],
412
+ languages=["Common", "Elvish"]
413
+ ),
414
+ "Dwarf": Race(
415
+ name="Dwarf",
416
+ ability_modifiers={"Constitution": 2},
417
+ traits=["Darkvision", "Dwarven Resilience", "Stonecunning"],
418
+ languages=["Common", "Dwarvish"]
419
+ ),
420
+ "Halfling": Race(
421
+ name="Halfling",
422
+ ability_modifiers={"Dexterity": 2},
423
+ traits=["Lucky", "Brave", "Halfling Nimbleness"],
424
+ languages=["Common", "Halfling"]
425
+ ),
426
+ "Dragonborn": Race(
427
+ name="Dragonborn",
428
+ ability_modifiers={"Strength": 2, "Charisma": 1},
429
+ traits=["Draconic Ancestry", "Breath Weapon", "Damage Resistance"],
430
+ languages=["Common", "Draconic"]
431
+ ),
432
+ "Gnome": Race(
433
+ name="Gnome",
434
+ ability_modifiers={"Intelligence": 2},
435
+ traits=["Darkvision", "Gnome Cunning"],
436
+ languages=["Common", "Gnomish"]
437
+ ),
438
+ "Half-Elf": Race(
439
+ name="Half-Elf",
440
+ ability_modifiers={"Charisma": 2, "Choice": 1},
441
+ traits=["Darkvision", "Fey Ancestry", "Two Skills"],
442
+ languages=["Common", "Elvish", "One other"]
443
+ ),
444
+ "Half-Orc": Race(
445
+ name="Half-Orc",
446
+ ability_modifiers={"Strength": 2, "Constitution": 1},
447
+ traits=["Darkvision", "Relentless Endurance", "Savage Attacks"],
448
+ languages=["Common", "Orc"]
449
+ ),
450
+ "Tiefling": Race(
451
+ name="Tiefling",
452
+ ability_modifiers={"Intelligence": 1, "Charisma": 2},
453
+ traits=["Darkvision", "Hellish Resistance", "Infernal Legacy"],
454
+ languages=["Common", "Infernal"]
455
+ )
456
+ }
457
+
458
+ def _get_backgrounds(self) -> List[str]:
459
+ return [
460
+ "Acolyte", "Criminal", "Folk Hero", "Noble", "Sage", "Soldier",
461
+ "Charlatan", "Entertainer", "Guild Artisan", "Hermit", "Outlander", "Sailor"
462
+ ]
463
+
464
+ def roll_ability_scores(self) -> Dict[str, int]:
465
+ """Roll 4d6, drop lowest, for each ability score"""
466
+ abilities = {}
467
+ for ability in ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"]:
468
+ rolls = [random.randint(1, 6) for _ in range(4)]
469
+ rolls.sort(reverse=True)
470
+ abilities[ability] = sum(rolls[:3]) # Take top 3
471
+ return abilities
472
+
473
+ def calculate_ability_modifier(self, score: int) -> int:
474
+ """Calculate ability modifier from score"""
475
+ return (score - 10) // 2
476
+
477
+ def calculate_hit_points(self, char_class: str, level: int, constitution_modifier: int) -> int:
478
+ """Calculate hit points based on class, level, and CON modifier"""
479
+ hit_die = self.classes[char_class].hit_die
480
+ base_hp = hit_die + constitution_modifier # Max HP at level 1
481
+
482
+ # Add average HP for additional levels
483
+ for _ in range(level - 1):
484
+ base_hp += (hit_die // 2 + 1) + constitution_modifier
485
+
486
+ return max(1, base_hp) # Minimum 1 HP
487
+
488
+ def apply_racial_modifiers(self, base_abilities: Dict[str, int], race: str) -> Dict[str, int]:
489
+ """Apply racial ability score modifiers"""
490
+ modified_abilities = base_abilities.copy()
491
+ race_data = self.races[race]
492
+
493
+ for ability, modifier in race_data.ability_modifiers.items():
494
+ if ability == "All":
495
+ for ability_name in modified_abilities:
496
+ modified_abilities[ability_name] += modifier
497
+ elif ability == "Choice":
498
+ # For simplicity, add to lowest score
499
+ lowest_ability = min(modified_abilities, key=modified_abilities.get)
500
+ modified_abilities[lowest_ability] += modifier
501
+ else:
502
+ modified_abilities[ability] += modifier
503
+
504
+ return modified_abilities
505
+
506
+ # ===== IMAGE GENERATION =====
507
+ def generate_image(prompt: str) -> str:
508
+ """Generate image using OpenAI DALL-E"""
509
+ try:
510
+ from openai import OpenAI
511
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
512
+
513
+ response = client.images.generate(
514
+ model="dall-e-3",
515
+ prompt=prompt,
516
+ size="1024x1024",
517
+ quality="standard",
518
+ n=1,
519
+ )
520
+
521
+ return response.data[0].url
522
+
523
+ except Exception as e:
524
+ logger.error(f"Image generation failed: {e}")
525
+ return "https://via.placeholder.com/512x512/dc2626/ffffff?text=Image+Generation+Failed"
526
+
527
+ # ===== MAIN INTERFACE =====
528
+ def create_main_interface():
529
+ """Create the main D&D Campaign Manager interface"""
530
+
531
+ # Initialize agents
532
+ dm_agent = DungeonMasterAgent()
533
+ npc_agent = NPCAgent()
534
+ world_agent = WorldBuilderAgent()
535
+ loot_agent = LootMasterAgent()
536
+ character_creator = CharacterCreator()
537
+
538
+ custom_css = """
539
+ .agent-card {
540
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
541
+ padding: 20px;
542
+ border-radius: 15px;
543
+ margin: 10px 0;
544
+ color: white;
545
+ }
546
+
547
+ .output-box {
548
+ background: #f8f9fa;
549
+ border: 1px solid #dee2e6;
550
+ border-radius: 10px;
551
+ padding: 15px;
552
+ margin: 10px 0;
553
+ }
554
+ """
555
+
556
+ with gr.Blocks(css=custom_css, title="D&D Campaign Manager") as demo:
557
+ gr.Markdown("""
558
+ # 🏰 Advanced D&D Campaign Manager
559
+
560
+ *Your AI-powered toolkit for epic adventures*
561
+
562
+ **Features:**
563
+ - 🎭 AI Dungeon Master Assistant
564
+ - 👥 Intelligent NPC Creator & Roleplay
565
+ - 🗺️ World Builder Agent
566
+ - 💰 Loot Master & Magic Item Designer
567
+ - 🐉 Enhanced Character Creator
568
+ """)
569
+
570
+ with gr.Tabs():
571
+ # ===== CHARACTER CREATOR TAB =====
572
+ with gr.TabItem("🐉 Character Creator"):
573
+ with gr.Row():
574
+ with gr.Column(scale=2):
575
+ gr.Markdown("## 📝 Character Details")
576
+
577
+ character_name = gr.Textbox(label="Character Name", placeholder="Enter name...")
578
+
579
+ with gr.Row():
580
+ race_dropdown = gr.Dropdown(
581
+ choices=list(character_creator.races.keys()),
582
+ label="Race", value="Human"
583
+ )
584
+ class_dropdown = gr.Dropdown(
585
+ choices=list(character_creator.classes.keys()),
586
+ label="Class", value="Fighter"
587
+ )
588
+ gender_dropdown = gr.Dropdown(
589
+ choices=["Male", "Female", "Non-binary", "Transgender Male", "Transgender Female", "Genderfluid", "Agender", "Other"],
590
+ label="Gender", value="Male"
591
+ )
592
+
593
+ with gr.Row():
594
+ level_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Level")
595
+ alignment_dropdown = gr.Dropdown(
596
+ choices=[alignment.value for alignment in Alignment],
597
+ label="Alignment", value=Alignment.LAWFUL_GOOD.value
598
+ )
599
+
600
+ background_dropdown = gr.Dropdown(
601
+ choices=character_creator._get_backgrounds(),
602
+ label="Background", value="Folk Hero"
603
+ )
604
+
605
+ # AI Enhancement Buttons
606
+ with gr.Row():
607
+ generate_name_btn = gr.Button("✨ AI Generate Name", variant="secondary")
608
+ generate_backstory_btn = gr.Button("📚 AI Generate Backstory", variant="secondary")
609
+
610
+ backstory = gr.Textbox(label="Backstory", lines=4, placeholder="Character background...")
611
+
612
+ # Ability Scores
613
+ gr.Markdown("## 🎲 Ability Scores")
614
+ roll_btn = gr.Button("🎲 Roll Ability Scores", variant="primary")
615
+
616
+ with gr.Row():
617
+ str_score = gr.Number(label="Strength", value=10, precision=0)
618
+ dex_score = gr.Number(label="Dexterity", value=10, precision=0)
619
+ con_score = gr.Number(label="Constitution", value=10, precision=0)
620
+
621
+ with gr.Row():
622
+ int_score = gr.Number(label="Intelligence", value=10, precision=0)
623
+ wis_score = gr.Number(label="Wisdom", value=10, precision=0)
624
+ cha_score = gr.Number(label="Charisma", value=10, precision=0)
625
+
626
+ with gr.Column(scale=1):
627
+ gr.Markdown("## 📊 Character Summary")
628
+ character_summary = gr.Markdown("*Create character to see summary*")
629
+
630
+ gr.Markdown("## 🎨 Character Portrait")
631
+ portrait_btn = gr.Button("🎨 Generate AI Portrait", variant="primary")
632
+ character_portrait = gr.Image(label="Portrait", height=300)
633
+
634
+ gr.Markdown("## 💾 Export")
635
+ export_btn = gr.Button("📥 Export Character")
636
+ export_file = gr.File(label="Character JSON")
637
+
638
+ # Hidden state
639
+ character_data = gr.State()
640
+
641
+ # ===== CAMPAIGN CREATOR TAB =====
642
+ with gr.TabItem("🎭 AI Dungeon Master"):
643
+ gr.Markdown("## 🎯 Campaign Generation", elem_classes=["agent-card"])
644
+
645
+ with gr.Row():
646
+ with gr.Column():
647
+ campaign_theme = gr.Dropdown(
648
+ choices=["High Fantasy", "Dark Fantasy", "Urban Fantasy", "Steampunk", "Horror", "Comedy", "Political Intrigue", "Exploration"],
649
+ label="Campaign Theme", value="High Fantasy"
650
+ )
651
+ campaign_level = gr.Slider(minimum=1, maximum=20, value=5, label="Starting Level")
652
+ player_count = gr.Slider(minimum=1, maximum=8, value=4, label="Number of Players")
653
+
654
+ generate_campaign_btn = gr.Button("🎲 Generate Campaign Concept", variant="primary", size="lg")
655
+
656
+ with gr.Column():
657
+ campaign_visual_btn = gr.Button("🖼️ Generate Campaign Art")
658
+ campaign_image = gr.Image(label="Campaign Visual", height=300)
659
+
660
+ campaign_output = gr.Textbox(label="Campaign Concept", lines=10, elem_classes=["output-box"])
661
+
662
+ gr.Markdown("## 📅 Session Planning")
663
+ with gr.Row():
664
+ session_number = gr.Number(label="Session Number", value=1, precision=0)
665
+ generate_session_btn = gr.Button("📝 Generate Session Content", variant="secondary")
666
+
667
+ session_output = gr.Textbox(label="Session Content", lines=8, elem_classes=["output-box"])
668
+
669
+ # ===== NPC CREATOR TAB =====
670
+ with gr.TabItem("👥 NPC Agent"):
671
+ gr.Markdown("## 🎭 NPC Creator & Roleplay Assistant", elem_classes=["agent-card"])
672
+
673
+ with gr.Row():
674
+ with gr.Column():
675
+ gr.Markdown("### Create New NPC")
676
+ npc_context = gr.Textbox(label="Campaign/Scene Context", placeholder="Describe the setting or situation...")
677
+ npc_role = gr.Dropdown(
678
+ choices=["Ally", "Neutral", "Antagonist", "Quest Giver", "Merchant", "Authority Figure", "Mysterious Stranger"],
679
+ label="NPC Role", value="Neutral"
680
+ )
681
+ npc_importance = gr.Dropdown(
682
+ choices=["Minor", "Moderate", "Major", "Recurring"],
683
+ label="Importance Level", value="Moderate"
684
+ )
685
+
686
+ create_npc_btn = gr.Button("🎭 Generate NPC", variant="primary")
687
+
688
+ npc_portrait_btn = gr.Button("🎨 Generate NPC Portrait")
689
+ npc_portrait = gr.Image(label="NPC Portrait", height=300)
690
+
691
+ with gr.Column():
692
+ npc_output = gr.Textbox(label="Generated NPC", lines=12, elem_classes=["output-box"])
693
+
694
+ gr.Markdown("### 💬 NPC Roleplay")
695
+ with gr.Row():
696
+ with gr.Column(scale=2):
697
+ player_input = gr.Textbox(label="Player Action/Dialogue", placeholder="What does the player say or do?")
698
+ roleplay_context = gr.Textbox(label="Scene Context", placeholder="Describe the current situation...")
699
+
700
+ with gr.Column(scale=1):
701
+ roleplay_btn = gr.Button("🎭 NPC Response", variant="secondary", size="lg")
702
+
703
+ npc_response = gr.Textbox(label="NPC Response", lines=4, elem_classes=["output-box"])
704
+
705
+ # Hidden states for NPC
706
+ current_npc_data = gr.State()
707
+
708
+ # ===== WORLD BUILDER TAB =====
709
+ with gr.TabItem("🗺️ World Builder"):
710
+ gr.Markdown("## 🏗️ Location & World Generator", elem_classes=["agent-card"])
711
+
712
+ with gr.Row():
713
+ with gr.Column():
714
+ location_type = gr.Dropdown(
715
+ choices=["Tavern", "Dungeon", "City", "Forest", "Castle", "Temple", "Shop", "Wilderness", "Mansion", "Cave System"],
716
+ label="Location Type", value="Tavern"
717
+ )
718
+ location_theme = gr.Dropdown(
719
+ choices=["Standard Fantasy", "Dark/Gothic", "Mystical", "Abandoned/Ruined", "Luxurious", "Dangerous", "Peaceful", "Mysterious"],
720
+ label="Theme", value="Standard Fantasy"
721
+ )
722
+ location_purpose = gr.Textbox(label="Purpose in Campaign", placeholder="How will this location be used?")
723
+
724
+ generate_location_btn = gr.Button("🏗️ Generate Location", variant="primary")
725
+ location_art_btn = gr.Button("🖼️ Generate Location Art")
726
+
727
+ with gr.Column():
728
+ location_image = gr.Image(label="Location Visual", height=300)
729
+
730
+ location_output = gr.Textbox(label="Location Details", lines=12, elem_classes=["output-box"])
731
+
732
+ # ===== LOOT MASTER TAB =====
733
+ with gr.TabItem("💰 Loot Master"):
734
+ gr.Markdown("## 💎 Treasure & Magic Item Generator", elem_classes=["agent-card"])
735
+
736
+ with gr.Row():
737
+ with gr.Column():
738
+ gr.Markdown("### 🎲 Loot Table Generator")
739
+ loot_level = gr.Slider(minimum=1, maximum=20, value=5, label="Party Level")
740
+ encounter_type = gr.Dropdown(
741
+ choices=["Boss Fight", "Mini-boss", "Standard Combat", "Exploration Reward", "Quest Completion", "Treasure Hoard"],
742
+ label="Encounter Type", value="Standard Combat"
743
+ )
744
+ loot_rarity = gr.Dropdown(
745
+ choices=["Poor", "Standard", "Rich", "Legendary"],
746
+ label="Treasure Quality", value="Standard"
747
+ )
748
+
749
+ generate_loot_btn = gr.Button("💰 Generate Loot Table", variant="primary")
750
+
751
+ with gr.Column():
752
+ gr.Markdown("### ✨ Custom Magic Item")
753
+ item_concept = gr.Textbox(label="Item Concept", placeholder="What kind of item? (sword, ring, staff, etc.)")
754
+ power_level = gr.Dropdown(
755
+ choices=["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"],
756
+ label="Power Level", value="Uncommon"
757
+ )
758
+ campaign_theme_item = gr.Textbox(label="Campaign Theme", placeholder="How should it fit your campaign?")
759
+
760
+ create_item_btn = gr.Button("✨ Create Magic Item", variant="secondary")
761
+ item_art_btn = gr.Button("🎨 Generate Item Art")
762
+
763
+ with gr.Row():
764
+ loot_output = gr.Textbox(label="Loot Table", lines=8, elem_classes=["output-box"])
765
+ magic_item_output = gr.Textbox(label="Magic Item Details", lines=8, elem_classes=["output-box"])
766
+
767
+ item_image = gr.Image(label="Magic Item Visual", height=250)
768
+
769
+ # ===== CAMPAIGN TOOLS TAB =====
770
+ with gr.TabItem("🛠️ Campaign Tools"):
771
+ gr.Markdown("## 🎯 Advanced Campaign Management")
772
+
773
+ with gr.Tabs():
774
+ with gr.TabItem("📊 Initiative Tracker"):
775
+ gr.Markdown("### ⚔️ Combat Management")
776
+
777
+ with gr.Row():
778
+ add_character = gr.Textbox(label="Character Name", placeholder="Add to initiative...")
779
+ add_initiative = gr.Number(label="Initiative Roll", value=10)
780
+ add_btn = gr.Button("➕ Add to Initiative")
781
+
782
+ initiative_list = gr.Dataframe(
783
+ headers=["Name", "Initiative", "HP", "AC", "Status"],
784
+ label="Initiative Order",
785
+ interactive=True
786
+ )
787
+
788
+ with gr.Row():
789
+ next_turn_btn = gr.Button("⏭️ Next Turn", variant="primary")
790
+ reset_combat_btn = gr.Button("🔄 Reset Combat")
791
+
792
+ with gr.TabItem("📝 Session Notes"):
793
+ gr.Markdown("### 📖 Session Management")
794
+
795
+ session_date = gr.Textbox(label="Session Date", value="Session 1")
796
+
797
+ with gr.Row():
798
+ with gr.Column():
799
+ key_events = gr.Textbox(label="Key Events", lines=4, placeholder="What happened this session?")
800
+ npc_interactions = gr.Textbox(label="NPC Interactions", lines=3, placeholder="Who did they meet?")
801
+
802
+ with gr.Column():
803
+ player_actions = gr.Textbox(label="Notable Player Actions", lines=4, placeholder="What did the players do?")
804
+ next_session_prep = gr.Textbox(label="Next Session Prep", lines=3, placeholder="What to prepare for next time?")
805
+
806
+ save_notes_btn = gr.Button("💾 Save Session Notes", variant="primary")
807
+ notes_output = gr.File(label="Session Notes File")
808
+
809
+ with gr.TabItem("🎲 Random Generators"):
810
+ gr.Markdown("### 🎯 Quick Generators")
811
+
812
+ with gr.Row():
813
+ with gr.Column():
814
+ gr.Markdown("**Name Generators**")
815
+ name_type = gr.Dropdown(
816
+ choices=["Human Male", "Human Female", "Elven", "Dwarven", "Orcish", "Fantasy Place", "Tavern", "Shop"],
817
+ value="Human Male"
818
+ )
819
+ gen_name_btn = gr.Button("🎲 Generate Name")
820
+ random_name = gr.Textbox(label="Generated Name")
821
+
822
+ with gr.Column():
823
+ gr.Markdown("**Quick Encounters**")
824
+ encounter_level = gr.Slider(1, 20, value=5, label="Party Level")
825
+ encounter_difficulty = gr.Dropdown(
826
+ choices=["Easy", "Medium", "Hard", "Deadly"],
827
+ value="Medium"
828
+ )
829
+ gen_encounter_btn = gr.Button("⚔️ Generate Encounter")
830
+ random_encounter = gr.Textbox(label="Encounter", lines=3)
831
+
832
+ with gr.Row():
833
+ with gr.Column():
834
+ gr.Markdown("**Plot Hooks**")
835
+ hook_theme = gr.Dropdown(
836
+ choices=["Mystery", "Adventure", "Political", "Personal", "Rescue", "Exploration"],
837
+ value="Adventure"
838
+ )
839
+ gen_hook_btn = gr.Button("🎣 Generate Plot Hook")
840
+ plot_hook = gr.Textbox(label="Plot Hook", lines=2)
841
+
842
+ with gr.Column():
843
+ gr.Markdown("**Weather & Atmosphere**")
844
+ climate = gr.Dropdown(
845
+ choices=["Temperate", "Tropical", "Arctic", "Desert", "Mountainous"],
846
+ value="Temperate"
847
+ )
848
+ gen_weather_btn = gr.Button("🌤️ Generate Weather")
849
+ weather = gr.Textbox(label="Weather & Mood", lines=2)
850
+
851
+ # ===== EVENT HANDLERS =====
852
+
853
+ # Character Creator Events
854
+ def roll_abilities():
855
+ abilities = character_creator.roll_ability_scores()
856
+ return (
857
+ abilities["Strength"], abilities["Dexterity"], abilities["Constitution"],
858
+ abilities["Intelligence"], abilities["Wisdom"], abilities["Charisma"]
859
+ )
860
+
861
+ def generate_character_name(race, char_class, gender, alignment):
862
+ try:
863
+ from openai import OpenAI
864
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
865
+
866
+ prompt = f"Generate a {gender} {race} {char_class} name appropriate for D&D. Return only the name."
867
+
868
+ response = client.chat.completions.create(
869
+ model="gpt-4",
870
+ messages=[{"role": "user", "content": prompt}],
871
+ max_tokens=30
872
+ )
873
+
874
+ return response.choices[0].message.content.strip()
875
+ except Exception as e:
876
+ # Fallback to simple name generation
877
+ names = {
878
+ "Human": {
879
+ "Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
880
+ "Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
881
+ "Non-binary": ["Alex", "Jordan", "Riley", "Casey", "Taylor"],
882
+ "Other": ["River", "Sage", "Phoenix", "Ash", "Rowan"]
883
+ },
884
+ "Elf": {
885
+ "Male": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
886
+ "Female": ["Adrie", "Althaea", "Anastrianna", "Andraste", "Antinua"],
887
+ "Non-binary": ["Aerdyl", "Ahvir", "Aramil", "Aranea", "Berris"],
888
+ "Other": ["Dayereth", "Enna", "Galinndan", "Hadarai", "Halimath"]
889
+ },
890
+ "Dwarf": {
891
+ "Male": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
892
+ "Female": ["Amber", "Bardryn", "Diesa", "Eldeth", "Gunnloda"],
893
+ "Non-binary": ["Alberich", "Balin", "Dain", "Fundin", "Gloín"],
894
+ "Other": ["Hávar", "Kíli", "Náli", "Ori", "Thorek"]
895
+ },
896
+ "Halfling": {
897
+ "Male": ["Alton", "Ander", "Cade", "Corrin", "Eldon"],
898
+ "Female": ["Andry", "Bree", "Callie", "Cora", "Euphemia"],
899
+ "Non-binary": ["Finnan", "Garret", "Lindal", "Lyle", "Merric"],
900
+ "Other": ["Nedda", "Paela", "Portia", "Seraphina", "Shaena"]
901
+ }
902
+ }
903
+
904
+ # Handle various gender identities
905
+ gender_key = gender
906
+ if gender in ["Transgender Male", "Male"]:
907
+ gender_key = "Male"
908
+ elif gender in ["Transgender Female", "Female"]:
909
+ gender_key = "Female"
910
+ elif gender in ["Non-binary", "Genderfluid", "Agender"]:
911
+ gender_key = "Non-binary"
912
+ else:
913
+ gender_key = "Other"
914
+
915
+ race_names = names.get(race, names["Human"])
916
+ return random.choice(race_names.get(gender_key, race_names["Other"]))
917
+
918
+ def generate_character_backstory(name, race, char_class, gender, alignment, background):
919
+ try:
920
+ from openai import OpenAI
921
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
922
+
923
+ prompt = f"""Create a compelling backstory for {name}, a {gender} {race} {char_class} with {alignment} alignment and {background} background.
924
+ Write 2-3 paragraphs about their origins, motivations, and key life events. Be respectful and inclusive in your portrayal."""
925
+
926
+ response = client.chat.completions.create(
927
+ model="gpt-4",
928
+ messages=[{"role": "user", "content": prompt}],
929
+ max_tokens=300
930
+ )
931
+
932
+ return response.choices[0].message.content.strip()
933
+ except Exception as e:
934
+ return f"{name} is a {gender} {race} {char_class} with a {background} background. Their journey began in their homeland, where they learned the skills that would define their path as an adventurer, embracing their identity and forging their own destiny."
935
+
936
+ def generate_character_portrait(character_data):
937
+ if not character_data:
938
+ return "Please create a character first"
939
+
940
+ prompt = f"Fantasy portrait of a {character_data.gender.lower()} {character_data.race.lower()} {character_data.character_class.lower()}, professional D&D character art, respectful and inclusive representation"
941
+ return generate_image(prompt)
942
+
943
+ def update_character_summary(name, race, char_class, level, gender, alignment,
944
+ str_val, dex_val, con_val, int_val, wis_val, cha_val, background, backstory):
945
+ if not all([str_val, dex_val, con_val, int_val, wis_val, cha_val]):
946
+ return "*Roll ability scores to see summary*", None
947
+
948
+ base_abilities = {
949
+ "Strength": int(str_val), "Dexterity": int(dex_val), "Constitution": int(con_val),
950
+ "Intelligence": int(int_val), "Wisdom": int(wis_val), "Charisma": int(cha_val)
951
+ }
952
+
953
+ final_abilities = character_creator.apply_racial_modifiers(base_abilities, race)
954
+ con_modifier = character_creator.calculate_ability_modifier(final_abilities["Constitution"])
955
+ hit_points = character_creator.calculate_hit_points(char_class, int(level), con_modifier)
956
+
957
+ character = Character(
958
+ name=name or "Unnamed Character", race=race, character_class=char_class,
959
+ level=int(level), gender=gender, alignment=Alignment(alignment), abilities=final_abilities,
960
+ hit_points=hit_points, skills=character_creator.classes[char_class].skills[:2],
961
+ background=background, backstory=backstory
962
+ )
963
+
964
+ summary = f"""**{character.name}**
965
+ *Level {level} {gender} {race} {char_class} ({alignment})*
966
+
967
+ **Ability Scores:**
968
+ - STR: {final_abilities['Strength']} ({character_creator.calculate_ability_modifier(final_abilities['Strength']):+d})
969
+ - DEX: {final_abilities['Dexterity']} ({character_creator.calculate_ability_modifier(final_abilities['Dexterity']):+d})
970
+ - CON: {final_abilities['Constitution']} ({character_creator.calculate_ability_modifier(final_abilities['Constitution']):+d})
971
+ - INT: {final_abilities['Intelligence']} ({character_creator.calculate_ability_modifier(final_abilities['Intelligence']):+d})
972
+ - WIS: {final_abilities['Wisdom']} ({character_creator.calculate_ability_modifier(final_abilities['Wisdom']):+d})
973
+ - CHA: {final_abilities['Charisma']} ({character_creator.calculate_ability_modifier(final_abilities['Charisma']):+d})
974
+
975
+ **Combat Stats:**
976
+ - Hit Points: {hit_points}
977
+ - Hit Die: d{character_creator.classes[char_class].hit_die}
978
+
979
+ **Background:** {background}"""
980
+
981
+ return summary, character
982
+
983
+ # Campaign Events
984
+ def generate_campaign_concept(theme, level, players):
985
+ result = dm_agent.generate_campaign_concept(theme, level, players)
986
+ return result.get("content", "Error generating campaign")
987
+
988
+ def generate_campaign_art(theme, level):
989
+ prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration"
990
+ return generate_image(prompt)
991
+
992
+ def generate_session_content(campaign_output, session_num):
993
+ if not campaign_output:
994
+ return "Please generate a campaign concept first"
995
+ result = dm_agent.generate_session_content(campaign_output, session_num)
996
+ return result.get("content", "Error generating session")
997
+
998
+ # NPC Events
999
+ def create_npc(context, role, importance):
1000
+ result = npc_agent.generate_npc(context, role, importance)
1001
+ return result.get("content", "Error creating NPC")
1002
+
1003
+ def generate_npc_portrait(npc_data):
1004
+ if not npc_data:
1005
+ prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art"
1006
+ else:
1007
+ # Extract key details from NPC description for better portraits
1008
+ prompt = f"Fantasy portrait of a D&D NPC character based on: {npc_data[:200]}..., detailed face, professional RPG art"
1009
+ return generate_image(prompt)
1010
+
1011
+ def npc_roleplay_response(npc_data, player_input, context):
1012
+ if not npc_data or not player_input:
1013
+ return "Please create an NPC and enter player input"
1014
+ result = npc_agent.roleplay_npc(npc_data, player_input, context)
1015
+ return result.get("content", "Error in roleplay")
1016
+
1017
+ # World Builder Events
1018
+ def create_location(loc_type, theme, purpose):
1019
+ result = world_agent.generate_location(loc_type, theme, purpose)
1020
+ return result.get("content", "Error creating location")
1021
+
1022
+ def generate_location_art(loc_type, theme):
1023
+ prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting"
1024
+ return generate_image(prompt)
1025
+
1026
+ # Loot Events
1027
+ def create_loot_table(level, encounter, rarity):
1028
+ result = loot_agent.generate_loot_table(level, encounter, rarity)
1029
+ return result.get("content", "Error creating loot")
1030
+
1031
+ def create_magic_item(concept, power, theme):
1032
+ result = loot_agent.create_custom_magic_item(concept, power, theme)
1033
+ return result.get("content", "Error creating item")
1034
+
1035
+ def generate_item_art(concept, power):
1036
+ prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects"
1037
+ return generate_image(prompt)
1038
+
1039
+ # Random Generator Events
1040
+ def generate_random_name(name_type):
1041
+ names = {
1042
+ "Human Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
1043
+ "Human Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
1044
+ "Elven": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
1045
+ "Dwarven": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
1046
+ "Orcish": ["Grul", "Thark", "Dench", "Feng", "Gell"],
1047
+ "Fantasy Place": ["Ravenshollow", "Goldbrook", "Thornfield", "Mistral Keep"],
1048
+ "Tavern": ["The Prancing Pony", "Dragon's Rest", "The Silver Tankard"],
1049
+ "Shop": ["Magical Mysteries", "Fine Blades & More", "Potions & Remedies"]
1050
+ }
1051
+ return random.choice(names.get(name_type, ["Unknown"]))
1052
+
1053
+ def generate_random_encounter(level, difficulty):
1054
+ encounters = [
1055
+ f"Bandit ambush (adapted for level {level})",
1056
+ f"Wild animal encounter (CR {max(1, level//4)})",
1057
+ f"Mysterious traveler with a quest",
1058
+ f"Ancient ruins with {difficulty.lower()} traps",
1059
+ f"Rival adventuring party",
1060
+ f"Magical phenomenon requiring investigation"
1061
+ ]
1062
+ return random.choice(encounters)
1063
+
1064
+ def generate_plot_hook(theme):
1065
+ hooks = {
1066
+ "Mystery": "A beloved local figure has vanished without a trace, leaving behind only cryptic clues.",
1067
+ "Adventure": "Ancient maps surface pointing to a legendary treasure thought lost forever.",
1068
+ "Political": "A diplomatic envoy requests secret protection during dangerous negotiations.",
1069
+ "Personal": "A character's past catches up with them in an unexpected way.",
1070
+ "Rescue": "Innocent people are trapped in a dangerous situation and need immediate help.",
1071
+ "Exploration": "Uncharted territories beckon with promises of discovery and danger."
1072
+ }
1073
+ return hooks.get(theme, "A mysterious stranger approaches with an urgent request.")
1074
+
1075
+ def generate_weather(climate):
1076
+ weather_options = {
1077
+ "Temperate": ["Sunny and mild", "Light rain showers", "Overcast skies", "Gentle breeze"],
1078
+ "Tropical": ["Hot and humid", "Sudden thunderstorm", "Sweltering heat", "Monsoon rains"],
1079
+ "Arctic": ["Bitter cold winds", "Heavy snowfall", "Blizzard conditions", "Icy fog"],
1080
+ "Desert": ["Scorching sun", "Sandstorm approaching", "Cool desert night", "Rare rainfall"],
1081
+ "Mountainous": ["Mountain mist", "Alpine winds", "Rocky terrain", "Sudden weather change"]
1082
+ }
1083
+ return random.choice(weather_options.get(climate, ["Pleasant weather"]))
1084
+
1085
+ # Wire up all events
1086
+ roll_btn.click(roll_abilities, outputs=[str_score, dex_score, con_score, int_score, wis_score, cha_score])
1087
+
1088
+ generate_name_btn.click(
1089
+ generate_character_name,
1090
+ inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown],
1091
+ outputs=[character_name]
1092
+ )
1093
+
1094
+ generate_backstory_btn.click(
1095
+ generate_character_backstory,
1096
+ inputs=[character_name, race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown, background_dropdown],
1097
+ outputs=[backstory]
1098
+ )
1099
+
1100
+ portrait_btn.click(
1101
+ generate_character_portrait,
1102
+ inputs=[character_data],
1103
+ outputs=[character_portrait]
1104
+ )
1105
+
1106
+ # Update character summary when inputs change
1107
+ for component in [character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1108
+ str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]:
1109
+ component.change(
1110
+ update_character_summary,
1111
+ inputs=[character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1112
+ str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory],
1113
+ outputs=[character_summary, character_data]
1114
+ )
1115
+
1116
+ # Campaign events
1117
+ generate_campaign_btn.click(
1118
+ generate_campaign_concept,
1119
+ inputs=[campaign_theme, campaign_level, player_count],
1120
+ outputs=[campaign_output]
1121
+ )
1122
+
1123
+ campaign_visual_btn.click(
1124
+ generate_campaign_art,
1125
+ inputs=[campaign_theme, campaign_level],
1126
+ outputs=[campaign_image]
1127
+ )
1128
+
1129
+ generate_session_btn.click(
1130
+ generate_session_content,
1131
+ inputs=[campaign_output, session_number],
1132
+ outputs=[session_output]
1133
+ )
1134
+
1135
+ # NPC events
1136
+ create_npc_btn.click(
1137
+ create_npc,
1138
+ inputs=[npc_context, npc_role, npc_importance],
1139
+ outputs=[npc_output]
1140
+ )
1141
+
1142
+ npc_portrait_btn.click(
1143
+ generate_npc_portrait,
1144
+ inputs=[npc_output], # Changed from current_npc_data to npc_output
1145
+ outputs=[npc_portrait]
1146
+ )
1147
+
1148
+ roleplay_btn.click(
1149
+ npc_roleplay_response,
1150
+ inputs=[npc_output, player_input, roleplay_context],
1151
+ outputs=[npc_response]
1152
+ )
1153
+
1154
+ # World builder events
1155
+ generate_location_btn.click(
1156
+ create_location,
1157
+ inputs=[location_type, location_theme, location_purpose],
1158
+ outputs=[location_output]
1159
+ )
1160
+
1161
+ location_art_btn.click(
1162
+ generate_location_art,
1163
+ inputs=[location_type, location_theme],
1164
+ outputs=[location_image]
1165
+ )
1166
+
1167
+ # Loot events
1168
+ generate_loot_btn.click(
1169
+ create_loot_table,
1170
+ inputs=[loot_level, encounter_type, loot_rarity],
1171
+ outputs=[loot_output]
1172
+ )
1173
+
1174
+ create_item_btn.click(
1175
+ create_magic_item,
1176
+ inputs=[item_concept, power_level, campaign_theme_item],
1177
+ outputs=[magic_item_output]
1178
+ )
1179
+
1180
+ item_art_btn.click(
1181
+ generate_item_art,
1182
+ inputs=[item_concept, power_level],
1183
+ outputs=[item_image]
1184
+ )
1185
+
1186
+ # Random generator events
1187
+ gen_name_btn.click(generate_random_name, inputs=[name_type], outputs=[random_name])
1188
+ gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter])
1189
+ gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook])
1190
+ gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather])
1191
+
1192
+ # Additional missing event handlers
1193
+
1194
+ # Export character functionality
1195
+ def export_character_json(character_data):
1196
+ if not character_data:
1197
+ return None
1198
+
1199
+ try:
1200
+ # Convert character to dict for JSON export
1201
+ char_dict = {
1202
+ "name": character_data.name,
1203
+ "race": character_data.race,
1204
+ "class": character_data.character_class,
1205
+ "level": character_data.level,
1206
+ "gender": character_data.gender,
1207
+ "alignment": character_data.alignment.value,
1208
+ "abilities": character_data.abilities,
1209
+ "hit_points": character_data.hit_points,
1210
+ "skills": character_data.skills,
1211
+ "background": character_data.background,
1212
+ "backstory": character_data.backstory
1213
+ }
1214
+
1215
+ # Create temporary file
1216
+ import tempfile
1217
+ import json
1218
+
1219
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
1220
+ json.dump(char_dict, f, indent=2)
1221
+ return f.name
1222
+
1223
+ except Exception as e:
1224
+ logger.error(f"Character export failed: {e}")
1225
+ return None
1226
+
1227
+ export_btn.click(
1228
+ export_character_json,
1229
+ inputs=[character_data],
1230
+ outputs=[export_file]
1231
+ )
1232
+
1233
+ # Session notes functionality
1234
+ def save_session_notes(session_date, key_events, npc_interactions, player_actions, next_session_prep):
1235
+ try:
1236
+ import tempfile
1237
+
1238
+ notes_content = f"""# {session_date}
1239
+
1240
+ ## Key Events
1241
+ {key_events}
1242
+
1243
+ ## NPC Interactions
1244
+ {npc_interactions}
1245
+
1246
+ ## Notable Player Actions
1247
+ {player_actions}
1248
+
1249
+ ## Next Session Preparation
1250
+ {next_session_prep}
1251
+
1252
+ ---
1253
+ *Generated by D&D Campaign Manager*
1254
+ """
1255
+
1256
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
1257
+ f.write(notes_content)
1258
+ return f.name
1259
+
1260
+ except Exception as e:
1261
+ logger.error(f"Session notes save failed: {e}")
1262
+ return None
1263
+
1264
+ save_notes_btn.click(
1265
+ save_session_notes,
1266
+ inputs=[session_date, key_events, npc_interactions, player_actions, next_session_prep],
1267
+ outputs=[notes_output]
1268
+ )
1269
+
1270
+ # Initiative tracker functionality
1271
+ initiative_data = gr.State([])
1272
+
1273
+ def add_to_initiative(current_data, char_name, initiative_roll):
1274
+ if not char_name:
1275
+ return current_data
1276
+
1277
+ if current_data is None:
1278
+ current_data = []
1279
+
1280
+ # Add new character
1281
+ new_entry = [char_name, int(initiative_roll), "Full", "10", "Active"]
1282
+ current_data.append(new_entry)
1283
+
1284
+ # Sort by initiative (descending)
1285
+ current_data.sort(key=lambda x: x[1], reverse=True)
1286
+
1287
+ return current_data
1288
+
1289
+ def reset_initiative():
1290
+ return []
1291
+
1292
+ add_btn.click(
1293
+ add_to_initiative,
1294
+ inputs=[initiative_data, add_character, add_initiative],
1295
+ outputs=[initiative_data]
1296
+ )
1297
+
1298
+ # Update the dataframe when initiative_data changes
1299
+ initiative_data.change(
1300
+ lambda data: data,
1301
+ inputs=[initiative_data],
1302
+ outputs=[initiative_list]
1303
+ )
1304
+
1305
+ reset_combat_btn.click(
1306
+ reset_initiative,
1307
+ outputs=[initiative_data]
1308
+ )
1309
+
1310
+ return demo
1311
+
1312
+ # Main entry point
1313
+ if __name__ == "__main__":
1314
+ logger.info("🏰 Starting Advanced D&D Campaign Manager...")
1315
+
1316
+ # Check for OpenAI API key
1317
+ api_key = os.getenv("OPENAI_API_KEY")
1318
+ if api_key:
1319
+ logger.info("✅ OpenAI API key found - All AI features enabled!")
1320
+ else:
1321
+ logger.warning("⚠️ No OpenAI API key found - AI features will be limited")
1322
+ logger.info("💡 Add OPENAI_API_KEY as a repository secret in HF Spaces settings")
1323
+
1324
+ demo = create_main_interface()
1325
+
1326
+ try:
1327
+ # HF Spaces compatible launch
1328
+ demo.launch(
1329
+ share=False, # HF Spaces handles sharing
1330
+ inbrowser=False # Don't try to open browser in cloud environment
1331
+ )
1332
+ logger.info("🌐 App launched successfully!")
1333
+
1334
+ except Exception as e:
1335
+ logger.error(f"❌ Launch failed: {e}")
1336
+ # Fallback launch for HF Spaces
1337
+ demo.launch()
components/campaign.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/campaign.py
2
+ import gradio as gr
3
+ from datetime import datetime
4
+ import json
5
+ import os
6
+
7
+ # Placeholder logic; replace with your own AI integration
8
+ def generate_campaign_concept(theme, level, players):
9
+ return f"**Campaign Theme:** {theme}\n**Starting Level:** {level}\n**Party Size:** {players}\n\nA mysterious prophecy drives the PCs into lost lands..."
10
+
11
+ def generate_session_content(campaign_summary, session_number):
12
+ return f"**Session {session_number} Outline:**\nThe heroes investigate a hidden temple, encounter guardians, and uncover a lost relic."
13
+
14
+ def save_to_gallery(content_type: str, data: dict, image_path=None):
15
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
16
+ os.makedirs("gallery", exist_ok=True)
17
+ filename = f"gallery/{content_type}_{timestamp}.json"
18
+ with open(filename, "w") as f:
19
+ json.dump({"type": content_type, "data": data, "image": image_path}, f, indent=2)
20
+
21
+ def campaign_tab():
22
+ with gr.TabItem("🎲 Campaign Planner"):
23
+ with gr.Row():
24
+ with gr.Column():
25
+ theme = gr.Dropdown(label="Campaign Theme", choices=["High Fantasy", "Dark Fantasy", "Mystery", "Political Intrigue"], value="High Fantasy")
26
+ level = gr.Slider(label="Starting Level", minimum=1, maximum=20, step=1, value=5)
27
+ players = gr.Slider(label="Party Size", minimum=1, maximum=8, step=1, value=4)
28
+ gen_campaign_btn = gr.Button("🎯 Generate Campaign Concept")
29
+ campaign_output = gr.Markdown(label="Campaign Summary")
30
+
31
+ session_num = gr.Slider(label="Session Number", minimum=1, maximum=20, step=1, value=1)
32
+ gen_session_btn = gr.Button("📝 Generate Session Outline")
33
+ session_output = gr.Markdown(label="Session Outline")
34
+
35
+ with gr.Column():
36
+ export_campaign = gr.Button("💾 Save Campaign to Gallery")
37
+ export_campaign_output = gr.Textbox(label="Export Status")
38
+
39
+ # Actions
40
+ gen_campaign_btn.click(
41
+ generate_campaign_concept,
42
+ inputs=[theme, level, players],
43
+ outputs=[campaign_output]
44
+ )
45
+ gen_session_btn.click(
46
+ generate_session_content,
47
+ inputs=[campaign_output, session_num],
48
+ outputs=[session_output]
49
+ )
50
+ export_campaign.click(
51
+ lambda th, lv, pl, camp: save_to_gallery(
52
+ "campaign",
53
+ {
54
+ "theme": th,
55
+ "level": lv,
56
+ "players": pl,
57
+ "campaign_summary": camp
58
+ }
59
+ ) or "✅ Campaign saved!",
60
+ inputs=[theme, level, players, campaign_output],
61
+ outputs=[export_campaign_output]
62
+ )
components/characters.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/characters.py
2
+ import gradio as gr
3
+ from image_utils import generate_character_image
4
+ from datetime import datetime
5
+ import json
6
+ import os
7
+
8
+ def save_to_gallery(content_type: str, data: dict, image_path: str):
9
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
10
+ os.makedirs("gallery", exist_ok=True)
11
+ filename = f"gallery/{content_type}_{timestamp}.json"
12
+ with open(filename, "w") as f:
13
+ json.dump({"type": content_type, "data": data, "image": image_path}, f, indent=2)
14
+
15
+ def character_tab():
16
+ with gr.TabItem("🐉 Character Creator"):
17
+ with gr.Row():
18
+ with gr.Column():
19
+ name = gr.Textbox(label="Name", placeholder="Arannis Silverleaf")
20
+ race = gr.Dropdown(label="Race", choices=["Human", "Elf", "Dwarf", "Halfling", "Tiefl
components/factions_deities.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/factions_deities.py
2
+ import gradio as gr
3
+ from image_utils import generate_faction_image, generate_deity_image
4
+ from datetime import datetime
5
+ import json
6
+ import os
7
+
8
+ def save_to_gallery(content_type: str, data: dict, image_path: str):
9
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
10
+ os.makedirs("gallery", exist_ok=True)
11
+ filename = f"gallery/{content_type}_{timestamp}.json"
12
+ with open(filename, "w") as f:
13
+ json.dump({"type": content_type, "data": data, "image": image_path}, f, indent=2)
14
+
15
+ def faction_deity_tab():
16
+ with gr.TabItem("🛡️ Faction & Deity Generator"):
17
+ with gr.Row():
18
+ with gr.Column():
19
+ entity_type = gr.Radio(label="Generate", choices=["Faction", "Deity"], value="Faction")
20
+ entity_name = gr.Textbox(label="Name", placeholder="The Silver Flame")
21
+ domain_or_type = gr.Textbox(label="Domain or Type", placeholder="War / Guild")
22
+ alignment = gr.Dropdown(label="Alignment",
23
+ choices=["Lawful Good", "Neutral Good", "Chaotic Good",
24
+ "Lawful Neutral", "True Neutral", "Chaotic Neutral",
25
+ "Lawful Evil", "Neutral Evil", "Chaotic Evil"],
26
+ value="Lawful Neutral")
27
+ power_level = gr.Dropdown(label="Power Level (Deity Only)",
28
+ choices=["Lesser", "Intermediate", "Greater", "Overgod"],
29
+ value="Intermediate")
30
+ deity_sex = gr.Radio(label="Deity Sex (if applicable)",
31
+ choices=["Male", "Female", "Nonbinary"],
32
+ value="Nonbinary")
33
+ gen_btn = gr.Button("🎨 Generate Image")
34
+ image_out = gr.Image(label="Image", height=512)
35
+ export_btn = gr.Button("💾 Save to Gallery")
36
+ export_status = gr.Textbox(label="Export Status")
37
+
38
+ def build_and_generate(entity_type, name, domain_or_type, alignment, power_level, deity_sex):
39
+ data = {
40
+ "name": name or f"Unnamed {entity_type}",
41
+ "alignment": alignment
42
+ }
43
+ if entity_type == "Faction":
44
+ data["faction_type"] = domain_or_type or "Guild"
45
+ return generate_faction_image(data, art_style="Concept Art")
46
+ else:
47
+ data.update({
48
+ "domain": domain_or_type or "War",
49
+ "power_level": power_level,
50
+ "pantheon": "Generic",
51
+ "sex": deity_sex
52
+ })
53
+ return generate_deity_image(data, art_style="Concept Art")
54
+
55
+ gen_btn.click(
56
+ build_and_generate,
57
+ inputs=[entity_type, entity_name, domain_or_type, alignment, power_level, deity_sex],
58
+ outputs=image_out
59
+ )
60
+
61
+ export_btn.click(
62
+ lambda et, n, dt, a, pl, ds, img: save_to_gallery(
63
+ et.lower(),
64
+ {
65
+ "name": n,
66
+ "domain_or_type": dt,
67
+ "alignment": a,
68
+ "power_level": pl if et == "Deity" else None,
69
+ "sex": ds if et == "Deity" else None
70
+ },
71
+ img
72
+ ) or "✅ Saved!",
73
+ inputs=[entity_type, entity_name, domain_or_type, alignment, power_level, deity_sex, image_out],
74
+ outputs=export_status
75
+ )
components/items.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/items.py
2
+ import gradio as gr
3
+ from image_utils import generate_item_image
4
+ from datetime import datetime
5
+ import json
6
+ import os
7
+
8
+ def save_to_gallery(content_type: str, data: dict, image_path: str):
9
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
10
+ os.makedirs("gallery", exist_ok=True)
11
+ filename = f"gallery/{content_type}_{timestamp}.json"
12
+ with open(filename, "w") as f:
13
+ json.dump({"type": content_type, "data": data, "image": image_path}, f, indent=2)
14
+
15
+ def item_tab():
16
+ with gr.TabItem("🪙 Loot Generator"):
17
+ with gr.Row():
18
+ with gr.Column():
19
+ name = gr.Textbox(label="Item Name", placeholder="Amulet of Twilight")
20
+ item_type = gr.Dropdown(label="Item Type", choices=["Weapon", "Armor", "Accessory", "Consumable", "Tool"], value="Accessory")
21
+ rarity = gr.Dropdown(label="Rarity", choices=["Common", "Uncommon", "Rare", "Very Rare", "Legendary"], value="Rare")
22
+ is_magical = gr.Checkbox(label="Magical", value=True)
23
+ is_cursed = gr.Checkbox(label="Cursed", value=False)
24
+ gen_button = gr.Button("✨ Generate Item Image")
25
+ item_img = gr.Image(label="Item Image", height=512)
26
+
27
+ export_button = gr.Button("💾 Save Item to Gallery")
28
+ export_status = gr.Textbox(label="Export Status")
29
+
30
+ def build_and_generate(name, item_type, rarity, is_magical, is_cursed):
31
+ data = {
32
+ "name": name,
33
+ "item_type": item_type,
34
+ "rarity": rarity,
35
+ "magical": is_magical,
36
+ "cursed": is_cursed
37
+ }
38
+ return generate_item_image(data, art_style="Concept Art")
39
+
40
+ gen_button.click(
41
+ build_and_generate,
42
+ inputs=[name, item_type, rarity, is_magical, is_cursed],
43
+ outputs=item_img
44
+ )
45
+
46
+ export_button.click(
47
+ lambda n, t, r, m, c, img: save_to_gallery(
48
+ "item",
49
+ {"name": n, "item_type": t, "rarity": r, "magical": m, "cursed": c},
50
+ img
51
+ ) or "✅ Item saved!",
52
+ inputs=[name, item_type, rarity, is_magical, is_cursed, item_img],
53
+ outputs=export_status
54
+ )
components/locations.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/locations.py
2
+ :contentReference[oaicite:2]{index=2}
3
+ :contentReference[oaicite:3]{index=3}
4
+ :contentReference[oaicite:4]{index=4}
5
+ import json
6
+ import os
7
+
8
+ :contentReference[oaicite:5]{index=5}
9
+ :contentReference[oaicite:6]{index=6}
10
+ :contentReference[oaicite:7]{index=7}
11
+ :contentReference[oaicite:8]{index=8}
12
+ :contentReference[oaicite:9]{index=9}
13
+ :contentReference[oaicite:10]{index=10}
14
+ :contentReference[oaicite:11]{index=11}
15
+
16
+ def location_tab():
17
+ :contentReference[oaicite:12]{index=12}
18
+ :contentReference[oaicite:13]{index=13}
19
+ :contentReference[oaicite:14]{index=14}
20
+ :contentReference[oaicite:15]{index=15}
21
+ :contentReference[oaicite:16]{index=16}
22
+ :contentReference[oaicite:17]{index=17}
23
+ :contentReference[oaicite:18]{index=18}
24
+ value="Forest"
25
+ )
26
+ :contentReference[oaicite:19]{index=19}
27
+ label="Size",
28
+ :contentReference[oaicite:20]{index=20}
29
+ value="Medium"
30
+ )
31
+ :contentReference[oaicite:21]{index=21}
32
+ :contentReference[oaicite:22]{index=22}
33
+ :contentReference[oaicite:23]{index=23}
34
+ value="Moderate"
35
+ )
36
+ :contentReference[oaicite:24]{index=24}
37
+ label="Theme",
38
+ :contentReference[oaicite:25]{index=25}
39
+ value="Mystical"
40
+ )
41
+ :contentReference[oaicite:26]{index=26}
42
+ :contentReference[oaicite:27]{index=27}
43
+
44
+ export_btn = gr.Button("💾 Save Location to Gallery")
45
+ export_status = gr.Textbox(label="Export Status")
46
+
47
+ def build_and_generate(name, loc_type, size, danger, theme):
48
+ data = {
49
+ "name": name,
50
+ "location_type": loc_type,
51
+ "size": size,
52
+ "danger_level": danger,
53
+ "theme": theme
54
+ }
55
+ return generate_location_image(data, art_style="Concept Art")
56
+
57
+ gen_btn.click(
58
+ build_and_generate,
59
+ inputs=[name, loc_type, size, danger, theme],
60
+ outputs=image_out
61
+ )
62
+
63
+ export_btn.click(
64
+ lambda n, lt, sz, dg, th, img: save_to_gallery(
65
+ "location",
66
+ {"name": n, "type": lt, "size": sz, "danger_level": dg, "theme": th},
67
+ img
68
+ ) or "✅ Location saved!",
69
+ inputs=[name, loc_type, size, danger, theme, image_out],
70
+ outputs=export_status
71
+ )
components/npcs.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/npcs.py
2
+ :contentReference[oaicite:1]{index=1}
3
+ :contentReference[oaicite:2]{index=2}
4
+ :contentReference[oaicite:3]{index=3}
5
+ import json
6
+ import os
7
+ :contentReference[oaicite:4]{index=4}
8
+
9
+ # Reuse Alignment from main app (or import it)
10
+ :contentReference[oaicite:5]{index=5}
11
+ :contentReference[oaicite:6]{index=6}
12
+ :contentReference[oaicite:7]{index=7}
13
+ :contentReference[oaicite:8]{index=8}
14
+ :contentReference[oaicite:9]{index=9}
15
+ :contentReference[oaicite:10]{index=10}
16
+ :contentReference[oaicite:11]{index=11}
17
+ :contentReference[oaicite:12]{index=12}
18
+ :contentReference[oaicite:13]{index=13}
19
+ :contentReference[oaicite:14]{index=14}
20
+
21
+ :contentReference[oaicite:15]{index=15}
22
+ :contentReference[oaicite:16]{index=16}
23
+ :contentReference[oaicite:17]{index=17}
24
+ :contentReference[oaicite:18]{index=18}
25
+ :contentReference[oaicite:19]{index=19}
26
+ :contentReference[oaicite:20]{index=20}
27
+
28
+ def npc_tab():
29
+ :contentReference[oaicite:21]{index=21}
30
+ :contentReference[oaicite:22]{index=22}
31
+ :contentReference[oaicite:23]{index=23}
32
+ :contentReference[oaicite:24]{index=24}
33
+ :contentReference[oaicite:25]{index=25}
34
+ :contentReference[oaicite:26]{index=26}
35
+ :contentReference[oaicite:27]{index=27}
36
+ :contentReference[oaicite:28]{index=28}
37
+ :contentReference[oaicite:29]{index=29}
38
+ :contentReference[oaicite:30]{index=30}
39
+
40
+ export_button = gr.Button("💾 Save NPC to Gallery")
41
+ export_status = gr.Textbox(label="Export Status")
42
+
43
+ def build_and_generate(name, race, occupation, alignment, sex):
44
+ data = {
45
+ "name": name,
46
+ "race": race,
47
+ "occupation": occupation,
48
+ "alignment": alignment,
49
+ "sex": sex
50
+ }
51
+ return generate_npc_image(data, art_style="Concept Art")
52
+
53
+ gen_button.click(
54
+ build_and_generate,
55
+ inputs=[name, race, occupation, alignment, sex],
56
+ outputs=npc_img
57
+ )
58
+
59
+ export_button.click(
60
+ lambda n, r, o, a, s, img: save_to_gallery(
61
+ "npc",
62
+ {"name": n, "race": r, "occupation": o, "alignment": a, "sex": s},
63
+ img
64
+ ) or "✅ NPC saved!",
65
+ inputs=[name, race, occupation, alignment, sex, npc_img],
66
+ outputs=export_status
67
+ )
components/scenarios.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/scenarios.py
2
+ :contentReference[oaicite:1]{index=1}
3
+ :contentReference[oaicite:2]{index=2}
4
+ :contentReference[oaicite:3]{index=3}
5
+ import json
6
+ import os
7
+
8
+ :contentReference[oaicite:4]{index=4}
9
+ :contentReference[oaicite:5]{index=5}
10
+ :contentReference[oaicite:6]{index=6}
11
+ :contentReference[oaicite:7]{index=7}
12
+ :contentReference[oaicite:8]{index=8}
13
+ :contentReference[oaicite:9]{index=9}
14
+
15
+ def scenario_tab():
16
+ :contentReference[oaicite:10]{index=10}
17
+ :contentReference[oaicite:11]{index=11}
18
+ :contentReference[oaicite:12]{index=12}
19
+ :contentReference[oaicite:13]{index=13}
20
+ :contentReference[oaicite:14]{index=14}
21
+ :contentReference[oaicite:15]{index=15}
22
+ value="Exploration")
23
+ :contentReference[oaicite:16]{index=16}
24
+ :contentReference[oaicite:17]{index=17}
25
+ :contentReference[oaicite:18]{index=18}
26
+ :contentReference[oaicite:19]{index=19}
27
+ :contentReference[oaicite:20]{index=20}
28
+
29
+ export_button = gr.Button("💾 Save Scenario to Gallery")
30
+ export_status = gr.Textbox(label="Export Status")
31
+
32
+ def build_and_generate(title, scenario_type, difficulty, duration, setting):
33
+ data = {
34
+ "title": title,
35
+ "scenario_type": scenario_type,
36
+ "difficulty_level": difficulty,
37
+ "duration": duration,
38
+ "setting": setting
39
+ }
40
+ return generate_scenario_image(data, art_style="Concept Art")
41
+
42
+ gen_button.click(
43
+ build_and_generate,
44
+ inputs=[title, scenario_type, difficulty, duration, setting],
45
+ outputs=scenario_img
46
+ )
47
+
48
+ export_button.click(
49
+ lambda t, st, d, du, se, img: save_to_gallery(
50
+ "scenario",
51
+ {"title": t, "type": st, "difficulty": d, "duration": du, "setting": se},
52
+ img
53
+ ) or "✅ Scenario saved!",
54
+ inputs=[title, scenario_type, difficulty, duration, setting, scenario_img],
55
+ outputs=export_status
56
+ )