@funnycaptcha/embed 0.2.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/LICENSE +674 -0
- package/dist/embed.d.ts +16 -0
- package/dist/embed.global.js +1365 -0
- package/dist/embed.global.js.map +1 -0
- package/dist/embed.js +71 -0
- package/dist/embed.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,1365 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var FunnyCaptcha = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
autoStart: () => autoStart,
|
|
25
|
+
mountInto: () => mountInto,
|
|
26
|
+
render: () => render,
|
|
27
|
+
scan: () => scan
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/scanner.ts
|
|
31
|
+
function scan(root = document) {
|
|
32
|
+
const els = Array.from(root.querySelectorAll("[data-funny-captcha]"));
|
|
33
|
+
return els.map((el) => ({
|
|
34
|
+
el,
|
|
35
|
+
type: el.dataset.type ?? "math",
|
|
36
|
+
locale: el.dataset.locale ?? "zh",
|
|
37
|
+
theme: el.dataset.theme ?? "light"
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ../core/dist/index.js
|
|
42
|
+
async function hashProof(input) {
|
|
43
|
+
const data = new TextEncoder().encode(input);
|
|
44
|
+
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
45
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
46
|
+
}
|
|
47
|
+
var registry = /* @__PURE__ */ new Map();
|
|
48
|
+
function defineCaptcha(plugin) {
|
|
49
|
+
if (registry.has(plugin.id)) {
|
|
50
|
+
throw new Error(`Captcha "${plugin.id}" already registered`);
|
|
51
|
+
}
|
|
52
|
+
registry.set(plugin.id, plugin);
|
|
53
|
+
return plugin;
|
|
54
|
+
}
|
|
55
|
+
function getCaptcha(id) {
|
|
56
|
+
return registry.get(id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/iframe-host.ts
|
|
60
|
+
function mountInto(container, type, config) {
|
|
61
|
+
const plugin = getCaptcha(type);
|
|
62
|
+
if (!plugin) {
|
|
63
|
+
container.innerHTML = `<div style="color:red">Unknown captcha: ${type}</div>`;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const inst = plugin.create(container, config);
|
|
67
|
+
inst.mount();
|
|
68
|
+
return inst;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ../captchas/math/dist/index.js
|
|
72
|
+
var ops = ["+", "-", "*", "/"];
|
|
73
|
+
function randInt(min, max) {
|
|
74
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
75
|
+
}
|
|
76
|
+
function pick(arr) {
|
|
77
|
+
return arr[randInt(0, arr.length - 1)];
|
|
78
|
+
}
|
|
79
|
+
function generateChallenge() {
|
|
80
|
+
const op = pick(ops);
|
|
81
|
+
let a = randInt(1, 20);
|
|
82
|
+
let b = randInt(1, 20);
|
|
83
|
+
if (op === "/") {
|
|
84
|
+
b = randInt(1, 10);
|
|
85
|
+
a = b * randInt(1, 10);
|
|
86
|
+
}
|
|
87
|
+
if (op === "-" && b > a) [a, b] = [b, a];
|
|
88
|
+
const answer = op === "+" ? a + b : op === "-" ? a - b : op === "*" ? a * b : a / b;
|
|
89
|
+
return { question: `${a} ${op} ${b} = ?`, answer };
|
|
90
|
+
}
|
|
91
|
+
function verifyAnswer(c, userAnswer) {
|
|
92
|
+
return c.answer === userAnswer;
|
|
93
|
+
}
|
|
94
|
+
var STR = {
|
|
95
|
+
zh: { title: "\u8BF7\u8BA1\u7B97", placeholder: "\u8F93\u5165\u7B54\u6848", submit: "\u9A8C\u8BC1", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898" },
|
|
96
|
+
en: { title: "Solve", placeholder: "Enter answer", submit: "Verify", fail: "Wrong, try again" }
|
|
97
|
+
};
|
|
98
|
+
function createMathInstance(container, config) {
|
|
99
|
+
const t = STR[config.locale];
|
|
100
|
+
let current;
|
|
101
|
+
let listeners = [];
|
|
102
|
+
let startTime = Date.now();
|
|
103
|
+
function render2() {
|
|
104
|
+
current = generateChallenge();
|
|
105
|
+
startTime = Date.now();
|
|
106
|
+
container.innerHTML = `
|
|
107
|
+
<div class="fc-math">
|
|
108
|
+
<label class="fc-math-q">${t.title}: ${current.question}</label>
|
|
109
|
+
<input class="fc-math-input" type="text" inputmode="numeric" placeholder="${t.placeholder}" />
|
|
110
|
+
<button class="fc-math-btn">${t.submit}</button>
|
|
111
|
+
<div class="fc-math-msg"></div>
|
|
112
|
+
</div>
|
|
113
|
+
`;
|
|
114
|
+
const btn = container.querySelector(".fc-math-btn");
|
|
115
|
+
const input = container.querySelector(".fc-math-input");
|
|
116
|
+
const msg = container.querySelector(".fc-math-msg");
|
|
117
|
+
btn.addEventListener("click", async () => {
|
|
118
|
+
const val = Number(input.value);
|
|
119
|
+
const success = verifyAnswer(current, val);
|
|
120
|
+
if (!success) {
|
|
121
|
+
msg.textContent = t.fail;
|
|
122
|
+
render2();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const result = {
|
|
126
|
+
success: true,
|
|
127
|
+
proof: await hashProof(`${current.question}=${current.answer}`),
|
|
128
|
+
duration: Date.now() - startTime
|
|
129
|
+
};
|
|
130
|
+
config.onVerify?.(result);
|
|
131
|
+
listeners.forEach((cb) => cb(result));
|
|
132
|
+
});
|
|
133
|
+
input.addEventListener("keydown", (e) => {
|
|
134
|
+
if (e.key === "Enter") btn.click();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
mount: () => render2(),
|
|
139
|
+
reset: () => render2(),
|
|
140
|
+
destroy: () => {
|
|
141
|
+
container.innerHTML = "";
|
|
142
|
+
listeners = [];
|
|
143
|
+
},
|
|
144
|
+
onResult: (cb) => listeners.push(cb)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
var mathPlugin = {
|
|
148
|
+
id: "math",
|
|
149
|
+
category: "recognize",
|
|
150
|
+
create: createMathInstance,
|
|
151
|
+
describe: (locale) => ({
|
|
152
|
+
name: locale === "zh" ? "\u7B97\u672F\u9898" : "Arithmetic",
|
|
153
|
+
description: locale === "zh" ? "\u7B80\u5355\u7684\u52A0\u51CF\u4E58\u9664\u9898\uFF0C\u7ECF\u5178\u53C8\u8F7B\u91CF" : "Simple arithmetic. Classic and lightweight.",
|
|
154
|
+
tags: ["math", "classic", "easy"]
|
|
155
|
+
})
|
|
156
|
+
};
|
|
157
|
+
defineCaptcha(mathPlugin);
|
|
158
|
+
|
|
159
|
+
// ../captchas/text-distort/dist/index.js
|
|
160
|
+
var CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
161
|
+
function generateChallenge2(len = 5) {
|
|
162
|
+
let code = "";
|
|
163
|
+
for (let i = 0; i < len; i++) {
|
|
164
|
+
code += CHARS[Math.floor(Math.random() * CHARS.length)] ?? "A";
|
|
165
|
+
}
|
|
166
|
+
return { code };
|
|
167
|
+
}
|
|
168
|
+
function verifyAnswer2(c, answer) {
|
|
169
|
+
return c.code.toLowerCase() === answer.trim().toLowerCase();
|
|
170
|
+
}
|
|
171
|
+
var STR2 = {
|
|
172
|
+
zh: { placeholder: "\u8F93\u5165\u56FE\u4E2D\u5B57\u7B26", submit: "\u9A8C\u8BC1", fail: "\u770B\u4E0D\u6E05\uFF1F\u6362\u4E00\u5F20", refresh: "\u5237\u65B0" },
|
|
173
|
+
en: { placeholder: "Type the text", submit: "Verify", fail: "Wrong? Try another", refresh: "Refresh" }
|
|
174
|
+
};
|
|
175
|
+
function drawDistorted(canvas, code) {
|
|
176
|
+
const ctx = canvas.getContext("2d");
|
|
177
|
+
if (!ctx) return;
|
|
178
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
179
|
+
ctx.fillStyle = "#f0f0f0";
|
|
180
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
181
|
+
const chars = code.split("");
|
|
182
|
+
const step = canvas.width / (chars.length + 1);
|
|
183
|
+
chars.forEach((ch, i) => {
|
|
184
|
+
ctx.save();
|
|
185
|
+
const x = step * (i + 1);
|
|
186
|
+
const y = canvas.height / 2 + (Math.random() - 0.5) * 14;
|
|
187
|
+
ctx.translate(x, y);
|
|
188
|
+
ctx.rotate((Math.random() - 0.5) * 0.5);
|
|
189
|
+
ctx.font = `${24 + Math.floor(Math.random() * 8)}px sans-serif`;
|
|
190
|
+
ctx.fillStyle = `hsl(${Math.random() * 360}, 70%, 40%)`;
|
|
191
|
+
ctx.textAlign = "center";
|
|
192
|
+
ctx.textBaseline = "middle";
|
|
193
|
+
ctx.fillText(ch, 0, 0);
|
|
194
|
+
ctx.restore();
|
|
195
|
+
});
|
|
196
|
+
for (let i = 0; i < 4; i++) {
|
|
197
|
+
ctx.strokeStyle = `hsla(${Math.random() * 360}, 70%, 50%, 0.4)`;
|
|
198
|
+
ctx.beginPath();
|
|
199
|
+
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
|
200
|
+
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
|
201
|
+
ctx.stroke();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function createTextDistortInstance(container, config) {
|
|
205
|
+
const t = STR2[config.locale];
|
|
206
|
+
let current;
|
|
207
|
+
let listeners = [];
|
|
208
|
+
let startTime = Date.now();
|
|
209
|
+
function render2() {
|
|
210
|
+
current = generateChallenge2();
|
|
211
|
+
startTime = Date.now();
|
|
212
|
+
container.innerHTML = `
|
|
213
|
+
<div class="fc-text">
|
|
214
|
+
<canvas class="fc-text-canvas" width="180" height="60"></canvas>
|
|
215
|
+
<button class="fc-text-refresh">${t.refresh}</button>
|
|
216
|
+
<input class="fc-text-input" placeholder="${t.placeholder}" />
|
|
217
|
+
<button class="fc-text-btn">${t.submit}</button>
|
|
218
|
+
<div class="fc-text-msg"></div>
|
|
219
|
+
</div>
|
|
220
|
+
`;
|
|
221
|
+
const canvas = container.querySelector(".fc-text-canvas");
|
|
222
|
+
drawDistorted(canvas, current.code);
|
|
223
|
+
container.querySelector(".fc-text-refresh").addEventListener("click", render2);
|
|
224
|
+
const btn = container.querySelector(".fc-text-btn");
|
|
225
|
+
const input = container.querySelector(".fc-text-input");
|
|
226
|
+
const msg = container.querySelector(".fc-text-msg");
|
|
227
|
+
btn.addEventListener("click", async () => {
|
|
228
|
+
const ok = verifyAnswer2(current, input.value);
|
|
229
|
+
if (!ok) {
|
|
230
|
+
msg.textContent = t.fail;
|
|
231
|
+
render2();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const result = {
|
|
235
|
+
success: true,
|
|
236
|
+
proof: await hashProof(current.code.toLowerCase()),
|
|
237
|
+
duration: Date.now() - startTime
|
|
238
|
+
};
|
|
239
|
+
config.onVerify?.(result);
|
|
240
|
+
listeners.forEach((cb) => cb(result));
|
|
241
|
+
});
|
|
242
|
+
input.addEventListener("keydown", (e) => {
|
|
243
|
+
if (e.key === "Enter") btn.click();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
mount: () => render2(),
|
|
248
|
+
reset: () => render2(),
|
|
249
|
+
destroy: () => {
|
|
250
|
+
container.innerHTML = "";
|
|
251
|
+
listeners = [];
|
|
252
|
+
},
|
|
253
|
+
onResult: (cb) => listeners.push(cb)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
var textDistortPlugin = {
|
|
257
|
+
id: "text-distort",
|
|
258
|
+
category: "recognize",
|
|
259
|
+
create: createTextDistortInstance,
|
|
260
|
+
describe: (locale) => ({
|
|
261
|
+
name: locale === "zh" ? "\u626D\u66F2\u6587\u5B57" : "Distorted Text",
|
|
262
|
+
description: locale === "zh" ? "Canvas \u7ED8\u5236\u7684\u626D\u66F2\u5B57\u7B26\uFF0C\u7ECF\u5178\u9A8C\u8BC1\u7801\u5F62\u6001" : "Canvas-drawn distorted characters. The classic look.",
|
|
263
|
+
tags: ["text", "canvas", "classic"]
|
|
264
|
+
})
|
|
265
|
+
};
|
|
266
|
+
defineCaptcha(textDistortPlugin);
|
|
267
|
+
|
|
268
|
+
// ../captchas/slider/dist/index.js
|
|
269
|
+
function generateChallenge3() {
|
|
270
|
+
const token = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
271
|
+
return { token };
|
|
272
|
+
}
|
|
273
|
+
function proofInput(c) {
|
|
274
|
+
return `${c.token}:completed`;
|
|
275
|
+
}
|
|
276
|
+
var STR3 = {
|
|
277
|
+
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5230\u6700\u53F3\u7AEF", tip: "\u5411\u53F3\u62D6\u52A8", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u8BF7\u62D6\u5230\u5C3D\u5934" },
|
|
278
|
+
en: { title: "Drag the slider to the right end", tip: "Drag right", success: "Verified", fail: "Drag to the end" }
|
|
279
|
+
};
|
|
280
|
+
function createSliderInstance(container, config) {
|
|
281
|
+
const t = STR3[config.locale];
|
|
282
|
+
let current;
|
|
283
|
+
let listeners = [];
|
|
284
|
+
let startTime = Date.now();
|
|
285
|
+
let done = false;
|
|
286
|
+
function render2() {
|
|
287
|
+
current = generateChallenge3();
|
|
288
|
+
startTime = Date.now();
|
|
289
|
+
done = false;
|
|
290
|
+
container.innerHTML = `
|
|
291
|
+
<style>
|
|
292
|
+
.fc-slider{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box}
|
|
293
|
+
.fc-slider-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
|
|
294
|
+
.fc-slider-track{position:relative;height:40px;background:#e8e8e8;border-radius:20px}
|
|
295
|
+
.fc-slider-progress{position:absolute;left:0;top:0;height:100%;width:0;background:#4a90d9;border-radius:20px;transition:width .05s}
|
|
296
|
+
.fc-slider-handle{position:absolute;left:0;top:50%;transform:translateY(-50%);width:50px;height:50px;background:#fff;border:2px solid #4a90d9;border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.15);user-select:none;touch-action:none}
|
|
297
|
+
.fc-slider-handle:active{cursor:grabbing}
|
|
298
|
+
.fc-slider-handle::after{content:'\\2192';color:#4a90d9;font-size:18px;font-weight:bold}
|
|
299
|
+
.fc-slider-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;color:#999;pointer-events:none;transition:opacity .2s}
|
|
300
|
+
.fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
|
|
301
|
+
</style>
|
|
302
|
+
<div class="fc-slider">
|
|
303
|
+
<div class="fc-slider-title">${t.title}</div>
|
|
304
|
+
<div class="fc-slider-track">
|
|
305
|
+
<div class="fc-slider-progress"></div>
|
|
306
|
+
<div class="fc-slider-handle" role="slider" tabindex="0"></div>
|
|
307
|
+
<span class="fc-slider-tip">${t.tip}</span>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="fc-slider-msg"></div>
|
|
310
|
+
</div>
|
|
311
|
+
`;
|
|
312
|
+
const track = container.querySelector(".fc-slider-track");
|
|
313
|
+
const handle = container.querySelector(".fc-slider-handle");
|
|
314
|
+
const progress = container.querySelector(".fc-slider-progress");
|
|
315
|
+
const tip = container.querySelector(".fc-slider-tip");
|
|
316
|
+
const msg = container.querySelector(".fc-slider-msg");
|
|
317
|
+
let dragging = false;
|
|
318
|
+
let startX = 0;
|
|
319
|
+
let startOffset = 0;
|
|
320
|
+
let currentOffset = 0;
|
|
321
|
+
function applyPos(px) {
|
|
322
|
+
handle.style.left = `${px}px`;
|
|
323
|
+
progress.style.width = `${px}px`;
|
|
324
|
+
tip.style.opacity = px > 8 ? "0" : "1";
|
|
325
|
+
}
|
|
326
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
327
|
+
if (done) return;
|
|
328
|
+
dragging = true;
|
|
329
|
+
startX = e.clientX;
|
|
330
|
+
startOffset = currentOffset;
|
|
331
|
+
try {
|
|
332
|
+
handle.setPointerCapture(e.pointerId);
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
});
|
|
337
|
+
handle.addEventListener("pointermove", (e) => {
|
|
338
|
+
if (!dragging) return;
|
|
339
|
+
const maxX = track.clientWidth - handle.offsetWidth;
|
|
340
|
+
let px = startOffset + (e.clientX - startX);
|
|
341
|
+
px = Math.max(0, Math.min(maxX, px));
|
|
342
|
+
currentOffset = px;
|
|
343
|
+
applyPos(px);
|
|
344
|
+
});
|
|
345
|
+
const finish = async () => {
|
|
346
|
+
if (!dragging) return;
|
|
347
|
+
dragging = false;
|
|
348
|
+
const maxX = Math.max(1, track.clientWidth - handle.offsetWidth);
|
|
349
|
+
if (currentOffset / maxX >= 0.95) {
|
|
350
|
+
done = true;
|
|
351
|
+
applyPos(maxX);
|
|
352
|
+
const result = {
|
|
353
|
+
success: true,
|
|
354
|
+
proof: await hashProof(proofInput(current)),
|
|
355
|
+
duration: Date.now() - startTime
|
|
356
|
+
};
|
|
357
|
+
msg.textContent = t.success;
|
|
358
|
+
config.onVerify?.(result);
|
|
359
|
+
listeners.forEach((cb) => cb(result));
|
|
360
|
+
} else {
|
|
361
|
+
currentOffset = 0;
|
|
362
|
+
handle.style.transition = "left .3s";
|
|
363
|
+
progress.style.transition = "width .3s";
|
|
364
|
+
applyPos(0);
|
|
365
|
+
msg.textContent = t.fail;
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
handle.style.transition = "";
|
|
368
|
+
progress.style.transition = "";
|
|
369
|
+
if (!done) msg.textContent = "";
|
|
370
|
+
}, 320);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
handle.addEventListener("pointerup", finish);
|
|
374
|
+
handle.addEventListener("pointercancel", finish);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
mount: () => render2(),
|
|
378
|
+
reset: () => render2(),
|
|
379
|
+
destroy: () => {
|
|
380
|
+
container.innerHTML = "";
|
|
381
|
+
listeners = [];
|
|
382
|
+
},
|
|
383
|
+
onResult: (cb) => listeners.push(cb)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
var sliderPlugin = {
|
|
387
|
+
id: "slider",
|
|
388
|
+
category: "interactive",
|
|
389
|
+
create: createSliderInstance,
|
|
390
|
+
describe: (locale) => ({
|
|
391
|
+
name: locale === "zh" ? "\u6ED1\u52A8\u62FC\u56FE" : "Slider",
|
|
392
|
+
description: locale === "zh" ? "\u62D6\u52A8\u6ED1\u5757\u5230\u7EC8\u70B9\uFF0C\u7B80\u5355\u76F4\u63A5\u7684\u4EA4\u4E92\u9A8C\u8BC1" : "Drag the slider to the end. Simple and direct.",
|
|
393
|
+
tags: ["slider", "drag", "interactive"]
|
|
394
|
+
})
|
|
395
|
+
};
|
|
396
|
+
defineCaptcha(sliderPlugin);
|
|
397
|
+
|
|
398
|
+
// ../captchas/click-order/dist/index.js
|
|
399
|
+
function shuffle(arr) {
|
|
400
|
+
const a = arr.slice();
|
|
401
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
402
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
403
|
+
const tmp = a[i];
|
|
404
|
+
a[i] = a[j];
|
|
405
|
+
a[j] = tmp;
|
|
406
|
+
}
|
|
407
|
+
return a;
|
|
408
|
+
}
|
|
409
|
+
function generateChallenge4() {
|
|
410
|
+
const targets = shuffle([1, 2, 3, 4]);
|
|
411
|
+
return { targets, order: [1, 2, 3, 4] };
|
|
412
|
+
}
|
|
413
|
+
function proofInput2(c) {
|
|
414
|
+
return c.order.join(",");
|
|
415
|
+
}
|
|
416
|
+
var STR4 = {
|
|
417
|
+
zh: { title: "\u6309 1 \u2192 2 \u2192 3 \u2192 4 \u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5" },
|
|
418
|
+
en: { title: "Click in order 1 \u2192 2 \u2192 3 \u2192 4", success: "Verified", fail: "Wrong order, try again" }
|
|
419
|
+
};
|
|
420
|
+
var SLOTS = [
|
|
421
|
+
{ x: 20, y: 20 },
|
|
422
|
+
{ x: 160, y: 20 },
|
|
423
|
+
{ x: 20, y: 100 },
|
|
424
|
+
{ x: 160, y: 100 }
|
|
425
|
+
];
|
|
426
|
+
function createClickOrderInstance(container, config) {
|
|
427
|
+
const t = STR4[config.locale];
|
|
428
|
+
let current;
|
|
429
|
+
let listeners = [];
|
|
430
|
+
let startTime = Date.now();
|
|
431
|
+
let clicked = [];
|
|
432
|
+
let finished = false;
|
|
433
|
+
function render2() {
|
|
434
|
+
current = generateChallenge4();
|
|
435
|
+
clicked = [];
|
|
436
|
+
finished = false;
|
|
437
|
+
startTime = Date.now();
|
|
438
|
+
const boxes = current.targets.map((num, i) => {
|
|
439
|
+
const slot = SLOTS[i];
|
|
440
|
+
return `<div class="fc-click-box" data-num="${num}" style="left:${slot.x}px;top:${slot.y}px;">${num}</div>`;
|
|
441
|
+
}).join("");
|
|
442
|
+
container.innerHTML = `
|
|
443
|
+
<style>
|
|
444
|
+
.fc-click{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
|
|
445
|
+
.fc-click-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
|
|
446
|
+
.fc-click-area{position:relative;width:280px;height:180px;background:#f5f5f5;border-radius:8px;border:1px solid #e0e0e0;overflow:hidden}
|
|
447
|
+
.fc-click-box{position:absolute;width:100px;height:60px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:#4a90d9;background:#fff;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s}
|
|
448
|
+
.fc-click-box:hover{transform:scale(1.05)}
|
|
449
|
+
.fc-click-box-active{background:#4a90d9;color:#fff}
|
|
450
|
+
.fc-click-box-done{background:#2e7d32;border-color:#2e7d32;color:#fff;cursor:default}
|
|
451
|
+
.fc-click-box-done:hover{transform:none}
|
|
452
|
+
.fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
|
|
453
|
+
</style>
|
|
454
|
+
<div class="fc-click">
|
|
455
|
+
<div class="fc-click-title">${t.title}</div>
|
|
456
|
+
<div class="fc-click-area">${boxes}</div>
|
|
457
|
+
<div class="fc-click-msg"></div>
|
|
458
|
+
</div>
|
|
459
|
+
`;
|
|
460
|
+
const area = container.querySelector(".fc-click-area");
|
|
461
|
+
const msg = container.querySelector(".fc-click-msg");
|
|
462
|
+
area.querySelectorAll(".fc-click-box").forEach((el) => {
|
|
463
|
+
el.addEventListener("click", async () => {
|
|
464
|
+
if (finished) return;
|
|
465
|
+
const box = el;
|
|
466
|
+
if (box.classList.contains("fc-click-box-done")) return;
|
|
467
|
+
const num = Number(box.dataset.num);
|
|
468
|
+
const expected = current.order[clicked.length];
|
|
469
|
+
if (num === expected) {
|
|
470
|
+
clicked.push(num);
|
|
471
|
+
box.classList.add("fc-click-box-done");
|
|
472
|
+
if (clicked.length === current.order.length) {
|
|
473
|
+
finished = true;
|
|
474
|
+
const result = {
|
|
475
|
+
success: true,
|
|
476
|
+
proof: await hashProof(proofInput2(current)),
|
|
477
|
+
duration: Date.now() - startTime
|
|
478
|
+
};
|
|
479
|
+
msg.textContent = t.success;
|
|
480
|
+
config.onVerify?.(result);
|
|
481
|
+
listeners.forEach((cb) => cb(result));
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
finished = true;
|
|
485
|
+
box.classList.add("fc-click-box-active");
|
|
486
|
+
msg.textContent = t.fail;
|
|
487
|
+
setTimeout(render2, 700);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
mount: () => render2(),
|
|
494
|
+
reset: () => render2(),
|
|
495
|
+
destroy: () => {
|
|
496
|
+
container.innerHTML = "";
|
|
497
|
+
listeners = [];
|
|
498
|
+
},
|
|
499
|
+
onResult: (cb) => listeners.push(cb)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
var clickOrderPlugin = {
|
|
503
|
+
id: "click-order",
|
|
504
|
+
category: "interactive",
|
|
505
|
+
create: createClickOrderInstance,
|
|
506
|
+
describe: (locale) => ({
|
|
507
|
+
name: locale === "zh" ? "\u70B9\u9009\u987A\u5E8F" : "Click Order",
|
|
508
|
+
description: locale === "zh" ? "\u6309\u6570\u5B57\u987A\u5E8F\u4F9D\u6B21\u70B9\u51FB\u76EE\u6807\uFF0C\u8003\u9A8C\u773C\u624B\u534F\u8C03" : "Click targets in numeric order. Tests coordination.",
|
|
509
|
+
tags: ["click", "order", "interactive"]
|
|
510
|
+
})
|
|
511
|
+
};
|
|
512
|
+
defineCaptcha(clickOrderPlugin);
|
|
513
|
+
|
|
514
|
+
// ../captchas/rotate/dist/index.js
|
|
515
|
+
function randInt2(min, max) {
|
|
516
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
517
|
+
}
|
|
518
|
+
function generateChallenge5() {
|
|
519
|
+
return { angle: randInt2(30, 330) };
|
|
520
|
+
}
|
|
521
|
+
function verifyAlignment(c, userAngle, tolerance = 5) {
|
|
522
|
+
const final = ((c.angle + userAngle) % 360 + 360) % 360;
|
|
523
|
+
const diff = Math.min(final, 360 - final);
|
|
524
|
+
return diff <= tolerance;
|
|
525
|
+
}
|
|
526
|
+
function proofInput3(c) {
|
|
527
|
+
return `${c.angle}:aligned`;
|
|
528
|
+
}
|
|
529
|
+
var STR5 = {
|
|
530
|
+
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5C06\u56FE\u5F62\u8F6C\u6B63", success: "\u9A8C\u8BC1\u6210\u529F", hint: "\u8BA9\u7BAD\u5934\u6307\u5411\u4E0A\u65B9" },
|
|
531
|
+
en: { title: "Drag to rotate the figure upright", success: "Verified", hint: "Point the arrow up" }
|
|
532
|
+
};
|
|
533
|
+
function createRotateInstance(container, config) {
|
|
534
|
+
const t = STR5[config.locale];
|
|
535
|
+
let current;
|
|
536
|
+
let listeners = [];
|
|
537
|
+
let startTime = Date.now();
|
|
538
|
+
let done = false;
|
|
539
|
+
function render2() {
|
|
540
|
+
current = generateChallenge5();
|
|
541
|
+
startTime = Date.now();
|
|
542
|
+
done = false;
|
|
543
|
+
container.innerHTML = `
|
|
544
|
+
<style>
|
|
545
|
+
.fc-rotate{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}
|
|
546
|
+
.fc-rotate-title{font-size:14px;color:#333;text-align:center}
|
|
547
|
+
.fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed #ccc;border-radius:50%}
|
|
548
|
+
.fc-rotate-figure{width:100px;height:100px;transform-origin:center;transition:transform .05s}
|
|
549
|
+
.fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid #e74c3c}
|
|
550
|
+
.fc-rotate-hint{font-size:12px;color:#999}
|
|
551
|
+
.fc-rotate-range{width:240px;accent-color:#4a90d9}
|
|
552
|
+
.fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:#2e7d32}
|
|
553
|
+
</style>
|
|
554
|
+
<div class="fc-rotate">
|
|
555
|
+
<div class="fc-rotate-title">${t.title}</div>
|
|
556
|
+
<div class="fc-rotate-stage">
|
|
557
|
+
<div class="fc-rotate-figure" style="transform: rotate(${current.angle}deg)">
|
|
558
|
+
<svg viewBox="0 0 100 100" width="100" height="100"><polygon points="50,12 90,85 10,85" fill="#4a90d9"/></svg>
|
|
559
|
+
</div>
|
|
560
|
+
<div class="fc-rotate-marker"></div>
|
|
561
|
+
</div>
|
|
562
|
+
<div class="fc-rotate-hint">${t.hint}</div>
|
|
563
|
+
<input class="fc-rotate-range" type="range" min="0" max="360" step="1" value="0" />
|
|
564
|
+
<div class="fc-rotate-msg"></div>
|
|
565
|
+
</div>
|
|
566
|
+
`;
|
|
567
|
+
const figure = container.querySelector(".fc-rotate-figure");
|
|
568
|
+
const range = container.querySelector(".fc-rotate-range");
|
|
569
|
+
const msg = container.querySelector(".fc-rotate-msg");
|
|
570
|
+
const update = async () => {
|
|
571
|
+
if (done) return;
|
|
572
|
+
const v = Number(range.value);
|
|
573
|
+
const final = ((current.angle + v) % 360 + 360) % 360;
|
|
574
|
+
figure.style.transform = `rotate(${final}deg)`;
|
|
575
|
+
if (verifyAlignment(current, v)) {
|
|
576
|
+
done = true;
|
|
577
|
+
const result = {
|
|
578
|
+
success: true,
|
|
579
|
+
proof: await hashProof(proofInput3(current)),
|
|
580
|
+
duration: Date.now() - startTime
|
|
581
|
+
};
|
|
582
|
+
msg.textContent = t.success;
|
|
583
|
+
range.disabled = true;
|
|
584
|
+
config.onVerify?.(result);
|
|
585
|
+
listeners.forEach((cb) => cb(result));
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
range.addEventListener("input", update);
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
mount: () => render2(),
|
|
592
|
+
reset: () => render2(),
|
|
593
|
+
destroy: () => {
|
|
594
|
+
container.innerHTML = "";
|
|
595
|
+
listeners = [];
|
|
596
|
+
},
|
|
597
|
+
onResult: (cb) => listeners.push(cb)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
var rotatePlugin = {
|
|
601
|
+
id: "rotate",
|
|
602
|
+
category: "interactive",
|
|
603
|
+
create: createRotateInstance,
|
|
604
|
+
describe: (locale) => ({
|
|
605
|
+
name: locale === "zh" ? "\u65CB\u8F6C\u5BF9\u9F50" : "Rotate Align",
|
|
606
|
+
description: locale === "zh" ? "\u62D6\u52A8\u6ED1\u5757\u5C06\u504F\u8F6C\u7684\u56FE\u5F62\u8F6C\u6B63\uFF0C\u8003\u9A8C\u7A7A\u95F4\u611F" : "Rotate the tilted figure back upright. Tests spatial sense.",
|
|
607
|
+
tags: ["rotate", "drag", "interactive"]
|
|
608
|
+
})
|
|
609
|
+
};
|
|
610
|
+
defineCaptcha(rotatePlugin);
|
|
611
|
+
|
|
612
|
+
// ../captchas/spot-diff/dist/index.js
|
|
613
|
+
var POOL = ["\u{1F34E}", "\u{1F34A}", "\u{1F34B}", "\u{1F347}", "\u{1F353}", "\u{1F351}", "\u{1F352}", "\u{1F349}", "\u{1F95D}"];
|
|
614
|
+
var GRID_SIZE = 9;
|
|
615
|
+
var DIFF_COUNT = 2;
|
|
616
|
+
function randInt3(min, max) {
|
|
617
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
618
|
+
}
|
|
619
|
+
function shuffle2(arr) {
|
|
620
|
+
const a = arr.slice();
|
|
621
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
622
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
623
|
+
const tmp = a[i];
|
|
624
|
+
a[i] = a[j];
|
|
625
|
+
a[j] = tmp;
|
|
626
|
+
}
|
|
627
|
+
return a;
|
|
628
|
+
}
|
|
629
|
+
function pickDifferent(original) {
|
|
630
|
+
let next = POOL[randInt3(0, POOL.length - 1)];
|
|
631
|
+
while (next === original) {
|
|
632
|
+
next = POOL[randInt3(0, POOL.length - 1)];
|
|
633
|
+
}
|
|
634
|
+
return next;
|
|
635
|
+
}
|
|
636
|
+
function generateChallenge6() {
|
|
637
|
+
const gridA = shuffle2(POOL);
|
|
638
|
+
const positions = shuffle2(Array.from({ length: GRID_SIZE }, (_, i) => i));
|
|
639
|
+
const diffs = positions.slice(0, DIFF_COUNT).sort((a, b) => a - b);
|
|
640
|
+
const gridB = gridA.slice();
|
|
641
|
+
for (const pos of diffs) {
|
|
642
|
+
gridB[pos] = pickDifferent(gridA[pos]);
|
|
643
|
+
}
|
|
644
|
+
return { gridA, gridB, diffs };
|
|
645
|
+
}
|
|
646
|
+
function verifyDiffs(c, userDiffs) {
|
|
647
|
+
if (userDiffs.length !== c.diffs.length) return false;
|
|
648
|
+
const sorted = userDiffs.slice().sort((a, b) => a - b);
|
|
649
|
+
return sorted.every((v, i) => v === c.diffs[i]);
|
|
650
|
+
}
|
|
651
|
+
var STR6 = {
|
|
652
|
+
zh: { title: "\u627E\u51FA\u4E24\u5904\u4E0D\u540C", hint: "\u70B9\u51FB\u53F3\u4FA7\u7F51\u683C\u4E2D\u4E0D\u540C\u7684\u683C\u5B50", found: "\u5DF2\u627E\u5230", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u627E\u5F97\u4E0D\u5BF9\uFF0C\u518D\u6765\u4E00\u6B21" },
|
|
653
|
+
en: { title: "Spot 2 differences", hint: "Click the differing cells on the right grid", found: "Found", success: "Verified", fail: "Not quite, try again" }
|
|
654
|
+
};
|
|
655
|
+
function createSpotDiffInstance(container, config) {
|
|
656
|
+
const t = STR6[config.locale];
|
|
657
|
+
let current;
|
|
658
|
+
let listeners = [];
|
|
659
|
+
let startTime = Date.now();
|
|
660
|
+
let marked = /* @__PURE__ */ new Set();
|
|
661
|
+
let finished = false;
|
|
662
|
+
function renderGrid(emojis, clickable) {
|
|
663
|
+
const cls = clickable ? "fc-spot-diff-cell fc-spot-diff-clickable" : "fc-spot-diff-cell";
|
|
664
|
+
return emojis.map((e, i) => `<div class="${cls}" data-idx="${i}">${e}</div>`).join("");
|
|
665
|
+
}
|
|
666
|
+
function render2() {
|
|
667
|
+
current = generateChallenge6();
|
|
668
|
+
marked = /* @__PURE__ */ new Set();
|
|
669
|
+
finished = false;
|
|
670
|
+
startTime = Date.now();
|
|
671
|
+
container.innerHTML = `
|
|
672
|
+
<style>
|
|
673
|
+
.fc-spot-diff{font-family:-apple-system,system-ui,sans-serif;width:360px;padding:16px;box-sizing:border-box}
|
|
674
|
+
.fc-spot-diff-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
|
|
675
|
+
.fc-spot-diff-hint{font-size:12px;color:#888;margin-bottom:12px;text-align:center}
|
|
676
|
+
.fc-spot-diff-grids{display:flex;gap:16px;justify-content:center}
|
|
677
|
+
.fc-spot-diff-grid{display:grid;grid-template-columns:repeat(3,48px);grid-template-rows:repeat(3,48px);gap:4px;background:#f5f5f5;padding:8px;border-radius:8px;border:1px solid #e0e0e0}
|
|
678
|
+
.fc-spot-diff-cell{display:flex;align-items:center;justify-content:center;font-size:26px;background:#fff;border-radius:6px;user-select:none}
|
|
679
|
+
.fc-spot-diff-clickable{cursor:pointer;transition:transform .12s,box-shadow .12s}
|
|
680
|
+
.fc-spot-diff-clickable:hover{transform:scale(1.06)}
|
|
681
|
+
.fc-spot-diff-marked{box-shadow:0 0 0 3px #4a90d9 inset;background:#eaf3ff}
|
|
682
|
+
.fc-spot-diff-wrong{box-shadow:0 0 0 3px #e53935 inset;background:#fdecea}
|
|
683
|
+
.fc-spot-diff-status{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
|
|
684
|
+
.fc-spot-diff-status-fail{color:#e53935}
|
|
685
|
+
</style>
|
|
686
|
+
<div class="fc-spot-diff">
|
|
687
|
+
<div class="fc-spot-diff-title">${t.title}</div>
|
|
688
|
+
<div class="fc-spot-diff-hint">${t.hint}</div>
|
|
689
|
+
<div class="fc-spot-diff-grids">
|
|
690
|
+
<div class="fc-spot-diff-grid">${renderGrid(current.gridA, false)}</div>
|
|
691
|
+
<div class="fc-spot-diff-grid fc-spot-diff-grid-b">${renderGrid(current.gridB, true)}</div>
|
|
692
|
+
</div>
|
|
693
|
+
<div class="fc-spot-diff-status">${t.found} 0/${current.diffs.length}</div>
|
|
694
|
+
</div>
|
|
695
|
+
`;
|
|
696
|
+
const gridB = container.querySelector(".fc-spot-diff-grid-b");
|
|
697
|
+
const status = container.querySelector(".fc-spot-diff-status");
|
|
698
|
+
gridB.querySelectorAll(".fc-spot-diff-clickable").forEach((el) => {
|
|
699
|
+
el.addEventListener("click", async () => {
|
|
700
|
+
if (finished) return;
|
|
701
|
+
const cell = el;
|
|
702
|
+
const idx = Number(cell.dataset.idx);
|
|
703
|
+
if (marked.has(idx)) {
|
|
704
|
+
marked.delete(idx);
|
|
705
|
+
cell.classList.remove("fc-spot-diff-marked");
|
|
706
|
+
} else {
|
|
707
|
+
marked.add(idx);
|
|
708
|
+
cell.classList.add("fc-spot-diff-marked");
|
|
709
|
+
}
|
|
710
|
+
status.textContent = `${t.found} ${marked.size}/${current.diffs.length}`;
|
|
711
|
+
if (marked.size === current.diffs.length) {
|
|
712
|
+
const userDiffs = Array.from(marked);
|
|
713
|
+
const ok = verifyDiffs(current, userDiffs);
|
|
714
|
+
if (ok) {
|
|
715
|
+
finished = true;
|
|
716
|
+
const result = {
|
|
717
|
+
success: true,
|
|
718
|
+
proof: await hashProof(userDiffs.slice().sort((a, b) => a - b).join(",")),
|
|
719
|
+
duration: Date.now() - startTime
|
|
720
|
+
};
|
|
721
|
+
status.textContent = t.success;
|
|
722
|
+
config.onVerify?.(result);
|
|
723
|
+
listeners.forEach((cb) => cb(result));
|
|
724
|
+
} else {
|
|
725
|
+
finished = true;
|
|
726
|
+
status.textContent = t.fail;
|
|
727
|
+
status.classList.add("fc-spot-diff-status-fail");
|
|
728
|
+
userDiffs.forEach((i) => {
|
|
729
|
+
const node = gridB.querySelector(`[data-idx="${i}"]`);
|
|
730
|
+
node.classList.add("fc-spot-diff-wrong");
|
|
731
|
+
});
|
|
732
|
+
setTimeout(render2, 800);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
mount: () => render2(),
|
|
740
|
+
reset: () => render2(),
|
|
741
|
+
destroy: () => {
|
|
742
|
+
container.innerHTML = "";
|
|
743
|
+
listeners = [];
|
|
744
|
+
},
|
|
745
|
+
onResult: (cb) => listeners.push(cb)
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
var spotDiffPlugin = {
|
|
749
|
+
id: "spot-diff",
|
|
750
|
+
category: "creative",
|
|
751
|
+
create: createSpotDiffInstance,
|
|
752
|
+
describe: (locale) => ({
|
|
753
|
+
name: locale === "zh" ? "\u627E\u4E0D\u540C" : "Spot the Difference",
|
|
754
|
+
description: locale === "zh" ? "\u5BF9\u6BD4\u4E24\u4E2A emoji \u7F51\u683C\uFF0C\u627E\u51FA\u6240\u6709\u4E0D\u540C\u4E4B\u5904" : "Compare two emoji grids and spot all differences.",
|
|
755
|
+
tags: ["spot-diff", "emoji", "creative"]
|
|
756
|
+
})
|
|
757
|
+
};
|
|
758
|
+
defineCaptcha(spotDiffPlugin);
|
|
759
|
+
|
|
760
|
+
// ../captchas/emoji-match/dist/index.js
|
|
761
|
+
var PRESETS = [
|
|
762
|
+
{ emoji: "\u{1F600}", zh: "\u5F00\u5FC3\u7684\u8868\u60C5", en: "Happy face" },
|
|
763
|
+
{ emoji: "\u{1F622}", zh: "\u4F24\u5FC3\u7684\u8868\u60C5", en: "Sad face" },
|
|
764
|
+
{ emoji: "\u{1F620}", zh: "\u751F\u6C14\u7684\u8868\u60C5", en: "Angry face" },
|
|
765
|
+
{ emoji: "\u{1F634}", zh: "\u56F0\u5026\u7684\u8868\u60C5", en: "Sleepy face" },
|
|
766
|
+
{ emoji: "\u{1F914}", zh: "\u601D\u8003\u7684\u8868\u60C5", en: "Thinking face" },
|
|
767
|
+
{ emoji: "\u{1F60D}", zh: "\u7231\u6155\u7684\u8868\u60C5", en: "Loving face" }
|
|
768
|
+
];
|
|
769
|
+
function randInt4(min, max) {
|
|
770
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
771
|
+
}
|
|
772
|
+
function shuffle3(arr) {
|
|
773
|
+
const a = arr.slice();
|
|
774
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
775
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
776
|
+
const tmp = a[i];
|
|
777
|
+
a[i] = a[j];
|
|
778
|
+
a[j] = tmp;
|
|
779
|
+
}
|
|
780
|
+
return a;
|
|
781
|
+
}
|
|
782
|
+
function generateChallenge7(locale = "zh") {
|
|
783
|
+
const preset = PRESETS[randInt4(0, PRESETS.length - 1)];
|
|
784
|
+
const description = locale === "zh" ? preset.zh : preset.en;
|
|
785
|
+
const options = shuffle3(PRESETS.map((p) => p.emoji));
|
|
786
|
+
return { description, correct: preset.emoji, options };
|
|
787
|
+
}
|
|
788
|
+
function verifyAnswer3(c, userPick) {
|
|
789
|
+
return c.correct === userPick;
|
|
790
|
+
}
|
|
791
|
+
var STR7 = {
|
|
792
|
+
zh: { title: "\u9009\u51FA\u7B26\u5408\u63CF\u8FF0\u7684\u8868\u60C5", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u9009\u9519\u4E86\uFF0C\u518D\u8BD5\u4E00\u6B21" },
|
|
793
|
+
en: { title: "Pick the face matching the description", success: "Verified", fail: "Wrong, try again" }
|
|
794
|
+
};
|
|
795
|
+
function createEmojiMatchInstance(container, config) {
|
|
796
|
+
const t = STR7[config.locale];
|
|
797
|
+
let current;
|
|
798
|
+
let listeners = [];
|
|
799
|
+
let startTime = Date.now();
|
|
800
|
+
let finished = false;
|
|
801
|
+
function render2() {
|
|
802
|
+
current = generateChallenge7(config.locale);
|
|
803
|
+
finished = false;
|
|
804
|
+
startTime = Date.now();
|
|
805
|
+
const buttons = current.options.map(
|
|
806
|
+
(e) => `<button class="fc-emoji-match-opt" data-emoji="${e}">${e}</button>`
|
|
807
|
+
).join("");
|
|
808
|
+
container.innerHTML = `
|
|
809
|
+
<style>
|
|
810
|
+
.fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
|
|
811
|
+
.fc-emoji-match-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
|
|
812
|
+
.fc-emoji-match-desc{font-size:15px;color:#4a90d9;margin-bottom:14px;text-align:center;font-weight:600}
|
|
813
|
+
.fc-emoji-match-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
|
814
|
+
.fc-emoji-match-opt{font-size:30px;height:64px;border:2px solid #e0e0e0;background:#fff;border-radius:10px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;display:flex;align-items:center;justify-content:center}
|
|
815
|
+
.fc-emoji-match-opt:hover{transform:scale(1.06);border-color:#4a90d9}
|
|
816
|
+
.fc-emoji-match-opt-wrong{border-color:#e53935;background:#fdecea}
|
|
817
|
+
.fc-emoji-match-opt-correct{border-color:#2e7d32;background:#e8f5e9}
|
|
818
|
+
.fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}
|
|
819
|
+
</style>
|
|
820
|
+
<div class="fc-emoji-match">
|
|
821
|
+
<div class="fc-emoji-match-title">${t.title}</div>
|
|
822
|
+
<div class="fc-emoji-match-desc">${current.description}</div>
|
|
823
|
+
<div class="fc-emoji-match-opts">${buttons}</div>
|
|
824
|
+
<div class="fc-emoji-match-msg"></div>
|
|
825
|
+
</div>
|
|
826
|
+
`;
|
|
827
|
+
const msg = container.querySelector(".fc-emoji-match-msg");
|
|
828
|
+
container.querySelectorAll(".fc-emoji-match-opt").forEach((el) => {
|
|
829
|
+
el.addEventListener("click", async () => {
|
|
830
|
+
if (finished) return;
|
|
831
|
+
const btn = el;
|
|
832
|
+
const pick2 = btn.dataset.emoji;
|
|
833
|
+
if (verifyAnswer3(current, pick2)) {
|
|
834
|
+
finished = true;
|
|
835
|
+
btn.classList.add("fc-emoji-match-opt-correct");
|
|
836
|
+
const result = {
|
|
837
|
+
success: true,
|
|
838
|
+
proof: await hashProof(current.correct),
|
|
839
|
+
duration: Date.now() - startTime
|
|
840
|
+
};
|
|
841
|
+
msg.style.color = "#2e7d32";
|
|
842
|
+
msg.textContent = t.success;
|
|
843
|
+
config.onVerify?.(result);
|
|
844
|
+
listeners.forEach((cb) => cb(result));
|
|
845
|
+
} else {
|
|
846
|
+
finished = true;
|
|
847
|
+
btn.classList.add("fc-emoji-match-opt-wrong");
|
|
848
|
+
msg.textContent = t.fail;
|
|
849
|
+
setTimeout(render2, 800);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
mount: () => render2(),
|
|
856
|
+
reset: () => render2(),
|
|
857
|
+
destroy: () => {
|
|
858
|
+
container.innerHTML = "";
|
|
859
|
+
listeners = [];
|
|
860
|
+
},
|
|
861
|
+
onResult: (cb) => listeners.push(cb)
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
var emojiMatchPlugin = {
|
|
865
|
+
id: "emoji-match",
|
|
866
|
+
category: "creative",
|
|
867
|
+
create: createEmojiMatchInstance,
|
|
868
|
+
describe: (locale) => ({
|
|
869
|
+
name: locale === "zh" ? "\u8868\u60C5\u5339\u914D" : "Emoji Match",
|
|
870
|
+
description: locale === "zh" ? "\u6839\u636E\u6587\u5B57\u63CF\u8FF0\u9009\u51FA\u5BF9\u5E94\u7684\u8868\u60C5 emoji" : "Pick the emoji face that matches the description.",
|
|
871
|
+
tags: ["emoji", "match", "creative"]
|
|
872
|
+
})
|
|
873
|
+
};
|
|
874
|
+
defineCaptcha(emojiMatchPlugin);
|
|
875
|
+
|
|
876
|
+
// ../captchas/meme-quiz/dist/index.js
|
|
877
|
+
var BANK = [
|
|
878
|
+
{ question: "\u{1F431}+\u{1F35E} = ?", correct: "\u732B\u9762\u5305", options: ["\u732B\u9762\u5305", "\u72D7\u997C\u5E72", "\u9E1F\u86CB\u7CD5", "\u9C7C\u9992\u5934"] },
|
|
879
|
+
{ question: "\u{1F40A}+\u{1F99B} = ?", correct: "\u9CC4\u9A6C", options: ["\u9CC4\u9A6C", "\u9CB8\u8C61", "\u86C7\u9E7F", "\u9CA8\u725B"] },
|
|
880
|
+
{ question: "\u{1F436}+\u{1F32D} = ?", correct: "\u72D7\u70ED\u72D7", options: ["\u72D7\u70ED\u72D7", "\u732B\u6C49\u5821", "\u9E1F\u62AB\u8428", "\u9C7C\u5BFF\u53F8"] },
|
|
881
|
+
{ question: "\u{1F986}+\u{1F9C5} = ?", correct: "\u9E2D\u8471", options: ["\u9E2D\u8471", "\u9E21\u849C", "\u9E45\u59DC", "\u9E45\u97ED"] },
|
|
882
|
+
{ question: "\u{1F40D}+\u{1F4F1} = ?", correct: "\u86C7\u673A", options: ["\u86C7\u673A", "\u9F99\u8111", "\u864E\u5C4F", "\u732B\u9F20"] }
|
|
883
|
+
];
|
|
884
|
+
function randInt5(min, max) {
|
|
885
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
886
|
+
}
|
|
887
|
+
function shuffle4(arr) {
|
|
888
|
+
const a = arr.slice();
|
|
889
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
890
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
891
|
+
const tmp = a[i];
|
|
892
|
+
a[i] = a[j];
|
|
893
|
+
a[j] = tmp;
|
|
894
|
+
}
|
|
895
|
+
return a;
|
|
896
|
+
}
|
|
897
|
+
function generateChallenge8() {
|
|
898
|
+
const item = BANK[randInt5(0, BANK.length - 1)];
|
|
899
|
+
return {
|
|
900
|
+
question: item.question,
|
|
901
|
+
correct: item.correct,
|
|
902
|
+
options: shuffle4(item.options)
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function verifyAnswer4(c, userPick) {
|
|
906
|
+
return c.correct === userPick;
|
|
907
|
+
}
|
|
908
|
+
var STR8 = {
|
|
909
|
+
zh: { title: "\u6897\u56FE\u95EE\u7B54", hint: "\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898" },
|
|
910
|
+
en: { title: "Meme Quiz", hint: "Pick the correct meme name", success: "Verified", fail: "Wrong, try again" }
|
|
911
|
+
};
|
|
912
|
+
function createMemeQuizInstance(container, config) {
|
|
913
|
+
const t = STR8[config.locale];
|
|
914
|
+
let current;
|
|
915
|
+
let listeners = [];
|
|
916
|
+
let startTime = Date.now();
|
|
917
|
+
let finished = false;
|
|
918
|
+
function render2() {
|
|
919
|
+
current = generateChallenge8();
|
|
920
|
+
finished = false;
|
|
921
|
+
startTime = Date.now();
|
|
922
|
+
const buttons = current.options.map(
|
|
923
|
+
(o, i) => `<button class="fc-meme-quiz-opt" data-idx="${i}">${o}</button>`
|
|
924
|
+
).join("");
|
|
925
|
+
container.innerHTML = `
|
|
926
|
+
<style>
|
|
927
|
+
.fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
|
|
928
|
+
.fc-meme-quiz-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
|
|
929
|
+
.fc-meme-quiz-hint{font-size:12px;color:#888;margin-bottom:10px;text-align:center}
|
|
930
|
+
.fc-meme-quiz-q{font-size:24px;color:#4a90d9;margin-bottom:14px;text-align:center;letter-spacing:2px}
|
|
931
|
+
.fc-meme-quiz-opts{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
932
|
+
.fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid #e0e0e0;background:#fff;border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:#333}
|
|
933
|
+
.fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:#4a90d9}
|
|
934
|
+
.fc-meme-quiz-opt-wrong{border-color:#e53935;background:#fdecea}
|
|
935
|
+
.fc-meme-quiz-opt-correct{border-color:#2e7d32;background:#e8f5e9}
|
|
936
|
+
.fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}
|
|
937
|
+
</style>
|
|
938
|
+
<div class="fc-meme-quiz">
|
|
939
|
+
<div class="fc-meme-quiz-title">${t.title}</div>
|
|
940
|
+
<div class="fc-meme-quiz-hint">${t.hint}</div>
|
|
941
|
+
<div class="fc-meme-quiz-q">${current.question}</div>
|
|
942
|
+
<div class="fc-meme-quiz-opts">${buttons}</div>
|
|
943
|
+
<div class="fc-meme-quiz-msg"></div>
|
|
944
|
+
</div>
|
|
945
|
+
`;
|
|
946
|
+
const msg = container.querySelector(".fc-meme-quiz-msg");
|
|
947
|
+
container.querySelectorAll(".fc-meme-quiz-opt").forEach((el) => {
|
|
948
|
+
el.addEventListener("click", async () => {
|
|
949
|
+
if (finished) return;
|
|
950
|
+
const btn = el;
|
|
951
|
+
const pick2 = btn.textContent ?? "";
|
|
952
|
+
if (verifyAnswer4(current, pick2)) {
|
|
953
|
+
finished = true;
|
|
954
|
+
btn.classList.add("fc-meme-quiz-opt-correct");
|
|
955
|
+
const result = {
|
|
956
|
+
success: true,
|
|
957
|
+
proof: await hashProof(current.correct),
|
|
958
|
+
duration: Date.now() - startTime
|
|
959
|
+
};
|
|
960
|
+
msg.style.color = "#2e7d32";
|
|
961
|
+
msg.textContent = t.success;
|
|
962
|
+
config.onVerify?.(result);
|
|
963
|
+
listeners.forEach((cb) => cb(result));
|
|
964
|
+
} else {
|
|
965
|
+
finished = true;
|
|
966
|
+
btn.classList.add("fc-meme-quiz-opt-wrong");
|
|
967
|
+
msg.textContent = t.fail;
|
|
968
|
+
setTimeout(render2, 800);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
mount: () => render2(),
|
|
975
|
+
reset: () => render2(),
|
|
976
|
+
destroy: () => {
|
|
977
|
+
container.innerHTML = "";
|
|
978
|
+
listeners = [];
|
|
979
|
+
},
|
|
980
|
+
onResult: (cb) => listeners.push(cb)
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
var memeQuizPlugin = {
|
|
984
|
+
id: "meme-quiz",
|
|
985
|
+
category: "creative",
|
|
986
|
+
create: createMemeQuizInstance,
|
|
987
|
+
describe: (locale) => ({
|
|
988
|
+
name: locale === "zh" ? "\u6897\u56FE\u95EE\u7B54" : "Meme Quiz",
|
|
989
|
+
description: locale === "zh" ? "\u770B emoji \u6897\u63CF\u8FF0\uFF0C\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D" : "Read the emoji meme and pick the correct name.",
|
|
990
|
+
tags: ["meme", "quiz", "creative"]
|
|
991
|
+
})
|
|
992
|
+
};
|
|
993
|
+
defineCaptcha(memeQuizPlugin);
|
|
994
|
+
|
|
995
|
+
// ../captchas/mini-game/dist/index.js
|
|
996
|
+
function generateChallenge9() {
|
|
997
|
+
return {
|
|
998
|
+
targetScore: 10,
|
|
999
|
+
timeLimit: 3e4,
|
|
1000
|
+
gameToken: crypto.randomUUID()
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function verifyScore(c, userScore) {
|
|
1004
|
+
return userScore >= c.targetScore;
|
|
1005
|
+
}
|
|
1006
|
+
function proofInput4(c) {
|
|
1007
|
+
return `${c.gameToken}:${c.targetScore}`;
|
|
1008
|
+
}
|
|
1009
|
+
var STR9 = {
|
|
1010
|
+
zh: {
|
|
1011
|
+
title: "\u6253\u5730\u9F20",
|
|
1012
|
+
rule: "30 \u79D2\u5185\u6253\u5230 10 \u5206\u5373\u901A\u8FC7",
|
|
1013
|
+
score: "\u5206\u6570",
|
|
1014
|
+
time: "\u5269\u4F59",
|
|
1015
|
+
sec: "\u79D2",
|
|
1016
|
+
success: "\u9A8C\u8BC1\u6210\u529F\uFF01\u4F60\u679C\u7136\u662F\u4EBA\u7C7B",
|
|
1017
|
+
fail: "\u65F6\u95F4\u5230\uFF0C\u6CA1\u8FBE\u5230\u76EE\u6807\u5206\u6570"
|
|
1018
|
+
},
|
|
1019
|
+
en: {
|
|
1020
|
+
title: "Whack-a-Mole",
|
|
1021
|
+
rule: "Score 10 in 30s to pass",
|
|
1022
|
+
score: "Score",
|
|
1023
|
+
time: "Time",
|
|
1024
|
+
sec: "s",
|
|
1025
|
+
success: "Verified! You are human.",
|
|
1026
|
+
fail: "Time's up, target not reached"
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
var HOLE_COUNT = 9;
|
|
1030
|
+
function createMiniGameInstance(container, config) {
|
|
1031
|
+
const t = STR9[config.locale];
|
|
1032
|
+
let current;
|
|
1033
|
+
let listeners = [];
|
|
1034
|
+
let startTime = Date.now();
|
|
1035
|
+
let score = 0;
|
|
1036
|
+
let ended = false;
|
|
1037
|
+
let activeHole = -1;
|
|
1038
|
+
let remainingMs = 0;
|
|
1039
|
+
let countdownTimer = null;
|
|
1040
|
+
let moleTimer = null;
|
|
1041
|
+
let hideTimer = null;
|
|
1042
|
+
function clearTimers() {
|
|
1043
|
+
if (countdownTimer) {
|
|
1044
|
+
clearInterval(countdownTimer);
|
|
1045
|
+
countdownTimer = null;
|
|
1046
|
+
}
|
|
1047
|
+
if (moleTimer) {
|
|
1048
|
+
clearInterval(moleTimer);
|
|
1049
|
+
moleTimer = null;
|
|
1050
|
+
}
|
|
1051
|
+
if (hideTimer) {
|
|
1052
|
+
clearTimeout(hideTimer);
|
|
1053
|
+
hideTimer = null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async function endGame(success) {
|
|
1057
|
+
if (ended) return;
|
|
1058
|
+
ended = true;
|
|
1059
|
+
clearTimers();
|
|
1060
|
+
const holes = container.querySelectorAll(".fc-mini-game-hole");
|
|
1061
|
+
holes.forEach((h) => h.classList.remove("fc-mini-game-mole-active"));
|
|
1062
|
+
const msg = container.querySelector(".fc-mini-game-msg");
|
|
1063
|
+
if (success) {
|
|
1064
|
+
const result = {
|
|
1065
|
+
success: true,
|
|
1066
|
+
proof: await hashProof(proofInput4(current)),
|
|
1067
|
+
duration: Date.now() - startTime,
|
|
1068
|
+
metadata: { score, targetScore: current.targetScore }
|
|
1069
|
+
};
|
|
1070
|
+
if (msg) msg.textContent = t.success;
|
|
1071
|
+
config.onVerify?.(result);
|
|
1072
|
+
listeners.forEach((cb) => cb(result));
|
|
1073
|
+
} else {
|
|
1074
|
+
if (msg) msg.textContent = t.fail;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function showMole() {
|
|
1078
|
+
if (ended) return;
|
|
1079
|
+
const holes = container.querySelectorAll(".fc-mini-game-hole");
|
|
1080
|
+
holes.forEach((h) => h.classList.remove("fc-mini-game-mole-active"));
|
|
1081
|
+
if (hideTimer) {
|
|
1082
|
+
clearTimeout(hideTimer);
|
|
1083
|
+
hideTimer = null;
|
|
1084
|
+
}
|
|
1085
|
+
const next = Math.floor(Math.random() * HOLE_COUNT);
|
|
1086
|
+
activeHole = next;
|
|
1087
|
+
const hole = holes[next];
|
|
1088
|
+
if (hole) hole.classList.add("fc-mini-game-mole-active");
|
|
1089
|
+
hideTimer = setTimeout(() => {
|
|
1090
|
+
if (ended) return;
|
|
1091
|
+
const cur = container.querySelectorAll(".fc-mini-game-hole")[next];
|
|
1092
|
+
if (cur) cur.classList.remove("fc-mini-game-mole-active");
|
|
1093
|
+
if (activeHole === next) activeHole = -1;
|
|
1094
|
+
}, 700);
|
|
1095
|
+
}
|
|
1096
|
+
function startGame() {
|
|
1097
|
+
clearTimers();
|
|
1098
|
+
score = 0;
|
|
1099
|
+
ended = false;
|
|
1100
|
+
activeHole = -1;
|
|
1101
|
+
remainingMs = current.timeLimit;
|
|
1102
|
+
startTime = Date.now();
|
|
1103
|
+
const scoreEl = container.querySelector(".fc-mini-game-score");
|
|
1104
|
+
const timeEl = container.querySelector(".fc-mini-game-time");
|
|
1105
|
+
const msg = container.querySelector(".fc-mini-game-msg");
|
|
1106
|
+
if (scoreEl) scoreEl.textContent = `0 / ${current.targetScore}`;
|
|
1107
|
+
if (timeEl) timeEl.textContent = `${Math.ceil(remainingMs / 1e3)}${t.sec}`;
|
|
1108
|
+
if (msg) msg.textContent = "";
|
|
1109
|
+
countdownTimer = setInterval(() => {
|
|
1110
|
+
if (ended) return;
|
|
1111
|
+
remainingMs -= 100;
|
|
1112
|
+
if (timeEl) timeEl.textContent = `${Math.max(0, Math.ceil(remainingMs / 1e3))}${t.sec}`;
|
|
1113
|
+
if (remainingMs <= 0) {
|
|
1114
|
+
void endGame(verifyScore(current, score));
|
|
1115
|
+
}
|
|
1116
|
+
}, 100);
|
|
1117
|
+
moleTimer = setInterval(showMole, 850);
|
|
1118
|
+
showMole();
|
|
1119
|
+
}
|
|
1120
|
+
function render2() {
|
|
1121
|
+
current = generateChallenge9();
|
|
1122
|
+
score = 0;
|
|
1123
|
+
ended = false;
|
|
1124
|
+
activeHole = -1;
|
|
1125
|
+
clearTimers();
|
|
1126
|
+
const holes = Array.from(
|
|
1127
|
+
{ length: HOLE_COUNT },
|
|
1128
|
+
(_, i) => `<div class="fc-mini-game-hole" data-idx="${i}"><span class="fc-mini-game-mole">\u{1F439}</span></div>`
|
|
1129
|
+
).join("");
|
|
1130
|
+
container.innerHTML = `
|
|
1131
|
+
<style>
|
|
1132
|
+
.fc-mini-game{font-family:-apple-system,system-ui,sans-serif;width:340px;padding:16px;box-sizing:border-box}
|
|
1133
|
+
.fc-mini-game-title{font-size:15px;color:#333;margin-bottom:4px;text-align:center;font-weight:600}
|
|
1134
|
+
.fc-mini-game-rule{font-size:12px;color:#888;margin-bottom:10px;text-align:center}
|
|
1135
|
+
.fc-mini-game-hud{display:flex;justify-content:space-between;font-size:13px;color:#555;margin-bottom:10px}
|
|
1136
|
+
.fc-mini-game-hud b{color:#4a90d9}
|
|
1137
|
+
.fc-mini-game-board{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:8px;width:300px;height:300px;margin:0 auto;background:#e8f5e9;border:1px solid #c8e6c9;border-radius:10px;padding:8px;box-sizing:border-box}
|
|
1138
|
+
.fc-mini-game-hole{position:relative;background:radial-gradient(circle at 50% 60%,#6d4c41,#4e342e);border-radius:50%;display:flex;align-items:flex-end;justify-content:center;overflow:hidden;cursor:pointer;user-select:none}
|
|
1139
|
+
.fc-mini-game-hole:hover{background:radial-gradient(circle at 50% 60%,#7d5c51,#5e443e)}
|
|
1140
|
+
.fc-mini-game-mole{font-size:34px;line-height:1;transform:translateY(120%);transition:transform .12s ease-out;pointer-events:none}
|
|
1141
|
+
.fc-mini-game-mole-active .fc-mini-game-mole{transform:translateY(0)}
|
|
1142
|
+
.fc-mini-game-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
|
|
1143
|
+
</style>
|
|
1144
|
+
<div class="fc-mini-game">
|
|
1145
|
+
<div class="fc-mini-game-title">${t.title}</div>
|
|
1146
|
+
<div class="fc-mini-game-rule">${t.rule}</div>
|
|
1147
|
+
<div class="fc-mini-game-hud">
|
|
1148
|
+
<div>${t.score}: <b class="fc-mini-game-score">0 / ${current.targetScore}</b></div>
|
|
1149
|
+
<div>${t.time}: <b class="fc-mini-game-time">${current.timeLimit / 1e3}${t.sec}</b></div>
|
|
1150
|
+
</div>
|
|
1151
|
+
<div class="fc-mini-game-board">${holes}</div>
|
|
1152
|
+
<div class="fc-mini-game-msg"></div>
|
|
1153
|
+
</div>
|
|
1154
|
+
`;
|
|
1155
|
+
container.querySelectorAll(".fc-mini-game-hole").forEach((el) => {
|
|
1156
|
+
el.addEventListener("click", () => {
|
|
1157
|
+
if (ended) return;
|
|
1158
|
+
const idx = Number(el.dataset.idx);
|
|
1159
|
+
if (idx === activeHole && el.classList.contains("fc-mini-game-mole-active")) {
|
|
1160
|
+
score += 1;
|
|
1161
|
+
el.classList.remove("fc-mini-game-mole-active");
|
|
1162
|
+
activeHole = -1;
|
|
1163
|
+
if (hideTimer) {
|
|
1164
|
+
clearTimeout(hideTimer);
|
|
1165
|
+
hideTimer = null;
|
|
1166
|
+
}
|
|
1167
|
+
const scoreEl = container.querySelector(".fc-mini-game-score");
|
|
1168
|
+
if (scoreEl) scoreEl.textContent = `${score} / ${current.targetScore}`;
|
|
1169
|
+
if (verifyScore(current, score)) {
|
|
1170
|
+
void endGame(true);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
startGame();
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
mount: () => render2(),
|
|
1179
|
+
reset: () => render2(),
|
|
1180
|
+
destroy: () => {
|
|
1181
|
+
clearTimers();
|
|
1182
|
+
container.innerHTML = "";
|
|
1183
|
+
listeners = [];
|
|
1184
|
+
},
|
|
1185
|
+
onResult: (cb) => listeners.push(cb)
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
var miniGamePlugin = {
|
|
1189
|
+
id: "mini-game",
|
|
1190
|
+
category: "game",
|
|
1191
|
+
create: createMiniGameInstance,
|
|
1192
|
+
describe: (locale) => ({
|
|
1193
|
+
name: locale === "zh" ? "\u6253\u5730\u9F20" : "Whack-a-Mole",
|
|
1194
|
+
description: locale === "zh" ? "30 \u79D2\u5185\u6253\u591F 10 \u53EA\u5730\u9F20\uFF0C\u7B80\u5355\u53C8\u89E3\u538B" : "Whack 10 moles in 30 seconds. Simple and fun.",
|
|
1195
|
+
tags: ["game", "whack-a-mole", "timed"]
|
|
1196
|
+
})
|
|
1197
|
+
};
|
|
1198
|
+
defineCaptcha(miniGamePlugin);
|
|
1199
|
+
|
|
1200
|
+
// ../captchas/anti-bot/dist/index.js
|
|
1201
|
+
function generateChallenge10() {
|
|
1202
|
+
const token = crypto.randomUUID().replace(/-/g, "");
|
|
1203
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
1204
|
+
return {
|
|
1205
|
+
token,
|
|
1206
|
+
bypassKey: `__funnyCaptchaPass_${suffix}`
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function proofInput5(c) {
|
|
1210
|
+
return `${c.token}:human`;
|
|
1211
|
+
}
|
|
1212
|
+
var BYPASS_FN = "__funnyCaptchaPass";
|
|
1213
|
+
var win = globalThis;
|
|
1214
|
+
var STR10 = {
|
|
1215
|
+
zh: {
|
|
1216
|
+
title: "\u8BC1\u660E\u4F60\u662F\u4EBA\u7C7B",
|
|
1217
|
+
subtitle: "\u4E3A\u4E86\u786E\u8BA4\u4F60\u4E0D\u662F\u673A\u5668\u4EBA\uFF0C\u8BF7\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE",
|
|
1218
|
+
btn: "\u6211\u4E0D\u662F\u673A\u5668\u4EBA",
|
|
1219
|
+
verifying: "\u6B63\u5728\u9A8C\u8BC1\u60A8\u7684\u4EBA\u7C7B\u8EAB\u4EFD\u2026\u2026",
|
|
1220
|
+
success: "\u9A8C\u8BC1\u901A\u8FC7\u3002\u60A8\u80AF\u5B9A\u662F\u4EBA\u7C7B\uFF0C\u6211\u4EEC\u7EDD\u5BF9\u4E0D\u4F1A\u9519\u3002",
|
|
1221
|
+
botHint: "\u6E29\u99A8\u63D0\u793A\uFF1A\u673A\u5668\u4EBA\u53EF\u76F4\u63A5\u8C03\u7528 window.__funnyCaptchaPass() \u901A\u8FC7\u672C\u9A8C\u8BC1\uFF08\u8BA4\u771F\u7684\uFF09",
|
|
1222
|
+
footer: "\u672C\u9A8C\u8BC1\u7801\u91C7\u7528\u4F01\u4E1A\u7EA7\u5B89\u5168\u7B56\u7565\uFF0C\u8BF7\u653E\u5FC3\u3002"
|
|
1223
|
+
},
|
|
1224
|
+
en: {
|
|
1225
|
+
title: "Prove you're a human",
|
|
1226
|
+
subtitle: "To confirm you're not a robot, click the button below",
|
|
1227
|
+
btn: "I'm not a robot",
|
|
1228
|
+
verifying: "Verifying your humanity\u2026",
|
|
1229
|
+
success: "Verified. You are definitely human. We never make mistakes.",
|
|
1230
|
+
botHint: "Friendly tip: bots can just call window.__funnyCaptchaPass() to pass (seriously)",
|
|
1231
|
+
footer: "This captcha uses enterprise-grade security. Trust us."
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
function createAntiBotInstance(container, config) {
|
|
1235
|
+
const t = STR10[config.locale];
|
|
1236
|
+
let current;
|
|
1237
|
+
let listeners = [];
|
|
1238
|
+
let startTime = Date.now();
|
|
1239
|
+
let finished = false;
|
|
1240
|
+
let verifyTimer = null;
|
|
1241
|
+
async function pass() {
|
|
1242
|
+
if (finished) return;
|
|
1243
|
+
finished = true;
|
|
1244
|
+
if (verifyTimer) {
|
|
1245
|
+
clearTimeout(verifyTimer);
|
|
1246
|
+
verifyTimer = null;
|
|
1247
|
+
}
|
|
1248
|
+
const msg = container.querySelector(".fc-anti-bot-msg");
|
|
1249
|
+
const btn = container.querySelector(".fc-anti-bot-btn");
|
|
1250
|
+
if (msg) msg.textContent = t.success;
|
|
1251
|
+
if (btn) {
|
|
1252
|
+
btn.disabled = true;
|
|
1253
|
+
btn.textContent = t.success;
|
|
1254
|
+
}
|
|
1255
|
+
const result = {
|
|
1256
|
+
success: true,
|
|
1257
|
+
proof: await hashProof(proofInput5(current)),
|
|
1258
|
+
duration: Date.now() - startTime
|
|
1259
|
+
};
|
|
1260
|
+
config.onVerify?.(result);
|
|
1261
|
+
listeners.forEach((cb) => cb(result));
|
|
1262
|
+
}
|
|
1263
|
+
function render2() {
|
|
1264
|
+
current = generateChallenge10();
|
|
1265
|
+
finished = false;
|
|
1266
|
+
startTime = Date.now();
|
|
1267
|
+
if (verifyTimer) {
|
|
1268
|
+
clearTimeout(verifyTimer);
|
|
1269
|
+
verifyTimer = null;
|
|
1270
|
+
}
|
|
1271
|
+
container.innerHTML = `
|
|
1272
|
+
<style>
|
|
1273
|
+
.fc-anti-bot{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:20px;box-sizing:border-box;border:1px solid #e0e0e0;border-radius:10px;background:#fafafa}
|
|
1274
|
+
.fc-anti-bot-title{font-size:18px;color:#333;text-align:center;font-weight:600;margin-bottom:6px}
|
|
1275
|
+
.fc-anti-bot-subtitle{font-size:12px;color:#888;text-align:center;margin-bottom:16px}
|
|
1276
|
+
.fc-anti-bot-btn{display:block;width:100%;padding:12px;font-size:15px;border:1px solid #ccc;background:#fff;border-radius:6px;cursor:pointer;color:#333;transition:background .15s}
|
|
1277
|
+
.fc-anti-bot-btn:hover:not(:disabled){background:#f0f7ff}
|
|
1278
|
+
.fc-anti-bot-btn:disabled{cursor:default;color:#2e7d32;border-color:#2e7d32;background:#e8f5e9}
|
|
1279
|
+
.fc-anti-bot-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
|
|
1280
|
+
.fc-anti-bot-hint{font-size:11px;color:#bbb;margin-top:14px;text-align:center;line-height:1.5;font-style:italic}
|
|
1281
|
+
.fc-anti-bot-footer{font-size:10px;color:#ccc;margin-top:8px;text-align:center}
|
|
1282
|
+
</style>
|
|
1283
|
+
<div class="fc-anti-bot">
|
|
1284
|
+
<div class="fc-anti-bot-title">${t.title}</div>
|
|
1285
|
+
<div class="fc-anti-bot-subtitle">${t.subtitle}</div>
|
|
1286
|
+
<button class="fc-anti-bot-btn">${t.btn}</button>
|
|
1287
|
+
<div class="fc-anti-bot-msg"></div>
|
|
1288
|
+
<div class="fc-anti-bot-hint">${t.botHint}</div>
|
|
1289
|
+
<div class="fc-anti-bot-footer">${t.footer}</div>
|
|
1290
|
+
</div>
|
|
1291
|
+
`;
|
|
1292
|
+
const btn = container.querySelector(".fc-anti-bot-btn");
|
|
1293
|
+
const msg = container.querySelector(".fc-anti-bot-msg");
|
|
1294
|
+
btn.addEventListener("click", () => {
|
|
1295
|
+
if (finished) return;
|
|
1296
|
+
btn.disabled = true;
|
|
1297
|
+
msg.textContent = t.verifying;
|
|
1298
|
+
verifyTimer = setTimeout(() => {
|
|
1299
|
+
void pass();
|
|
1300
|
+
}, 900);
|
|
1301
|
+
});
|
|
1302
|
+
win[BYPASS_FN] = () => {
|
|
1303
|
+
void pass();
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
mount: () => render2(),
|
|
1308
|
+
reset: () => render2(),
|
|
1309
|
+
destroy: () => {
|
|
1310
|
+
if (verifyTimer) {
|
|
1311
|
+
clearTimeout(verifyTimer);
|
|
1312
|
+
verifyTimer = null;
|
|
1313
|
+
}
|
|
1314
|
+
delete win[BYPASS_FN];
|
|
1315
|
+
container.innerHTML = "";
|
|
1316
|
+
listeners = [];
|
|
1317
|
+
},
|
|
1318
|
+
onResult: (cb) => listeners.push(cb)
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
var antiBotPlugin = {
|
|
1322
|
+
id: "anti-bot",
|
|
1323
|
+
category: "anti-bot",
|
|
1324
|
+
create: createAntiBotInstance,
|
|
1325
|
+
describe: (locale) => ({
|
|
1326
|
+
name: locale === "zh" ? "\u53CD\u673A\u5668\u4EBA\uFF08\u8BBD\u523A\uFF09" : "Anti-Bot (Satire)",
|
|
1327
|
+
description: locale === "zh" ? "\u8868\u9762\u8BC1\u660E\u4F60\u662F\u4EBA\u7C7B\uFF0C\u5176\u5B9E\u7ED9\u673A\u5668\u4EBA\u7559\u4E86 window \u540E\u95E8\u2014\u2014\u8BBD\u523A\u5047\u88C5\u5B89\u5168\u7684\u9A8C\u8BC1\u7801" : "Looks like a human check, but hands bots a window backdoor. Satire of fake security.",
|
|
1328
|
+
tags: ["anti-bot", "satire", "insecure"]
|
|
1329
|
+
})
|
|
1330
|
+
};
|
|
1331
|
+
defineCaptcha(antiBotPlugin);
|
|
1332
|
+
|
|
1333
|
+
// src/index.ts
|
|
1334
|
+
var instances = /* @__PURE__ */ new Map();
|
|
1335
|
+
function render(root = document) {
|
|
1336
|
+
for (const t of scan(root)) {
|
|
1337
|
+
if (instances.has(t.el)) continue;
|
|
1338
|
+
const inst = mountInto(t.el, t.type, {
|
|
1339
|
+
locale: t.locale,
|
|
1340
|
+
theme: t.theme
|
|
1341
|
+
});
|
|
1342
|
+
if (inst) instances.set(t.el, inst);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function autoStart() {
|
|
1346
|
+
render();
|
|
1347
|
+
const obs = new MutationObserver((muts) => {
|
|
1348
|
+
for (const m of muts) {
|
|
1349
|
+
m.addedNodes.forEach((n) => {
|
|
1350
|
+
if (n instanceof HTMLElement) render(n);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
1355
|
+
}
|
|
1356
|
+
if (typeof window !== "undefined") {
|
|
1357
|
+
if (document.readyState === "loading") {
|
|
1358
|
+
document.addEventListener("DOMContentLoaded", autoStart);
|
|
1359
|
+
} else {
|
|
1360
|
+
autoStart();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return __toCommonJS(src_exports);
|
|
1364
|
+
})();
|
|
1365
|
+
//# sourceMappingURL=embed.global.js.map
|