dm / commit
feat: add World Notes — GM meta-context injected into system prompt
public/index.html | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 5b751ec..44c3bf3 100644 --- a/public/index.html +++ b/public/index.html @@ -723,6 +723,94 @@ body { } .attribution a:hover { color: var(--gold); } +/* ── WORLD NOTES ────────────────────────────────── */ + +.world-notes-section { + border: 2px solid #d97706; + border-radius: 8px; + padding: 16px; + background: rgba(217, 119, 6, 0.06); + margin: 20px 0; +} +.world-notes-label { + font-family: 'Cinzel', serif; + font-size: 0.7rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #f59e0b; + margin-bottom: 6px; +} +.world-notes-desc { + font-size: 0.78rem; + color: var(--text-dim); + margin-bottom: 10px; + line-height: 1.5; +} +.wn-textarea { + width: 100%; + min-height: 90px; + background: rgba(0,0,0,0.35); + border: 1px solid #b45309; + border-radius: 5px; + color: var(--text); + font-size: 0.82rem; + font-family: 'EB Garamond', serif; + padding: 8px 10px; + resize: vertical; + box-sizing: border-box; + line-height: 1.5; +} +.wn-textarea:focus { outline: none; border-color: #f59e0b; } +.wn-textarea::placeholder { color: #6b5a3a; font-style: italic; } +.wn-save-btn { + margin-top: 10px; + width: 100%; + padding: 8px; + background: #92400e; + border: 1px solid #d97706; + border-radius: 5px; + color: #fde68a; + font-family: 'Cinzel', serif; + font-size: 0.72rem; + letter-spacing: 0.1em; + cursor: pointer; + transition: background 0.15s; +} +.wn-save-btn:hover { background: #b45309; } +.wn-save-btn:disabled { opacity: 0.4; cursor: default; } + +/* Sidebar world notes — overrides */ +.sidebar-section.wn-sidebar { + border-bottom: 1px solid #7c3b00; + background: rgba(217, 119, 6, 0.05); + outline: 2px solid #d97706; + outline-offset: -2px; +} +.sidebar-section.wn-sidebar .sidebar-title { color: #f59e0b; } + +/* Chat event banner when notes are applied mid-game */ +.world-notes-event { + margin: 12px 0; + padding: 12px 14px; + border: 2px solid #d97706; + border-radius: 8px; + background: rgba(217, 119, 6, 0.08); +} +.wne-title { + font-family: 'Cinzel', serif; + font-size: 0.7rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #f59e0b; + margin-bottom: 6px; +} +.wne-body { + font-size: 0.82rem; + color: #d4b896; + white-space: pre-wrap; + line-height: 1.5; +} + /* ── SHARE MODAL ────────────────────────────────── */ #share-modal { @@ -978,6 +1066,12 @@ body { </div> </div> + <div class="world-notes-section"> + <div class="world-notes-label">📜 World Notes <span style="font-size:0.65rem;opacity:0.6;text-transform:none;letter-spacing:0;font-family:sans-serif">(optional)</span></div> + <p class="world-notes-desc">Background lore, house rules, world physics — anything Claude should treat as absolute truth. Written as GM, not player. Keep it out of the story itself.</p> + <textarea class="wn-textarea" id="creation-world-notes" placeholder="e.g. Gravity is half of Earth normal. Magic draws from crystallised starlight and leaves frostburn scars. The empire collapsed 50 years ago and is now a patchwork of city-states…"></textarea> + </div> + <button class="begin-btn" id="begin-btn" disabled>Begin Adventure</button> <p style="margin-top:12px;text-align:center"> @@ -1090,6 +1184,13 @@ body { <a href="https://www.flaticon.com/" target="_blank" rel="noopener">Flaticon</a> </p> </div> + + <div class="sidebar-section wn-sidebar"> + <div class="sidebar-title">📜 World Notes</div> + <p class="world-notes-desc" style="margin-bottom:8px">Meta context — world laws, lore, constraints — applied outside the narrative.</p> + <textarea class="wn-textarea" id="world-notes-textarea" placeholder="e.g. Reduced gravity. Magic leaves frostburn scars…" style="min-height:80px"></textarea> + <button class="wn-save-btn" id="world-notes-save-btn">Apply to World ↵</button> + </div> </div> </div> @@ -1405,7 +1506,7 @@ const UPDATE_TOOL = { }, }; -function buildSystemPrompt(character, theme) { +function buildSystemPrompt(character, theme, notes) { return `You are a masterful Dungeon Master running a solo D&D 5e adventure. CHARACTER: ${character.name} — Level ${character.level} ${character.race} ${character.class} @@ -1420,7 +1521,7 @@ NARRATIVE RULES STAT TRACKING — call update_character whenever HP changes, gold is gained/lost, XP is earned, or items change hands. Call the tool BEFORE writing your narrative text. -TONE: Cinematic. Dangerous. Immersive. Legendary.`; +TONE: Cinematic. Dangerous. Immersive. Legendary.${notes ? `\n\nWORLD NOTES — ABSOLUTE TRUTHS\nThe GM has defined the following facts about this world. Treat them as immutable and weave them naturally into every scene:\n\n${notes}` : ''}`; } // ── STATE ───────────────────────────────────────────── @@ -1431,6 +1532,7 @@ let gameState = null; let conversationHistory = []; let isBusy = false; let currentApiKey = null; +let worldNotes = ''; // ── IDs ─────────────────────────────────────────────── // player_id persists across games; game_id is fresh each adventure. @@ -1505,6 +1607,7 @@ function saveSession() { conversationHistory, messagesHTML: document.getElementById('messages').innerHTML, sessionCost, + worldNotes, })); } catch (e) { console.warn('Session save failed (storage full?):', e); @@ -1530,10 +1633,12 @@ async function tryResumeSession() { sessionCost = session.sessionCost || 0; lastTurnCost = 0; + worldNotes = session.worldNotes || ''; updateSidebar(); buildDiceRoller(); updateCostDisplay(); + syncWorldNotesSidebar(); const msgEl = document.getElementById('messages'); msgEl.scrollTop = msgEl.scrollHeight; @@ -1587,6 +1692,7 @@ async function beginAdventure() { const name = document.getElementById('char-name').value.trim() || `The ${selectedClass.name}`; const theme = document.getElementById('adventure-theme').value.trim(); + worldNotes = document.getElementById('creation-world-notes').value.trim(); gameState = { apiKey, @@ -1613,6 +1719,7 @@ async function beginAdventure() { updateSidebar(); buildDiceRoller(); + syncWorldNotesSidebar(); document.getElementById('header-title').textContent = `${name} — ${selectedRace.name} ${selectedClass.name}`; @@ -1740,6 +1847,45 @@ function buildDiceRoller() { document.getElementById('roll-btn').addEventListener('click', rollDice); } +// ── WORLD NOTES ─────────────────────────────────────── + +function syncWorldNotesSidebar() { + const ta = document.getElementById('world-notes-textarea'); + if (ta) ta.value = worldNotes; +} + +function addWorldNotesEvent(notes) { + const msgs = document.getElementById('messages'); + const banner = document.createElement('div'); + banner.className = 'world-notes-event'; + const title = document.createElement('div'); + title.className = 'wne-title'; + title.textContent = '📜 World Notes Updated'; + banner.appendChild(title); + const body = document.createElement('div'); + body.className = 'wne-body'; + body.textContent = notes || '(cleared)'; + banner.appendChild(body); + msgs.appendChild(banner); + msgs.scrollTop = msgs.scrollHeight; +} + +async function applyWorldNotes() { + const ta = document.getElementById('world-notes-textarea'); + worldNotes = ta ? ta.value.trim() : ''; + saveSession(); + addWorldNotesEvent(worldNotes); + if (worldNotes) { + const metaMsg = `[GM WORLD UPDATE] The world context has been updated by the GM outside the narrative. New world notes: "${worldNotes}". Acknowledge this shift in 2–3 immersive sentences — describe a subtle change felt in the world, a cosmic tremor, or a moment of heightened awareness that the character perceives. Then invite the player to continue.`; + await sendMessage(metaMsg, false); + } +} + +document.getElementById('world-notes-save-btn').addEventListener('click', () => { + if (!gameState) return; + applyWorldNotes(); +}); + function rollDice() { if (isBusy) return; const count = Math.min(10, Math.max(1, parseInt(document.getElementById('dice-count').value, 10) || 1)); @@ -1887,7 +2033,7 @@ async function sendMessage(text, showAsPlayer = true) { model: 'claude-haiku-4-5-20251001', max_tokens: 1500, stream: true, - system: buildSystemPrompt(gameState.character, gameState.theme), + system: buildSystemPrompt(gameState.character, gameState.theme, worldNotes), tools: [UPDATE_TOOL], messages: apiMessages, metadata: { user_id: `p_${getOrCreatePlayerId()}:g_${gameState.gameId}` },