fuzzyworld

dm / commit

feat: add World Notes — GM meta-context injected into system prompt

0a0dca6f5bf469f66c0a23de8301d259d2d63ab0 · Pavel Safronov · 2026-05-27 21:39:23 -0700 · parents 2ab9b086d317

feat: add World Notes — GM meta-context injected into system prompt

Adds an orange-bordered World Notes section on the creation screen and
in the game sidebar. Notes entered before starting a game carry into
the sidebar; notes updated mid-game trigger Claude to acknowledge the
world change with a narrative beat. Notes are included in every API
call's system prompt as absolute world truths and persisted to the
saved session in localStorage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 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}` },