koishi-plugin-mcdle 0.1.4 → 0.1.6

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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # koishi-plugin-mcdle
2
+
3
+ Minecraft 猜词游戏,词库覆盖生物、物品与方块
4
+
5
+ ## 安装
6
+
7
+ ```sh
8
+ yarn add koishi-plugin-mcdle
9
+ ```
10
+
11
+ 在 Koishi 配置中启用,并提供 database 服务;puppeteer 可选,用于图片输出。
12
+
13
+ ## 指令
14
+
15
+ 发送 `mcdle` 查看玩法与全部指令。
16
+
17
+ | 指令 | 说明 |
18
+ | --- | --- |
19
+ | `mcdle` | 玩法与指令说明 |
20
+ | `mcdle.猜 [名称]` | 用有效词条开局或猜测 |
21
+ | `mcdle.裸猜 [开/关]` | 切换本群无前缀续猜 |
22
+ | `mcdle.排行榜` | 群内战绩 |
23
+ | `mcdle.词库` | 全部词条 |
24
+
25
+ 首次猜测的词条决定题目类型。裸猜设置只对当前群生效,插件重载后恢复默认。
26
+
27
+ 提示符号:🟩 完全匹配,🟨 部分匹配,🟥 不匹配;🟥⬆️ 或 🟥⬇️ 表示答案更大或更小。
28
+
29
+ ## 许可证
30
+
31
+ 可按 [Apache-2.0](LICENSE-APACHE) 或 [MIT](LICENSE-MIT) 使用。
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Context, Schema } from 'koishi';
1
+ import { Context, Schema, Session } from 'koishi';
2
2
  import { MobData, BlockData, ItemData } from './data';
3
3
  export declare const name = "mcdle";
4
- export declare const usage = "## \u4F7F\u7528\n\n`mcdle.\u731C [\u540D\u79F0]` \u5F00\u5C40\u6216\u731C\u6D4B\u3002\u7CFB\u7EDF\u968F\u673A\u9009\u62E9\u751F\u7269\u3001\u7269\u54C1\u6216\u65B9\u5757\uFF0C\u6309\u5C5E\u6027\u989C\u8272\u63D0\u793A\u63A5\u8FD1\u7A0B\u5EA6\u3002\n\n## \u63D0\u793A\n\n| \u7B26\u53F7 | \u542B\u4E49 |\n| --- | --- |\n| \uD83D\uDFE9 | \u5B8C\u5168\u5339\u914D |\n| \uD83D\uDFE8 | \u90E8\u5206\u5339\u914D |\n| \uD83D\uDFE5 | \u4E0D\u5339\u914D |\n| \uD83D\uDFE5\u2B06\uFE0F / \uD83D\uDFE5\u2B07\uFE0F | \u7B54\u6848\u66F4\u5927 / \u66F4\u5C0F |\n\n## \u6307\u4EE4\n\n| \u6307\u4EE4 | \u8BF4\u660E |\n| --- | --- |\n| `mcdle` | \u67E5\u770B\u5E2E\u52A9 |\n| `mcdle.\u731C [\u540D\u79F0]` | \u5F00\u5C40\u6216\u731C\u6D4B |\n| `mcdle.\u5E2E\u52A9` | \u5B8C\u6574\u8BF4\u660E |\n| `mcdle.\u6392\u884C\u699C` | \u7FA4\u5185\u6218\u7EE9 |\n| `mcdle.\u8BCD\u5E93` | \u5168\u90E8\u8BCD\u6761 |";
4
+ export declare const usage = "## \u4F7F\u7528\n\n\u53D1\u9001 `mcdle` \u67E5\u770B\u73A9\u6CD5\u4E0E\u5168\u90E8\u6307\u4EE4\u3002`mcdle.\u731C [\u540D\u79F0]` \u7528\u6709\u6548\u8BCD\u6761\u5F00\u5C40\u6216\u731C\u6D4B\u3002\u9996\u6B21\u731C\u6D4B\u7684\u8BCD\u6761\u51B3\u5B9A\u751F\u7269\u3001\u7269\u54C1\u6216\u65B9\u5757\u6A21\u5F0F\uFF0C\u7CFB\u7EDF\u518D\u4ECE\u540C\u7C7B\u8BCD\u5E93\u4E2D\u62BD\u53D6\u7B54\u6848\u3002\n\n## \u63D0\u793A\n\n| \u7B26\u53F7 | \u542B\u4E49 |\n| --- | --- |\n| \uD83D\uDFE9 | \u5B8C\u5168\u5339\u914D |\n| \uD83D\uDFE8 | \u90E8\u5206\u5339\u914D |\n| \uD83D\uDFE5 | \u4E0D\u5339\u914D |\n| \uD83D\uDFE5\u2B06\uFE0F / \uD83D\uDFE5\u2B07\uFE0F | \u7B54\u6848\u66F4\u5927 / \u66F4\u5C0F |\n\n## \u6307\u4EE4\n\n| \u6307\u4EE4 | \u8BF4\u660E |\n| --- | --- |\n| `mcdle` | \u73A9\u6CD5\u4E0E\u6307\u4EE4\u8BF4\u660E |\n| `mcdle.\u731C [\u540D\u79F0]` | \u7528\u6709\u6548\u8BCD\u6761\u5F00\u5C40\u6216\u731C\u6D4B |\n| `mcdle.\u88F8\u731C [\u5F00/\u5173]` | \u5207\u6362\u672C\u7FA4\u65E0\u524D\u7F00\u7EED\u731C |\n| `mcdle.\u6392\u884C\u699C` | \u7FA4\u5185\u6218\u7EE9 |\n| `mcdle.\u8BCD\u5E93` | \u5168\u90E8\u8BCD\u6761 |";
5
5
  export declare const inject: {
6
6
  required: string[];
7
7
  optional: string[];
@@ -24,7 +24,7 @@ declare module "koishi" {
24
24
  mcdle_rank: McdleRank;
25
25
  }
26
26
  }
27
- interface Mcdle {
27
+ export interface Mcdle {
28
28
  id: number;
29
29
  channelId: string;
30
30
  answer: MobData | ItemData | BlockData | null;
@@ -46,5 +46,25 @@ interface McdleRank {
46
46
  blockSuccessCount: number;
47
47
  totalSuccessCount: number;
48
48
  }
49
+ /**
50
+ * 裸猜只接受一条完整、无修饰、且与 MCDLE 词库精确匹配的纯文本消息。
51
+ * 引用、@、图片、富文本及机器人消息都可能只是普通聊天的一部分,不能据此猜测。
52
+ */
53
+ export declare function getMiddlewareGuess(session: Session): string | null;
54
+ /** 裸猜只能加入当前模式下已开始的有效局面,绝不能代替完整指令开新局。 */
55
+ export declare function canHandleMiddlewareGuess(game: Mcdle | null | undefined, guess: string, allowRepeatedGuesses?: boolean): boolean;
56
+ export type MiddlewareSwitch = {
57
+ error: string;
58
+ } | {
59
+ /** 写回本群覆盖表的新值;undefined 表示清除覆盖,重新跟随插件配置。 */
60
+ override: boolean | undefined;
61
+ on: boolean;
62
+ reverted: boolean;
63
+ };
64
+ /**
65
+ * 无参调用在「配置默认」与「本群临时相反」间往返;显式开/关若与配置相同,
66
+ * 就清除冗余覆盖。临时状态只保存在内存中,插件重载后自然回到配置默认。
67
+ */
68
+ export declare function resolveMiddlewareSwitch(config: boolean, override: boolean | undefined, action: string | null | undefined): MiddlewareSwitch;
49
69
  export declare function apply(ctx: Context, cfg: Config): void;
50
70
  export {};
package/lib/index.js CHANGED
@@ -18,15 +18,18 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
23
  Config: () => Config,
24
24
  apply: () => apply,
25
+ canHandleMiddlewareGuess: () => canHandleMiddlewareGuess,
26
+ getMiddlewareGuess: () => getMiddlewareGuess,
25
27
  inject: () => inject,
26
28
  name: () => name,
29
+ resolveMiddlewareSwitch: () => resolveMiddlewareSwitch,
27
30
  usage: () => usage
28
31
  });
29
- module.exports = __toCommonJS(index_exports);
32
+ module.exports = __toCommonJS(src_exports);
30
33
  var import_koishi = require("koishi");
31
34
 
32
35
  // src/data.ts
@@ -15395,7 +15398,7 @@ function boardCard(opts) {
15395
15398
  return shell(body, { accent: meta.accent });
15396
15399
  }
15397
15400
  __name(boardCard, "boardCard");
15398
- function startCard(mode, played, limit) {
15401
+ function startCard(mode, played, limit, middleware) {
15399
15402
  const meta = MODES[mode];
15400
15403
  const fields = FIELDS[mode];
15401
15404
  const attrs = fields.map((k) => `<div class="attr">${px(fieldIcon(k), 13)}<span>${esc(keyMap[k] || k)}</span></div>`).join("");
@@ -15415,7 +15418,7 @@ function startCard(mode, played, limit) {
15415
15418
  <div class="sec">
15416
15419
  <div class="sec-t"><span>下 一 步</span></div>
15417
15420
  <div class="panel"><div class="rows">
15418
- <div class="row"><span class="cmd">mcdle.猜 [名称]</span><span class="m">提交一次猜测</span></div>
15421
+ <div class="row"><span class="cmd">${middleware ? `直接发送${esc(meta.name)}名称` : "mcdle.猜 [名称]"}</span><span class="m">提交后续猜测</span></div>
15419
15422
  <div class="row"><span class="cmd">mcdle.词库</span><span class="m">忘了有哪些词条时翻一翻</span></div>
15420
15423
  </div></div>
15421
15424
  </div>
@@ -15466,7 +15469,7 @@ function winCard(o) {
15466
15469
  return shell(body, { accent: meta.accent, width: 720 });
15467
15470
  }
15468
15471
  __name(winCard, "winCard");
15469
- function helpCard(dailyLimit, allowRepeat) {
15472
+ function helpCard(dailyLimit, allowRepeat, middleware) {
15470
15473
  const modeCards = ["mob", "item", "block"].map((m) => {
15471
15474
  const meta = MODES[m];
15472
15475
  return `<div class="mode-card" style="--accent:${meta.accent}">
@@ -15476,7 +15479,15 @@ function helpCard(dailyLimit, allowRepeat) {
15476
15479
  </div>`;
15477
15480
  }).join("");
15478
15481
  const legend = legendChips();
15479
- const body = header(null, "我的世界猜谜 · 玩法说明", { big: `${dailyLimit}`, cap: "每日局数上限" }) + `<div class="pad">
15482
+ const total = mobData.length + itemData.length + blockData.length;
15483
+ const body = header(null, "猜生物 · 猜物品 · 猜方块", { big: `${total}`, cap: "词条总量" }) + `<div class="pad">
15484
+ <div class="sec">
15485
+ <div class="sec-t"><span>玩 什 么</span></div>
15486
+ <div class="panel"><div class="rows">
15487
+ <div class="row"><span>每局随机抽一个生物、物品或方块,你的每次猜测都会逐项与答案比对,用颜色告诉你差在哪里。</span></div>
15488
+ <div class="row"><span>首次猜测的词条属于哪一类,本局就在哪一类里出题。</span></div>
15489
+ </div></div>
15490
+ </div>
15480
15491
  <div class="sec">
15481
15492
  <div class="sec-t"><span>三 种 模 式</span></div>
15482
15493
  <div class="grid3">${modeCards}</div>
@@ -15491,11 +15502,21 @@ function helpCard(dailyLimit, allowRepeat) {
15491
15502
  </div>
15492
15503
  </div>
15493
15504
  </div>
15505
+ <div class="sec">
15506
+ <div class="sec-t"><span>三 步 上 手</span></div>
15507
+ <div class="panel"><div class="rows">
15508
+ <div class="row"><span class="k">壹</span><span class="cmd">mcdle.猜 苦力怕</span><span class="m">用一个有效词条开局</span></div>
15509
+ <div class="row"><span class="k">贰</span><span>${middleware ? "开局后可直接发送当前模式的完整词条名称继续猜测。" : "对照颜色与箭头缩小范围,每一次猜测都会留在棋盘上。"}</span></div>
15510
+ <div class="row"><span class="k">叁</span><span>锁定答案,战绩自动记入 <span class="cmd">mcdle.排行榜</span>。</span></div>
15511
+ </div></div>
15512
+ </div>
15494
15513
  <div class="sec">
15495
15514
  <div class="sec-t"><span>指 令</span></div>
15496
15515
  <div class="panel"><div class="rows">
15516
+ <div class="row"><span class="cmd">mcdle</span><span class="m">这份玩法与指令说明</span></div>
15497
15517
  <div class="row"><span class="cmd">mcdle.猜 [名称]</span><span class="m">开始一局,或提交猜测</span></div>
15498
15518
  <div class="row"><span class="cmd">mcdle.猜</span><span class="m">局中直接使用可回看当前棋盘</span></div>
15519
+ <div class="row"><span class="cmd">mcdle.裸猜 [开/关]</span><span class="m">临时切换本群无前缀续猜</span></div>
15499
15520
  <div class="row"><span class="cmd">mcdle.排行榜</span><span class="m">查看群内战绩</span></div>
15500
15521
  <div class="row"><span class="cmd">mcdle.词库</span><span class="m">查阅全部候选词条</span></div>
15501
15522
  </div></div>
@@ -15505,48 +15526,19 @@ function helpCard(dailyLimit, allowRepeat) {
15505
15526
  <div class="panel"><div class="rows">
15506
15527
  <div class="row"><span class="k">·</span><span>每个群每日可开始 <b>${dailyLimit}</b> 局,跨零点重置。</span></div>
15507
15528
  <div class="row"><span class="k">·</span><span>${allowRepeat ? "允许重复提交已经猜过的词条。" : "同一局内不能重复提交已猜过的词条。"}</span></div>
15529
+ <div class="row"><span class="k">·</span><span>${middleware ? "本群裸猜已开启:只接受当前模式词库中的完整纯文本词条。" : "本群裸猜已停用:续猜请使用完整指令。"}</span></div>
15530
+ <div class="row"><span class="k">·</span><span>空指令与词库外名称不会开启新局或消耗额度。</span></div>
15508
15531
  <div class="row"><span class="k">·</span><span>词条与数据来自 Minecraft Wiki,版本号按发布先后比较。</span></div>
15509
15532
  </div></div>
15510
15533
  </div>
15511
- </div>` + noteRow("词条数据来自 zh.minecraft.wiki");
15534
+ </div>` + noteRow("灵感来自 Wordle 与 mcdle.net");
15512
15535
  return shell(body, { accent: "#8ec96a", width: 760 });
15513
15536
  }
15514
15537
  __name(helpCard, "helpCard");
15515
- function introCard(dailyLimit) {
15516
- const modeCards = ["mob", "item", "block"].map((m) => {
15517
- const meta = MODES[m];
15518
- return `<div class="mode-card" style="--accent:${meta.accent}">
15519
- <div class="top" style="color:${meta.accent}">${px(meta.icon, 17)}<span>${meta.name}</span></div>
15520
- <div class="cnt">${meta.total} 个词条</div>
15521
- </div>`;
15522
- }).join("");
15523
- const body = header(null, "猜生物 · 猜物品 · 猜方块", { big: `${mobData.length + itemData.length + blockData.length}`, cap: "词条总量" }) + `<div class="pad">
15524
- <div class="sec">
15525
- <div class="sec-t"><span>玩 什 么</span></div>
15526
- <div class="grid3">${modeCards}</div>
15527
- </div>
15528
- <div class="sec">
15529
- <div class="sec-t"><span>三 步 上 手</span></div>
15530
- <div class="panel"><div class="rows">
15531
- <div class="row"><span class="k">壹</span><span class="cmd">mcdle.猜 苦力怕</span><span class="m">随便报一个名字即可开局</span></div>
15532
- <div class="row"><span class="k">贰</span><span>对照颜色与箭头缩小范围,每一次猜测都会留在棋盘上。</span></div>
15533
- <div class="row"><span class="k">叁</span><span>锁定答案,战绩自动记入 <span class="cmd">mcdle.排行榜</span>。</span></div>
15534
- </div></div>
15535
- </div>
15536
- <div class="sec">
15537
- <div class="sec-t"><span>今 日 额 度</span></div>
15538
- <div class="panel"><div class="rows">
15539
- <div class="row"><span class="k">·</span><span>每个群每日 <b>${dailyLimit}</b> 局,跨零点重置;详细规则见 <span class="cmd">mcdle.帮助</span>。</span></div>
15540
- </div></div>
15541
- </div>
15542
- </div>` + noteRow("灵感来自 Wordle 与 mcdle.net");
15543
- return shell(body, { accent: "#8ec96a", width: 700 });
15544
- }
15545
- __name(introCard, "introCard");
15546
15538
  function rankCard(entries, shown) {
15547
15539
  if (!entries.length) {
15548
15540
  const body2 = header(null, "排行榜", { big: "0", cap: "上榜人数" }) + `<div class="pad"><div class="empty">${px("trophy", 44)}
15549
- 榜单还空着<br><span style="font-size:12px">用 <span class="cmd">mcdle.猜</span> 开出第一局,成为榜首</span></div></div>`;
15541
+ 榜单还空着<br><span style="font-size:12px">用 <span class="cmd">mcdle.猜 苦力怕</span> 开出第一局,成为榜首</span></div></div>`;
15550
15542
  return shell(body2, { accent: "#e8b53c", width: 560 });
15551
15543
  }
15552
15544
  const max = Math.max(...entries.map((e) => e.total), 1);
@@ -15574,7 +15566,7 @@ __name(rankCard, "rankCard");
15574
15566
  var name = "mcdle";
15575
15567
  var usage = `## 使用
15576
15568
 
15577
- \`mcdle.猜 [名称]\` 开局或猜测。系统随机选择生物、物品或方块,按属性颜色提示接近程度。
15569
+ 发送 \`mcdle\` 查看玩法与全部指令。\`mcdle.猜 [名称]\` 用有效词条开局或猜测。首次猜测的词条决定生物、物品或方块模式,系统再从同类词库中抽取答案。
15578
15570
 
15579
15571
  ## 提示
15580
15572
 
@@ -15589,16 +15581,16 @@ var usage = `## 使用
15589
15581
 
15590
15582
  | 指令 | 说明 |
15591
15583
  | --- | --- |
15592
- | \`mcdle\` | 查看帮助 |
15593
- | \`mcdle.猜 [名称]\` | 开局或猜测 |
15594
- | \`mcdle.帮助\` | 完整说明 |
15584
+ | \`mcdle\` | 玩法与指令说明 |
15585
+ | \`mcdle.猜 [名称]\` | 用有效词条开局或猜测 |
15586
+ | \`mcdle.裸猜 [开/关]\` | 切换本群无前缀续猜 |
15595
15587
  | \`mcdle.排行榜\` | 群内战绩 |
15596
15588
  | \`mcdle.词库\` | 全部词条 |`;
15597
15589
  var inject = { required: ["database"], optional: ["puppeteer"] };
15598
15590
  var Config = import_koishi.Schema.object({
15599
15591
  atReply: import_koishi.Schema.boolean().default(false).description("响应时@用户"),
15600
- quoteReply: import_koishi.Schema.boolean().default(true).description("响应时引用消息"),
15601
- isEnableMiddleware: import_koishi.Schema.boolean().default(false).description("启用中间件(无需指令直接猜测)"),
15592
+ quoteReply: import_koishi.Schema.boolean().default(false).description("响应时引用消息"),
15593
+ isEnableMiddleware: import_koishi.Schema.boolean().default(false).description("是否默认启用无前缀续猜(仅在游戏进行中生效;mcdle.裸猜 可在单个群内临时切换)"),
15602
15594
  addStatusTextAfterEmoji: import_koishi.Schema.boolean().default(true).description("在状态表情后添加文字说明"),
15603
15595
  maxRank: import_koishi.Schema.number().default(10).min(0).description("排行榜最大显示人数"),
15604
15596
  dailyPlayLimit: import_koishi.Schema.number().default(1).min(1).description("每日游玩次数上限"),
@@ -15626,6 +15618,56 @@ var guessesField = {
15626
15618
  }
15627
15619
  }
15628
15620
  };
15621
+ var DATA_BY_MODE = {
15622
+ mob: mobData,
15623
+ item: itemData,
15624
+ block: blockData
15625
+ };
15626
+ var allChineseTitles = new Set(
15627
+ Object.values(DATA_BY_MODE).flatMap(
15628
+ (entries) => entries.map((entry) => entry.chinese_title)
15629
+ )
15630
+ );
15631
+ function getMiddlewareGuess(session) {
15632
+ const message = session.event.message;
15633
+ if (!message || message.quote || session.event.user?.isBot || message.user?.isBot) {
15634
+ return null;
15635
+ }
15636
+ const elements = message.elements ?? [];
15637
+ if (elements.length !== 1 || elements[0].type !== "text") return null;
15638
+ const text = elements[0].attrs.content;
15639
+ if (typeof text !== "string" || text !== text.trim() || !text || !allChineseTitles.has(text)) {
15640
+ return null;
15641
+ }
15642
+ return text;
15643
+ }
15644
+ __name(getMiddlewareGuess, "getMiddlewareGuess");
15645
+ function canHandleMiddlewareGuess(game, guess, allowRepeatedGuesses = false) {
15646
+ if (!game || game.gameEnded || !game.answer) return false;
15647
+ const mode = game.gameMode;
15648
+ if (!mode || !DATA_BY_MODE[mode]) return false;
15649
+ if (!allowRepeatedGuesses && game.guessedChineseTitles?.includes(guess)) return false;
15650
+ return DATA_BY_MODE[mode].some((entry) => entry.chinese_title === guess);
15651
+ }
15652
+ __name(canHandleMiddlewareGuess, "canHandleMiddlewareGuess");
15653
+ function resolveMiddlewareSwitch(config, override, action) {
15654
+ const arg = action?.trim();
15655
+ if (!arg) {
15656
+ return override === void 0 ? { override: !config, on: !config, reverted: false } : { override: void 0, on: config, reverted: true };
15657
+ }
15658
+ if (arg === "状态") {
15659
+ return { override, on: override ?? config, reverted: false };
15660
+ }
15661
+ const on = arg === "开" || arg === "开启";
15662
+ if (!on && arg !== "关" && arg !== "关闭") {
15663
+ return { error: "裸猜开关只认 开 / 关 / 状态" };
15664
+ }
15665
+ if (on === config) {
15666
+ return { override: void 0, on: config, reverted: override !== void 0 };
15667
+ }
15668
+ return { override: on, on, reverted: false };
15669
+ }
15670
+ __name(resolveMiddlewareSwitch, "resolveMiddlewareSwitch");
15629
15671
  function apply(ctx, cfg) {
15630
15672
  ctx.model.extend(
15631
15673
  "mcdle",
@@ -15661,41 +15703,48 @@ function apply(ctx, cfg) {
15661
15703
  }
15662
15704
  );
15663
15705
  const logger = ctx.logger("mcdle");
15664
- if (cfg.isEnableMiddleware) {
15665
- ctx.middleware(async (session, next) => {
15666
- let gameRecords = await ctx.database.get("mcdle", { channelId: session.channelId });
15667
- if (!gameRecords || gameRecords.length === 0) {
15668
- return next();
15669
- }
15670
- const game = gameRecords[0];
15671
- if (game.gameEnded) {
15672
- return next();
15673
- }
15674
- const content = session.content.trim();
15675
- if (content.length > 10 || /[,。!?;:'"“”‘’【】()《》.,!?;:'"\[\]()<>]/.test(content)) return next();
15676
- if (!cfg.allowRepeatedGuesses && game.guessedChineseTitles.includes(content)) {
15677
- return next();
15678
- }
15679
- let dataSource = [];
15680
- if (game.gameMode === "mob") dataSource = mobData;
15681
- else if (game.gameMode === "item") dataSource = itemData;
15682
- else if (game.gameMode === "block") dataSource = blockData;
15683
- else return next();
15684
- const matched = dataSource.find(
15685
- (d) => d.chinese_title === content
15706
+ const middlewareOverrides = /* @__PURE__ */ new Map();
15707
+ const middlewareOn = /* @__PURE__ */ __name((channelId) => channelId === void 0 ? cfg.isEnableMiddleware : middlewareOverrides.get(channelId) ?? cfg.isEnableMiddleware, "middlewareOn");
15708
+ ctx.middleware(async (session, next) => {
15709
+ if (!session.channelId || !middlewareOn(session.channelId)) return await next();
15710
+ const guess = getMiddlewareGuess(session);
15711
+ if (!guess) return await next();
15712
+ try {
15713
+ const [game] = await ctx.database.get("mcdle", { channelId: session.channelId });
15714
+ if (!canHandleMiddlewareGuess(game, guess, cfg.allowRepeatedGuesses)) return await next();
15715
+ const handled = await c(session, guess, false);
15716
+ return handled ? void 0 : await next();
15717
+ } catch (error) {
15718
+ logger.warn(
15719
+ "中间件读取或处理游戏状态失败,本次消息不作猜测:%s",
15720
+ error instanceof Error ? error.message : String(error)
15686
15721
  );
15687
- if (!matched) {
15688
- return next();
15689
- }
15690
- await session.execute(`mcdle.猜 ${content}`);
15691
- return;
15692
- });
15693
- }
15694
- ctx.command("mcdle", "我的世界猜谜游戏").action(async ({ session }) => mcdle(session));
15695
- ctx.command("mcdle.猜 [guess:string]").action(async ({ session }, guess) => c(session, guess?.trim()));
15696
- ctx.command("mcdle.帮助").action(async ({ session }) => bz(session));
15722
+ return await next();
15723
+ }
15724
+ });
15725
+ ctx.command("mcdle", "我的世界猜谜游戏 · 玩法与指令").action(async ({ session }) => mcdle(session));
15726
+ ctx.command("mcdle.猜 [guess:string]").action(async ({ session }, guess) => {
15727
+ await c(session, guess?.trim());
15728
+ });
15697
15729
  ctx.command("mcdle.排行榜").action(async ({ session }) => phb(session));
15698
15730
  ctx.command("mcdle.词库").action(async ({ session }) => ck(session));
15731
+ ctx.command("mcdle.裸猜 [state:string]", "临时开关本群的无前缀续猜").usage("例:mcdle.裸猜(切换)· mcdle.裸猜 开 · mcdle.裸猜 关 · mcdle.裸猜 状态").action(async ({ session }, state) => {
15732
+ const result = resolveMiddlewareSwitch(
15733
+ cfg.isEnableMiddleware,
15734
+ session.channelId === void 0 ? void 0 : middlewareOverrides.get(session.channelId),
15735
+ state
15736
+ );
15737
+ if ("error" in result) {
15738
+ await sendMsg(session, `⚠️ ${result.error}。例:mcdle.裸猜 开`);
15739
+ return;
15740
+ }
15741
+ if (result.override === void 0) middlewareOverrides.delete(session.channelId);
15742
+ else middlewareOverrides.set(session.channelId, result.override);
15743
+ await sendMsg(
15744
+ session,
15745
+ middlewareSwitchText(result.on, result.override !== void 0, result.reverted)
15746
+ );
15747
+ });
15699
15748
  const RULE = "────────────";
15700
15749
  function textCard(title, ...sections) {
15701
15750
  const body = sections.map((s) => Array.isArray(s) ? s.filter(Boolean).join("\n") : s).filter((s) => !!s && !!s.trim()).join("\n\n");
@@ -15755,13 +15804,16 @@ function apply(ctx, cfg) {
15755
15804
  );
15756
15805
  }
15757
15806
  __name(boardText, "boardText");
15758
- function startText(mode, played) {
15807
+ function startText(mode, played, channelOn) {
15759
15808
  const meta = MODES[mode];
15760
15809
  return textCard(
15761
15810
  `新的一局 · ${meta.name}模式`,
15762
15811
  `${meta.tagline},从 ${meta.total} 个候选中锁定唯一答案。`,
15763
15812
  `可推理属性:${FIELDS[mode].map((k) => keyMap[k] || k).join(" / ")}`,
15764
- [`mcdle.猜 [名称] 提交一次猜测`, `今日进度 ${played}/${cfg.dailyPlayLimit}`]
15813
+ [
15814
+ channelOn ? `直接发送词库中的${meta.name}名称,或使用 mcdle.猜 [名称]` : `mcdle.猜 [名称] 提交一次猜测`,
15815
+ `今日进度 ${played}/${cfg.dailyPlayLimit}`
15816
+ ]
15765
15817
  );
15766
15818
  }
15767
15819
  __name(startText, "startText");
@@ -15818,27 +15870,28 @@ function apply(ctx, cfg) {
15818
15870
  }
15819
15871
  __name(sendCard, "sendCard");
15820
15872
  async function mcdle(session) {
15821
- await sendCard(session, introCard(cfg.dailyPlayLimit), introText());
15873
+ const channelOn = middlewareOn(session.channelId);
15874
+ await sendCard(
15875
+ session,
15876
+ helpCard(cfg.dailyPlayLimit, cfg.allowRepeatedGuesses, channelOn),
15877
+ helpText(channelOn)
15878
+ );
15822
15879
  }
15823
15880
  __name(mcdle, "mcdle");
15824
- function introText() {
15825
- const total = mobData.length + itemData.length + blockData.length;
15826
- return textCard(
15827
- "MCDLE · 我的世界猜谜",
15828
- `从 ${total} 个词条里,只凭属性提示锁定唯一答案。`,
15829
- [
15830
- `壹 mcdle.猜 苦力怕 随便报个名字即可开局`,
15831
- `贰 对照颜色与箭头缩小范围,每次猜测都会留在棋盘上`,
15832
- `叁 猜中后自动记入 mcdle.排行榜`
15833
- ],
15834
- [
15835
- `生物 ${mobData.length} · 物品 ${itemData.length} · 方块 ${blockData.length}`,
15836
- `每日 ${cfg.dailyPlayLimit} 局,跨零点重置`,
15837
- `完整规则:mcdle.帮助`
15838
- ]
15839
- );
15881
+ function middlewareSwitchText(on, temporary, reverted) {
15882
+ const mark = on ? "✅" : "⛔";
15883
+ const state = on ? "开启" : "停用";
15884
+ const config = cfg.isEnableMiddleware ? "开启" : "停用";
15885
+ if (!temporary) {
15886
+ return `${mark} 裸猜续局 · ${reverted ? "已复原," : ""}跟随插件配置(${state})`;
15887
+ }
15888
+ const hint = on ? "仅在已有对局中,直接发送当前模式的完整词条名称才会响应" : "续猜请使用 mcdle.猜 [名称]";
15889
+ return [
15890
+ `${mark} 裸猜续局 · 本群临时${state}(插件配置:${config})`,
15891
+ `${hint};再次发送 mcdle.裸猜 可复原`
15892
+ ].join("\n");
15840
15893
  }
15841
- __name(introText, "introText");
15894
+ __name(middlewareSwitchText, "middlewareSwitchText");
15842
15895
  async function ck(session) {
15843
15896
  const sections = [
15844
15897
  [`生物词库 · ${mobChineseTitle.length} 条`, mobChineseTitle],
@@ -15866,11 +15919,7 @@ ${words.join(" ")}`).join("\n\n")
15866
15919
  );
15867
15920
  }
15868
15921
  __name(ck, "ck");
15869
- const POOLS = {
15870
- mob: mobData,
15871
- item: itemData,
15872
- block: blockData
15873
- };
15922
+ const POOLS = DATA_BY_MODE;
15874
15923
  function modeOf(entry) {
15875
15924
  if (mobData.includes(entry)) return "mob";
15876
15925
  if (itemData.includes(entry)) return "item";
@@ -15927,7 +15976,7 @@ ${words.join(" ")}`).join("\n\n")
15927
15976
  return candidates[Math.floor(Math.random() * candidates.length)];
15928
15977
  }
15929
15978
  __name(pickAnswer, "pickAnswer");
15930
- async function c(session, guess) {
15979
+ async function c(session, guess, allowStart = true) {
15931
15980
  const record = await ensureRecord(session);
15932
15981
  const now = /* @__PURE__ */ new Date();
15933
15982
  await resetDailyIfNeeded(session, record, now);
@@ -15943,12 +15992,12 @@ ${words.join(" ")}`).join("\n\n")
15943
15992
  record.guesses = restored;
15944
15993
  record.formatVersion = FORMAT_VERSION;
15945
15994
  } else {
15946
- await rollbackLegacyGame(session, record, guess, now);
15947
- return;
15995
+ await rollbackLegacyGame(session, record, guess, now, allowStart);
15996
+ return true;
15948
15997
  }
15949
15998
  } else if (record.guesses.length) {
15950
- await rollbackLegacyGame(session, record, guess, now);
15951
- return;
15999
+ await rollbackLegacyGame(session, record, guess, now, allowStart);
16000
+ return true;
15952
16001
  } else {
15953
16002
  await ctx.database.set(
15954
16003
  "mcdle",
@@ -15959,10 +16008,12 @@ ${words.join(" ")}`).join("\n\n")
15959
16008
  }
15960
16009
  }
15961
16010
  if (record.gameEnded) {
16011
+ if (!allowStart) return false;
15962
16012
  await startNewGame(session, record, guess, now);
15963
16013
  } else {
15964
16014
  await continueGame(session, record, guess);
15965
16015
  }
16016
+ return true;
15966
16017
  }
15967
16018
  __name(c, "c");
15968
16019
  function restoreGuessHistory(record) {
@@ -15975,7 +16026,7 @@ ${words.join(" ")}`).join("\n\n")
15975
16026
  return entries.map((entry) => compareData(entry, record.answer));
15976
16027
  }
15977
16028
  __name(restoreGuessHistory, "restoreGuessHistory");
15978
- async function rollbackLegacyGame(session, record, guess, now) {
16029
+ async function rollbackLegacyGame(session, record, guess, now, allowStart) {
15979
16030
  const answerTitle = record.answer?.chinese_title;
15980
16031
  const history = answerTitle ? record.historyChineseTitles.filter((title) => title !== answerTitle) : record.historyChineseTitles;
15981
16032
  const refunded = Math.max(0, record.dailyPlayedToday - 1);
@@ -15999,24 +16050,50 @@ ${words.join(" ")}`).join("\n\n")
15999
16050
  "检测到旧版本遗留的一局",
16000
16051
  "这局是在旧版本下开始的,已有的猜测记录没法用新版图片牌面渲染。",
16001
16052
  answerTitle ? `「${answerTitle}」已退回词库,之后仍有机会被抽到。` : null,
16002
- "这一局已作废并退还今日额度,马上为你重新开一局。"
16053
+ allowStart ? "这一局已作废并退还今日额度,马上按这次的完整指令重新开局。" : "这一局已作废并退还今日额度,请用 mcdle.猜 [名称] 重新开局。"
16003
16054
  )
16004
16055
  );
16005
- await startNewGame(
16006
- session,
16007
- {
16008
- ...record,
16009
- historyChineseTitles: history,
16010
- dailyPlayedToday: refunded,
16011
- gameEnded: true,
16012
- formatVersion: FORMAT_VERSION
16013
- },
16014
- guess,
16015
- now
16016
- );
16056
+ if (allowStart) {
16057
+ await startNewGame(
16058
+ session,
16059
+ {
16060
+ ...record,
16061
+ historyChineseTitles: history,
16062
+ dailyPlayedToday: refunded,
16063
+ gameEnded: true,
16064
+ formatVersion: FORMAT_VERSION
16065
+ },
16066
+ guess,
16067
+ now
16068
+ );
16069
+ }
16017
16070
  }
16018
16071
  __name(rollbackLegacyGame, "rollbackLegacyGame");
16019
16072
  async function startNewGame(session, record, guess, now) {
16073
+ if (!guess) {
16074
+ await sendMsg(
16075
+ session,
16076
+ textCard(
16077
+ "还没有进行中的游戏",
16078
+ "请发送完整指令 mcdle.猜 [名称] 开局,例如 mcdle.猜 苦力怕。",
16079
+ "名称必须来自 mcdle.词库。"
16080
+ )
16081
+ );
16082
+ return;
16083
+ }
16084
+ const opener = findAnywhere(guess);
16085
+ if (!opener) {
16086
+ const near = [...allChineseTitles].filter((title) => title.includes(guess) || guess.includes(title)).slice(0, 3);
16087
+ await sendMsg(
16088
+ session,
16089
+ textCard(
16090
+ `⚠️ 「${guess}」不在 MCDLE 词库里`,
16091
+ near.length ? `是不是想猜:${near.join("、")}?` : null,
16092
+ "没有开启新局,也没有消耗今日额度。请用 mcdle.猜 [词库名称] 开局。"
16093
+ )
16094
+ );
16095
+ return;
16096
+ }
16020
16097
  if (record.dailyPlayedToday >= cfg.dailyPlayLimit) {
16021
16098
  await sendMsg(
16022
16099
  session,
@@ -16028,8 +16105,7 @@ ${words.join(" ")}`).join("\n\n")
16028
16105
  );
16029
16106
  return;
16030
16107
  }
16031
- const opener = guess ? findAnywhere(guess) : void 0;
16032
- const mode = opener ? modeOf(opener) : ["mob", "item", "block"][Math.floor(Math.random() * 3)];
16108
+ const mode = modeOf(opener);
16033
16109
  const answer = await pickAnswer(session, record, mode);
16034
16110
  const played = record.dailyPlayedToday + 1;
16035
16111
  await ctx.database.set(
@@ -16047,14 +16123,17 @@ ${words.join(" ")}`).join("\n\n")
16047
16123
  formatVersion: FORMAT_VERSION
16048
16124
  }
16049
16125
  );
16050
- await sendCard(session, startCard(mode, played, cfg.dailyPlayLimit), startText(mode, played));
16051
- if (opener) {
16052
- await applyGuess(
16053
- session,
16054
- { mode, answer, guesses: [], titles: [], startedAt: now, played },
16055
- opener
16056
- );
16057
- }
16126
+ const channelOn = middlewareOn(session.channelId);
16127
+ await sendCard(
16128
+ session,
16129
+ startCard(mode, played, cfg.dailyPlayLimit, channelOn),
16130
+ startText(mode, played, channelOn)
16131
+ );
16132
+ await applyGuess(
16133
+ session,
16134
+ { mode, answer, guesses: [], titles: [], startedAt: now, played },
16135
+ opener
16136
+ );
16058
16137
  }
16059
16138
  __name(startNewGame, "startNewGame");
16060
16139
  async function continueGame(session, record, guess) {
@@ -16171,17 +16250,14 @@ ${words.join(" ")}`).join("\n\n")
16171
16250
  );
16172
16251
  }
16173
16252
  __name(applyGuess, "applyGuess");
16174
- function bz(session) {
16175
- return sendCard(session, helpCard(cfg.dailyPlayLimit, cfg.allowRepeatedGuesses), helpText());
16176
- }
16177
- __name(bz, "bz");
16178
- function helpText() {
16253
+ function helpText(channelOn) {
16254
+ const total = mobData.length + itemData.length + blockData.length;
16179
16255
  const legend = ["true", "mixed", "false", "false_up", "false_down"].map(
16180
16256
  (s) => `${STATUS_META[s].emoji} ${STATUS_META[s].label}`
16181
16257
  );
16182
16258
  return textCard(
16183
- "MCDLE 玩法说明",
16184
- "每局随机抽一个生物、物品或方块,你的每次猜测都会逐项与答案比对,用颜色告诉你差在哪里。",
16259
+ "MCDLE · 我的世界猜谜",
16260
+ `从 ${total} 个词条里,只凭属性提示锁定唯一答案。每局随机抽一个生物、物品或方块,首次猜测的词条属于哪一类,本局就在哪一类里出题。`,
16185
16261
  legend,
16186
16262
  [
16187
16263
  "部分匹配的字段里,【】括起来的就是与答案重合的那几项。",
@@ -16193,14 +16269,23 @@ ${words.join(" ")}`).join("\n\n")
16193
16269
  `方块 ${blockData.length} 条 · ${FIELDS.block.length} 项属性`
16194
16270
  ],
16195
16271
  [
16272
+ `壹 mcdle.猜 苦力怕 用一个有效词条开局`,
16273
+ channelOn ? `贰 开局后可直接发送当前模式的词条名称继续猜测` : `贰 对照颜色与箭头缩小范围,每次猜测都会留在棋盘上`,
16274
+ `叁 猜中后自动记入 mcdle.排行榜`
16275
+ ],
16276
+ [
16277
+ "mcdle 这份玩法与指令说明",
16196
16278
  "mcdle.猜 [名称] 开始一局,或提交猜测",
16197
16279
  "mcdle.猜 局中直接使用可回看当前棋盘",
16280
+ "mcdle.裸猜 [开/关] 临时切换本群无前缀续猜",
16198
16281
  "mcdle.排行榜 查看群内战绩",
16199
16282
  "mcdle.词库 查阅全部候选词条"
16200
16283
  ],
16201
16284
  [
16202
16285
  `每个群每日 ${cfg.dailyPlayLimit} 局,跨零点重置。`,
16203
16286
  cfg.allowRepeatedGuesses ? "允许重复提交已经猜过的词条。" : "同一局内不能重复提交已猜过的词条。",
16287
+ channelOn ? "本群裸猜已开启:仅已有对局、纯文本完整词条、且符合当前模式时响应。" : "本群裸猜已停用:请使用完整猜测指令。",
16288
+ "空指令和词库外名称不会开启新局或消耗额度。",
16204
16289
  "词条与数据来自 zh.minecraft.wiki,版本号按发布先后比较。"
16205
16290
  ]
16206
16291
  );
@@ -16381,7 +16466,10 @@ __name(apply, "apply");
16381
16466
  0 && (module.exports = {
16382
16467
  Config,
16383
16468
  apply,
16469
+ canHandleMiddlewareGuess,
16470
+ getMiddlewareGuess,
16384
16471
  inject,
16385
16472
  name,
16473
+ resolveMiddlewareSwitch,
16386
16474
  usage
16387
16475
  });
package/lib/view.d.ts CHANGED
@@ -42,7 +42,7 @@ export interface BoardOptions {
42
42
  tip?: string;
43
43
  }
44
44
  export declare function boardCard(opts: BoardOptions): string;
45
- export declare function startCard(mode: Mode, played: number, limit: number): string;
45
+ export declare function startCard(mode: Mode, played: number, limit: number, middleware: boolean): string;
46
46
  export interface WinOptions {
47
47
  mode: Mode;
48
48
  answer: any;
@@ -56,8 +56,7 @@ export interface WinOptions {
56
56
  /** 把秒数说成人话;秒级以内也要有话说,异常才落到破折号。 */
57
57
  export declare function formatDuration(seconds: number): string;
58
58
  export declare function winCard(o: WinOptions): string;
59
- export declare function helpCard(dailyLimit: number, allowRepeat: boolean): string;
60
- export declare function introCard(dailyLimit: number): string;
59
+ export declare function helpCard(dailyLimit: number, allowRepeat: boolean, middleware: boolean): string;
61
60
  export interface RankEntry {
62
61
  username: string;
63
62
  userId: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-mcdle",
3
3
  "description": "Koishi 的 Minecraft 猜词游戏插件。",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -59,6 +59,10 @@
59
59
  }
60
60
  },
61
61
  "devDependencies": {
62
- "koishi-plugin-puppeteer": "^3.9.0"
62
+ "koishi-plugin-puppeteer": "^3.9.0",
63
+ "tsx": "^4.23.13"
64
+ },
65
+ "scripts": {
66
+ "test": "tsx --test test/*.test.ts"
63
67
  }
64
68
  }
package/readme.md DELETED
@@ -1,51 +0,0 @@
1
- koishi-plugin-mcdle
2
- ===================
3
-
4
- [<img alt="github" src="https://img.shields.io/badge/github-araea/koishi__plugin__mcdle-8da0cb?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/araea/koishi-plugin-mcdle)
5
- [<img alt="npm" src="https://img.shields.io/npm/v/koishi-plugin-mcdle.svg?style=for-the-badge&color=fc8d62&logo=npm" height="20">](https://www.npmjs.com/package/koishi-plugin-mcdle)
6
-
7
- Koishi 的 Minecraft 猜词游戏插件。
8
-
9
- ## 使用
10
-
11
- `mcdle.猜 [名称]` 开局或猜测。系统随机选择生物、物品或方块,按属性颜色提示接近程度。
12
-
13
- ## 提示
14
-
15
- | 符号 | 含义 |
16
- | --- | --- |
17
- | 🟩 | 完全匹配 |
18
- | 🟨 | 部分匹配 |
19
- | 🟥 | 不匹配 |
20
- | 🟥⬆️ / 🟥⬇️ | 答案更大 / 更小 |
21
-
22
- ## 指令
23
-
24
- | 指令 | 说明 |
25
- | --- | --- |
26
- | `mcdle` | 查看帮助 |
27
- | `mcdle.猜 [名称]` | 开局或猜测 |
28
- | `mcdle.帮助` | 完整说明 |
29
- | `mcdle.排行榜` | 群内战绩 |
30
- | `mcdle.词库` | 全部词条 |
31
-
32
- ## QQ 群
33
-
34
- 956758505
35
-
36
- <br>
37
-
38
- #### License
39
-
40
- <sup>
41
- Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
42
- 2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
43
- </sup>
44
-
45
- <br>
46
-
47
- <sub>
48
- Unless you explicitly state otherwise, any contribution intentionally submitted
49
- for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
50
- be dual licensed as above, without any additional terms or conditions.
51
- </sub>