client.js (5632B)
1 (() => { 2 if (!window?.BzlPluginHost?.register) return; 3 4 const PLUGIN_ID = "dice"; 5 6 function esc(s) { 7 return String(s ?? "") 8 .replace(/&/g, "&") 9 .replace(/</g, "<") 10 .replace(/>/g, ">") 11 .replace(/\"/g, """) 12 .replace(/'/g, "'"); 13 } 14 15 function safeJsonParse(str) { 16 try { 17 return JSON.parse(str); 18 } catch { 19 return null; 20 } 21 } 22 23 function fmtTime(t) { 24 try { 25 return new Date(Number(t || Date.now())).toLocaleTimeString(); 26 } catch { 27 return ""; 28 } 29 } 30 31 window.BzlPluginHost.register(PLUGIN_ID, (ctx) => { 32 const ws = window.__bzlWs; 33 let mountEl = null; 34 let inputEl = null; 35 let rollBtnEl = null; 36 let listEl = null; 37 let errEl = null; 38 let history = []; 39 40 function setError(msg) { 41 if (!errEl) return; 42 errEl.textContent = String(msg || ""); 43 errEl.classList.toggle("hidden", !errEl.textContent); 44 } 45 46 function renderList() { 47 if (!listEl) return; 48 if (!history.length) { 49 listEl.innerHTML = `<div class="small muted" style="padding:10px">No rolls yet.</div>`; 50 return; 51 } 52 const rows = history 53 .slice() 54 .reverse() 55 .slice(0, 60) 56 .map((r) => { 57 const rolls = Array.isArray(r.rolls) ? r.rolls.join(", ") : ""; 58 return `<div class="diceRow"> 59 <div class="diceTop"> 60 <span class="diceUser">@${esc(r.user || "unknown")}</span> 61 <span class="muted">•</span> 62 <span class="diceExpr">${esc(r.expr || "")}</span> 63 <span class="muted">•</span> 64 <span class="muted">${esc(fmtTime(r.createdAt))}</span> 65 </div> 66 <div class="diceBottom"> 67 <span class="muted">rolls:</span> <span>${esc(rolls)}</span> 68 <span class="muted">→</span> 69 <span class="diceTotal">total: ${esc(String(r.total))}</span> 70 </div> 71 </div>`; 72 }) 73 .join(""); 74 listEl.innerHTML = rows; 75 } 76 77 function doRoll() { 78 const expr = String(inputEl?.value || "").trim(); 79 if (!expr) return; 80 setError(""); 81 ctx.send("roll", { expr }); 82 } 83 84 function ensureUi() { 85 if (!mountEl) return; 86 mountEl.innerHTML = ` 87 <style> 88 .diceWrap { display:flex; flex-direction:column; gap:10px; } 89 .diceControls { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } 90 .diceControls input { flex: 1; min-width: 140px; } 91 .diceErr { color: color-mix(in srgb, var(--bad) 80%, var(--text)); } 92 .diceList { max-height: 340px; overflow:auto; border:1px solid rgba(246,240,255,0.10); border-radius:12px; } 93 .diceRow { padding:10px 12px; border-bottom:1px solid rgba(246,240,255,0.06); } 94 .diceRow:last-child { border-bottom:0; } 95 .diceTop { display:flex; gap:8px; flex-wrap:wrap; align-items:baseline; } 96 .diceBottom { display:flex; gap:8px; flex-wrap:wrap; margin-top:4px; } 97 .diceUser { font-weight: 900; } 98 .diceExpr { font-weight: 900; letter-spacing: 0.2px; } 99 .diceTotal { font-weight: 900; color: color-mix(in srgb, var(--warn) 70%, var(--text)); } 100 </style> 101 <div class="diceWrap"> 102 <div class="diceControls"> 103 <input type="text" maxlength="40" placeholder="XdY+Z (e.g. 2d6+1, d20, 4d8-2)" data-diceexpr="1" /> 104 <button type="button" class="primary smallBtn" data-diceroll="1">Roll</button> 105 </div> 106 <div class="small muted">Rolls are server-generated and broadcast to everyone.</div> 107 <div class="small diceErr hidden" data-diceerr="1"></div> 108 <div class="diceList" data-dicelist="1"></div> 109 </div> 110 `; 111 inputEl = mountEl.querySelector("[data-diceexpr='1']"); 112 rollBtnEl = mountEl.querySelector("[data-diceroll='1']"); 113 listEl = mountEl.querySelector("[data-dicelist='1']"); 114 errEl = mountEl.querySelector("[data-diceerr='1']"); 115 116 if (rollBtnEl) rollBtnEl.addEventListener("click", doRoll); 117 if (inputEl) { 118 inputEl.addEventListener("keydown", (e) => { 119 if (e.key === "Enter") { 120 e.preventDefault(); 121 doRoll(); 122 } 123 }); 124 } 125 renderList(); 126 } 127 128 function onWsMessage(evt) { 129 const msg = safeJsonParse(evt.data); 130 if (!msg || typeof msg !== "object") return; 131 const type = String(msg.type || ""); 132 if (type === "plugin:dice:history") { 133 history = Array.isArray(msg.history) ? msg.history : []; 134 renderList(); 135 return; 136 } 137 if (type === "plugin:dice:rolled") { 138 if (msg.roll && typeof msg.roll === "object") { 139 history = [...history, msg.roll].slice(-120); 140 renderList(); 141 } 142 return; 143 } 144 if (type === "plugin:dice:error") { 145 setError(String(msg.message || "Error.")); 146 } 147 } 148 149 ctx.ui?.registerPanel?.({ 150 id: "dice", 151 title: "Dice", 152 icon: "🎲", 153 defaultRack: "right", 154 role: "utility", 155 presetHints: { 156 defaultSocial: { place: "docked.bottom" }, 157 ops: { place: "docked.bottom" } 158 }, 159 render(mount) { 160 mountEl = mount; 161 history = []; 162 ensureUi(); 163 if (ws && ws.addEventListener) ws.addEventListener("message", onWsMessage); 164 ctx.send("stateReq", {}); 165 return () => { 166 if (ws && ws.removeEventListener) ws.removeEventListener("message", onWsMessage); 167 mountEl = null; 168 }; 169 } 170 }); 171 }); 172 })(); 173