MelkortheCorrupt commited on
Commit
409063d
·
1 Parent(s): 7c6055c

Deploy D&D Toolkit

Browse files
Files changed (4) hide show
  1. .gitignore +23 -0
  2. utils/__init__.py +0 -0
  3. utils/generators.py +661 -0
  4. utils/image_utils.py +181 -0
.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Virtual environments
3
+ env/
4
+ venv/
5
+ .env
6
+
7
+ # Python cache
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+
12
+ # Temporary files
13
+ temp_*.png
14
+ placeholder_*.png
15
+ *.tmp
16
+ generation_history_*.json
17
+
18
+ # Mac files
19
+ .DS_Store
20
+
21
+ # IDE files
22
+ .vscode/
23
+ .idea/
utils/__init__.py ADDED
File without changes
utils/generators.py ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from typing import Dict, List, Any, Optional
3
+
4
+ # ===== DATA BANKS =====
5
+
6
+ RACES = ["Human", "Elf", "Dwarf", "Halfling", "Dragonborn", "Gnome", "Half-Elf", "Half-Orc", "Tiefling"]
7
+ CLASSES = ["Fighter", "Wizard", "Rogue", "Cleric", "Ranger", "Paladin", "Barbarian", "Bard", "Druid", "Monk", "Sorcerer", "Warlock"]
8
+ ALIGNMENTS = ["Lawful Good", "Neutral Good", "Chaotic Good", "Lawful Neutral", "True Neutral", "Chaotic Neutral", "Lawful Evil", "Neutral Evil", "Chaotic Evil"]
9
+ BACKGROUNDS = ["Acolyte", "Criminal", "Folk Hero", "Noble", "Sage", "Soldier", "Charlatan", "Entertainer", "Guild Artisan", "Hermit", "Outlander", "Sailor"]
10
+
11
+ PERSONALITY_TRAITS = [
12
+ "I always have a plan for what to do when things go wrong",
13
+ "I am always calm, no matter what the situation",
14
+ "I would rather make a new friend than a new enemy",
15
+ "I have a strong sense of fair play and always try to find the most equitable solution",
16
+ "I'm confident in my own abilities and do what I can to instill confidence in others"
17
+ ]
18
+
19
+ IDEALS = [
20
+ "Respect. People deserve to be treated with dignity and respect",
21
+ "Fairness. No one should get preferential treatment before the law",
22
+ "Freedom. Chains are meant to be broken, as are those who would forge them",
23
+ "Might. The strongest are meant to rule",
24
+ "Sincerity. There's no good in pretending to be something I'm not"
25
+ ]
26
+
27
+ BONDS = [
28
+ "I would die to recover an ancient relic that was stolen from my temple",
29
+ "Someone I loved died because of a mistake I made",
30
+ "I owe my life to the priest who took me in when my parents died",
31
+ "Everything I do is for the common people",
32
+ "I will face any challenge to win the approval of my family"
33
+ ]
34
+
35
+ FLAWS = [
36
+ "I turn tail and run when things look bad",
37
+ "I have a 'tell' that reveals when I'm lying",
38
+ "I can't keep a secret to save my life",
39
+ "I'm too greedy for my own good",
40
+ "I can't resist a pretty face"
41
+ ]
42
+
43
+ OCCUPATIONS = [
44
+ "Merchant", "Guard", "Innkeeper", "Blacksmith", "Priest", "Noble", "Farmer",
45
+ "Scholar", "Thief", "Soldier", "Alchemist", "Baker", "Carpenter", "Hunter"
46
+ ]
47
+
48
+ MOTIVATIONS = [
49
+ "Seeking wealth and power", "Protecting their family", "Serving their god", "Pursuing knowledge",
50
+ "Seeking revenge", "Finding love", "Escaping their past", "Gaining recognition"
51
+ ]
52
+
53
+ SECRETS = [
54
+ "Is actually a spy for a rival faction", "Has a hidden magical talent", "Is fleeing from a dark past",
55
+ "Is secretly in love with someone inappropriate", "Has a gambling addiction", "Is being blackmailed"
56
+ ]
57
+
58
+ ITEM_TYPES = {
59
+ "Weapon": ["Sword", "Axe", "Bow", "Dagger", "Mace", "Spear", "Crossbow", "Warhammer"],
60
+ "Armor": ["Chain Mail", "Plate Armor", "Leather Armor", "Scale Mail", "Hide Armor"],
61
+ "Shield": ["Round Shield", "Tower Shield", "Buckler", "Kite Shield"],
62
+ "Accessory": ["Ring", "Amulet", "Cloak", "Belt", "Bracers", "Crown", "Boots"],
63
+ "Consumable": ["Potion", "Scroll", "Oil", "Poison", "Bomb", "Elixir"],
64
+ "Tool": ["Lockpicks", "Rope", "Lantern", "Compass", "Spyglass", "Hammer"],
65
+ "Treasure": ["Gem", "Coins", "Art Object", "Jewelry", "Statue", "Book"]
66
+ }
67
+
68
+ MAGICAL_PROPERTIES = [
69
+ "+1 to attack and damage rolls", "Resistance to fire damage", "Advantage on stealth checks",
70
+ "Cast a spell once per day", "Glows in the presence of evil", "Never breaks or dulls",
71
+ "Returns when thrown", "Speaks telepathically", "Changes color based on emotion"
72
+ ]
73
+
74
+ LOCATION_TYPES = {
75
+ "City": ["Metropolis", "Trading Hub", "Capital", "Port City", "Fortress City"],
76
+ "Village": ["Farming Village", "Mining Town", "Fishing Village", "Trading Post"],
77
+ "Dungeon": ["Ancient Tomb", "Underground Lair", "Ruined Tower", "Cave System"],
78
+ "Castle": ["Royal Castle", "Fortress", "Ruined Keep", "Wizard's Tower"],
79
+ "Forest": ["Ancient Woods", "Dark Forest", "Enchanted Grove", "Sacred Grove"],
80
+ "Temple": ["Cathedral", "Shrine", "Monastery", "Sacred Site"],
81
+ "Tavern": ["Roadside Inn", "City Tavern", "Adventurer's Rest", "Noble's Club"]
82
+ }
83
+
84
+ FACTION_TYPES = {
85
+ "Guild": ["Merchant Guild", "Thieves Guild", "Artisan Guild", "Adventurer's Guild"],
86
+ "Religious Order": ["Temple", "Monastery", "Cult", "Holy Order"],
87
+ "Military": ["Army", "Guard", "Mercenaries", "Knights"],
88
+ "Criminal": ["Thieves", "Smugglers", "Assassins", "Pirates"],
89
+ "Political": ["Royal Court", "City Council", "Rebels", "Loyalists"],
90
+ "Academic": ["University", "Research Group", "Library", "School"]
91
+ }
92
+
93
+ DEITY_DOMAINS = {
94
+ "War": ["Battle", "Strategy", "Conquest", "Honor"],
95
+ "Knowledge": ["Wisdom", "Learning", "Magic", "Secrets"],
96
+ "Life": ["Healing", "Growth", "Birth", "Protection"],
97
+ "Death": ["Endings", "Judgment", "Undeath", "Graves"],
98
+ "Nature": ["Animals", "Plants", "Seasons", "Weather"],
99
+ "Light": ["Sun", "Dawn", "Hope", "Renewal"],
100
+ "Trickery": ["Deception", "Theft", "Luck", "Mischief"]
101
+ }
102
+
103
+ # ===== MAIN GENERATION FUNCTIONS =====
104
+
105
+ def generate_character(name: str = "", race: str = "Random", char_class: str = "Random",
106
+ level: int = 5, alignment: str = "Random") -> Dict[str, Any]:
107
+ """Generate a complete D&D character"""
108
+
109
+ if not name:
110
+ name = generate_fantasy_name()
111
+ if race == "Random":
112
+ race = random.choice(RACES)
113
+ if char_class == "Random":
114
+ char_class = random.choice(CLASSES)
115
+ if alignment == "Random":
116
+ alignment = random.choice(ALIGNMENTS)
117
+
118
+ background = random.choice(BACKGROUNDS)
119
+ personality = random.choice(PERSONALITY_TRAITS)
120
+ ideal = random.choice(IDEALS)
121
+ bond = random.choice(BONDS)
122
+ flaw = random.choice(FLAWS)
123
+ appearance = generate_appearance(race)
124
+ equipment = generate_class_equipment(char_class, level)
125
+ backstory = generate_backstory(name, race, char_class, background)
126
+ hooks = generate_adventure_hooks(name, background)
127
+
128
+ return {
129
+ "name": name,
130
+ "race": race,
131
+ "character_class": char_class,
132
+ "level": level,
133
+ "alignment": alignment,
134
+ "background": background,
135
+ "personality": personality,
136
+ "ideal": ideal,
137
+ "bond": bond,
138
+ "flaw": flaw,
139
+ "appearance": appearance,
140
+ "equipment": equipment,
141
+ "backstory": backstory,
142
+ "hooks": hooks
143
+ }
144
+
145
+ def generate_npc(name: str = "", race: str = "Random", occupation: str = "Random",
146
+ location: str = "", relationship: str = "Neutral") -> Dict[str, Any]:
147
+ """Generate a detailed NPC"""
148
+
149
+ if not name:
150
+ name = generate_fantasy_name()
151
+ if race == "Random":
152
+ race = random.choice(RACES)
153
+ if occupation == "Random":
154
+ occupation = random.choice(OCCUPATIONS)
155
+
156
+ alignment = random.choice(ALIGNMENTS)
157
+ personality = random.choice(PERSONALITY_TRAITS)
158
+ appearance = generate_appearance(race)
159
+ motivation = random.choice(MOTIVATIONS)
160
+ secret = random.choice(SECRETS)
161
+ relationships = generate_relationships(name, occupation)
162
+
163
+ return {
164
+ "name": name,
165
+ "race": race,
166
+ "occupation": occupation,
167
+ "alignment": alignment,
168
+ "personality": personality,
169
+ "appearance": appearance,
170
+ "motivation": motivation,
171
+ "secret": secret,
172
+ "relationships": relationships,
173
+ "location": location or "Unknown",
174
+ "relationship_to_party": relationship
175
+ }
176
+
177
+ def generate_item(name: str = "", item_type: str = "Random", rarity: str = "Random",
178
+ magical: bool = True, cursed: bool = False) -> Dict[str, Any]:
179
+ """Generate a magical or mundane item"""
180
+
181
+ if item_type == "Random":
182
+ item_type = random.choice(list(ITEM_TYPES.keys()))
183
+
184
+ if not name:
185
+ base_item = random.choice(ITEM_TYPES[item_type])
186
+ name = generate_item_name(base_item, magical)
187
+
188
+ if rarity == "Random":
189
+ rarity = random.choice(["Common", "Uncommon", "Rare", "Very Rare", "Legendary"])
190
+
191
+ description = generate_item_description(name, item_type, magical)
192
+ properties = []
193
+
194
+ if magical:
195
+ num_properties = {"Common": 1, "Uncommon": 1, "Rare": 2, "Very Rare": 3, "Legendary": 4}.get(rarity, 1)
196
+ properties = random.sample(MAGICAL_PROPERTIES, min(num_properties, len(MAGICAL_PROPERTIES)))
197
+
198
+ if cursed:
199
+ properties.append(generate_curse())
200
+
201
+ base_values = {"Common": 50, "Uncommon": 500, "Rare": 5000, "Very Rare": 50000, "Legendary": 500000}
202
+ value = f"{base_values.get(rarity, 50):,} gp"
203
+
204
+ requirements = "Attunement" if rarity in ["Rare", "Very Rare", "Legendary"] and magical else "None"
205
+
206
+ return {
207
+ "name": name,
208
+ "item_type": item_type,
209
+ "rarity": rarity,
210
+ "description": description,
211
+ "properties": properties,
212
+ "value": value,
213
+ "requirements": requirements,
214
+ "magical": magical,
215
+ "cursed": cursed
216
+ }
217
+
218
+ def generate_location(name: str = "", loc_type: str = "Random", size: str = "Medium",
219
+ danger_level: str = "Moderate", theme: str = "Standard Fantasy") -> Dict[str, Any]:
220
+ """Generate a detailed location"""
221
+
222
+ if loc_type == "Random":
223
+ loc_type = random.choice(list(LOCATION_TYPES.keys()))
224
+
225
+ if not name:
226
+ subtype = random.choice(LOCATION_TYPES[loc_type])
227
+ name = generate_location_name(subtype)
228
+
229
+ description = generate_location_description(name, loc_type, size, theme)
230
+ inhabitants = generate_inhabitants(loc_type, size)
231
+ features = generate_location_features(loc_type)
232
+ dangers = generate_location_dangers(danger_level)
233
+ treasures = generate_location_treasures(danger_level)
234
+ atmosphere = generate_atmosphere(loc_type, theme)
235
+
236
+ return {
237
+ "name": name,
238
+ "location_type": loc_type,
239
+ "size": size,
240
+ "danger_level": danger_level,
241
+ "theme": theme,
242
+ "description": description,
243
+ "inhabitants": inhabitants,
244
+ "features": features,
245
+ "dangers": dangers,
246
+ "treasures": treasures,
247
+ "atmosphere": atmosphere
248
+ }
249
+
250
+ def generate_faction(name: str = "", faction_type: str = "Random", alignment: str = "Random",
251
+ size: str = "Medium", influence: str = "Regional") -> Dict[str, Any]:
252
+ """Generate a detailed faction"""
253
+
254
+ if faction_type == "Random":
255
+ faction_type = random.choice(list(FACTION_TYPES.keys()))
256
+
257
+ if not name:
258
+ subtype = random.choice(FACTION_TYPES[faction_type])
259
+ name = generate_faction_name(subtype)
260
+
261
+ if alignment == "Random":
262
+ alignment = random.choice(ALIGNMENTS)
263
+
264
+ goals = generate_faction_goals(faction_type)
265
+ methods = generate_faction_methods(alignment)
266
+ resources = generate_faction_resources(size)
267
+ enemies = generate_faction_enemies(faction_type)
268
+ allies = generate_faction_allies(faction_type)
269
+ headquarters = generate_faction_headquarters(faction_type)
270
+
271
+ return {
272
+ "name": name,
273
+ "faction_type": faction_type,
274
+ "alignment": alignment,
275
+ "size": size,
276
+ "influence": influence,
277
+ "goals": goals,
278
+ "methods": methods,
279
+ "resources": resources,
280
+ "enemies": enemies,
281
+ "allies": allies,
282
+ "headquarters": headquarters
283
+ }
284
+
285
+ def generate_deity(name: str = "", domain: str = "Random", alignment: str = "Random",
286
+ pantheon: str = "Generic Fantasy", power_level: str = "Intermediate") -> Dict[str, Any]:
287
+ """Generate a detailed deity"""
288
+
289
+ if domain == "Random":
290
+ domain = random.choice(list(DEITY_DOMAINS.keys()))
291
+
292
+ if not name:
293
+ name = generate_deity_name(domain, pantheon)
294
+
295
+ if alignment == "Random":
296
+ alignment = random.choice(ALIGNMENTS)
297
+
298
+ appearance = generate_deity_appearance(domain, alignment)
299
+ personality = generate_deity_personality(domain)
300
+ portfolio = random.sample(DEITY_DOMAINS[domain], min(3, len(DEITY_DOMAINS[domain])))
301
+ worshippers = generate_deity_worshippers(domain)
302
+ temples = generate_deity_temples(power_level)
303
+ holy_symbol = generate_holy_symbol(domain)
304
+
305
+ return {
306
+ "name": name,
307
+ "domain": domain,
308
+ "alignment": alignment,
309
+ "pantheon": pantheon,
310
+ "power_level": power_level,
311
+ "appearance": appearance,
312
+ "personality": personality,
313
+ "portfolio": portfolio,
314
+ "worshippers": worshippers,
315
+ "temples": temples,
316
+ "holy_symbol": holy_symbol
317
+ }
318
+
319
+ def generate_scenario(title: str = "", scenario_type: str = "Random", level: str = "Medium",
320
+ duration: str = "Medium (2-3 hours)", setting: str = "Mixed") -> Dict[str, Any]:
321
+ """Generate a detailed scenario/adventure"""
322
+
323
+ scenario_types = ["Combat Encounter", "Social Encounter", "Exploration", "Mystery",
324
+ "Heist", "Rescue", "Dungeon Crawl", "Political Intrigue"]
325
+
326
+ if scenario_type == "Random":
327
+ scenario_type = random.choice(scenario_types)
328
+
329
+ if not title:
330
+ title = generate_scenario_title(scenario_type)
331
+
332
+ description = generate_scenario_description(title, scenario_type, setting)
333
+ objective = generate_scenario_objective(scenario_type)
334
+ complications = generate_scenario_complications(level)
335
+ npcs_involved = generate_scenario_npcs(scenario_type)
336
+ locations = generate_scenario_locations(setting)
337
+ rewards = generate_scenario_rewards(level)
338
+ hooks = generate_scenario_hooks(scenario_type, title)
339
+
340
+ return {
341
+ "title": title,
342
+ "scenario_type": scenario_type,
343
+ "difficulty_level": level,
344
+ "duration": duration,
345
+ "setting": setting,
346
+ "description": description,
347
+ "objective": objective,
348
+ "complications": complications,
349
+ "npcs_involved": npcs_involved,
350
+ "locations": locations,
351
+ "rewards": rewards,
352
+ "hooks": hooks
353
+ }
354
+
355
+ # ===== HELPER FUNCTIONS =====
356
+
357
+ def generate_fantasy_name() -> str:
358
+ """Generate a random fantasy name"""
359
+ prefixes = ["Ar", "El", "Gal", "Mor", "Sil", "Tha", "Val", "Zar"]
360
+ middles = ["and", "eth", "dor", "wen", "ion", "las", "mir", "nal"]
361
+ suffixes = ["iel", "wen", "dil", "las", "mir", "nal", "oth", "ril"]
362
+
363
+ return random.choice(prefixes) + random.choice(middles) + random.choice(suffixes)
364
+
365
+ def generate_appearance(race: str) -> str:
366
+ """Generate physical appearance based on race"""
367
+ appearances = {
368
+ "Human": "tall and sturdy with kind eyes",
369
+ "Elf": "tall and graceful with ethereal beauty",
370
+ "Dwarf": "short and stocky with a braided beard",
371
+ "Halfling": "small and cheerful with curly hair",
372
+ "Dragonborn": "scaled skin with draconic features",
373
+ "Tiefling": "demonic horns with exotic features"
374
+ }
375
+
376
+ return appearances.get(race, "distinctive appearance")
377
+
378
+ def generate_class_equipment(char_class: str, level: int) -> List[str]:
379
+ """Generate equipment based on class"""
380
+ base_equipment = {
381
+ "Fighter": ["Sword", "Shield", "Armor"],
382
+ "Wizard": ["Spellbook", "Staff", "Robes"],
383
+ "Rogue": ["Daggers", "Thieves' Tools", "Leather Armor"],
384
+ "Cleric": ["Mace", "Shield", "Holy Symbol"],
385
+ "Ranger": ["Bow", "Arrows", "Survival Gear"]
386
+ }
387
+
388
+ equipment = base_equipment.get(char_class, ["Basic Gear"])
389
+
390
+ if level > 5:
391
+ equipment.append("Magic Item")
392
+
393
+ return equipment
394
+
395
+ def generate_backstory(name: str, race: str, char_class: str, background: str) -> str:
396
+ """Generate character backstory"""
397
+ return f"{name} grew up as a {background.lower()} among the {race.lower()} people, drawn to the path of the {char_class.lower()}."
398
+
399
+ def generate_adventure_hooks(name: str, background: str) -> List[str]:
400
+ """Generate adventure hooks"""
401
+ return [
402
+ f"{name}'s {background.lower()} background connects to this adventure",
403
+ f"An old contact needs {name}'s help",
404
+ f"The character's past comes back to haunt them"
405
+ ]
406
+
407
+ def generate_item_name(base_item: str, magical: bool) -> str:
408
+ """Generate item names"""
409
+ if not magical:
410
+ return base_item
411
+
412
+ prefixes = ["Enchanted", "Mystic", "Ancient", "Blessed", "Legendary"]
413
+ return f"{random.choice(prefixes)} {base_item}"
414
+
415
+ def generate_curse() -> str:
416
+ """Generate item curse"""
417
+ curses = [
418
+ "Compels the wielder to attack allies",
419
+ "Drains health slowly",
420
+ "Causes the wielder to speak in riddles",
421
+ "Makes the wielder tell the truth"
422
+ ]
423
+ return random.choice(curses)
424
+
425
+ def generate_item_description(name: str, item_type: str, magical: bool) -> str:
426
+ """Generate item descriptions"""
427
+ base = f"A well-crafted {item_type.lower()}"
428
+ if magical:
429
+ return f"{base} emanating with magical energy"
430
+ return f"{base} of excellent quality"
431
+
432
+ def generate_location_name(subtype: str) -> str:
433
+ """Generate location names"""
434
+ prefixes = ["Ancient", "Lost", "Hidden", "Sacred", "Golden"]
435
+ return f"{random.choice(prefixes)} {subtype}"
436
+
437
+ def generate_location_description(name: str, loc_type: str, size: str, theme: str) -> str:
438
+ """Generate location descriptions"""
439
+ return f"A {size.lower()} {loc_type.lower()} with {theme.lower()} characteristics"
440
+
441
+ def generate_inhabitants(loc_type: str, size: str) -> List[str]:
442
+ """Generate location inhabitants"""
443
+ base_inhabitants = {
444
+ "City": ["Merchants", "Guards", "Nobles"],
445
+ "Village": ["Farmers", "Blacksmith", "Elder"],
446
+ "Dungeon": ["Monsters", "Undead", "Bandits"]
447
+ }
448
+ return base_inhabitants.get(loc_type, ["Various Creatures"])
449
+
450
+ def generate_location_features(loc_type: str) -> List[str]:
451
+ """Generate location features"""
452
+ return ["Notable landmark", "Gathering place", "Hidden area"]
453
+
454
+ def generate_location_dangers(danger_level: str) -> List[str]:
455
+ """Generate location dangers"""
456
+ dangers = {
457
+ "Safe": ["Pickpockets", "Overpriced goods"],
458
+ "Low": ["Wild animals", "Natural hazards"],
459
+ "Moderate": ["Monsters", "Traps"],
460
+ "High": ["Deadly creatures", "Curses"],
461
+ "Extreme": ["Ancient evils", "Reality distortions"]
462
+ }
463
+ return dangers.get(danger_level, ["Unknown threats"])
464
+
465
+ def generate_location_treasures(danger_level: str) -> List[str]:
466
+ """Generate location treasures"""
467
+ treasures = {
468
+ "Safe": ["Copper coins", "Common items"],
469
+ "Moderate": ["Gold coins", "Rare items"],
470
+ "Extreme": ["Legendary items", "Artifacts"]
471
+ }
472
+ return treasures.get(danger_level, ["Hidden treasures"])
473
+
474
+ def generate_atmosphere(loc_type: str, theme: str) -> str:
475
+ """Generate atmospheric description"""
476
+ return f"The air is thick with {random.choice(['mystery', 'tension', 'magic'])}"
477
+
478
+ def generate_relationships(name: str, occupation: str) -> List[str]:
479
+ """Generate NPC relationships"""
480
+ return [
481
+ f"Knows the local {random.choice(['merchant', 'guard', 'priest'])}",
482
+ f"Has connections to {random.choice(['traders', 'criminals', 'officials'])}",
483
+ f"Family ties to nearby communities"
484
+ ]
485
+
486
+ # Faction functions
487
+ def generate_faction_name(subtype: str) -> str:
488
+ """Generate faction names"""
489
+ prefixes = ["Order of the", "Brotherhood of", "Guild of"]
490
+ themes = ["Silver Dawn", "Golden Eagle", "Iron Fist"]
491
+ return f"{random.choice(prefixes)} {random.choice(themes)}"
492
+
493
+ def generate_faction_goals(faction_type: str) -> List[str]:
494
+ """Generate faction goals"""
495
+ goals = {
496
+ "Guild": ["Increase profits", "Expand influence"],
497
+ "Military": ["Defend territory", "Maintain order"],
498
+ "Criminal": ["Control trade", "Eliminate rivals"]
499
+ }
500
+ return goals.get(faction_type, ["Gain power", "Protect interests"])
501
+
502
+ def generate_faction_methods(alignment: str) -> List[str]:
503
+ """Generate faction methods"""
504
+ if "Good" in alignment:
505
+ return ["Diplomacy", "Charity", "Cooperation"]
506
+ elif "Evil" in alignment:
507
+ return ["Intimidation", "Corruption", "Violence"]
508
+ else:
509
+ return ["Politics", "Economics", "Information"]
510
+
511
+ def generate_faction_resources(size: str) -> List[str]:
512
+ """Generate faction resources"""
513
+ resources = {
514
+ "Small": ["Limited funds", "Few contacts"],
515
+ "Medium": ["Moderate wealth", "Local connections"],
516
+ "Large": ["Significant wealth", "Regional network"],
517
+ "Massive": ["Vast fortune", "International contacts"]
518
+ }
519
+ return resources.get(size, ["Basic resources"])
520
+
521
+ def generate_faction_enemies(faction_type: str) -> List[str]:
522
+ """Generate faction enemies"""
523
+ return [f"Rival {faction_type.lower()} groups", "Government opposition", "Ancient enemies"]
524
+
525
+ def generate_faction_allies(faction_type: str) -> List[str]:
526
+ """Generate faction allies"""
527
+ return [f"Friendly {faction_type.lower()} groups", "Sympathetic nobles", "Mercenary companies"]
528
+
529
+ def generate_faction_headquarters(faction_type: str) -> str:
530
+ """Generate faction headquarters"""
531
+ headquarters = {
532
+ "Guild": "Guild Hall",
533
+ "Military": "Fortress",
534
+ "Criminal": "Hidden Safehouse",
535
+ "Religious Order": "Temple"
536
+ }
537
+ return headquarters.get(faction_type, "Secure Location")
538
+
539
+ # Deity functions
540
+ def generate_deity_name(domain: str, pantheon: str) -> str:
541
+ """Generate deity names"""
542
+ names = {
543
+ "War": ["Valorian", "Marticus", "Bellona"],
544
+ "Knowledge": ["Athenis", "Scholaris", "Wisdomia"],
545
+ "Life": ["Vitalis", "Celestine", "Healara"]
546
+ }
547
+ return random.choice(names.get(domain, ["Divinus", "Godara", "Sanctus"]))
548
+
549
+ def generate_deity_appearance(domain: str, alignment: str) -> str:
550
+ """Generate deity appearance"""
551
+ appearances = {
552
+ "War": "battle-scarred warrior in ornate armor",
553
+ "Knowledge": "wise figure surrounded by floating books",
554
+ "Life": "radiant being with healing aura"
555
+ }
556
+ return appearances.get(domain, "divine otherworldly presence")
557
+
558
+ def generate_deity_personality(domain: str) -> str:
559
+ """Generate deity personality"""
560
+ personalities = {
561
+ "War": "Strategic and honorable, values courage",
562
+ "Knowledge": "Wise and patient, values learning",
563
+ "Life": "Nurturing and protective, values growth"
564
+ }
565
+ return personalities.get(domain, "Mysterious and powerful")
566
+
567
+ def generate_deity_worshippers(domain: str) -> List[str]:
568
+ """Generate deity worshippers"""
569
+ worshippers = {
570
+ "War": ["Soldiers", "Warriors", "Generals"],
571
+ "Knowledge": ["Scholars", "Wizards", "Students"],
572
+ "Life": ["Healers", "Farmers", "Clerics"]
573
+ }
574
+ return worshippers.get(domain, ["Various followers"])
575
+
576
+ def generate_deity_temples(power_level: str) -> str:
577
+ """Generate deity temples"""
578
+ temples = {
579
+ "Lesser": "Small local shrines",
580
+ "Intermediate": "Regional temples with clergy",
581
+ "Greater": "Grand cathedrals and pilgrimage sites"
582
+ }
583
+ return temples.get(power_level, "Modest temples")
584
+
585
+ def generate_holy_symbol(domain: str) -> str:
586
+ """Generate holy symbols"""
587
+ symbols = {
588
+ "War": "Crossed swords",
589
+ "Knowledge": "Open book with eye",
590
+ "Life": "Tree of life"
591
+ }
592
+ return symbols.get(domain, "Sacred emblem")
593
+
594
+ # Scenario functions
595
+ def generate_scenario_title(scenario_type: str) -> str:
596
+ """Generate scenario titles"""
597
+ titles = {
598
+ "Combat Encounter": ["The Bandit Ambush", "Goblin Raid", "Dragon's Lair"],
599
+ "Social Encounter": ["The Noble's Feast", "Tavern Negotiations", "Court Intrigue"],
600
+ "Mystery": ["The Missing Merchant", "Murder at the Inn", "The Cursed Artifact"]
601
+ }
602
+ return random.choice(titles.get(scenario_type, ["The Unknown Adventure"]))
603
+
604
+ def generate_scenario_description(title: str, scenario_type: str, setting: str) -> str:
605
+ """Generate scenario descriptions"""
606
+ return f"In this {scenario_type.lower()}, the party must deal with {title.lower()} in a {setting.lower()} environment"
607
+
608
+ def generate_scenario_objective(scenario_type: str) -> str:
609
+ """Generate scenario objectives"""
610
+ objectives = {
611
+ "Combat Encounter": "Defeat the hostile creatures",
612
+ "Social Encounter": "Navigate complex social dynamics",
613
+ "Mystery": "Solve the central mystery"
614
+ }
615
+ return objectives.get(scenario_type, "Complete the adventure successfully")
616
+
617
+ def generate_scenario_complications(level: str) -> List[str]:
618
+ """Generate scenario complications"""
619
+ complications = {
620
+ "Easy": ["Minor obstacles", "Simple challenges"],
621
+ "Medium": ["Competing factions", "Hidden enemies"],
622
+ "Hard": ["Powerful adversaries", "Complex puzzles"],
623
+ "Deadly": ["Overwhelming odds", "Reality-threatening consequences"]
624
+ }
625
+ return complications.get(level, ["Unexpected challenges"])
626
+
627
+ def generate_scenario_npcs(scenario_type: str) -> List[str]:
628
+ """Generate scenario NPCs"""
629
+ npcs = {
630
+ "Combat Encounter": ["Enemy Leader", "Local Guard"],
631
+ "Social Encounter": ["Influential Noble", "Wise Elder"],
632
+ "Mystery": ["Prime Suspect", "Key Witness"]
633
+ }
634
+ return npcs.get(scenario_type, ["Important NPC"])
635
+
636
+ def generate_scenario_locations(setting: str) -> List[str]:
637
+ """Generate scenario locations"""
638
+ locations = {
639
+ "Urban": ["City Square", "Noble District", "Marketplace"],
640
+ "Wilderness": ["Forest Clearing", "Mountain Pass", "Cave"],
641
+ "Mixed": ["Village Center", "Roadside Inn", "Ancient Shrine"]
642
+ }
643
+ return locations.get(setting, ["Notable Location"])
644
+
645
+ def generate_scenario_rewards(level: str) -> List[str]:
646
+ """Generate scenario rewards"""
647
+ rewards = {
648
+ "Easy": ["50-100 gold", "Common magic item"],
649
+ "Medium": ["200-500 gold", "Uncommon magic item"],
650
+ "Hard": ["1000+ gold", "Rare magic item"],
651
+ "Deadly": ["5000+ gold", "Very rare magic item"]
652
+ }
653
+ return rewards.get(level, ["Appropriate treasure"])
654
+
655
+ def generate_scenario_hooks(scenario_type: str, title: str) -> List[str]:
656
+ """Generate scenario hooks"""
657
+ return [
658
+ f"A messenger arrives seeking help with {title.lower()}",
659
+ f"The party overhears rumors about {title.lower()}",
660
+ f"An old contact asks for help investigating {title.lower()}"
661
+ ]
utils/image_utils.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import requests
4
+ import base64
5
+ from io import BytesIO
6
+ from PIL import Image, ImageDraw, ImageFont
7
+ import openai
8
+ from typing import Optional, Dict, Any
9
+ import time
10
+ import random
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class ImageGenerationError(Exception):
15
+ """Custom exception for image generation failures"""
16
+ pass
17
+
18
+ def generate_image(prompt: str, content_type: str, content_info: str = "") -> Optional[str]:
19
+ """
20
+ Main function to generate images for any D&D content type
21
+
22
+ Args:
23
+ prompt: Detailed image generation prompt
24
+ content_type: Type of content (character_portrait, item, etc.)
25
+ content_info: Brief info about content for placeholder
26
+
27
+ Returns:
28
+ str: Path to generated image or None if all methods fail
29
+ """
30
+
31
+ # Check for API keys
32
+ openai_key = os.getenv('OPENAI_API_KEY')
33
+ stability_key = os.getenv('STABILITY_API_KEY')
34
+
35
+ if openai_key:
36
+ try:
37
+ return generate_with_dalle(prompt)
38
+ except Exception as e:
39
+ logger.warning(f"DALL-E failed: {e}")
40
+
41
+ if stability_key:
42
+ try:
43
+ return generate_with_stability(prompt)
44
+ except Exception as e:
45
+ logger.warning(f"Stability AI failed: {e}")
46
+
47
+ # Generate placeholder if no APIs work
48
+ return generate_placeholder(content_type, content_info)
49
+
50
+ def generate_with_dalle(prompt: str, size: str = "1024x1024") -> Optional[str]:
51
+ """Generate image using OpenAI's DALL-E"""
52
+ try:
53
+ logger.info(f"Generating image with DALL-E: {prompt[:50]}...")
54
+
55
+ openai.api_key = os.getenv('OPENAI_API_KEY')
56
+
57
+ response = openai.images.generate(
58
+ model="dall-e-3",
59
+ prompt=prompt,
60
+ size=size,
61
+ quality="standard",
62
+ n=1,
63
+ )
64
+
65
+ image_url = response.data[0].url
66
+ logger.info("DALL-E image generated successfully")
67
+ return image_url
68
+
69
+ except Exception as e:
70
+ logger.error(f"DALL-E generation failed: {e}")
71
+ raise ImageGenerationError(f"DALL-E failed: {str(e)}")
72
+
73
+ def generate_with_stability(prompt: str) -> Optional[str]:
74
+ """Generate image using Stability AI"""
75
+ try:
76
+ logger.info(f"Generating image with Stability AI: {prompt[:50]}...")
77
+
78
+ url = "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image"
79
+
80
+ headers = {
81
+ "Accept": "application/json",
82
+ "Content-Type": "application/json",
83
+ "Authorization": f"Bearer {os.getenv('STABILITY_API_KEY')}",
84
+ }
85
+
86
+ body = {
87
+ "text_prompts": [{"text": prompt, "weight": 1}],
88
+ "cfg_scale": 7,
89
+ "height": 1024,
90
+ "width": 1024,
91
+ "samples": 1,
92
+ "steps": 30,
93
+ }
94
+
95
+ response = requests.post(url, headers=headers, json=body)
96
+
97
+ if response.status_code != 200:
98
+ raise ImageGenerationError(f"Stability API error: {response.status_code}")
99
+
100
+ data = response.json()
101
+
102
+ # Convert base64 to image
103
+ image_data = base64.b64decode(data["artifacts"][0]["base64"])
104
+ img = Image.open(BytesIO(image_data))
105
+
106
+ # Save temporarily and return path
107
+ temp_path = f"temp_{int(time.time())}_{random.randint(1000,9999)}.png"
108
+ img.save(temp_path)
109
+ logger.info("Stability AI image generated successfully")
110
+ return temp_path
111
+
112
+ except Exception as e:
113
+ logger.error(f"Stability AI generation failed: {e}")
114
+ raise ImageGenerationError(f"Stability AI failed: {str(e)}")
115
+
116
+ def generate_placeholder(content_type: str, content_info: str) -> str:
117
+ """Generate a themed placeholder image when AI services fail"""
118
+ try:
119
+ # Create different colored placeholders for different content types
120
+ colors = {
121
+ "character_portrait": (70, 130, 180), # Steel Blue
122
+ "npc_portrait": (139, 69, 19), # Saddle Brown
123
+ "item": (255, 215, 0), # Gold
124
+ "location": (34, 139, 34), # Forest Green
125
+ "faction_symbol": (128, 0, 128), # Purple
126
+ "deity": (255, 255, 255), # White
127
+ "scenario": (220, 20, 60) # Crimson
128
+ }
129
+
130
+ color = colors.get(content_type, (128, 128, 128))
131
+
132
+ # Create image
133
+ img = Image.new('RGB', (512, 512), color=color)
134
+ draw = ImageDraw.Draw(img)
135
+
136
+ # Try to load a font, fall back to default if not available
137
+ try:
138
+ font = ImageFont.truetype("arial.ttf", 24)
139
+ except:
140
+ font = ImageFont.load_default()
141
+
142
+ # Add text
143
+ text_lines = [
144
+ f"{content_type.replace('_', ' ').title()}",
145
+ "Placeholder Image",
146
+ content_info[:30] + "..." if len(content_info) > 30 else content_info
147
+ ]
148
+
149
+ y_offset = 200
150
+ for line in text_lines:
151
+ # Get text bounding box
152
+ bbox = draw.textbbox((0, 0), line, font=font)
153
+ text_width = bbox[2] - bbox[0]
154
+ text_height = bbox[3] - bbox[1]
155
+
156
+ # Center text
157
+ x = (512 - text_width) // 2
158
+ draw.text((x, y_offset), line, fill=(255, 255, 255), font=font)
159
+ y_offset += text_height + 10
160
+
161
+ # Save placeholder
162
+ temp_path = f"placeholder_{content_type}_{int(time.time())}.png"
163
+ img.save(temp_path)
164
+
165
+ logger.info(f"Generated {content_type} placeholder image")
166
+ return temp_path
167
+
168
+ except Exception as e:
169
+ logger.error(f"Failed to create placeholder: {e}")
170
+ return None
171
+
172
+ def cleanup_temp_files():
173
+ """Clean up temporary image files"""
174
+ import glob
175
+ temp_files = glob.glob("temp_*.png") + glob.glob("placeholder_*.png")
176
+ for file in temp_files:
177
+ try:
178
+ os.remove(file)
179
+ logger.info(f"Cleaned up temporary file: {file}")
180
+ except Exception as e:
181
+ logger.warning(f"Failed to clean up {file}: {e}")