eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

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.
17import { defineApp, addItem } from "../apps.js";
18
19const 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.
24export 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
33export 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