// ๐Ÿค– DISABLED โ€” not wired into shell.js, not shipped in the live app. This was // the proof-of-concept for `quiet: true` (apps/README.md ยง Quiet apps, issue // #227): a personal app whose `/roll` answers right where you typed it and // still records every roll into its own Dice thread, but that thread never // forces its own way into the sidebar โ€” the reducer, the command, and the // `quiet` declaration below are exactly what a real quiet app looks like. // Kept here (not deleted) as a living reference and for its unit tests // (test/apps.test.mjs); it never ran live outside a dev-server verification // pass. Move it back to web/static/apps/ + re-add the shell.js import if it's // ever wanted as a real feature. // // The dice mini-app: `/roll` (or `/roll d20`) answers right where you typed // it โ€” a QUIET personal app. Every roll is still recorded into your own Dice // thread, so your history is real and syncs across devices โ€” quiet only // means the thread never forces its own way into your sidebar; your own tap // (the "Roll history" chip, or the Apps menu) opens it. import { defineApp, addItem } from "../apps.js"; const ROLL_RE = /^d?(\d{1,3})$/i; // parseSides pulls the die size out of "/roll d20" โ†’ 20, defaulting to a d6 // for a bare "/roll", or null for anything unparseable or out of range (2..1000). // Pure, so it's unit-testable in Node. export function parseSides(args) { const text = (args || "").trim(); if (!text) return 6; const m = ROLL_RE.exec(text); if (!m) return null; const n = parseInt(m[1], 10); return n >= 2 && n <= 1000 ? n : null; } export const app = defineApp({ name: "dice", title: "Dice", glyph: "๐ŸŽฒ", personal: true, quiet: true, // the whole point: /roll never forces the Dice thread into view initialState: () => ({ rolls: [] }), events: { "dice.roll": (s, e) => Number.isInteger(e.sides) && Number.isInteger(e.result) ? { rolls: addItem(s.rolls, { id: e.id, sides: e.sides, result: e.result, by: e.name || "" }) } : s, }, commands: { "/roll": { desc: "Roll a die", args: "[dN]", run(args, ctx) { const sides = parseSides(args); if (sides === null) return ctx.reply({ text: "Usage: /roll or /roll d20 โ€” a die between d2 and d1000." }); const result = 1 + Math.floor(Math.random() * sides); ctx.emit({ type: "dice.roll", id: ctx.id(), sides, result }); ctx.reply({ text: `๐ŸŽฒ Rolled a ${result} on a d${sides}.`, short: `rolled ${result} (d${sides})`, actions: [{ label: "Roll history", run: ctx.open }], }); }, }, }, styles: ` .dice-item { display: flex; justify-content: space-between; gap: 8px; padding: 6px 2px; border-bottom: 1px solid var(--border); } .dice-result { font-weight: 600; } .dice-empty { color: var(--muted); padding: 8px 2px; } `, render(state, el) { const rolls = (state && state.rolls) || []; if (!rolls.length) { const empty = document.createElement("div"); empty.className = "dice-empty"; empty.textContent = "No rolls yet โ€” try /roll from any chat."; el.appendChild(empty); return; } for (const r of [...rolls].reverse()) { const row = document.createElement("div"); row.className = "dice-item"; const label = document.createElement("span"); label.textContent = `d${r.sides}`; const result = document.createElement("span"); result.className = "dice-result"; result.textContent = String(r.result); row.append(label, result); el.appendChild(row); } }, });