Spaces:
Sleeping
Sleeping
File size: 3,079 Bytes
43904b7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | """Monster builder: input handling, generation, rendering, export."""
import html
import random
import tempfile
from pathlib import Path
from .shared import ai_client
from .shared.ratelimit import RateLimiter
from .prompts import SCHEMA_SPEC, SYSTEM_PROMPT, build_user_prompt
from .fixtures import FIXTURE_DROWNLIGHT_SCUTTLER
limiter = RateLimiter()
CHALLENGE_RATINGS = ["1/8", "1/4", "1/2"] + [str(n) for n in range(1, 21)]
SIZES = ["Tiny", "Small", "Medium", "Large", "Huge", "Gargantuan"]
CREATURE_TYPES = [
"Aberration", "Beast", "Celestial", "Construct", "Dragon", "Elemental",
"Fey", "Fiend", "Giant", "Humanoid", "Monstrosity", "Ooze", "Plant", "Undead",
]
COMBAT_ROLES = [
"Brute", "Skirmisher", "Artillery", "Controller", "Lurker", "Leader",
"Minion swarm",
]
MAX_FIELD = 300
MAX_NOTES = 800
def randomize():
"""Fill the core dropdowns with a random buildable combination."""
return (
random.choice(CHALLENGE_RATINGS),
random.choice(SIZES),
random.choice(CREATURE_TYPES),
random.choice(COMBAT_ROLES),
)
def _clip(value: str, limit: int) -> str:
return (value or "").strip()[:limit]
def generate(concept, cr, size, creature_type, role, environment, notes,
request=None):
"""Returns (markdown, plain_text, download_path, error_message)."""
allowed, message = limiter.check(request)
if not allowed:
return None, None, None, message
fields = {
"Monster concept": _clip(concept, MAX_FIELD),
"Challenge rating target": _clip(cr, 20),
"Size": _clip(size, 40),
"Creature type": _clip(creature_type, MAX_FIELD),
"Combat role": _clip(role, MAX_FIELD),
"Environment": _clip(environment, MAX_FIELD),
"Extra notes": _clip(notes, MAX_NOTES),
}
user_prompt = build_user_prompt(fields)
try:
data = ai_client.generate_json(
SYSTEM_PROMPT, user_prompt, SCHEMA_SPEC,
fixture=FIXTURE_DROWNLIGHT_SCUTTLER,
)
except ai_client.AIUnavailable as exc:
return None, None, None, str(exc)
md = render_markdown(data)
path = Path(tempfile.mkdtemp()) / "dnd-monster.md"
path.write_text(md, encoding="utf-8")
return md, md, str(path), None
def render_markdown(d: dict) -> str:
traits = "\n".join(f"- {t}" for t in d["traits"])
actions = "\n".join(f"- {a}" for a in d["actions"])
return f"""## {d['name']}
*{d['size_type_alignment']}*
{d['lore']}
**Armor Class** {d['armor_class']} \n**Hit Points** {d['hit_points']} \n**Speed** {d['speed']} \n**Ability Scores** {d['ability_scores']} \n**Saves & Skills** {d['saves_and_skills']} \n**Defenses** {d['defenses']} \n**Senses & Languages** {d['senses_and_languages']} \n**Challenge** {d['challenge']}
### Traits
{traits}
### Actions
{actions}
### Tactics
{d['tactics']}
### Loot
{d['loot']}
"""
def render_error(message: str) -> str:
return (
'<div class="lf-output" role="alert" style="border-color:#A7343B;">'
f"<p>{html.escape(message)}</p></div>"
)
|