ghost-logic commited on
Commit
758e190
Β·
verified Β·
1 Parent(s): b197a8c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1227 -43
app.py CHANGED
@@ -1,14 +1,14 @@
 
 
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,10 +17,22 @@ 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,61 +41,1233 @@ class Alignment(Enum):
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  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
  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
+ return "Create an NPC first"
1062
+ prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art"
1063
+ return generate_image(prompt)
1064
+
1065
+ def npc_roleplay_response(npc_data, player_input, context):
1066
+ if not npc_data or not player_input:
1067
+ return "Please create an NPC and enter player input"
1068
+ result = npc_agent.roleplay_npc(npc_data, player_input, context)
1069
+ return result.get("content", "Error in roleplay")
1070
+
1071
+ # World Builder Events
1072
+ def create_location(loc_type, theme, purpose):
1073
+ result = world_agent.generate_location(loc_type, theme, purpose)
1074
+ return result.get("content", "Error creating location")
1075
+
1076
+ def generate_location_art(loc_type, theme):
1077
+ prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting"
1078
+ return generate_image(prompt)
1079
+
1080
+ # Loot Events
1081
+ def create_loot_table(level, encounter, rarity):
1082
+ result = loot_agent.generate_loot_table(level, encounter, rarity)
1083
+ return result.get("content", "Error creating loot")
1084
+
1085
+ def create_magic_item(concept, power, theme):
1086
+ result = loot_agent.create_custom_magic_item(concept, power, theme)
1087
+ return result.get("content", "Error creating item")
1088
+
1089
+ def generate_item_art(concept, power):
1090
+ prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects"
1091
+ return generate_image(prompt)
1092
+
1093
+ # Random Generator Events
1094
+ def generate_random_name(name_type):
1095
+ names = {
1096
+ "Human Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
1097
+ "Human Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
1098
+ "Elven": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
1099
+ "Dwarven": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
1100
+ "Orcish": ["Grul", "Thark", "Dench", "Feng", "Gell"],
1101
+ "Fantasy Place": ["Ravenshollow", "Goldbrook", "Thornfield", "Mistral Keep"],
1102
+ "Tavern": ["The Prancing Pony", "Dragon's Rest", "The Silver Tankard"],
1103
+ "Shop": ["Magical Mysteries", "Fine Blades & More", "Potions & Remedies"]
1104
+ }
1105
+ return random.choice(names.get(name_type, ["Unknown"]))
1106
+
1107
+ def generate_random_encounter(level, difficulty):
1108
+ encounters = [
1109
+ f"Bandit ambush (adapted for level {level})",
1110
+ f"Wild animal encounter (CR {max(1, level//4)})",
1111
+ f"Mysterious traveler with a quest",
1112
+ f"Ancient ruins with {difficulty.lower()} traps",
1113
+ f"Rival adventuring party",
1114
+ f"Magical phenomenon requiring investigation"
1115
+ ]
1116
+ return random.choice(encounters)
1117
+
1118
+ def generate_plot_hook(theme):
1119
+ hooks = {
1120
+ "Mystery": "A beloved local figure has vanished without a trace, leaving behind only cryptic clues.",
1121
+ "Adventure": "Ancient maps surface pointing to a legendary treasure thought lost forever.",
1122
+ "Political": "A diplomatic envoy requests secret protection during dangerous negotiations.",
1123
+ "Personal": "A character's past catches up with them in an unexpected way.",
1124
+ "Rescue": "Innocent people are trapped in a dangerous situation and need immediate help.",
1125
+ "Exploration": "Uncharted territories beckon with promises of discovery and danger."
1126
+ }
1127
+ return hooks.get(theme, "A mysterious stranger approaches with an urgent request.")
1128
+
1129
+ def generate_weather(climate):
1130
+ weather_options = {
1131
+ "Temperate": ["Sunny and mild", "Light rain showers", "Overcast skies", "Gentle breeze"],
1132
+ "Tropical": ["Hot and humid", "Sudden thunderstorm", "Sweltering heat", "Monsoon rains"],
1133
+ "Arctic": ["Bitter cold winds", "Heavy snowfall", "Blizzard conditions", "Icy fog"],
1134
+ "Desert": ["Scorching sun", "Sandstorm approaching", "Cool desert night", "Rare rainfall"],
1135
+ "Mountainous": ["Mountain mist", "Alpine winds", "Rocky terrain", "Sudden weather change"]
1136
+ }
1137
+ return random.choice(weather_options.get(climate, ["Pleasant weather"]))
1138
 
1139
+ # Wire up all events
1140
+ roll_btn.click(roll_abilities, outputs=[str_score, dex_score, con_score, int_score, wis_score, cha_score])
1141
+
1142
+ generate_name_btn.click(
1143
+ generate_character_name,
1144
+ inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown],
1145
+ outputs=[character_name]
1146
+ )
1147
+
1148
+ generate_backstory_btn.click(
1149
+ generate_character_backstory,
1150
+ inputs=[character_name, race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown, background_dropdown],
1151
+ outputs=[backstory]
1152
+ )
1153
+
1154
+ portrait_btn.click(
1155
+ generate_character_portrait,
1156
+ inputs=[character_data],
1157
+ outputs=[character_portrait]
1158
+ )
1159
+
1160
+ # Update character summary when inputs change
1161
+ for component in [character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1162
+ str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]:
1163
+ component.change(
1164
+ update_character_summary,
1165
+ inputs=[character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1166
+ str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory],
1167
+ outputs=[character_summary, character_data]
1168
+ )
1169
+
1170
+ # Campaign events
1171
+ generate_campaign_btn.click(
1172
+ generate_campaign_concept,
1173
+ inputs=[campaign_theme, campaign_level, player_count],
1174
+ outputs=[campaign_output]
1175
+ )
1176
+
1177
+ campaign_visual_btn.click(
1178
+ generate_campaign_art,
1179
+ inputs=[campaign_theme, campaign_level],
1180
+ outputs=[campaign_image]
1181
+ )
1182
+
1183
+ generate_session_btn.click(
1184
+ generate_session_content,
1185
+ inputs=[campaign_output, session_number],
1186
+ outputs=[session_output]
1187
+ )
1188
+
1189
+ # NPC events
1190
+ create_npc_btn.click(
1191
+ create_npc,
1192
+ inputs=[npc_context, npc_role, npc_importance],
1193
+ outputs=[npc_output]
1194
+ )
1195
+
1196
+ npc_portrait_btn.click(
1197
+ generate_npc_portrait,
1198
+ inputs=[current_npc_data],
1199
+ outputs=[npc_portrait]
1200
+ )
1201
+
1202
+ roleplay_btn.click(
1203
+ npc_roleplay_response,
1204
+ inputs=[npc_output, player_input, roleplay_context],
1205
+ outputs=[npc_response]
1206
+ )
1207
+
1208
+ # World builder events
1209
+ generate_location_btn.click(
1210
+ create_location,
1211
+ inputs=[location_type, location_theme, location_purpose],
1212
+ outputs=[location_output]
1213
+ )
1214
+
1215
+ location_art_btn.click(
1216
+ generate_location_art,
1217
+ inputs=[location_type, location_theme],
1218
+ outputs=[location_image]
1219
+ )
1220
+
1221
+ # Loot events
1222
+ generate_loot_btn.click(
1223
+ create_loot_table,
1224
+ inputs=[loot_level, encounter_type, loot_rarity],
1225
+ outputs=[loot_output]
1226
+ )
1227
+
1228
+ create_item_btn.click(
1229
+ create_magic_item,
1230
+ inputs=[item_concept, power_level, campaign_theme_item],
1231
+ outputs=[magic_item_output]
1232
+ )
1233
+
1234
+ item_art_btn.click(
1235
+ generate_item_art,
1236
+ inputs=[item_concept, power_level],
1237
+ outputs=[item_image]
1238
+ )
1239
+
1240
+ # Random generator events
1241
+ gen_name_btn.click(generate_random_name, inputs=[name_type], outputs=[random_name])
1242
+ gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter])
1243
+ gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook])
1244
+ gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather])
1245
+
1246
  return demo
1247
 
1248
+ # Main entry point
1249
  if __name__ == "__main__":
1250
+ logger.info("🏰 Starting Advanced D&D Campaign Manager...")
1251
+
1252
+ # Check for OpenAI API key
1253
+ api_key = os.getenv("OPENAI_API_KEY")
1254
+ if api_key:
1255
+ logger.info("βœ… OpenAI API key found - All AI features enabled!")
1256
+ else:
1257
+ logger.warning("⚠️ No OpenAI API key found - AI features will be limited")
1258
+ logger.info("πŸ’‘ Add OPENAI_API_KEY as a repository secret in HF Spaces settings")
1259
+
1260
+ demo = create_main_interface()
1261
+
1262
+ try:
1263
+ # HF Spaces compatible launch
1264
+ demo.launch(
1265
+ share=False, # HF Spaces handles sharing
1266
+ inbrowser=False # Don't try to open browser in cloud environment
1267
+ )
1268
+ logger.info("🌐 App launched successfully!")
1269
+
1270
+ except Exception as e:
1271
+ logger.error(f"❌ Launch failed: {e}")
1272
+ # Fallback launch for HF Spaces
1273
+ demo.launch()