@double_geste/agent-standup 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kokoro-worker.js +6 -1
- package/kokoro.js +2 -2
- package/package.json +1 -1
- package/public/index.html +105 -58
- package/public/locales/de.json +4 -4
- package/public/locales/en.json +4 -4
- package/public/locales/es.json +4 -4
- package/public/locales/ja.json +4 -4
- package/public/locales/pt-BR.json +4 -4
- package/public/locales/zh-CN.json +4 -4
- package/public/locales/zh-TW.json +4 -4
- package/server.js +42 -5
- package/public/meme.css +0 -60
- package/public/meme.js +0 -235
- package/public/sfx/oh-my-god.mp3 +0 -0
- package/public/sfx/scratch-1.mp3 +0 -0
- package/public/sfx/scratch-2.mp3 +0 -0
- package/public/sfx/scratch-3.mp3 +0 -0
package/public/locales/es.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"☕ Buy the (replaced) dev a coffee": "☕ Invita un café al dev (reemplazado)",
|
|
13
13
|
"☕ This dev was replaced by his agents — buy him a coffee": "☕ Este dev fue reemplazado por sus agentes — invítale un café",
|
|
14
14
|
"🎞 Export video": "🎞 Exportar vídeo",
|
|
15
|
+
"🎬 Clip to share (25 s)": "🎬 Clip para compartir (25 s)",
|
|
15
16
|
"📋 Recap": "📋 Resumen",
|
|
16
17
|
"Agent voices": "Voces de los agentes",
|
|
17
18
|
"Agents": "Agentes",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"Files touched": "Archivos tocados",
|
|
59
60
|
"Final report": "Informe final",
|
|
60
61
|
"first error": "primer error",
|
|
62
|
+
"Hide code & file names in videos": "Ocultar código y archivos en los vídeos",
|
|
61
63
|
"Highlights": "Destacados",
|
|
62
64
|
"Human": "Humano",
|
|
63
65
|
"idle": "inactivo",
|
|
@@ -159,6 +161,7 @@
|
|
|
159
161
|
"Writing ": "Escribiendo ",
|
|
160
162
|
"You": "Tú",
|
|
161
163
|
"Your agents are done 🎉": "Tus agentes han terminado 🎉",
|
|
164
|
+
"Your Claude Code agents as a live team call": "Tus agentes de Claude Code, en videollamada de equipo en directo",
|
|
162
165
|
"your turn": "tu turno",
|
|
163
166
|
"Characters": "Personajes",
|
|
164
167
|
"Reload": "Recargar",
|
|
@@ -166,13 +169,10 @@
|
|
|
166
169
|
"custom character(s) loaded": "personaje(s) propio(s) cargado(s)",
|
|
167
170
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "Suelta tu pixel art (PNG) en esta carpeta para reemplazar los personajes:",
|
|
168
171
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = el agente principal · you.png = tú · el resto = los subagentes · name-talk.png = boca abierta",
|
|
169
|
-
"🔥 Meme mode (unhinged)": "🔥 Modo meme (desmadre total)",
|
|
170
172
|
"Downloading, you can carry on: system voices are used meanwhile.": "Descargando, puedes seguir: mientras tanto se usan las voces del sistema.",
|
|
171
|
-
"Effects volume": "Volumen de los efectos",
|
|
172
173
|
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Voces locales de buena calidad, sobre todo en inglés (una sola voz en francés). Gratis, nada sale de tu equipo.",
|
|
173
174
|
"Installing…": "Instalando…",
|
|
174
|
-
"
|
|
175
|
-
"Loading the model (downloaded on first use, about 155 MB)…": "Cargando el modelo (se descarga la primera vez, unos 155 MB)…",
|
|
175
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "Cargando el modelo (se descarga la primera vez, unos 305 MB)…",
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "No se encontró npm: instala Node.js para activar esta opción.",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "El motor se detuvo: se usan las voces del sistema.",
|
|
178
178
|
"Very natural voices (installs on demand)": "Voces muy naturales (se instalan bajo demanda)",
|
package/public/locales/ja.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"☕ Buy the (replaced) dev a coffee": "☕ (お役御免の)開発者にコーヒーを",
|
|
13
13
|
"☕ This dev was replaced by his agents — buy him a coffee": "☕ この開発者はエージェントに仕事を奪われました — コーヒーをおごってあげて",
|
|
14
14
|
"🎞 Export video": "🎞 動画を書き出す",
|
|
15
|
+
"🎬 Clip to share (25 s)": "🎬 シェア用クリップ(25秒)",
|
|
15
16
|
"📋 Recap": "📋 まとめ",
|
|
16
17
|
"Agent voices": "エージェントの声",
|
|
17
18
|
"Agents": "エージェント",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"Files touched": "触れたファイル",
|
|
59
60
|
"Final report": "最終レポート",
|
|
60
61
|
"first error": "最初のエラー",
|
|
62
|
+
"Hide code & file names in videos": "動画でコードとファイル名を隠す",
|
|
61
63
|
"Highlights": "ハイライト",
|
|
62
64
|
"Human": "人間",
|
|
63
65
|
"idle": "待機中",
|
|
@@ -159,6 +161,7 @@
|
|
|
159
161
|
"Writing ": "書き込み中: ",
|
|
160
162
|
"You": "あなた",
|
|
161
163
|
"Your agents are done 🎉": "エージェントが完了しました 🎉",
|
|
164
|
+
"Your Claude Code agents as a live team call": "Claude Code のエージェントを、ライブのチーム通話で",
|
|
162
165
|
"your turn": "あなたの番",
|
|
163
166
|
"Characters": "キャラクター",
|
|
164
167
|
"Reload": "再読み込み",
|
|
@@ -166,13 +169,10 @@
|
|
|
166
169
|
"custom character(s) loaded": "件のカスタムキャラクターを読み込みました",
|
|
167
170
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "このフォルダにドット絵(PNG)を入れると、キャラクターを差し替えられます:",
|
|
168
171
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = メインエージェント · you.png = あなた · それ以外 = サブエージェント · name-talk.png = 口を開けた状態",
|
|
169
|
-
"🔥 Meme mode (unhinged)": "🔥 ミームモード(やりすぎ)",
|
|
170
172
|
"Downloading, you can carry on: system voices are used meanwhile.": "ダウンロード中です。そのまま作業を続けられます。それまではシステム音声を使います。",
|
|
171
|
-
"Effects volume": "効果音の音量",
|
|
172
173
|
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "品質の良いローカル音声(主に英語、フランス語は1種類のみ)。無料で、データがPCの外に出ることはありません。",
|
|
173
174
|
"Installing…": "インストール中…",
|
|
174
|
-
"
|
|
175
|
-
"Loading the model (downloaded on first use, about 155 MB)…": "モデルを読み込み中(初回使用時にダウンロード、約155 MB)…",
|
|
175
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "モデルを読み込み中(初回使用時にダウンロード、約305 MB)…",
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "npm が見つかりません。このオプションを使うには Node.js をインストールしてください。",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "エンジンが停止しました。代わりにシステム音声を使います。",
|
|
178
178
|
"Very natural voices (installs on demand)": "とても自然な音声(必要時にインストール)",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"☕ Buy the (replaced) dev a coffee": "☕ Pague um café ao dev (substituído)",
|
|
19
19
|
"☕ This dev was replaced by his agents — buy him a coffee": "☕ Este dev foi substituído pelos agentes dele — pague um café a ele",
|
|
20
20
|
"🎞 Export video": "🎞 Exportar vídeo",
|
|
21
|
+
"🎬 Clip to share (25 s)": "🎬 Clipe para compartilhar (25 s)",
|
|
21
22
|
"📋 Recap": "📋 Resumo",
|
|
22
23
|
"Agent voices": "Vozes dos agentes",
|
|
23
24
|
"Agents": "Agentes",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"Files touched": "Arquivos modificados",
|
|
65
66
|
"Final report": "Relatório final",
|
|
66
67
|
"first error": "primeiro erro",
|
|
68
|
+
"Hide code & file names in videos": "Ocultar código e arquivos nos vídeos",
|
|
67
69
|
"Highlights": "Destaques",
|
|
68
70
|
"Human": "Humano",
|
|
69
71
|
"idle": "ocioso",
|
|
@@ -165,14 +167,12 @@
|
|
|
165
167
|
"Writing ": "Escrevendo ",
|
|
166
168
|
"You": "Você",
|
|
167
169
|
"Your agents are done 🎉": "Seus agentes terminaram 🎉",
|
|
170
|
+
"Your Claude Code agents as a live team call": "Seus agentes do Claude Code em uma chamada de equipe ao vivo",
|
|
168
171
|
"your turn": "sua vez",
|
|
169
|
-
"🔥 Meme mode (unhinged)": "🔥 Modo meme (sem noção)",
|
|
170
172
|
"Downloading, you can carry on: system voices are used meanwhile.": "Baixando, pode continuar: enquanto isso, as vozes do sistema são usadas.",
|
|
171
|
-
"Effects volume": "Volume dos efeitos",
|
|
172
173
|
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Vozes locais de boa qualidade, principalmente em inglês (só uma voz em francês). Grátis, nada sai do seu computador.",
|
|
173
174
|
"Installing…": "Instalando…",
|
|
174
|
-
"
|
|
175
|
-
"Loading the model (downloaded on first use, about 155 MB)…": "Carregando o modelo (baixado no primeiro uso, cerca de 155 MB)…",
|
|
175
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "Carregando o modelo (baixado no primeiro uso, cerca de 305 MB)…",
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "npm não encontrado: instale o Node.js para ativar esta opção.",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "O mecanismo parou: as vozes do sistema são usadas no lugar.",
|
|
178
178
|
"Very natural voices (installs on demand)": "Vozes muito naturais (instaladas sob demanda)",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"☕ Buy the (replaced) dev a coffee": "☕ 请(被取代的)开发者喝杯咖啡",
|
|
13
13
|
"☕ This dev was replaced by his agents — buy him a coffee": "☕ 这位开发者被他的代理取代了——请他喝杯咖啡吧",
|
|
14
14
|
"🎞 Export video": "🎞 导出视频",
|
|
15
|
+
"🎬 Clip to share (25 s)": "🎬 分享用短视频(25 秒)",
|
|
15
16
|
"📋 Recap": "📋 回顾",
|
|
16
17
|
"Agent voices": "代理语音",
|
|
17
18
|
"Agents": "代理",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"Files touched": "涉及的文件",
|
|
59
60
|
"Final report": "最终报告",
|
|
60
61
|
"first error": "首个错误",
|
|
62
|
+
"Hide code & file names in videos": "在视频中隐藏代码和文件名",
|
|
61
63
|
"Highlights": "精彩时刻",
|
|
62
64
|
"Human": "真人",
|
|
63
65
|
"idle": "空闲",
|
|
@@ -159,6 +161,7 @@
|
|
|
159
161
|
"Writing ": "正在写入 ",
|
|
160
162
|
"You": "你",
|
|
161
163
|
"Your agents are done 🎉": "你的代理都完成啦 🎉",
|
|
164
|
+
"Your Claude Code agents as a live team call": "把你的 Claude Code 代理变成实时团队通话",
|
|
162
165
|
"your turn": "轮到你了",
|
|
163
166
|
"Characters": "角色",
|
|
164
167
|
"Reload": "重新加载",
|
|
@@ -166,13 +169,10 @@
|
|
|
166
169
|
"custom character(s) loaded": "个自定义角色已加载",
|
|
167
170
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "把你的像素画(PNG)放进此文件夹即可替换角色:",
|
|
168
171
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = 主代理 · you.png = 你 · 其他 = 子代理 · name-talk.png = 张嘴",
|
|
169
|
-
"🔥 Meme mode (unhinged)": "🔥 梗图模式(彻底放飞)",
|
|
170
172
|
"Downloading, you can carry on: system voices are used meanwhile.": "正在下载,你可以继续使用:期间会使用系统语音。",
|
|
171
|
-
"Effects volume": "音效音量",
|
|
172
173
|
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "高质量的本地语音,以英语为主(法语只有一种声音)。免费,数据不会离开你的电脑。",
|
|
173
174
|
"Installing…": "正在安装…",
|
|
174
|
-
"
|
|
175
|
-
"Loading the model (downloaded on first use, about 155 MB)…": "正在加载模型(首次使用时下载,约 155 MB)…",
|
|
175
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "正在加载模型(首次使用时下载,约 305 MB)…",
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "未找到 npm:请安装 Node.js 以启用此选项。",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "引擎已停止:改用系统语音。",
|
|
178
178
|
"Very natural voices (installs on demand)": "超自然语音(按需安装)",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"☕ Buy the (replaced) dev a coffee": "☕ 請(被取代的)開發者喝杯咖啡",
|
|
13
13
|
"☕ This dev was replaced by his agents — buy him a coffee": "☕ 這位開發者被他的代理取代了 — 請他喝杯咖啡吧",
|
|
14
14
|
"🎞 Export video": "🎞 匯出影片",
|
|
15
|
+
"🎬 Clip to share (25 s)": "🎬 分享用短片(25 秒)",
|
|
15
16
|
"📋 Recap": "📋 摘要",
|
|
16
17
|
"Agent voices": "代理語音",
|
|
17
18
|
"Agents": "代理",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"Files touched": "異動的檔案",
|
|
65
66
|
"Final report": "最終報告",
|
|
66
67
|
"first error": "首次錯誤",
|
|
68
|
+
"Hide code & file names in videos": "在影片中隱藏程式碼與檔名",
|
|
67
69
|
"Highlights": "精華",
|
|
68
70
|
"Human": "真人",
|
|
69
71
|
"idle": "閒置",
|
|
@@ -165,14 +167,12 @@
|
|
|
165
167
|
"Writing ": "撰寫 ",
|
|
166
168
|
"You": "你",
|
|
167
169
|
"Your agents are done 🎉": "你的代理都完成了 🎉",
|
|
170
|
+
"Your Claude Code agents as a live team call": "把你的 Claude Code 代理變成即時團隊通話",
|
|
168
171
|
"your turn": "換你了",
|
|
169
|
-
"🔥 Meme mode (unhinged)": "🔥 迷因模式(完全失控)",
|
|
170
172
|
"Downloading, you can carry on: system voices are used meanwhile.": "下載中,你可以繼續使用:這段時間會先用系統語音。",
|
|
171
|
-
"Effects volume": "音效音量",
|
|
172
173
|
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "品質不錯的本機語音,以英文為主(法文只有一種聲音)。免費,資料不會離開你的電腦。",
|
|
173
174
|
"Installing…": "安裝中…",
|
|
174
|
-
"
|
|
175
|
-
"Loading the model (downloaded on first use, about 155 MB)…": "正在載入模型(第一次使用時下載,約 155 MB)…",
|
|
175
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "正在載入模型(第一次使用時下載,約 305 MB)…",
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "找不到 npm:請安裝 Node.js 才能啟用此選項。",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "引擎已停止:改用系統語音。",
|
|
178
178
|
"Very natural voices (installs on demand)": "超自然語音(需要時才安裝)",
|
package/server.js
CHANGED
|
@@ -46,6 +46,36 @@ const toolLabel = (b, ts) => {
|
|
|
46
46
|
return out;
|
|
47
47
|
};
|
|
48
48
|
const resultText = (c) => (typeof c === 'string' ? c : Array.isArray(c) ? c.map((x) => x.text || '').join('\n') : '');
|
|
49
|
+
// Pictures a tool returned (a screenshot, an image file that was read). They are served from the transcript itself, so nothing outside the session files is ever read, and only bitmap types are sent.
|
|
50
|
+
const IMG_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']), mediaMemo = new Map();
|
|
51
|
+
const isImg = (x) => !!x && x.type === 'image' && !!x.source && x.source.type === 'base64' && typeof x.source.data === 'string' && IMG_TYPES.has(x.source.media_type);
|
|
52
|
+
const resultImages = (c) => (Array.isArray(c) ? c.filter(isImg) : []);
|
|
53
|
+
function mediaFile(proj, sid, agent) {
|
|
54
|
+
if (agent === 'main') return path.join(ROOT, proj, sid + '.jsonl');
|
|
55
|
+
const dir = path.join(ROOT, proj, sid, 'subagents'); let hit = null;
|
|
56
|
+
try { hit = fs.readdirSync(dir).find((x) => x.endsWith('.jsonl') && x.replace(/^agent-/, '').replace(/\.jsonl$/, '').replace(/[^\w.-]/g, '_') === agent); } catch {}
|
|
57
|
+
return hit ? path.join(dir, hit) : null;
|
|
58
|
+
}
|
|
59
|
+
function toolImage(file, toolId, n) { // looks for the tool's result line in the transcript, 8 MB at a time; the few pictures asked last are kept in memory
|
|
60
|
+
const key = file + '|' + toolId + '|' + n; if (mediaMemo.has(key)) return mediaMemo.get(key);
|
|
61
|
+
const needle = Buffer.from(toolId), mark = Buffer.from('"image"'); let fd, found = null, carry = Buffer.alloc(0), pos = 0;
|
|
62
|
+
try {
|
|
63
|
+
fd = fs.openSync(file, 'r'); const size = fs.fstatSync(fd).size;
|
|
64
|
+
while (pos < size && !found) {
|
|
65
|
+
const buf = Buffer.alloc(Math.min(8 * 1024 * 1024, size - pos)); fs.readSync(fd, buf, 0, buf.length, pos); pos += buf.length;
|
|
66
|
+
const all = carry.length ? Buffer.concat([carry, buf]) : buf; let from = 0;
|
|
67
|
+
for (let hit; !found && (hit = all.indexOf(needle, from)) >= 0;) {
|
|
68
|
+
const a = all.lastIndexOf(10, hit) + 1, b = all.indexOf(10, hit); if (b < 0) break; // the line goes on in the next chunk
|
|
69
|
+
from = b + 1; const m = all.indexOf(mark, a); if (m < 0 || m > b) continue;
|
|
70
|
+
let r; try { r = JSON.parse(all.subarray(a, b).toString('utf8')); } catch { continue; }
|
|
71
|
+
for (const blk of blocks(r)) if (blk.type === 'tool_result' && blk.tool_use_id === toolId) { const im = resultImages(blk.content)[n]; if (im) found = { type: im.source.media_type, buf: Buffer.from(im.source.data, 'base64') }; }
|
|
72
|
+
}
|
|
73
|
+
carry = all.subarray(all.lastIndexOf(10) + 1);
|
|
74
|
+
}
|
|
75
|
+
} catch {} finally { if (fd != null) fs.closeSync(fd); }
|
|
76
|
+
if (found) { mediaMemo.set(key, found); if (mediaMemo.size > 12) mediaMemo.delete(mediaMemo.keys().next().value); }
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
49
79
|
const RESULT_MAX = { AskUserQuestion: 900, Skill: 300, ToolSearch: 500, Read: 1900, Bash: 1300, Grep: 900, Glob: 800, WebFetch: 800, WebSearch: 800, Edit: 200, MultiEdit: 200, Write: 200 };
|
|
50
80
|
const NO_RESULT = new Set(['Agent', 'Task', 'SendMessage', 'TodoWrite']); // internal bookkeeping, nothing to show
|
|
51
81
|
const HARNESS = /<(system-reminder|browser_instruction|ide_selection|ide_opened_file|task-notification|command-name|command-message|command-args|local-command-stdout|local-command-caveat)>[\s\S]*?<\/\1>\s*/g; // blocks the harness wraps around what the human typed
|
|
@@ -225,8 +255,8 @@ function parseRecord(st, r) {
|
|
|
225
255
|
const items = []; // tool results: the real output that appeared on the agent's screen
|
|
226
256
|
for (const b of blocks(r)) if (b.type === 'tool_result' && (st.toolName[b.tool_use_id] === 'Agent' || st.toolName[b.tool_use_id] === 'Task') && !(st.bg && st.bg[b.tool_use_id]) && !/^\s*Async agent launched/i.test(resultText(b.content))) st.raw.push({ ts, agent, kind: 'agentdone', toolId: b.tool_use_id }); // a synchronous call: the lead got the sub-agent's answer back, so it has finished (async launches answer immediately and are not a completion)
|
|
227
257
|
for (const b of blocks(r)) if (b.type === 'tool_result' && st.toolName[b.tool_use_id] && !NO_RESULT.has(st.toolName[b.tool_use_id])) {
|
|
228
|
-
const n = st.toolName[b.tool_use_id];
|
|
229
|
-
items.push({ id: b.tool_use_id, err: !!b.is_error, out: clip(resultText(b.content).replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trimEnd(), RESULT_MAX[n] || (/^mcp__/.test(n) ? 900 : 600)) });
|
|
258
|
+
const n = st.toolName[b.tool_use_id], img = resultImages(b.content).length;
|
|
259
|
+
items.push({ id: b.tool_use_id, err: !!b.is_error, out: clip(resultText(b.content).replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trimEnd(), RESULT_MAX[n] || (/^mcp__/.test(n) ? 900 : 600)), ...(img ? { img } : {}) }); // img: how many pictures came back; the page fetches them from /api/media
|
|
230
260
|
}
|
|
231
261
|
if (items.length) st.raw.push({ ts, agent, kind: 'results', items });
|
|
232
262
|
if (agent !== 'main') return; // a sub-agent's first user message is the order, emitted from the parent's side
|
|
@@ -461,7 +491,8 @@ function tts(text, voice, rate, engine) {
|
|
|
461
491
|
if (ttsJobs.has(key)) return ttsJobs.get(key);
|
|
462
492
|
fs.mkdirSync(ttsDir, { recursive: true });
|
|
463
493
|
const tmp = file + '.part.wav';
|
|
464
|
-
const job = (kk ? kokoro.synth(text, voice, kokoro.speedFor(rate), tmp)
|
|
494
|
+
const job = (kk ? kokoro.synth(text, voice, kokoro.speedFor(rate), tmp).catch((e) => { const vs = listVoices(), v = vs.voices.find((x) => x.locale.startsWith((voice.startsWith('ff_') ? 'fr' : 'en') + '_')); if (!v) throw e; return synthesize(v.name, rate, text, tmp); }) : synthesize(voice, rate, text, tmp)) // a line Kokoro cannot make goes to a system voice instead of being dropped
|
|
495
|
+
.then(() => { fs.renameSync(tmp, file); return file; }).finally(() => ttsJobs.delete(key));
|
|
465
496
|
ttsJobs.set(key, job); return job;
|
|
466
497
|
}
|
|
467
498
|
|
|
@@ -489,9 +520,9 @@ function start(port = PORT) {
|
|
|
489
520
|
let originOk = true; try { originOk = !req.headers.origin || local(new URL(req.headers.origin).host); } catch { originOk = false; }
|
|
490
521
|
if (!local(req.headers.host) || !originOk) { res.writeHead(403); return res.end('forbidden'); }
|
|
491
522
|
const json = (o, code = 200) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
|
|
492
|
-
// Endpoints with side effects (they spawn programs or claim the sound) refuse requests that a browser marks as coming from another site, e.g. an <img> on a web page.
|
|
523
|
+
// Endpoints with side effects (they spawn programs or claim the sound), and the one that serves pictures from a session, refuse requests that a browser marks as coming from another site, e.g. an <img> on a web page.
|
|
493
524
|
const sfs = req.headers['sec-fetch-site'];
|
|
494
|
-
if (sfs && sfs !== 'same-origin' && sfs !== 'none' && ['/api/translate', '/api/quips', '/api/warm', '/api/audio', '/api/tts', '/api/prefs', '/api/voices', '/api/kokoro', '/api/kokoro/install'].includes(url.pathname)) { res.writeHead(403); return res.end('forbidden'); }
|
|
525
|
+
if (sfs && sfs !== 'same-origin' && sfs !== 'none' && ['/api/translate', '/api/quips', '/api/warm', '/api/audio', '/api/tts', '/api/prefs', '/api/voices', '/api/kokoro', '/api/kokoro/install', '/api/media'].includes(url.pathname)) { res.writeHead(403); return res.end('forbidden'); }
|
|
495
526
|
const readBody = (cb) => { // request bodies are small JSON: stop reading beyond 256 KB and give up after 10 s
|
|
496
527
|
let body = '', over = false; req.setTimeout(10000, () => req.destroy());
|
|
497
528
|
req.on('data', (d) => { if (over) return; body += d; if (body.length > 262144) { over = true; res.writeHead(413); res.end('too large'); req.destroy(); } });
|
|
@@ -532,6 +563,12 @@ function start(port = PORT) {
|
|
|
532
563
|
if (url.pathname === '/api/current') return json(currentSession(url.searchParams.get('proj') || '', url.searchParams.get('id') || '', { proj: url.searchParams.get('sproj') || '', id: url.searchParams.get('sid') || '' }));
|
|
533
564
|
if (url.pathname === '/api/board') return json(boardRows());
|
|
534
565
|
if (url.pathname === '/api/characters') return json(listCharacters());
|
|
566
|
+
if (url.pathname === '/api/media') {
|
|
567
|
+
const q = (k) => url.searchParams.get(k) || '', SAFE = /^[\w.\-]+$/, n = +q('n') || 0, bad = [q('proj'), q('id'), q('agent'), q('tool')].some((v) => !SAFE.test(v) || v === '..' || v.length > 200);
|
|
568
|
+
const file = bad || !Number.isInteger(n) || n < 0 || n > 9 ? null : mediaFile(q('proj'), q('id'), q('agent')), im = file && toolImage(file, q('tool'), n);
|
|
569
|
+
if (!im) { res.writeHead(bad ? 400 : 404); return res.end(bad ? 'bad request' : 'not found'); }
|
|
570
|
+
res.writeHead(200, { 'content-type': im.type, 'content-length': im.buf.length, 'cache-control': 'private, max-age=3600', 'x-content-type-options': 'nosniff' }); return res.end(im.buf);
|
|
571
|
+
}
|
|
535
572
|
if (url.pathname === '/api/session') {
|
|
536
573
|
const proj = url.searchParams.get('proj') || '', sid = url.searchParams.get('id') || '', SAFE = /^[\w.\-]+$/;
|
|
537
574
|
if (!SAFE.test(proj) || !SAFE.test(sid) || proj === '..' || sid === '..' || proj.length > 200 || sid.length > 200) { res.writeHead(400); return res.end('bad request'); }
|
package/public/meme.css
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/* Meme mode (public/meme.js). Everything hangs off body.meme or lives in #memeLayer, so this file is inert while the mode is off. */
|
|
2
|
-
#memeLayer { position:absolute; inset:0; z-index:12; pointer-events:none; overflow:hidden; display:none }
|
|
3
|
-
body.meme #memeLayer { display:block }
|
|
4
|
-
.mm { position:absolute; font-family:Impact,"Arial Black",Anton,"Helvetica Neue",sans-serif; will-change:transform,opacity }
|
|
5
|
-
|
|
6
|
-
/* Impact text slamming in, then drifting off */
|
|
7
|
-
.mm.slam { transform:translate(-50%,-50%); text-transform:uppercase; font-weight:900; letter-spacing:.5px; line-height:1; white-space:nowrap; max-width:92%; overflow:hidden; text-overflow:ellipsis; -webkit-text-stroke:2px #000; paint-order:stroke fill; text-shadow:4px 4px 0 #000, -2px -2px 0 #000, 0 0 18px rgba(0,0,0,.6); animation:mmSlam 1.4s cubic-bezier(.2,1.6,.4,1) forwards }
|
|
8
|
-
@keyframes mmSlam { 0% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(4); opacity:0; filter:blur(6px) } 18% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(1); opacity:1; filter:none } 80% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(1.08); opacity:1 } 100% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(1.6); opacity:0 } }
|
|
9
|
-
|
|
10
|
-
/* top text / bottom text */
|
|
11
|
-
.mm.tb { inset:0; display:flex; flex-direction:column; justify-content:space-between; align-items:center; padding:3% 4%; text-align:center; animation:mmFade 2.2s forwards }
|
|
12
|
-
.mm.tb div { font-size:clamp(28px,6vw,72px); line-height:1; color:#fff; text-transform:uppercase; -webkit-text-stroke:2px #000; paint-order:stroke fill; text-shadow:3px 3px 0 #000; animation:mmPunch .5s cubic-bezier(.2,1.6,.4,1) backwards }
|
|
13
|
-
.mm.tb .bot { animation-delay:.25s }
|
|
14
|
-
@keyframes mmPunch { from { transform:scale(3); opacity:0 } }
|
|
15
|
-
@keyframes mmFade { 0%, 80% { opacity:1 } 100% { opacity:0 } }
|
|
16
|
-
|
|
17
|
-
/* lens flare: a spinning rainbow disc, screen-blended */
|
|
18
|
-
.mm.flare { transform:translate(-50%,-50%); border-radius:50%; mix-blend-mode:screen; opacity:.9; background:conic-gradient(from 0deg,#ff0040,#ff8a00,#ffe600,#3bff6e,#52d8ff,#8a5cff,#ff0040); -webkit-mask:radial-gradient(circle,#000 0,rgba(0,0,0,.6) 25%,transparent 70%); mask:radial-gradient(circle,#000 0,rgba(0,0,0,.6) 25%,transparent 70%); animation:mmFlare 1.6s ease-out forwards }
|
|
19
|
-
@keyframes mmFlare { 0% { transform:translate(-50%,-50%) rotate(0) scale(.2); opacity:0 } 25% { opacity:.95 } 100% { transform:translate(-50%,-50%) rotate(540deg) scale(1.4); opacity:0 } }
|
|
20
|
-
|
|
21
|
-
/* hitmarker */
|
|
22
|
-
.mm.hit { transform:translate(-50%,-50%); color:#fff; font-size:42px; line-height:1; text-shadow:0 0 4px #000, 2px 2px 0 #000; animation:mmHit .7s ease-out forwards }
|
|
23
|
-
@keyframes mmHit { 0% { transform:translate(-50%,-50%) scale(2.2); opacity:1 } 40% { transform:translate(-50%,-50%) scale(1) } 100% { transform:translate(-50%,-50%) scale(.9); opacity:0 } }
|
|
24
|
-
|
|
25
|
-
/* emoji rain, colour flash, star wipe */
|
|
26
|
-
.mm.rain { top:-50px; line-height:1; filter:drop-shadow(0 3px 4px rgba(0,0,0,.5)); animation:mmRain 2.4s ease-in forwards }
|
|
27
|
-
@keyframes mmRain { to { transform:translateY(120vh) rotate(720deg) } }
|
|
28
|
-
.mm.flash { inset:0; animation:mmFlash .11s ease-out forwards }
|
|
29
|
-
@keyframes mmFlash { from { opacity:1 } to { opacity:0 } }
|
|
30
|
-
.mm.wipe { inset:-20%; background:#fff }
|
|
31
|
-
.mm.wipe.star { clip-path:polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%); background:conic-gradient(#ff0040,#ffe600,#3bff6e,#52d8ff,#ff5cf0,#ff0040); animation:mmStar 1.3s ease-in-out forwards }
|
|
32
|
-
@keyframes mmStar { 0% { transform:scale(0) rotate(0) } 45% { transform:scale(3) rotate(180deg) } 55% { transform:scale(3) rotate(200deg) } 100% { transform:scale(0) rotate(360deg) } }
|
|
33
|
-
|
|
34
|
-
/* deal with it, Illuminati, to be continued */
|
|
35
|
-
.mm.shades { transform:translate(-50%,0); line-height:1; filter:drop-shadow(0 4px 6px rgba(0,0,0,.6)); animation:mmShades .6s cubic-bezier(.3,1.5,.5,1) forwards }
|
|
36
|
-
@keyframes mmShades { from { transform:translate(-50%,-120px); opacity:0 } to { transform:translate(-50%,var(--fall,40px)); opacity:1 } }
|
|
37
|
-
.mm.tri { left:50%; top:45%; width:220px; height:190px; margin:-95px 0 0 -110px; animation:mmTri 2.6s ease-in-out forwards }
|
|
38
|
-
.mm.tri i { display:block; width:0; height:0; border-left:110px solid transparent; border-right:110px solid transparent; border-bottom:190px solid #3bff6e; filter:drop-shadow(0 0 24px #3bff6e) }
|
|
39
|
-
.mm.tri span { position:absolute; left:50%; top:110px; transform:translate(-50%,-50%); font-size:60px; line-height:1 }
|
|
40
|
-
@keyframes mmTri { 0% { transform:scale(0) rotate(-180deg); opacity:0 } 30% { transform:scale(1) rotate(0); opacity:1 } 85% { transform:scale(1.1) rotate(20deg); opacity:1 } 100% { transform:scale(3) rotate(40deg); opacity:0 } }
|
|
41
|
-
#center.mm-freeze #stage, #center.mm-freeze #present, #center.mm-freeze #roster { filter:sepia(1) contrast(1.2) saturate(.6) }
|
|
42
|
-
.mm.tbc { left:4%; bottom:9%; font-size:clamp(26px,4vw,48px); line-height:1; font-style:italic; color:#fff; -webkit-text-stroke:1px #000; paint-order:stroke fill; text-shadow:3px 3px 0 #000; animation:mmTbc .5s ease-out }
|
|
43
|
-
@keyframes mmTbc { from { transform:translateX(-120%) } }
|
|
44
|
-
|
|
45
|
-
/* the tiles: punch-in zoom with chromatic aberration, deep-fried pulses */
|
|
46
|
-
body.meme .tile.mm-zoom { animation:mmZoom .5s cubic-bezier(.2,1.5,.4,1); z-index:3 }
|
|
47
|
-
@keyframes mmZoom { 0% { transform:scale(1) } 30% { transform:scale(1.18) rotate(-1.5deg); filter:drop-shadow(-4px 0 0 rgba(255,0,0,.7)) drop-shadow(4px 0 0 rgba(0,180,255,.7)) } 100% { transform:none } }
|
|
48
|
-
body.meme .tile.mm-fry { animation:mmFry .4s steps(2) infinite }
|
|
49
|
-
@keyframes mmFry { 0%, 100% { filter:contrast(1.7) saturate(2.6) brightness(1.1) } 50% { filter:contrast(2.2) saturate(4) brightness(1.2) hue-rotate(15deg) } }
|
|
50
|
-
|
|
51
|
-
/* the whole call in costume: Impact subtitles, a spinning logo, a rainbow glow round the stage */
|
|
52
|
-
body.meme .bubble, body.meme #pcap { font-family:Impact,"Arial Black",Anton,sans-serif; text-transform:uppercase; letter-spacing:.5px; -webkit-text-stroke:1px #000; paint-order:stroke fill; text-shadow:2px 2px 0 #000; font-size:15px }
|
|
53
|
-
body.meme .tile.focus .bubble { font-size:20px }
|
|
54
|
-
body.meme .brand .logo { animation:mmSpin 2s linear infinite }
|
|
55
|
-
@keyframes mmSpin { to { transform:rotate(360deg) } }
|
|
56
|
-
body.meme #center::after { content:""; position:absolute; inset:0; pointer-events:none; z-index:11; animation:mmGlow 4s linear infinite }
|
|
57
|
-
@keyframes mmGlow { 0% { box-shadow:inset 0 0 50px rgba(255,0,64,.45) } 25% { box-shadow:inset 0 0 50px rgba(255,230,0,.45) } 50% { box-shadow:inset 0 0 50px rgba(59,255,110,.45) } 75% { box-shadow:inset 0 0 50px rgba(82,216,255,.45) } 100% { box-shadow:inset 0 0 50px rgba(255,0,64,.45) } }
|
|
58
|
-
|
|
59
|
-
/* The switch is an explicit ask for motion, so reduced motion only removes the full-screen flashes and wipes (strobing). */
|
|
60
|
-
@media (prefers-reduced-motion:reduce) { .mm.flash, .mm.wipe { display:none } }
|
package/public/meme.js
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
// Meme mode: an optional costume over the call.
|
|
2
|
-
// Airhorns, hitmarkers, lens flares, Impact captions, vine booms, sad trombones, star wipes, emoji rain. It never changes
|
|
3
|
-
// what is shown or said, only how loudly. Off by default (⚙ → 🔥 Meme mode), intensity 1–11.
|
|
4
|
-
//
|
|
5
|
-
// Loaded before the page's own script. It reads that script's globals (prefs, S, AUD) at call time only: classic scripts
|
|
6
|
-
// share one global lexical scope, and nothing here runs before the page has started playing. The pure parts (CORE) have no DOM
|
|
7
|
-
// and also run under `node --test` (test/meme.test.js).
|
|
8
|
-
(function (root) {
|
|
9
|
-
'use strict';
|
|
10
|
-
|
|
11
|
-
// ---- pure parts: word picking, intensity curve, flash guard, deterministic choice
|
|
12
|
-
const CORE = {
|
|
13
|
-
hash(s) { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; },
|
|
14
|
-
pick(list, seed) { return list[(seed >>> 0) % list.length]; },
|
|
15
|
-
// The loud words of a line, for the kinetic captions: words already in capitals first, then the longest; no duplicates.
|
|
16
|
-
words(text, max = 3) {
|
|
17
|
-
const seen = new Set(), out = [];
|
|
18
|
-
const ws = String(text || '').split(/[^\p{L}\p{N}'’]+/u).map((w) => w.replace(/^['’]+|['’]+$/g, '')).filter((w) => w.length >= 3 && /\p{L}/u.test(w));
|
|
19
|
-
const score = (w) => (w === w.toUpperCase() ? 100 : 0) + w.length;
|
|
20
|
-
for (const w of ws.sort((a, b) => score(b) - score(a))) { const k = w.toLowerCase(); if (seen.has(k) || out.length >= max) continue; seen.add(k); out.push(w.toUpperCase()); }
|
|
21
|
-
return out;
|
|
22
|
-
},
|
|
23
|
-
// Intensity 1–11 → how often the optional extras fire, how long each effect rests, how hard the screen shakes, how much rains.
|
|
24
|
-
scale(level) {
|
|
25
|
-
const n = Math.round(+level), l = Number.isFinite(n) ? Math.max(1, Math.min(11, n)) : 11, t = (l - 1) / 10;
|
|
26
|
-
return { level: l, chance: +(0.2 + 0.8 * t).toFixed(3), cooldown: Math.round(6000 - 5400 * t), shake: +(2 + 16 * t).toFixed(1), particles: Math.round(6 + 34 * t) };
|
|
27
|
-
},
|
|
28
|
-
// Full-screen flashes are capped whatever the intensity: at most `max` in any `win` ms.
|
|
29
|
-
FlashGuard: class {
|
|
30
|
-
constructor(max = 3, win = 1000, now = () => Date.now()) { this.max = max; this.win = win; this.now = now; this.times = []; }
|
|
31
|
-
ok() { const t = this.now(); this.times = this.times.filter((x) => t - x < this.win); if (this.times.length >= this.max) return false; this.times.push(t); return true; }
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// ---- the mode itself: sounds (WebAudio synth + a few bundled clips) and visuals (#memeLayer over the stage)
|
|
36
|
-
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
37
|
-
const T = (fr, en) => (typeof prefs !== 'undefined' && prefs.lang === 'fr' ? fr : en);
|
|
38
|
-
const nameOf = (id) => (((typeof S !== 'undefined' && S.agents) || {})[id] || {}).name || id;
|
|
39
|
-
|
|
40
|
-
const MEME = {
|
|
41
|
-
CORE, on: false, bus: null, nb: null, clips: new Map(), guard: new CORE.FlashGuard(), last: {}, timer: 0, rm: false,
|
|
42
|
-
CLIPS: { omg: '/sfx/oh-my-god.mp3', scratch1: '/sfx/scratch-1.mp3', scratch2: '/sfx/scratch-2.mp3', scratch3: '/sfx/scratch-3.mp3' },
|
|
43
|
-
PAL: ['#fff', '#ffe600', '#ff3b3b', '#3bff6e', '#52d8ff', '#ff5cf0'],
|
|
44
|
-
WINS: () => ['GG', 'W', 'EZ', 'POGGERS', 'NO SCOPE', 'OH BABY', 'BASED', 'SHEEEESH', 'ABSOLUTE UNIT', 'LET HIM COOK'].concat(T(['BIEN OUEJ', 'C’EST PAS FAUX', 'OKLM', 'MAIS OUI C’EST CLAIR'], [])),
|
|
45
|
-
FAILS: () => ['BRUH', 'TASK FAILED SUCCESSFULLY', 'OH NO', 'EMOTIONAL DAMAGE', 'SKILL ISSUE', 'IT’S OVER', 'RIP', 'F IN THE CHAT', 'WHO DID THIS', 'WORKS ON MY MACHINE'].concat(T(['OH LE BOULET', 'C’EST LA CATA', 'MAIS NON'], [])),
|
|
46
|
-
STALLS: () => ['👁👄👁', T('tout va bien 🔥🐶', 'this is fine 🔥🐶'), 'hello?? 📞', T('⏳ ça vient ça vient', '⏳ any day now'), T('il est en train de CUISINER 🧑🍳', 'bro is COOKING 🧑🍳'), 'AFK?', T('*grillons*', '*crickets*')],
|
|
47
|
-
COMBOS: ['WOMBO COMBO', 'OH BABY A TRIPLE', 'TRIPLE KILL', 'HOLY MOLY', 'MULTI-TRACK DRIFTING'],
|
|
48
|
-
|
|
49
|
-
// ---------- lifecycle ----------
|
|
50
|
-
active() { return !!(this.on && typeof S !== 'undefined' && S.playing && !S.recomputing && !document.hidden); },
|
|
51
|
-
sound() { return !!(this.on && this.bus && typeof AUD !== 'undefined' && AUD.ctx && !AUD.off()); },
|
|
52
|
-
cfg() { return CORE.scale(typeof prefs !== 'undefined' ? prefs.memeLevel : 11); },
|
|
53
|
-
roll(k = 1) { return Math.random() < this.cfg().chance * k; },
|
|
54
|
-
rest(key, mul = 1) { const now = performance.now(); if (now < (this.last[key] || 0)) return false; this.last[key] = now + this.cfg().cooldown * mul; return true; },
|
|
55
|
-
apply() { // called when the switch, the mute state or the audio ownership changes, and once at start-up
|
|
56
|
-
this.on = !!(typeof prefs !== 'undefined' && prefs.meme);
|
|
57
|
-
document.body.classList.toggle('meme', this.on);
|
|
58
|
-
this.rm = !!(root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches); // the switch asks for motion; reduced motion only turns off flashes and wipes
|
|
59
|
-
this.ensureBus(); if (this.bus) this.bus.gain.value = this.sound() ? (prefs.memeVol ?? 0.8) : 0;
|
|
60
|
-
if (this.on && !this.timer) this.timer = setInterval(() => this.tick(), 4000);
|
|
61
|
-
if (!this.on) { clearInterval(this.timer); this.timer = 0; const l = document.getElementById('memeLayer'); if (l) l.innerHTML = ''; document.querySelectorAll('.mm-fry, .mm-zoom').forEach((t) => t.classList.remove('mm-fry', 'mm-zoom')); }
|
|
62
|
-
},
|
|
63
|
-
ensureBus() { if (this.bus || typeof AUD === 'undefined' || !AUD.ctx) return; const c = AUD.ctx; this.bus = c.createGain(); this.bus.connect(c.destination); if (AUD.rec) this.bus.connect(AUD.rec); }, // into the export mix too
|
|
64
|
-
fire(kind, info = {}) { if (!this.active()) return; try { this.ensureBus(); const h = this.ON[kind]; if (h) h.call(this, info); } catch {} },
|
|
65
|
-
tick() { // stalled agents (the tiles the page already marks .slow / .quiet) get crickets
|
|
66
|
-
if (!this.active()) return; const stuck = [...document.querySelectorAll('#stage .tile.quiet, #stage .tile.slow')].filter((t) => t.style.display !== 'none');
|
|
67
|
-
if (!stuck.length || !this.roll(0.5) || !this.rest('stall', 4)) return;
|
|
68
|
-
const t = stuck[Math.floor(Math.random() * stuck.length)], id = t.id.slice(2), r = this.rect(id); this.crickets();
|
|
69
|
-
this.slam(CORE.pick(this.STALLS(), CORE.hash(id + ((Date.now() / 10000) | 0))), { size: 30, left: r && r.cx + 'px', top: r && r.y + 'px', ms: 2500 });
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
// ---------- what each event does ----------
|
|
73
|
-
ON: {
|
|
74
|
-
talk(i) { if (i.id === 'user') return this.ON.user.call(this, i); if (!this.rest('talk', 0.5)) return; this.zoom(i.id); if (this.roll()) this.kinetic(i.text); if (this.cfg().level >= 9 && this.roll(0.6)) this.fry(i.id); },
|
|
75
|
-
interrupt() { this.scratched = performance.now(); this.clip('scratch' + (1 + Math.floor(Math.random() * 3)), () => this.scratch()); }, // the human cut a line being read: a real record scratch
|
|
76
|
-
user() { if (!this.rest('user', 2)) return; if (performance.now() - (this.scratched || 0) > 2500) this.scratch(); this.topBottom(T('L’HUMAIN EST', 'THE HUMAN HAS'), T('ENTRÉ DANS LE CHAT', 'ENTERED THE CHAT')); this.flash('rgba(255,255,255,.5)'); if (this.roll()) this.rain(['👀', '😳', '🫡'], Math.round(this.cfg().particles / 2)); },
|
|
77
|
-
order(i) {
|
|
78
|
-
if (!this.rest('order')) return; this.airhorn(1); const r = this.rect(i.to); if (r) this.flare(r.cx, r.cy, Math.max(200, r.w * 1.2));
|
|
79
|
-
this.slam(T(`${nameOf(i.to)} DÉPLOYÉ`, `DEPLOYING ${nameOf(i.to)}`), { size: 56, color: '#ffe600' }); if (this.roll(0.5)) this.flash('rgba(255,230,0,.35)');
|
|
80
|
-
},
|
|
81
|
-
report(i) {
|
|
82
|
-
if (!this.rest('report')) return; this.hitmarker(); this.sparkle(); const r = this.rect(i.from);
|
|
83
|
-
if (r) for (let k = 0; k < 3; k++) setTimeout(() => this.cross(r.x + Math.random() * r.w, r.y + Math.random() * r.h), k * 90);
|
|
84
|
-
const xp = 100 * (1 + Math.floor(Math.random() * 9)); this.slam(`+${xp} XP`, { size: 40, color: '#3bff6e', left: r && r.cx + 'px', top: r && r.y + 'px' });
|
|
85
|
-
if (this.roll(0.7)) setTimeout(() => this.active() && this.slam(CORE.pick(this.WINS(), CORE.hash(i.from + xp)), { size: 64 }), 250);
|
|
86
|
-
if (this.roll(0.45)) { this.airhorn(3); this.flash('rgba(255,255,255,.35)'); }
|
|
87
|
-
if (this.roll(0.5)) setTimeout(() => this.active() && this.slam('STONKS 📈', { size: 44, color: '#52d8ff', top: '8%' }), 600);
|
|
88
|
-
},
|
|
89
|
-
tool(i) {
|
|
90
|
-
if (!this.roll(0.08) || !this.rest('tool', 0.3)) return; this.hitmarker(); const r = this.rect(i.id); if (!r) return;
|
|
91
|
-
this.cross(r.cx + (Math.random() - 0.5) * r.w * 0.6, r.cy + (Math.random() - 0.5) * r.h * 0.6); this.slam('+10', { size: 22, left: r.cx + 'px', top: r.cy + 'px', ms: 800 });
|
|
92
|
-
},
|
|
93
|
-
fail(i) {
|
|
94
|
-
if (!this.rest('fail', 1.5)) return; this.shake(1.4); this.flash('rgba(255,0,0,.45)');
|
|
95
|
-
if (Math.random() < 0.4) this.clip('omg', () => this.boom()); else { this.boom(); setTimeout(() => this.active() && (Math.random() < 0.5 ? this.trombone() : this.bruh()), 450); }
|
|
96
|
-
this.slam(CORE.pick(this.FAILS(), CORE.hash(i.id + Date.now())), { size: 70, color: '#ff3b3b' });
|
|
97
|
-
if (this.roll()) this.rain(['💀', '🤡', '🔥', '😭'], Math.round(this.cfg().particles / 2)); const r = this.rect(i.id); if (r && this.roll(0.5)) this.slam('💀', { size: 60, left: r.cx + 'px', top: r.cy + 'px' });
|
|
98
|
-
},
|
|
99
|
-
leave(i) {
|
|
100
|
-
if (!this.rest('leave')) return;
|
|
101
|
-
if (Math.random() < 0.5) { this.yeet(); this.fling(i.id); const r = this.rect(i.id); this.slam('YEET', { size: 64, color: '#ffe600', left: r && r.cx + 'px', top: r && r.cy + 'px' }); } else { this.sting(); this.freeze(); }
|
|
102
|
-
},
|
|
103
|
-
win() {
|
|
104
|
-
if (!this.rest('win', 4)) return; this.airhorn(3); setTimeout(() => this.active() && this.badumtss(), 1300); setTimeout(() => this.active() && this.sparkle(), 1800);
|
|
105
|
-
this.rain(['🌮', '🥤', '🔥', '💯', '😂', '🏆']); const l = this.layer().getBoundingClientRect(); for (let k = 0; k < 4; k++) setTimeout(() => this.flare(Math.random() * l.width, Math.random() * l.height, 220 + Math.random() * 220), k * 250);
|
|
106
|
-
this.triangle(); this.slam(T('MAMAN VIENS VOIR', 'MOM GET THE CAMERA'), { size: 64, color: '#ffe600', top: '20%' }); setTimeout(() => this.shades('main'), 900); this.flash('rgba(255,255,255,.5)'); if (this.roll(0.6)) this.spin();
|
|
107
|
-
},
|
|
108
|
-
card() { if (!this.rest('card', 2)) return; this.sting(); this.wipe('star'); },
|
|
109
|
-
combo(i) {
|
|
110
|
-
if (!this.rest('combo', 3)) return; this.wub(); setTimeout(() => this.active() && this.ohh(), 300); this.shake(0.8);
|
|
111
|
-
this.slam(CORE.pick(this.COMBOS, CORE.hash(String(Date.now()))), { size: 80, color: '#ff5cf0', top: '30%' });
|
|
112
|
-
(i.ids || []).forEach((id, k) => { const r = this.rect(id); if (r) setTimeout(() => this.flare(r.cx, r.cy, r.w * 1.3), k * 150); }); if (this.roll(0.6)) this.spin();
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
// ---------- sounds: synth patches on the meme bus, plus bundled clips ----------
|
|
117
|
-
node(type, f) { const o = AUD.ctx.createOscillator(); o.type = type; o.frequency.value = f; return o; },
|
|
118
|
-
env(t, a, h, r, v) { const g = AUD.ctx.createGain(); g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(v, t + a); g.gain.setValueAtTime(v, t + a + h); g.gain.exponentialRampToValueAtTime(0.0001, t + a + h + r); return g; },
|
|
119
|
-
dist(k = 40) { const ws = AUD.ctx.createWaveShaper(), n = 256, curve = new Float32Array(n); for (let i = 0; i < n; i++) { const x = (i / (n - 1)) * 2 - 1; curve[i] = ((3 + k) * x * 20 * (Math.PI / 180)) / (Math.PI + k * Math.abs(x)); } ws.curve = curve; return ws; },
|
|
120
|
-
noise() { const c = AUD.ctx; if (!this.nb) { this.nb = c.createBuffer(1, c.sampleRate * 2, c.sampleRate); const d = this.nb.getChannelData(0); for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; } const s = c.createBufferSource(); s.buffer = this.nb; s.loop = true; return s; },
|
|
121
|
-
filt(type, f, q = 1) { const b = AUD.ctx.createBiquadFilter(); b.type = type; b.frequency.value = f; b.Q.value = q; return b; },
|
|
122
|
-
airhorn(n = 1) { // three detuned saws and a sub square, clipped hard, pitch sliding up into the note; the last of three holds
|
|
123
|
-
if (!this.sound()) return; const c = AUD.ctx;
|
|
124
|
-
for (let k = 0; k < n; k++) {
|
|
125
|
-
const t = c.currentTime + k * 0.32, len = n > 1 && k === n - 1 ? 0.85 : 0.28, g = this.env(t, 0.015, len, 0.15, 0.35), d = this.dist(50), lp = this.filt('lowpass', 2600, 2);
|
|
126
|
-
[[466, 'sawtooth'], [468.5, 'sawtooth'], [463.5, 'sawtooth'], [233, 'square']].forEach(([f, ty]) => { const o = this.node(ty, f * 0.88); o.frequency.exponentialRampToValueAtTime(f, t + 0.06); o.connect(d); o.start(t); o.stop(t + len + 0.2); });
|
|
127
|
-
d.connect(lp).connect(g).connect(this.bus);
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
boom() { // vine boom: a sine dropping from 120 to 32 Hz with a thud of low noise
|
|
131
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, o = this.node('sine', 120), g = this.env(t, 0.005, 0.05, 1.3, 0.9); o.frequency.exponentialRampToValueAtTime(32, t + 0.5); o.connect(this.dist(8)).connect(g).connect(this.bus); o.start(t); o.stop(t + 1.5);
|
|
132
|
-
const n = this.noise(), ng = this.env(t, 0.003, 0.03, 0.25, 0.5); n.connect(this.filt('lowpass', 220)).connect(ng).connect(this.bus); n.start(t); n.stop(t + 0.4);
|
|
133
|
-
},
|
|
134
|
-
hitmarker() { // two clicks of bright noise and a tink
|
|
135
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime;
|
|
136
|
-
for (let k = 0; k < 2; k++) { const at = t + k * 0.045, n = this.noise(), g = this.env(at, 0.002, 0.01, 0.05, 0.35); n.connect(this.filt('highpass', 3800)).connect(g).connect(this.bus); n.start(at); n.stop(at + 0.08); }
|
|
137
|
-
const o = this.node('sine', 2650), g = this.env(t, 0.002, 0.02, 0.07, 0.18); o.connect(g).connect(this.bus); o.start(t); o.stop(t + 0.12);
|
|
138
|
-
},
|
|
139
|
-
trombone() { // sad trombone: four notes, each sliding down with a closing wah
|
|
140
|
-
if (!this.sound()) return; const c = AUD.ctx; let t = c.currentTime;
|
|
141
|
-
[[233, 0.38], [220, 0.38], [208, 0.38], [196, 1.0]].forEach(([f, d]) => {
|
|
142
|
-
const o = this.node('sawtooth', f), bp = this.filt('bandpass', 900, 2.5); bp.frequency.exponentialRampToValueAtTime(380, t + d); o.frequency.setValueAtTime(f, t); o.frequency.exponentialRampToValueAtTime(f * 0.94, t + d);
|
|
143
|
-
const g = this.env(t, 0.04, d - 0.12, 0.1, 0.3); o.connect(bp).connect(g).connect(this.bus); o.start(t); o.stop(t + d + 0.1); t += d + 0.05;
|
|
144
|
-
});
|
|
145
|
-
},
|
|
146
|
-
bruh() { // one low vowel: two saws through a formant sweep, pitch sagging
|
|
147
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, bp = this.filt('bandpass', 350, 3); bp.frequency.linearRampToValueAtTime(950, t + 0.12); bp.frequency.linearRampToValueAtTime(260, t + 0.5);
|
|
148
|
-
[115, 116.5].forEach((f) => { const o = this.node('sawtooth', f); o.frequency.linearRampToValueAtTime(f * 0.74, t + 0.55); o.connect(bp); o.start(t); o.stop(t + 0.65); });
|
|
149
|
-
bp.connect(this.env(t, 0.03, 0.35, 0.2, 0.45)).connect(this.bus);
|
|
150
|
-
},
|
|
151
|
-
sting() { // dramatic "dun dun dunnn": three minor stabs, the last one held
|
|
152
|
-
if (!this.sound()) return; const c = AUD.ctx, t0 = c.currentTime;
|
|
153
|
-
[[0, 0.32, [196, 233, 294]], [0.42, 0.32, [185, 220, 277]], [0.84, 1.6, [147, 175, 220]]].forEach(([at, d, fs]) => {
|
|
154
|
-
const t = t0 + at, lp = this.filt('lowpass', 1200), g = this.env(t, 0.02, d, 0.3, 0.22);
|
|
155
|
-
fs.forEach((f) => [this.node('sawtooth', f), this.node('square', f / 2)].forEach((o) => { o.connect(lp); o.start(t); o.stop(t + d + 0.4); })); lp.connect(g).connect(this.bus);
|
|
156
|
-
});
|
|
157
|
-
},
|
|
158
|
-
scratch() { // record scratch: resonant noise swept down then back up (stands in when the clip cannot be loaded)
|
|
159
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, n = this.noise(), bp = this.filt('bandpass', 2400, 6); bp.frequency.exponentialRampToValueAtTime(300, t + 0.18); bp.frequency.exponentialRampToValueAtTime(1800, t + 0.36);
|
|
160
|
-
n.connect(bp).connect(this.env(t, 0.01, 0.3, 0.08, 0.5)).connect(this.bus); n.start(t); n.stop(t + 0.45);
|
|
161
|
-
},
|
|
162
|
-
badumtss() { // kick, snare, crash
|
|
163
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime;
|
|
164
|
-
const kick = (at) => { const o = this.node('sine', 160), g = this.env(at, 0.003, 0.03, 0.22, 0.8); o.frequency.exponentialRampToValueAtTime(48, at + 0.18); o.connect(g).connect(this.bus); o.start(at); o.stop(at + 0.3); };
|
|
165
|
-
const snare = (at) => { const n = this.noise(), g = this.env(at, 0.002, 0.03, 0.16, 0.5); n.connect(this.filt('highpass', 1100)).connect(g).connect(this.bus); n.start(at); n.stop(at + 0.25); const o = this.node('triangle', 190), og = this.env(at, 0.002, 0.02, 0.1, 0.4); o.connect(og).connect(this.bus); o.start(at); o.stop(at + 0.15); };
|
|
166
|
-
const crash = (at) => { const n = this.noise(), g = this.env(at, 0.003, 0.05, 1.4, 0.35); n.connect(this.filt('highpass', 5000)).connect(g).connect(this.bus); n.start(at); n.stop(at + 1.6); };
|
|
167
|
-
kick(t); snare(t + 0.17); crash(t + 0.36);
|
|
168
|
-
},
|
|
169
|
-
sparkle() { if (!this.sound()) return; const t = AUD.ctx.currentTime; [880, 1109, 1319, 1760, 2217].forEach((f, i) => { const at = t + i * 0.06, o = this.node('sine', f), g = this.env(at, 0.005, 0.04, 0.5, 0.14); o.connect(g).connect(this.bus); o.start(at); o.stop(at + 0.6); }); },
|
|
170
|
-
crickets() { // three chirps: a high sine chopped at 38 Hz
|
|
171
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime;
|
|
172
|
-
for (let k = 0; k < 3; k++) { const at = t + k * 0.5, o = this.node('sine', 4300), g = this.env(at, 0.01, 0.12, 0.05, 0.08), lfo = this.node('square', 38), lg = c.createGain(); lg.gain.value = 0.08; lfo.connect(lg).connect(g.gain); o.connect(g).connect(this.bus); o.start(at); lfo.start(at); o.stop(at + 0.2); lfo.stop(at + 0.2); }
|
|
173
|
-
},
|
|
174
|
-
ohh() { // a crowd going "ohhh": seven detuned saws sagging through a closing formant
|
|
175
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, bp = this.filt('bandpass', 700, 1.2); bp.frequency.exponentialRampToValueAtTime(380, t + 1.1);
|
|
176
|
-
for (let k = 0; k < 7; k++) { const f = 200 + k * 23 + Math.random() * 10, o = this.node('sawtooth', f); o.frequency.linearRampToValueAtTime(f * 0.8, t + 1.1); o.connect(bp); o.start(t); o.stop(t + 1.3); }
|
|
177
|
-
bp.connect(this.env(t, 0.18, 0.4, 0.6, 0.25)).connect(this.bus);
|
|
178
|
-
},
|
|
179
|
-
wub() { // dubstep: a sub saw under a resonant low-pass that an accelerating LFO opens and closes
|
|
180
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, lp = this.filt('lowpass', 400, 12), lfo = this.node('sine', 4), lg = c.createGain(); lg.gain.value = 350; lfo.frequency.linearRampToValueAtTime(12, t + 1.2); lfo.connect(lg).connect(lp.frequency);
|
|
181
|
-
[this.node('sawtooth', 55), this.node('square', 55.5)].forEach((o) => { o.connect(lp); o.start(t); o.stop(t + 1.5); }); lp.connect(this.dist(15)).connect(this.env(t, 0.03, 1.1, 0.25, 0.4)).connect(this.bus); lfo.start(t); lfo.stop(t + 1.5);
|
|
182
|
-
},
|
|
183
|
-
yeet() { // a rising whoosh
|
|
184
|
-
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, n = this.noise(), bp = this.filt('bandpass', 400, 1); bp.frequency.exponentialRampToValueAtTime(6500, t + 0.35); n.connect(bp).connect(this.env(t, 0.02, 0.2, 0.15, 0.35)).connect(this.bus); n.start(t); n.stop(t + 0.45);
|
|
185
|
-
const o = this.node('sine', 300); o.frequency.exponentialRampToValueAtTime(1400, t + 0.35); o.connect(this.env(t, 0.02, 0.2, 0.1, 0.12)).connect(this.bus); o.start(t); o.stop(t + 0.4);
|
|
186
|
-
},
|
|
187
|
-
async clip(name, fallback) { // a bundled clip, decoded once, played on the meme bus; the synth stands in if it cannot be loaded
|
|
188
|
-
if (!this.sound()) return;
|
|
189
|
-
try {
|
|
190
|
-
if (!this.clips.has(name)) this.clips.set(name, fetch(this.CLIPS[name]).then((r) => { if (!r.ok) throw new Error(String(r.status)); return r.arrayBuffer(); }).then((ab) => AUD.ctx.decodeAudioData(ab)));
|
|
191
|
-
const buf = await this.clips.get(name); if (!this.sound()) return; const s = AUD.ctx.createBufferSource(); s.buffer = buf; s.connect(this.bus); s.start();
|
|
192
|
-
} catch { this.clips.delete(name); if (fallback) fallback(); }
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
// ---------- visuals: everything lands in #memeLayer, nothing there is clickable ----------
|
|
196
|
-
layer() { let l = document.getElementById('memeLayer'); if (!l) { l = document.createElement('div'); l.id = 'memeLayer'; (document.getElementById('center') || document.body).appendChild(l); } return l; },
|
|
197
|
-
el(cls, html, style) { const d = document.createElement('div'); d.className = 'mm ' + cls; d.innerHTML = html; for (const k in style || {}) if (style[k] != null && style[k] !== false) d.style.setProperty(k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), String(style[k])); this.layer().appendChild(d); return d; },
|
|
198
|
-
gone(d, ms) { setTimeout(() => d.remove(), ms); },
|
|
199
|
-
rect(id) { // a tile's box in layer coordinates, or null when it is not on screen
|
|
200
|
-
const t = document.getElementById('t-' + id); if (!t || t.style.display === 'none' || document.body.classList.contains('tree') || document.body.classList.contains('board')) return null;
|
|
201
|
-
const r = t.getBoundingClientRect(), l = this.layer().getBoundingClientRect(); if (!r.width) return null;
|
|
202
|
-
return { x: r.left - l.left, y: r.top - l.top, w: r.width, h: r.height, cx: r.left - l.left + r.width / 2, cy: r.top - l.top + r.height / 2 };
|
|
203
|
-
},
|
|
204
|
-
slam(text, o = {}) { const d = this.el('slam', esc(text), { left: o.left || 15 + Math.random() * 70 + '%', top: o.top || 15 + Math.random() * 60 + '%', fontSize: (o.size || 48) + 'px', color: o.color || '#fff', '--rot': (o.rot ?? Math.random() * 24 - 12) + 'deg' }); this.gone(d, o.ms || 1400); return d; },
|
|
205
|
-
kinetic(text) { const n = this.cfg().level >= 8 ? 3 : this.cfg().level >= 4 ? 2 : 1; CORE.words(text, n).forEach((w, i) => setTimeout(() => this.active() && this.slam(w, { size: 34 + Math.random() * 40, color: this.PAL[i % this.PAL.length] }), 350 + i * 420)); },
|
|
206
|
-
topBottom(top, bottom, ms = 2200) { this.gone(this.el('tb', `<div class="top">${esc(top)}</div><div class="bot">${esc(bottom || '')}</div>`), ms); },
|
|
207
|
-
flare(x, y, size = 260, ms = 1600) { this.gone(this.el('flare', '', { left: x + 'px', top: y + 'px', width: size + 'px', height: size + 'px', animationDuration: ms + 'ms' }), ms); },
|
|
208
|
-
cross(x, y) { this.gone(this.el('hit', '✕', { left: x + 'px', top: y + 'px' }), 700); },
|
|
209
|
-
rain(emojis, n) { n = n || this.cfg().particles; for (let i = 0; i < n; i++) this.gone(this.el('rain', emojis[i % emojis.length], { left: Math.random() * 100 + '%', animationDelay: Math.random() * 0.8 + 's', animationDuration: 1.6 + Math.random() * 1.6 + 's', fontSize: 18 + Math.random() * 26 + 'px' }), 4200); },
|
|
210
|
-
shake(mul = 1) {
|
|
211
|
-
const amp = this.cfg().shake * mul, c = document.getElementById('center'); if (!c || !c.animate) return;
|
|
212
|
-
const kf = []; for (let i = 0; i < 10; i++) kf.push({ transform: `translate(${(Math.random() * 2 - 1) * amp}px,${(Math.random() * 2 - 1) * amp}px) rotate(${((Math.random() * 2 - 1) * amp) / 6}deg)` }); kf.push({ transform: 'none' });
|
|
213
|
-
c.animate(kf, { duration: 450, easing: 'ease-out' });
|
|
214
|
-
},
|
|
215
|
-
flash(color = '#fff', ms = 110) { if (this.rm || !this.guard.ok()) return; this.gone(this.el('flash', '', { background: color, animationDuration: ms + 'ms' }), ms + 20); }, // never under reduced motion, and at most 3 a second
|
|
216
|
-
zoom(id) { const t = document.getElementById('t-' + id); if (!t) return; t.classList.remove('mm-zoom'); void t.offsetWidth; t.classList.add('mm-zoom'); setTimeout(() => t.classList.remove('mm-zoom'), 520); },
|
|
217
|
-
fry(id, ms = 1800) { const t = document.getElementById('t-' + id); if (!t) return; t.classList.add('mm-fry'); setTimeout(() => t.classList.remove('mm-fry'), ms); },
|
|
218
|
-
wipe(kind = 'star') { if (this.rm) return; this.gone(this.el('wipe ' + kind, ''), 1300); },
|
|
219
|
-
spin() { const s = document.getElementById('stage'); if (!s || !s.animate) return; s.animate([{ transform: 'perspective(900px) rotateY(0)' }, { transform: 'perspective(900px) rotateY(360deg)' }], { duration: 1100, easing: 'cubic-bezier(.6,0,.3,1)' }); },
|
|
220
|
-
shades(id, ms = 3500) { const r = this.rect(id); if (!r) return; this.gone(this.el('shades', '🕶️', { left: r.cx + 'px', top: r.y + 'px', fontSize: Math.max(30, r.w * 0.28) + 'px', '--fall': r.h * 0.3 + 'px' }), ms); },
|
|
221
|
-
triangle(ms = 2600) { this.gone(this.el('tri', '<i></i><span>👁️</span>'), ms); },
|
|
222
|
-
freeze(ms = 2600) { // "To be continued": sepia over the stage and the arrow banner
|
|
223
|
-
const c = document.getElementById('center'); if (!c) return; c.classList.add('mm-freeze'); const d = this.el('tbc', `<span>${esc(T('À suivre', 'To Be Continued'))}</span> ➡︎`);
|
|
224
|
-
setTimeout(() => { c.classList.remove('mm-freeze'); d.remove(); }, ms);
|
|
225
|
-
},
|
|
226
|
-
fling(id) { // the leaving tile is thrown off-screen, then let go
|
|
227
|
-
const t = document.getElementById('t-' + id); if (!t || !t.animate) return; const dir = Math.random() < 0.5 ? -1 : 1;
|
|
228
|
-
const a = t.animate([{ transform: 'none', opacity: 1 }, { transform: `translate(${dir * 120}vw,-40vh) rotate(${dir * 540}deg) scale(.4)`, opacity: 0.6 }], { duration: 900, easing: 'cubic-bezier(.2,.6,.5,1)', fill: 'forwards' });
|
|
229
|
-
setTimeout(() => a.cancel(), 1500);
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = CORE;
|
|
234
|
-
if (typeof root.document !== 'undefined') root.MEME = MEME;
|
|
235
|
-
})(typeof window !== 'undefined' ? window : globalThis);
|
package/public/sfx/oh-my-god.mp3
DELETED
|
Binary file
|
package/public/sfx/scratch-1.mp3
DELETED
|
Binary file
|
package/public/sfx/scratch-2.mp3
DELETED
|
Binary file
|
package/public/sfx/scratch-3.mp3
DELETED
|
Binary file
|