| 1 | // π€ DISABLED β not wired into shell.js, not shipped in the live app. This was |
| 2 | // the proof-of-concept for `quiet: true` (apps/README.md Β§ Quiet apps, issue |
| 3 | // #227): a personal app whose `/roll` answers right where you typed it and |
| 4 | // still records every roll into its own Dice thread, but that thread never |
| 5 | // forces its own way into the sidebar β the reducer, the command, and the |
| 6 | // `quiet` declaration below are exactly what a real quiet app looks like. |
| 7 | // Kept here (not deleted) as a living reference and for its unit tests |
| 8 | // (test/apps.test.mjs); it never ran live outside a dev-server verification |
| 9 | // pass. Move it back to web/static/apps/ + re-add the shell.js import if it's |
| 10 | // ever wanted as a real feature. |
| 11 | // |
| 12 | // The dice mini-app: `/roll` (or `/roll d20`) answers right where you typed |
| 13 | // it β a QUIET personal app. Every roll is still recorded into your own Dice |
| 14 | // thread, so your history is real and syncs across devices β quiet only |
| 15 | // means the thread never forces its own way into your sidebar; your own tap |
| 16 | // (the "Roll history" chip, or the Apps menu) opens it. |
| 17 | import { defineApp, addItem } from "../apps.js"; |
| 18 | |
| 19 | const ROLL_RE = /^d?(\d{1,3})$/i; |
| 20 | |
| 21 | // parseSides pulls the die size out of "/roll d20" β 20, defaulting to a d6 |
| 22 | // for a bare "/roll", or null for anything unparseable or out of range (2..1000). |
| 23 | // Pure, so it's unit-testable in Node. |
| 24 | export function parseSides(args) { |
| 25 | const text = (args || "").trim(); |
| 26 | if (!text) return 6; |
| 27 | const m = ROLL_RE.exec(text); |
| 28 | if (!m) return null; |
| 29 | const n = parseInt(m[1], 10); |
| 30 | return n >= 2 && n <= 1000 ? n : null; |
| 31 | } |
| 32 | |
| 33 | export const app = defineApp({ |
| 34 | name: "dice", |
| 35 | title: "Dice", |
| 36 | glyph: "π²", |
| 37 | personal: true, |
| 38 | quiet: true, // the whole point: /roll never forces the Dice thread into view |
| 39 | initialState: () => ({ rolls: [] }), |
| 40 | events: { |
| 41 | "dice.roll": (s, e) => |
| 42 | Number.isInteger(e.sides) && Number.isInteger(e.result) |
| 43 | ? { rolls: addItem(s.rolls, { id: e.id, sides: e.sides, result: e.result, by: e.name || "" }) } |
| 44 | : s, |
| 45 | }, |
| 46 | commands: { |
| 47 | "/roll": { |
| 48 | desc: "Roll a die", |
| 49 | args: "[dN]", |
| 50 | run(args, ctx) { |
| 51 | const sides = parseSides(args); |
| 52 | if (sides === null) return ctx.reply({ text: "Usage: /roll or /roll d20 β a die between d2 and d1000." }); |
| 53 | const result = 1 + Math.floor(Math.random() * sides); |
| 54 | ctx.emit({ type: "dice.roll", id: ctx.id(), sides, result }); |
| 55 | ctx.reply({ |
| 56 | text: `π² Rolled a ${result} on a d${sides}.`, |
| 57 | short: `rolled ${result} (d${sides})`, |
| 58 | actions: [{ label: "Roll history", run: ctx.open }], |
| 59 | }); |
| 60 | }, |
| 61 | }, |
| 62 | }, |
| 63 | styles: ` |
| 64 | .dice-item { display: flex; justify-content: space-between; gap: 8px; padding: 6px 2px; border-bottom: 1px solid var(--border); } |
| 65 | .dice-result { font-weight: 600; } |
| 66 | .dice-empty { color: var(--muted); padding: 8px 2px; } |
| 67 | `, |
| 68 | render(state, el) { |
| 69 | const rolls = (state && state.rolls) || []; |
| 70 | if (!rolls.length) { |
| 71 | const empty = document.createElement("div"); |
| 72 | empty.className = "dice-empty"; |
| 73 | empty.textContent = "No rolls yet β try /roll from any chat."; |
| 74 | el.appendChild(empty); |
| 75 | return; |
| 76 | } |
| 77 | for (const r of [...rolls].reverse()) { |
| 78 | const row = document.createElement("div"); |
| 79 | row.className = "dice-item"; |
| 80 | const label = document.createElement("span"); |
| 81 | label.textContent = `d${r.sides}`; |
| 82 | const result = document.createElement("span"); |
| 83 | result.className = "dice-result"; |
| 84 | result.textContent = String(r.result); |
| 85 | row.append(label, result); |
| 86 | el.appendChild(row); |
| 87 | } |
| 88 | }, |
| 89 | }); |
| 90 | |