feat: World Builder, Item Forge, Quest Designer + sidebar navigation
- World Builder: AI-generated worlds with regions, landmarks, conflicts, cultures - Item Forge: AI magic item generator with rarity-colored headers, mechanical/lore sections - Quest Designer: AI quest generator with step-through navigation, choices, twist, reward - Sidebar now navigates to dedicated full-screen views for every tool - Bento dashboard cards have 'Expand' buttons opening detail views - Back arrow (←) in title bar returns to dashboard - Dashboard grid expanded to 5 rows to accommodate Item Forge - All three new tools also work in compact dashboard mode - Full production build clean
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface ItemResponse {
|
||||
name: string;
|
||||
rarity: string;
|
||||
type: string;
|
||||
description: string;
|
||||
mechanical: string;
|
||||
lore: string;
|
||||
}
|
||||
|
||||
const RARITIES = ["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"];
|
||||
const TYPES = ["Weapon", "Armor", "Potion", "Scroll", "Wondrous Item", "Ring", "Wand", "Staff"];
|
||||
|
||||
export function ItemForge() {
|
||||
const [rarity, setRarity] = useState("Rare");
|
||||
const [itemType, setItemType] = useState("Wondrous Item");
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [item, setItem] = useState<ItemResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function generate() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setItem(null);
|
||||
try {
|
||||
const result = await invoke<string>("generate", {
|
||||
req: {
|
||||
prompt: `Create a D&D 5e magic item with the following specifications:
|
||||
Rarity: ${rarity}
|
||||
Type: ${itemType}
|
||||
${prompt ? `Additional details: ${prompt}` : ""}
|
||||
|
||||
Provide the response as a JSON object with exactly these keys:
|
||||
- "name": the item's evocative name
|
||||
- "rarity": "${rarity}"
|
||||
- "type": "${itemType}"
|
||||
- "description": a vivid 2-3 sentence physical description
|
||||
- "mechanical": the item's game mechanics (what it does, charges, etc.)
|
||||
- "lore": a 1-2 sentence piece of lore or history about the item`,
|
||||
system: "You are a creative D&D item designer. Always respond with valid JSON only.",
|
||||
temperature: 0.85,
|
||||
max_tokens: 500,
|
||||
},
|
||||
});
|
||||
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
setItem(JSON.parse(jsonMatch[0]) as ItemResponse);
|
||||
} else {
|
||||
setError("LLM did not return valid JSON.\n" + result);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const rarityColor: Record<string, string> = {
|
||||
Common: "var(--color-text-secondary)",
|
||||
Uncommon: "#1eff00",
|
||||
Rare: "#0070dd",
|
||||
"Very Rare": "#a335ee",
|
||||
Legendary: "#ff8000",
|
||||
Artifact: "#e6cc80",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-[10px] font-medium">Rarity</label>
|
||||
<select
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs cursor-pointer"
|
||||
value={rarity}
|
||||
onChange={(e) => setRarity(e.target.value)}
|
||||
>
|
||||
{RARITIES.map((r) => <option key={r} value={r}>{r}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-[10px] font-medium">Type</label>
|
||||
<select
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-xs cursor-pointer"
|
||||
value={itemType}
|
||||
onChange={(e) => setItemType(e.target.value)}
|
||||
>
|
||||
{TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-[10px] font-medium">
|
||||
Additional details (optional)
|
||||
</label>
|
||||
<input
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="e.g. a flame dagger that grants fire resistance..."
|
||||
onKeyDown={(e) => e.key === "Enter" && generate()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={generate}
|
||||
disabled={loading}
|
||||
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{loading ? "⚔ Forging…" : "⚔ Forge Item"}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/30 p-3 text-[var(--color-danger)] text-xs">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item && (
|
||||
<div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-4 flex flex-col gap-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-heading text-lg font-bold" style={{ color: rarityColor[item.rarity] || "var(--color-gold-bright)" }}>
|
||||
{item.name}
|
||||
</h3>
|
||||
<div className="flex gap-2 text-xs text-[var(--color-text-dim)]">
|
||||
<span>{item.rarity}</span>
|
||||
<span>•</span>
|
||||
<span>{item.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--color-text-secondary)] text-[10px] font-medium uppercase tracking-wider">Description</span>
|
||||
<p className="text-[var(--color-text-primary)] text-sm mt-0.5 leading-relaxed">{item.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-glass)] p-3">
|
||||
<span className="text-[var(--color-gold-bright)] text-[10px] font-medium uppercase tracking-wider">Mechanics</span>
|
||||
<p className="text-[var(--color-text-primary)] text-sm mt-0.5">{item.mechanical}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--color-text-secondary)] text-[10px] font-medium uppercase tracking-wider">Lore</span>
|
||||
<p className="text-[var(--color-text-secondary)] text-sm mt-0.5 italic">{item.lore}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user