koishi-plugin-vsinger-gacha 1.2.0 → 1.2.1
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/lib/handbook.js +87 -45
- package/lib/index.js +16 -0
- package/package.json +1 -1
package/lib/handbook.js
CHANGED
|
@@ -334,69 +334,107 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
|
|
|
334
334
|
/**
|
|
335
335
|
* 抽卡结果图(不依赖合并转发,任何平台都能发)
|
|
336
336
|
* @param {object} opts
|
|
337
|
-
* @param {Array} opts.results [{h, card, tier, laser}]
|
|
338
|
-
* @param {string} opts.title
|
|
337
|
+
* @param {Array} opts.results [{hash|h, card, tier, laser}]
|
|
338
|
+
* @param {string} opts.title
|
|
339
339
|
*/
|
|
340
340
|
async function renderResult({ results, title, outDir, tagsDir, thumbsDir }) {
|
|
341
341
|
const Canvas = getCanvas();
|
|
342
342
|
const fs = require('node:fs');
|
|
343
343
|
const path = require('node:path');
|
|
344
344
|
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const
|
|
345
|
+
const TIER_NAME = { 40: '大隐藏', 30: '小隐藏', 20: '稀有', 10: '普通' };
|
|
346
|
+
const TIER_COLOR = { 40: '#ff6d84', 30: '#ffb44d', 20: '#8fc0ff', 10: '#c3cad8' };
|
|
347
|
+
const tierOf = (card) => {
|
|
348
|
+
const sc = Number((card && card.card_scarcity) || 10);
|
|
349
|
+
return sc === 40 ? 40 : sc === 30 ? 30 : sc === 20 ? 20 : 10;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const n = Math.max(1, results.length);
|
|
353
|
+
const PER_ROW = n <= 3 ? n : Math.min(5, n);
|
|
354
|
+
const rows = Math.ceil(n / PER_ROW);
|
|
355
|
+
const CW = n === 1 ? 300 : 170; // 卡宽(1 张时放大)
|
|
356
|
+
const CH = Math.round(CW * 1.43);
|
|
357
|
+
const LABEL = 58;
|
|
358
|
+
const GAP = 16;
|
|
359
|
+
const PAD = 32;
|
|
360
|
+
const HEAD = 108;
|
|
361
|
+
const W = Math.max(520, PAD * 2 + PER_ROW * CW + (PER_ROW - 1) * GAP);
|
|
362
|
+
const H = HEAD + rows * (CH + LABEL + GAP) + PAD;
|
|
355
363
|
|
|
356
364
|
const canvas = Canvas.createCanvas(W, H);
|
|
357
365
|
const g = canvas.getContext('2d');
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
366
|
+
g.textBaseline = 'top';
|
|
367
|
+
|
|
368
|
+
// 背景:深蓝渐变 + 顶部柔光
|
|
369
|
+
const bg = g.createLinearGradient(0, 0, W * 0.6, H);
|
|
370
|
+
bg.addColorStop(0, '#111c33');
|
|
371
|
+
bg.addColorStop(0.55, '#16263f');
|
|
372
|
+
bg.addColorStop(1, '#0d1524');
|
|
361
373
|
g.fillStyle = bg;
|
|
362
374
|
g.fillRect(0, 0, W, H);
|
|
375
|
+
const glow = g.createRadialGradient(W * 0.25, 0, 10, W * 0.25, 0, Math.max(W, H) * 0.75);
|
|
376
|
+
glow.addColorStop(0, 'rgba(120,180,255,0.28)');
|
|
377
|
+
glow.addColorStop(1, 'rgba(120,180,255,0)');
|
|
378
|
+
g.fillStyle = glow;
|
|
379
|
+
g.fillRect(0, 0, W, H);
|
|
363
380
|
|
|
381
|
+
// 标题
|
|
364
382
|
g.fillStyle = '#ffffff';
|
|
365
|
-
g.font = 'bold
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
383
|
+
g.font = 'bold 32px VSingerSans, sans-serif';
|
|
384
|
+
const t = String(title || '').replace(/\s+/g, ' ');
|
|
385
|
+
const maxTitle = W - PAD * 2;
|
|
386
|
+
let tt = t;
|
|
387
|
+
while (tt.length > 2 && g.measureText(tt).width > maxTitle) tt = tt.slice(0, -1);
|
|
388
|
+
if (tt !== t) tt = tt.slice(0, -1) + '…';
|
|
389
|
+
g.fillText(tt, PAD, 30);
|
|
390
|
+
g.fillStyle = 'rgba(200,215,240,0.65)';
|
|
391
|
+
g.font = '18px VSingerSans, sans-serif';
|
|
392
|
+
g.fillText(`共 ${n} 张 | 抽卡结果`, PAD, 72);
|
|
393
|
+
|
|
394
|
+
const rowW = PER_ROW * CW + (PER_ROW - 1) * GAP;
|
|
395
|
+
const x0 = Math.round((W - rowW) / 2);
|
|
371
396
|
|
|
372
397
|
for (let i = 0; i < results.length; i++) {
|
|
373
398
|
const r = results[i];
|
|
399
|
+
const hash = r.hash || r.h;
|
|
374
400
|
const col = i % PER_ROW;
|
|
375
401
|
const row = Math.floor(i / PER_ROW);
|
|
376
|
-
const x =
|
|
377
|
-
const y = HEAD + row * (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
402
|
+
const x = x0 + col * (CW + GAP);
|
|
403
|
+
const y = HEAD + row * (CH + LABEL + GAP);
|
|
404
|
+
const tier = tierOf(r.card);
|
|
405
|
+
|
|
406
|
+
// 卡片底
|
|
407
|
+
g.save();
|
|
408
|
+
g.beginPath();
|
|
409
|
+
g.roundRect(x, y, CW, CH, 14);
|
|
410
|
+
g.clip();
|
|
411
|
+
g.fillStyle = '#1b2436';
|
|
412
|
+
g.fillRect(x, y, CW, CH);
|
|
413
|
+
const thumb = hash ? path.join(thumbsDir, hash + '.jpg') : '';
|
|
414
|
+
if (thumb && fs.existsSync(thumb)) {
|
|
384
415
|
try {
|
|
385
416
|
const img = await Canvas.loadImage(thumb);
|
|
386
|
-
g.drawImage(img, x, y,
|
|
417
|
+
g.drawImage(img, x, y, CW, CH);
|
|
387
418
|
} catch { /* 忽略 */ }
|
|
388
419
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
420
|
+
g.restore();
|
|
421
|
+
|
|
422
|
+
// 边框(按档位上色)
|
|
423
|
+
g.save();
|
|
424
|
+
g.beginPath();
|
|
425
|
+
g.roundRect(x + 1, y + 1, CW - 2, CH - 2, 13);
|
|
426
|
+
g.strokeStyle = TIER_COLOR[tier];
|
|
427
|
+
g.lineWidth = 3;
|
|
428
|
+
g.stroke();
|
|
429
|
+
g.restore();
|
|
430
|
+
|
|
431
|
+
// 档位角标
|
|
394
432
|
const tag = tier === 40 ? path.join(tagsDir, 'tag_40.webp') : tier === 30 ? path.join(tagsDir, 'tag_30.webp') : null;
|
|
395
433
|
if (tag && fs.existsSync(tag)) {
|
|
396
434
|
try {
|
|
397
435
|
const ti = await Canvas.loadImage(tag);
|
|
398
|
-
const tw =
|
|
399
|
-
g.drawImage(ti, x +
|
|
436
|
+
const tw = CW * 0.46;
|
|
437
|
+
g.drawImage(ti, x + 4, y + 4, tw, (ti.height / ti.width) * tw);
|
|
400
438
|
} catch { /* 忽略 */ }
|
|
401
439
|
}
|
|
402
440
|
if (r.laser) {
|
|
@@ -404,25 +442,29 @@ async function renderResult({ results, title, outDir, tagsDir, thumbsDir }) {
|
|
|
404
442
|
if (fs.existsSync(holo)) {
|
|
405
443
|
try {
|
|
406
444
|
const hi = await Canvas.loadImage(holo);
|
|
407
|
-
const hw =
|
|
445
|
+
const hw = CW * 0.36;
|
|
408
446
|
const hh = (hi.height / hi.width) * hw;
|
|
409
|
-
g.drawImage(hi, x + (
|
|
447
|
+
g.drawImage(hi, x + (CW - hw) / 2, y + CH - hh - 6, hw, hh);
|
|
410
448
|
} catch { /* 忽略 */ }
|
|
411
449
|
}
|
|
412
450
|
}
|
|
413
451
|
|
|
452
|
+
// 名称 + 档位/概率
|
|
414
453
|
g.fillStyle = '#ffffff';
|
|
415
|
-
g.font = '
|
|
416
|
-
const name = String((r.card && r.card.card_name) || '')
|
|
417
|
-
|
|
454
|
+
g.font = 'bold 19px VSingerSans, sans-serif';
|
|
455
|
+
const name = String((r.card && r.card.card_name) || '');
|
|
456
|
+
let nn = name;
|
|
457
|
+
while (nn.length > 1 && g.measureText(nn).width > CW) nn = nn.slice(0, -1);
|
|
458
|
+
g.fillText(nn === name ? nn : nn.slice(0, -1) + '…', x, y + CH + 8);
|
|
418
459
|
g.fillStyle = TIER_COLOR[tier];
|
|
419
|
-
g.font = '
|
|
420
|
-
|
|
460
|
+
g.font = '15px VSingerSans, sans-serif';
|
|
461
|
+
const rate = r.card && r.card.rate_pct != null ? ` ${r.card.rate_pct}%` : '';
|
|
462
|
+
g.fillText(`${TIER_NAME[tier]}${rate}`, x, y + CH + 34);
|
|
421
463
|
}
|
|
422
464
|
|
|
423
465
|
fs.mkdirSync(outDir, { recursive: true });
|
|
424
466
|
const out = path.join(outDir, `draw_${Date.now()}.jpg`);
|
|
425
|
-
fs.writeFileSync(out, await canvas.encode('jpeg',
|
|
467
|
+
fs.writeFileSync(out, await canvas.encode('jpeg', 90));
|
|
426
468
|
return out;
|
|
427
469
|
}
|
|
428
470
|
|
package/lib/index.js
CHANGED
|
@@ -29,6 +29,8 @@ const Config = Schema.intersect([
|
|
|
29
29
|
.description('普卡出镭射版的概率'),
|
|
30
30
|
idMapFile: Schema.string().default('')
|
|
31
31
|
.description('可选:openid ↔ QQ 映射表(如 /root/qqbot/data/id-map.json),用于统一用户标识'),
|
|
32
|
+
ignoreAtOthers: Schema.boolean().default(true)
|
|
33
|
+
.description('消息里 @ 的是别的机器人/别人时,本插件不响应(避免同群两个机器人重复回复)'),
|
|
32
34
|
debug: Schema.boolean().default(false)
|
|
33
35
|
.description('调试模式:把收到的每条消息打到日志里'),
|
|
34
36
|
resultImage: Schema.union(['auto', 'always', 'off']).default('auto')
|
|
@@ -222,6 +224,20 @@ function apply(ctx, config) {
|
|
|
222
224
|
logger.info('抽卡数据就绪:%d 张卡 / %d 个奖池(卡图齐全)', game.cards.length, game.pools.size);
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
// ★ 同群多机器人时:消息 @ 的不是自己 → 不处理(避免重复回复)
|
|
228
|
+
if (config.ignoreAtOthers !== false) {
|
|
229
|
+
ctx.middleware((session, next) => {
|
|
230
|
+
try {
|
|
231
|
+
const els = (session.event && session.event.message && session.event.message.elements) || [];
|
|
232
|
+
const ats = els.filter((e) => e && e.type === 'at').map((e) => String(e.attrs && e.attrs.id || ''));
|
|
233
|
+
if (ats.length && !ats.includes('all') && !ats.includes(String(session.selfId || ''))) {
|
|
234
|
+
return; // 拦截:这条是给别的机器人的
|
|
235
|
+
}
|
|
236
|
+
} catch { /* 忽略 */ }
|
|
237
|
+
return next();
|
|
238
|
+
}, true);
|
|
239
|
+
}
|
|
240
|
+
|
|
225
241
|
if (config.debug) {
|
|
226
242
|
ctx.middleware((session, next) => {
|
|
227
243
|
try {
|