koishi-plugin-mcdle 0.0.7 → 0.1.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/index.js CHANGED
@@ -14618,6 +14618,958 @@ var itemChineseTitle = [
14618
14618
  "铜粒"
14619
14619
  ];
14620
14620
 
14621
+ // src/view.ts
14622
+ var PIXELS = {
14623
+ // 品牌与模式
14624
+ creeper: [
14625
+ "........",
14626
+ ".##..##.",
14627
+ ".##..##.",
14628
+ "...##...",
14629
+ "..####..",
14630
+ "..####..",
14631
+ "..#..#..",
14632
+ "........"
14633
+ ],
14634
+ grassblock: [
14635
+ "########",
14636
+ "########",
14637
+ "#++++++#",
14638
+ "#++++++#",
14639
+ "#++++++#",
14640
+ "#++++++#",
14641
+ "#++++++#",
14642
+ "########"
14643
+ ],
14644
+ ingot: [
14645
+ "........",
14646
+ "..####..",
14647
+ ".######.",
14648
+ "########",
14649
+ "########",
14650
+ ".######.",
14651
+ "..####..",
14652
+ "........"
14653
+ ],
14654
+ // 状态
14655
+ hit: [
14656
+ ".......#",
14657
+ "......##",
14658
+ "#....##.",
14659
+ "##..##..",
14660
+ ".##.##..",
14661
+ "..####..",
14662
+ "...##...",
14663
+ "........"
14664
+ ],
14665
+ half: [
14666
+ "########",
14667
+ "####...#",
14668
+ "####...#",
14669
+ "####...#",
14670
+ "####...#",
14671
+ "####...#",
14672
+ "####...#",
14673
+ "########"
14674
+ ],
14675
+ miss: [
14676
+ "##....##",
14677
+ "###..###",
14678
+ ".######.",
14679
+ "..####..",
14680
+ "..####..",
14681
+ ".######.",
14682
+ "###..###",
14683
+ "##....##"
14684
+ ],
14685
+ up: [
14686
+ "...##...",
14687
+ "..####..",
14688
+ ".######.",
14689
+ "########",
14690
+ "...##...",
14691
+ "...##...",
14692
+ "...##...",
14693
+ "...##..."
14694
+ ],
14695
+ down: [
14696
+ "...##...",
14697
+ "...##...",
14698
+ "...##...",
14699
+ "...##...",
14700
+ "########",
14701
+ ".######.",
14702
+ "..####..",
14703
+ "...##..."
14704
+ ],
14705
+ unknown: [
14706
+ "..####..",
14707
+ ".##..##.",
14708
+ ".....##.",
14709
+ "....##..",
14710
+ "...##...",
14711
+ "...##...",
14712
+ "........",
14713
+ "...##..."
14714
+ ],
14715
+ // 字段
14716
+ health: [
14717
+ ".##..##.",
14718
+ "########",
14719
+ "########",
14720
+ "########",
14721
+ ".######.",
14722
+ "..####..",
14723
+ "...##...",
14724
+ "........"
14725
+ ],
14726
+ height: [
14727
+ "...##...",
14728
+ "..####..",
14729
+ "...##...",
14730
+ "...##...",
14731
+ "...##...",
14732
+ "...##...",
14733
+ "..####..",
14734
+ "...##..."
14735
+ ],
14736
+ behavior: [
14737
+ "......##",
14738
+ ".....##+",
14739
+ "....##..",
14740
+ "...##...",
14741
+ "..##....",
14742
+ "+##.....",
14743
+ "##+.....",
14744
+ "#......."
14745
+ ],
14746
+ spawn: [
14747
+ ".+#..#+.",
14748
+ "+##.###.",
14749
+ ".####+..",
14750
+ "..###...",
14751
+ "...##...",
14752
+ "...##...",
14753
+ "..####..",
14754
+ ".++++++."
14755
+ ],
14756
+ classification: [
14757
+ "##..##..",
14758
+ "##..##..",
14759
+ "........",
14760
+ "..##..##",
14761
+ "..##..##",
14762
+ "........",
14763
+ "##..##..",
14764
+ "##..##.."
14765
+ ],
14766
+ release: [
14767
+ "..####..",
14768
+ ".#....#.",
14769
+ "#..#...#",
14770
+ "#..#...#",
14771
+ "#..####.",
14772
+ "#......#",
14773
+ ".#....#.",
14774
+ "..####.."
14775
+ ],
14776
+ renewable: [
14777
+ "..####..",
14778
+ ".##..##.",
14779
+ "##....##",
14780
+ "........",
14781
+ "........",
14782
+ "##....##",
14783
+ ".##..##.",
14784
+ "..####.."
14785
+ ],
14786
+ stackable: [
14787
+ "...#####",
14788
+ "...#...#",
14789
+ ".#####.#",
14790
+ ".#...#.#",
14791
+ ".#...###",
14792
+ "#####...",
14793
+ "#...#...",
14794
+ "#####..."
14795
+ ],
14796
+ tool: [
14797
+ "##....##",
14798
+ ".##..##.",
14799
+ "..####..",
14800
+ "...##...",
14801
+ "...##...",
14802
+ "...##...",
14803
+ "...##...",
14804
+ "...##..."
14805
+ ],
14806
+ blast: [
14807
+ "#..##..#",
14808
+ ".#.##.#.",
14809
+ "..####..",
14810
+ "########",
14811
+ "########",
14812
+ "..####..",
14813
+ ".#.##.#.",
14814
+ "#..##..#"
14815
+ ],
14816
+ hardness: [
14817
+ "..####..",
14818
+ ".#++###.",
14819
+ "##+#####",
14820
+ "########",
14821
+ ".###++#.",
14822
+ "..####..",
14823
+ "........",
14824
+ "........"
14825
+ ],
14826
+ luminous: [
14827
+ "..+##+..",
14828
+ ".+####+.",
14829
+ "..####..",
14830
+ "...##...",
14831
+ "...##...",
14832
+ "...##...",
14833
+ "...##...",
14834
+ "..####.."
14835
+ ],
14836
+ fullblock: [
14837
+ "########",
14838
+ "#++++++#",
14839
+ "#++++++#",
14840
+ "#++++++#",
14841
+ "#++++++#",
14842
+ "#++++++#",
14843
+ "#++++++#",
14844
+ "########"
14845
+ ],
14846
+ flammable: [
14847
+ "...##...",
14848
+ "..####..",
14849
+ "..####..",
14850
+ ".######.",
14851
+ ".##++##.",
14852
+ "##+..+##",
14853
+ "##+..+##",
14854
+ ".######."
14855
+ ],
14856
+ firecatch: [
14857
+ "+..##..+",
14858
+ "..####..",
14859
+ ".######.",
14860
+ ".##++##.",
14861
+ "##+..+##",
14862
+ "##+..+##",
14863
+ ".######.",
14864
+ "..####.."
14865
+ ],
14866
+ transparent: [
14867
+ "########",
14868
+ "#..##..#",
14869
+ "#..##..#",
14870
+ "########",
14871
+ "#..##..#",
14872
+ "#..##..#",
14873
+ "########",
14874
+ "........"
14875
+ ],
14876
+ rarity: [
14877
+ "...##...",
14878
+ "+..##..+",
14879
+ "..####..",
14880
+ "########",
14881
+ "########",
14882
+ "..####..",
14883
+ "+..##..+",
14884
+ "...##..."
14885
+ ],
14886
+ obtaining: [
14887
+ ".######.",
14888
+ "########",
14889
+ "#+.##.+#",
14890
+ "########",
14891
+ "#++++++#",
14892
+ "#++++++#",
14893
+ "########",
14894
+ "........"
14895
+ ],
14896
+ usage: [
14897
+ ".##..##.",
14898
+ ".##..##.",
14899
+ ".######.",
14900
+ "..####..",
14901
+ "..####..",
14902
+ "..####..",
14903
+ "..####..",
14904
+ "..####.."
14905
+ ],
14906
+ inventory: [
14907
+ "..#..#..",
14908
+ ".######.",
14909
+ "########",
14910
+ "#+####+#",
14911
+ "#+#..#+#",
14912
+ "#+####+#",
14913
+ "########",
14914
+ ".######."
14915
+ ],
14916
+ recipe: [
14917
+ ".######.",
14918
+ "#++++++#",
14919
+ "#+####+#",
14920
+ "#++++++#",
14921
+ "#+####+#",
14922
+ "#++++++#",
14923
+ "#+####+#",
14924
+ ".######."
14925
+ ],
14926
+ loot: [
14927
+ ".######.",
14928
+ "##+..+##",
14929
+ "########",
14930
+ ".######.",
14931
+ ".######.",
14932
+ "..####..",
14933
+ "..####..",
14934
+ "...##..."
14935
+ ],
14936
+ // 装饰
14937
+ crown: [
14938
+ "#......#",
14939
+ "#..##..#",
14940
+ "#.####.#",
14941
+ "##.##.##",
14942
+ "########",
14943
+ "#+####+#",
14944
+ "########",
14945
+ "........"
14946
+ ],
14947
+ trophy: [
14948
+ "########",
14949
+ "#+####+#",
14950
+ "########",
14951
+ ".######.",
14952
+ "..####..",
14953
+ "...##...",
14954
+ "..####..",
14955
+ ".######."
14956
+ ]
14957
+ };
14958
+ function px(name2, size = 14, cls = "") {
14959
+ const rows = PIXELS[name2];
14960
+ if (!rows) return "";
14961
+ const h2 = rows.length;
14962
+ const w = Math.max(...rows.map((r) => r.length));
14963
+ const rects = [];
14964
+ for (let y = 0; y < h2; y++) {
14965
+ const row = rows[y].padEnd(w, ".");
14966
+ let x = 0;
14967
+ while (x < w) {
14968
+ const ch = row[x];
14969
+ if (ch !== "#" && ch !== "+") {
14970
+ x++;
14971
+ continue;
14972
+ }
14973
+ let len = 1;
14974
+ while (x + len < w && row[x + len] === ch) len++;
14975
+ rects.push(
14976
+ `<rect x="${x}" y="${y}" width="${len}" height="1"${ch === "+" ? ' opacity=".42"' : ""}/>`
14977
+ );
14978
+ x += len;
14979
+ }
14980
+ }
14981
+ return `<svg class="px ${cls}" viewBox="0 0 ${w} ${h2}" width="${size}" height="${Math.round(size * h2 / w)}" fill="currentColor" shape-rendering="crispEdges" aria-hidden="true">${rects.join("")}</svg>`;
14982
+ }
14983
+ __name(px, "px");
14984
+ var MODES = {
14985
+ mob: {
14986
+ name: "生物",
14987
+ icon: "creeper",
14988
+ accent: "#8ec96a",
14989
+ pixelArt: false,
14990
+ tagline: "从生命、体型与生成方式里辨认它",
14991
+ total: mobData.length
14992
+ },
14993
+ item: {
14994
+ name: "物品",
14995
+ icon: "ingot",
14996
+ accent: "#e7b249",
14997
+ pixelArt: true,
14998
+ tagline: "从稀有度、来源与用途里辨认它",
14999
+ total: itemData.length
15000
+ },
15001
+ block: {
15002
+ name: "方块",
15003
+ icon: "grassblock",
15004
+ accent: "#7fb0d6",
15005
+ pixelArt: true,
15006
+ tagline: "从硬度、工具与光学性质里辨认它",
15007
+ total: blockData.length
15008
+ }
15009
+ };
15010
+ var FIELDS = {
15011
+ mob: ["health", "height", "behavior", "spawn", "classification", "initial_release"],
15012
+ block: [
15013
+ "hardness",
15014
+ "blast_resistance",
15015
+ "stackable",
15016
+ "tool",
15017
+ "renewable",
15018
+ "luminous",
15019
+ "full_block",
15020
+ "transparent",
15021
+ "flammable",
15022
+ "fire_catch",
15023
+ "initial_release"
15024
+ ],
15025
+ item: [
15026
+ "rarity",
15027
+ "stackable",
15028
+ "renewable",
15029
+ "obtaining",
15030
+ "usage",
15031
+ "recipe",
15032
+ "loot",
15033
+ "inventory_categories",
15034
+ "initial_release"
15035
+ ]
15036
+ };
15037
+ var FIELD_ICON = {
15038
+ health: "health",
15039
+ height: "height",
15040
+ behavior: "behavior",
15041
+ spawn: "spawn",
15042
+ classification: "classification",
15043
+ initial_release: "release",
15044
+ renewable: "renewable",
15045
+ stackable: "stackable",
15046
+ tool: "tool",
15047
+ blast_resistance: "blast",
15048
+ hardness: "hardness",
15049
+ luminous: "luminous",
15050
+ full_block: "fullblock",
15051
+ transparent: "transparent",
15052
+ flammable: "flammable",
15053
+ fire_catch: "firecatch",
15054
+ rarity: "rarity",
15055
+ obtaining: "obtaining",
15056
+ usage: "usage",
15057
+ inventory_categories: "inventory",
15058
+ recipe: "recipe",
15059
+ loot: "loot"
15060
+ };
15061
+ var SHORT_KEY = {
15062
+ full_block: "完整方块",
15063
+ fire_catch: "引燃性",
15064
+ inventory_categories: "类别",
15065
+ blast_resistance: "爆炸抗性",
15066
+ initial_release: "版本"
15067
+ };
15068
+ function fieldLabel(key) {
15069
+ return SHORT_KEY[key] || keyMap[key] || key;
15070
+ }
15071
+ __name(fieldLabel, "fieldLabel");
15072
+ function fieldIcon(key) {
15073
+ return FIELD_ICON[key] || "unknown";
15074
+ }
15075
+ __name(fieldIcon, "fieldIcon");
15076
+ function withUnit(key, value) {
15077
+ if (key === "stackable") {
15078
+ if (value <= 0) return "不适用";
15079
+ if (value === 1) return "不可堆叠";
15080
+ return `${value} 个`;
15081
+ }
15082
+ if (key === "health") return `${value} 点`;
15083
+ if (key === "height") return `${value} 格`;
15084
+ return String(value);
15085
+ }
15086
+ __name(withUnit, "withUnit");
15087
+ function displayValues(key, raw) {
15088
+ if (Array.isArray(raw)) {
15089
+ if (!raw.length) return [{ text: "无", raw: "" }];
15090
+ return raw.map((v) => ({ text: valueMap[key]?.[v] ?? String(v), raw: String(v) }));
15091
+ }
15092
+ if (typeof raw === "number") return [{ text: withUnit(key, raw), raw: String(raw) }];
15093
+ const s = String(raw ?? "");
15094
+ return [{ text: valueMap[key]?.[s] ?? s, raw: s }];
15095
+ }
15096
+ __name(displayValues, "displayValues");
15097
+ var STATUS_META = {
15098
+ true: { cls: "s-hit", icon: "hit", label: "完全匹配", emoji: "🟩" },
15099
+ mixed: { cls: "s-half", icon: "half", label: "部分匹配", emoji: "🟨" },
15100
+ false: { cls: "s-miss", icon: "miss", label: "不匹配", emoji: "🟥" },
15101
+ false_up: { cls: "s-miss s-up", icon: "up", label: "答案更大", emoji: "🟥⬆️" },
15102
+ false_down: { cls: "s-miss s-down", icon: "down", label: "答案更小", emoji: "🟥⬇️" },
15103
+ unknown: { cls: "s-none", icon: "unknown", label: "未知", emoji: "❓" }
15104
+ };
15105
+ function statusOf(guess, key) {
15106
+ const v = guess?.[`${key}_gui`];
15107
+ return STATUS_META[v] ? v : "unknown";
15108
+ }
15109
+ __name(statusOf, "statusOf");
15110
+ var FONT_STACK = `"Minecraft", "Fusion Pixel 12px monospaced", "Fusion Pixel 12px", "Zpix", "HarmonyOS Sans SC", "PingFang SC", "Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei", ui-sans-serif, sans-serif`;
15111
+ var CSS = `
15112
+ *{margin:0;padding:0;box-sizing:border-box}
15113
+ html,body{background:transparent}
15114
+ body{font-family:${FONT_STACK};-webkit-font-smoothing:antialiased}
15115
+ .px{display:block;flex:none}
15116
+
15117
+ #card{
15118
+ position:relative;
15119
+ display:inline-block;
15120
+ background:#1b201a;
15121
+ color:#e9ece5;
15122
+ border:4px solid #0b0d0a;
15123
+ box-shadow:inset 0 0 0 4px #3c4438, inset 0 0 0 5px #262c24, 0 0 0 1px #000;
15124
+ /* 石质底噪:两层错位的斜向条纹,比纯色更有材质但不抢内容 */
15125
+ background-image:
15126
+ repeating-linear-gradient(135deg, rgba(255,255,255,.016) 0 2px, transparent 2px 5px),
15127
+ repeating-linear-gradient(45deg, rgba(0,0,0,.10) 0 3px, transparent 3px 7px);
15128
+ }
15129
+ #card::after{
15130
+ content:"";position:absolute;inset:0;pointer-events:none;
15131
+ background:radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,.05), transparent 60%);
15132
+ }
15133
+ .pad{padding:22px 24px 20px}
15134
+
15135
+ /* ── 页眉:横切面的草方块 ────────────────────────────── */
15136
+ .hd{position:relative;overflow:hidden;background:#3a2a1c;
15137
+ background-image:
15138
+ repeating-linear-gradient(90deg, rgba(0,0,0,.14) 0 4px, transparent 4px 9px),
15139
+ repeating-linear-gradient(0deg, rgba(255,255,255,.05) 0 3px, transparent 3px 8px);
15140
+ border-bottom:4px solid #0b0d0a;padding:16px 24px 15px}
15141
+ .hd .turf{position:absolute;left:0;right:0;top:0;height:9px;background:var(--accent);
15142
+ box-shadow:inset 0 -3px 0 rgba(0,0,0,.28), 0 3px 0 rgba(0,0,0,.35)}
15143
+ .hd .turf::after{content:"";position:absolute;inset:0;
15144
+ background:repeating-linear-gradient(90deg, rgba(255,255,255,.16) 0 3px, transparent 3px 8px)}
15145
+ .hd-row{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-top:9px}
15146
+ .brand{display:flex;align-items:center;gap:10px}
15147
+ .brand .mark{color:var(--accent);filter:drop-shadow(2px 2px 0 rgba(0,0,0,.55))}
15148
+ .brand .word{font-size:26px;font-weight:800;letter-spacing:.22em;line-height:1;
15149
+ text-shadow:3px 3px 0 rgba(0,0,0,.55)}
15150
+ .brand .word b{color:var(--accent)}
15151
+ .hd .sub{margin-top:7px;font-size:12.5px;letter-spacing:.14em;color:#a8b09f;
15152
+ text-shadow:2px 2px 0 rgba(0,0,0,.4)}
15153
+ .hd-meta{text-align:right;flex:none}
15154
+ .hd-meta .big{font-size:22px;font-weight:800;color:var(--accent);line-height:1;
15155
+ text-shadow:3px 3px 0 rgba(0,0,0,.5)}
15156
+ .hd-meta .cap{font-size:11px;letter-spacing:.2em;color:#939b8b;margin-top:6px}
15157
+
15158
+ /* 模式徽记 */
15159
+ .badge{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;font-size:12.5px;
15160
+ letter-spacing:.1em;color:#12160f;background:var(--accent);font-weight:700;
15161
+ box-shadow:inset -3px -3px 0 rgba(0,0,0,.26), inset 3px 3px 0 rgba(255,255,255,.34)}
15162
+
15163
+ /* ── 猜测板 ───────────────────────────────────────── */
15164
+ table{border-collapse:separate;border-spacing:5px;margin:0 -5px}
15165
+ th{padding:0 0 6px;vertical-align:bottom}
15166
+ .col{display:flex;flex-direction:column;align-items:center;gap:5px;
15167
+ min-width:80px;max-width:132px;color:#8d9683}
15168
+ .col .lb{font-size:11px;letter-spacing:.08em;color:#a5ad9a;text-align:center;line-height:1.25}
15169
+ th.n .col{align-items:flex-start;min-width:150px}
15170
+
15171
+ td{padding:0}
15172
+ .tile{position:relative;overflow:hidden;height:100%;min-height:56px;min-width:76px;max-width:132px;
15173
+ padding:8px 9px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
15174
+ background:var(--tile);
15175
+ box-shadow:inset -4px -4px 0 rgba(0,0,0,.30), inset 4px 4px 0 rgba(255,255,255,.16)}
15176
+ /* 多标签字段给更宽的上限,否则一列里会折成很高的一坨 */
15177
+ .tile.list{max-width:168px}
15178
+ th.list .col{max-width:168px}
15179
+ .tile .v{font-size:13px;font-weight:700;line-height:1.3;text-align:center;color:#f2f4ee;
15180
+ text-shadow:2px 2px 0 rgba(0,0,0,.4);word-break:break-word}
15181
+ .tile .chips{display:flex;flex-wrap:wrap;gap:3px;justify-content:center}
15182
+ .chip{font-size:11.5px;line-height:1.35;padding:2px 5px;color:#eef1e9;
15183
+ background:rgba(0,0,0,.22);box-shadow:inset 0 0 0 1px rgba(255,255,255,.07)}
15184
+ /* 部分匹配时把真正命中的标签点亮,让「黄」变得可读 */
15185
+ .chip.on{background:rgba(255,255,255,.9);color:#1b2016;font-weight:700;
15186
+ box-shadow:inset -2px -2px 0 rgba(0,0,0,.2)}
15187
+ .tile .flag{position:absolute;top:4px;right:4px;color:rgba(255,255,255,.8)}
15188
+ /* 数值方向不只靠角标:整块的上/下边缘会亮起一条,指向答案所在的一侧 */
15189
+ .tile.s-up::after,.tile.s-down::after{content:"";position:absolute;left:0;right:0;height:5px;
15190
+ background:rgba(255,255,255,.45)}
15191
+ .tile.s-up::after{top:0}
15192
+ .tile.s-down::after{bottom:0}
15193
+
15194
+ .s-hit{--tile:#4a8a35}
15195
+ .s-half{--tile:#b8862a}
15196
+ .s-miss{--tile:#8e3f34}
15197
+ .s-none{--tile:#3a4036}
15198
+
15199
+ /* 名称格 */
15200
+ .name{display:flex;align-items:center;gap:11px;min-height:56px;min-width:150px;padding:7px 11px 7px 8px;
15201
+ background:#2a3027;
15202
+ box-shadow:inset -4px -4px 0 rgba(0,0,0,.3), inset 4px 4px 0 rgba(255,255,255,.10)}
15203
+ .name.correct{background:#4a8a35}
15204
+ .thumb{width:42px;height:42px;flex:none;display:flex;align-items:center;justify-content:center;
15205
+ background:rgba(0,0,0,.28);box-shadow:inset 0 0 0 2px rgba(255,255,255,.06)}
15206
+ .thumb img{max-width:38px;max-height:38px;display:block}
15207
+ .thumb img.sharp{image-rendering:pixelated}
15208
+ .thumb img.broken{display:none}
15209
+ .thumb .fb{display:none;color:#6f7866}
15210
+ .thumb.failed .fb{display:block}
15211
+ .name .txt{min-width:0}
15212
+ .name .cn{font-size:15px;font-weight:800;line-height:1.2;text-shadow:2px 2px 0 rgba(0,0,0,.45)}
15213
+ .name .en{font-size:10.5px;letter-spacing:.06em;color:#98a08e;margin-top:3px;
15214
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:150px}
15215
+ .name.correct .en{color:rgba(255,255,255,.72)}
15216
+ .name .no{font-size:10px;color:#79826e;margin-top:3px}
15217
+ .name.correct .no{color:rgba(255,255,255,.6)}
15218
+
15219
+ /* 最新一行的强调:外侧描边 + 左侧标记 */
15220
+ tr.fresh .tile,tr.fresh .name{box-shadow:inset -4px -4px 0 rgba(0,0,0,.30),
15221
+ inset 4px 4px 0 rgba(255,255,255,.16), 0 0 0 2px var(--accent)}
15222
+ tr.fresh .name{position:relative}
15223
+ tr.fresh .name::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--accent)}
15224
+
15225
+ /* ── 进度条 ───────────────────────────────────────── */
15226
+ .prog{display:flex;align-items:center;gap:10px;margin-top:18px}
15227
+ .prog .bar{flex:1;height:12px;background:#12160f;
15228
+ box-shadow:inset 2px 2px 0 rgba(0,0,0,.5), inset -2px -2px 0 rgba(255,255,255,.05)}
15229
+ .prog .fill{height:100%;background:var(--accent);
15230
+ box-shadow:inset 0 -3px 0 rgba(0,0,0,.25), inset 0 3px 0 rgba(255,255,255,.28)}
15231
+ .prog .num{font-size:12px;color:#a5ad9a;letter-spacing:.08em;flex:none}
15232
+
15233
+ /* ── 图例 / 页脚 ──────────────────────────────────── */
15234
+ .legend{display:flex;flex-wrap:wrap;gap:7px 9px;align-items:center;
15235
+ border-top:3px solid #0f120e;padding:13px 24px;background:rgba(0,0,0,.22)}
15236
+ .lg{display:flex;align-items:center;gap:6px;padding:4px 8px;font-size:11.5px;color:#dfe3d9;
15237
+ background:var(--tile);box-shadow:inset -2px -2px 0 rgba(0,0,0,.28), inset 2px 2px 0 rgba(255,255,255,.14)}
15238
+ .tip{margin-left:auto;font-size:11.5px;color:#828b78;letter-spacing:.05em}
15239
+
15240
+ /* ── 通用区块 ─────────────────────────────────────── */
15241
+ .sec{margin-top:20px}
15242
+ .sec:first-child{margin-top:0}
15243
+ .sec-t{display:flex;align-items:center;gap:8px;font-size:12px;letter-spacing:.18em;color:#8d9683;
15244
+ margin-bottom:11px}
15245
+ .sec-t::after{content:"";flex:1;height:3px;background:
15246
+ repeating-linear-gradient(90deg,#333a2f 0 4px,transparent 4px 8px)}
15247
+ .panel{background:#232820;padding:14px 16px;
15248
+ box-shadow:inset -3px -3px 0 rgba(0,0,0,.32), inset 3px 3px 0 rgba(255,255,255,.06)}
15249
+ .rows{display:flex;flex-direction:column;gap:8px}
15250
+ .row{display:flex;align-items:center;gap:10px;font-size:13.5px;color:#dfe3d9;line-height:1.5}
15251
+ .row .k{color:var(--accent);flex:none}
15252
+ .row .m{color:#8d9683;font-size:12px}
15253
+ .cmd{font-weight:700;color:#f0f3ea;background:rgba(0,0,0,.3);padding:2px 7px;
15254
+ box-shadow:inset 0 0 0 1px rgba(255,255,255,.07)}
15255
+
15256
+ .grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
15257
+ .mode-card{padding:13px 14px;background:#232820;
15258
+ box-shadow:inset -3px -3px 0 rgba(0,0,0,.32), inset 3px 3px 0 rgba(255,255,255,.06)}
15259
+ .mode-card .top{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:800}
15260
+ .mode-card .cnt{font-size:11.5px;color:#8d9683;margin-top:7px;letter-spacing:.06em}
15261
+ .mode-card .tl{font-size:11.5px;color:#aab29d;margin-top:5px;line-height:1.5}
15262
+
15263
+ .attrs{display:flex;flex-wrap:wrap;gap:7px}
15264
+ .attr{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:12.5px;color:#dfe3d9;
15265
+ background:#2b3128;box-shadow:inset -3px -3px 0 rgba(0,0,0,.3), inset 3px 3px 0 rgba(255,255,255,.08)}
15266
+ .attr .px{color:var(--accent)}
15267
+
15268
+ /* ── 揭晓卡 ───────────────────────────────────────── */
15269
+ .reveal{display:flex;align-items:center;gap:20px}
15270
+ .reveal .who{flex:1;min-width:0}
15271
+ .reveal .cn .crown{display:inline-block;vertical-align:middle;margin-left:12px;color:var(--accent)}
15272
+ .reveal .big-thumb{width:132px;height:132px;flex:none;display:flex;align-items:center;justify-content:center;
15273
+ background:#232820;box-shadow:inset -4px -4px 0 rgba(0,0,0,.34), inset 4px 4px 0 rgba(255,255,255,.08)}
15274
+ .reveal .big-thumb img{max-width:116px;max-height:116px;display:block}
15275
+ .reveal .big-thumb img.sharp{image-rendering:pixelated}
15276
+ .reveal .big-thumb img.broken{display:none}
15277
+ .reveal .big-thumb .fb{display:none;color:#5f6857}
15278
+ .reveal .big-thumb.failed .fb{display:block}
15279
+ .reveal .cn{font-size:34px;font-weight:800;line-height:1.1;text-shadow:3px 3px 0 rgba(0,0,0,.5)}
15280
+ .reveal .en{font-size:13px;letter-spacing:.14em;color:#98a08e;margin-top:8px}
15281
+ .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}
15282
+ .stat{padding:11px 14px;background:#2b3128;
15283
+ box-shadow:inset -3px -3px 0 rgba(0,0,0,.3), inset 3px 3px 0 rgba(255,255,255,.08)}
15284
+ .stat .n{font-size:19px;font-weight:800;color:var(--accent);line-height:1;
15285
+ text-shadow:2px 2px 0 rgba(0,0,0,.45)}
15286
+ .stat .c{font-size:10.5px;color:#8d9683;margin-top:6px;letter-spacing:.1em}
15287
+
15288
+ /* ── 排行榜 ───────────────────────────────────────── */
15289
+ .rank{display:flex;flex-direction:column;gap:6px}
15290
+ .rk{display:flex;align-items:center;gap:13px;padding:10px 13px;background:#232820;
15291
+ box-shadow:inset -3px -3px 0 rgba(0,0,0,.32), inset 3px 3px 0 rgba(255,255,255,.06)}
15292
+ .rk .pos{width:34px;height:34px;flex:none;display:flex;align-items:center;justify-content:center;
15293
+ font-size:15px;font-weight:800;color:#12160f;background:#59614f;
15294
+ box-shadow:inset -3px -3px 0 rgba(0,0,0,.24), inset 3px 3px 0 rgba(255,255,255,.28)}
15295
+ .rk.p1 .pos{background:#e8b53c}
15296
+ .rk.p2 .pos{background:#c3cad3}
15297
+ .rk.p3 .pos{background:#c47a45}
15298
+ .rk.p1{box-shadow:inset -3px -3px 0 rgba(0,0,0,.32), inset 3px 3px 0 rgba(255,255,255,.06), 0 0 0 2px #e8b53c}
15299
+ .rk .who{flex:1;min-width:0}
15300
+ .rk .nm{font-size:14.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
15301
+ max-width:260px}
15302
+ .rk .split{display:flex;gap:9px;margin-top:6px}
15303
+ .rk .sp{display:flex;align-items:center;gap:4px;font-size:11px;color:#98a08e}
15304
+ .rk .tot{flex:none;text-align:right}
15305
+ .rk .tot .n{font-size:21px;font-weight:800;color:var(--accent);line-height:1;
15306
+ text-shadow:2px 2px 0 rgba(0,0,0,.45)}
15307
+ .rk .tot .c{font-size:10px;color:#7c856f;margin-top:5px;letter-spacing:.1em}
15308
+
15309
+ .empty{padding:34px 20px;min-width:400px;text-align:center;color:#7c856f;font-size:13.5px;line-height:2}
15310
+ .empty .px{margin:0 auto 14px;color:#3f4739}
15311
+ `;
15312
+ function shell(body, opts) {
15313
+ const width = opts.width ? `width:${opts.width}px;` : "";
15314
+ return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><style>${CSS}</style></head>
15315
+ <body><div id="card" style="--accent:${opts.accent};${width}">${body}</div></body></html>`;
15316
+ }
15317
+ __name(shell, "shell");
15318
+ function esc(s) {
15319
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
15320
+ }
15321
+ __name(esc, "esc");
15322
+ function header(mode, sub, meta) {
15323
+ const m = mode ? MODES[mode] : null;
15324
+ const mark = px(m ? m.icon : "creeper", 22, "mark");
15325
+ const right = meta ? `<div class="hd-meta"><div class="big">${esc(meta.big)}</div><div class="cap">${esc(meta.cap)}</div></div>` : "";
15326
+ return `<header class="hd"><div class="turf"></div><div class="hd-row"><div>
15327
+ <div class="brand">${mark}<div class="word">MC<b>DLE</b></div></div>
15328
+ <div class="sub">${esc(sub)}</div>
15329
+ </div>${right}</div></header>`;
15330
+ }
15331
+ __name(header, "header");
15332
+ function thumb(url, mode, big = false) {
15333
+ const cls = MODES[mode].pixelArt ? ' class="sharp"' : "";
15334
+ const box = big ? "big-thumb" : "thumb";
15335
+ const fb = px(MODES[mode].icon, big ? 56 : 22, "fb");
15336
+ if (!url) return `<div class="${box} failed">${fb}</div>`;
15337
+ return `<div class="${box}">${fb}<img src="${esc(url)}"${cls} onerror="this.classList.add('broken');this.parentNode.classList.add('failed')"></div>`;
15338
+ }
15339
+ __name(thumb, "thumb");
15340
+ var LEGEND_ORDER = ["true", "mixed", "false", "false_up", "false_down"];
15341
+ function legendChips() {
15342
+ return LEGEND_ORDER.map((s) => {
15343
+ const m = STATUS_META[s];
15344
+ return `<div class="lg ${m.cls}">${px(m.icon, 11)}<span>${m.label}</span></div>`;
15345
+ }).join("");
15346
+ }
15347
+ __name(legendChips, "legendChips");
15348
+ function legendRow(tip) {
15349
+ return `<footer class="legend">${legendChips()}<div class="tip">${esc(tip)}</div></footer>`;
15350
+ }
15351
+ __name(legendRow, "legendRow");
15352
+ function noteRow(note) {
15353
+ return `<footer class="legend"><div class="tip" style="margin:0">${esc(note)}</div></footer>`;
15354
+ }
15355
+ __name(noteRow, "noteRow");
15356
+ function tileCell(guess, key) {
15357
+ const st = statusOf(guess, key);
15358
+ const m = STATUS_META[st];
15359
+ const values = displayValues(key, guess[key]);
15360
+ const hits = Array.isArray(guess[`${key}_hits`]) ? guess[`${key}_hits`].map(String) : [];
15361
+ const isList = Array.isArray(guess[key]);
15362
+ const body = isList ? `<div class="chips">${values.map(
15363
+ (v) => `<span class="chip${st === "mixed" && hits.includes(v.raw) ? " on" : ""}">${esc(v.text)}</span>`
15364
+ ).join("")}</div>` : `<div class="v">${esc(values[0].text)}</div>`;
15365
+ return `<td><div class="tile ${m.cls}${isList ? " list" : ""}"><span class="flag">${px(m.icon, 11)}</span>${body}</div></td>`;
15366
+ }
15367
+ __name(tileCell, "tileCell");
15368
+ function isListField(key, sample) {
15369
+ return Array.isArray(sample?.[key]);
15370
+ }
15371
+ __name(isListField, "isListField");
15372
+ function boardCard(opts) {
15373
+ const { mode, guesses } = opts;
15374
+ const meta = MODES[mode];
15375
+ const fields = FIELDS[mode];
15376
+ const last = guesses[guesses.length - 1];
15377
+ const head = `<tr><th class="n"><div class="col"><span class="lb">${guesses.length} 次猜测</span></div></th>` + fields.map(
15378
+ (k) => `<th${isListField(k, last) ? ' class="list"' : ""}><div class="col">${px(fieldIcon(k), 15)}<span class="lb">${esc(fieldLabel(k))}</span></div></th>`
15379
+ ).join("") + "</tr>";
15380
+ const rows = guesses.map((g, i) => {
15381
+ const fresh = i === guesses.length - 1 ? ' class="fresh"' : "";
15382
+ const solved = fields.every((k) => statusOf(g, k) === "true") && g.chinese_title_gui === "true";
15383
+ const name2 = `<td><div class="name${solved ? " correct" : ""}">${thumb(g.wiki_image_url, mode)}<div class="txt"><div class="cn">${esc(g.chinese_title)}${solved ? " " + px("crown", 13) : ""}</div><div class="en">${esc(g.title || "")}</div><div class="no">#${String(i + 1).padStart(2, "0")}</div></div></div></td>`;
15384
+ return `<tr${fresh}>${name2}${fields.map((k) => tileCell(g, k)).join("")}</tr>`;
15385
+ }).join("");
15386
+ const locked = fields.filter((k) => statusOf(last, k) === "true").length;
15387
+ const pct = Math.round(locked / fields.length * 100);
15388
+ const prog = `<div class="prog"><div class="num">已锁定 ${locked}/${fields.length}</div>
15389
+ <div class="bar"><div class="fill" style="width:${pct}%"></div></div>
15390
+ <div class="num">${pct}%</div></div>`;
15391
+ const body = header(mode, `${meta.name}模式 · ${meta.tagline}`, {
15392
+ big: `#${guesses.length}`,
15393
+ cap: "猜测次数"
15394
+ }) + `<div class="pad"><table>${head}${rows}</table>${prog}</div>` + legendRow(opts.tip || "箭头指向答案所在的方向");
15395
+ return shell(body, { accent: meta.accent });
15396
+ }
15397
+ __name(boardCard, "boardCard");
15398
+ function startCard(mode, played, limit) {
15399
+ const meta = MODES[mode];
15400
+ const fields = FIELDS[mode];
15401
+ const attrs = fields.map((k) => `<div class="attr">${px(fieldIcon(k), 13)}<span>${esc(keyMap[k] || k)}</span></div>`).join("");
15402
+ const body = header(mode, "新的一局已经开始", { big: `${played}/${limit}`, cap: "今日局数" }) + `<div class="pad">
15403
+ <div class="sec">
15404
+ <div class="sec-t"><span>本 局 目 标</span></div>
15405
+ <div class="panel"><div class="rows">
15406
+ <div class="row"><span class="badge">${px(meta.icon, 13)}${esc(meta.name)}模式</span>
15407
+ <span class="m">从 ${meta.total} 个候选中锁定唯一答案</span></div>
15408
+ <div class="row"><span>${esc(meta.tagline)}。</span></div>
15409
+ </div></div>
15410
+ </div>
15411
+ <div class="sec">
15412
+ <div class="sec-t"><span>可 供 推 理 的 属 性</span></div>
15413
+ <div class="attrs">${attrs}</div>
15414
+ </div>
15415
+ <div class="sec">
15416
+ <div class="sec-t"><span>下 一 步</span></div>
15417
+ <div class="panel"><div class="rows">
15418
+ <div class="row"><span class="cmd">mcdle.猜 [名称]</span><span class="m">提交一次猜测</span></div>
15419
+ <div class="row"><span class="cmd">mcdle.词库</span><span class="m">忘了有哪些词条时翻一翻</span></div>
15420
+ </div></div>
15421
+ </div>
15422
+ </div>` + legendRow("第一枪不妨挑一个属性分布很典型的词条");
15423
+ return shell(body, { accent: meta.accent, width: 700 });
15424
+ }
15425
+ __name(startCard, "startCard");
15426
+ function formatDuration(seconds) {
15427
+ if (!Number.isFinite(seconds) || seconds < 0) return "—";
15428
+ if (seconds === 0) return "不到 1 秒";
15429
+ const mm = Math.floor(seconds / 60);
15430
+ const ss = seconds % 60;
15431
+ if (mm >= 60) return `${Math.floor(mm / 60)}时${mm % 60}分`;
15432
+ return mm > 0 ? `${mm}分${ss}秒` : `${ss}秒`;
15433
+ }
15434
+ __name(formatDuration, "formatDuration");
15435
+ function winCard(o) {
15436
+ const meta = MODES[o.mode];
15437
+ const fields = FIELDS[o.mode];
15438
+ const attrs = fields.map((k) => {
15439
+ const vs = displayValues(k, o.answer[k]).map((v) => v.text).join("、");
15440
+ return `<div class="attr">${px(fieldIcon(k), 13)}<span>${esc(fieldLabel(k))} ${esc(vs)}</span></div>`;
15441
+ }).join("");
15442
+ const left = o.limit - o.played;
15443
+ const body = header(o.mode, `${esc(o.username)} 锁定了答案`, {
15444
+ big: formatDuration(o.seconds),
15445
+ cap: "本 局 用 时"
15446
+ }) + `<div class="pad">
15447
+ <div class="reveal">
15448
+ ${thumb(o.answer.wiki_image_url, o.mode, true)}
15449
+ <div class="who">
15450
+ <div class="cn">${esc(o.answer.chinese_title)}${px("crown", 22, "crown")}</div>
15451
+ <div class="en">${esc(o.answer.title || "")}</div>
15452
+ </div>
15453
+ </div>
15454
+ <div class="sec">
15455
+ <div class="stats">
15456
+ <div class="stat"><div class="n">${o.attempts}</div><div class="c">猜 测 次 数</div></div>
15457
+ <div class="stat"><div class="n">${o.total}</div><div class="c">累 计 猜 中</div></div>
15458
+ <div class="stat"><div class="n">${left > 0 ? left : 0}</div><div class="c">今 日 剩 余 局 数</div></div>
15459
+ </div>
15460
+ </div>
15461
+ <div class="sec">
15462
+ <div class="sec-t"><span>答 案 档 案</span></div>
15463
+ <div class="attrs">${attrs}</div>
15464
+ </div>
15465
+ </div>` + noteRow("mcdle.排行榜 查看战绩");
15466
+ return shell(body, { accent: meta.accent, width: 720 });
15467
+ }
15468
+ __name(winCard, "winCard");
15469
+ function helpCard(dailyLimit, allowRepeat) {
15470
+ const modeCards = ["mob", "item", "block"].map((m) => {
15471
+ const meta = MODES[m];
15472
+ return `<div class="mode-card" style="--accent:${meta.accent}">
15473
+ <div class="top" style="color:${meta.accent}">${px(meta.icon, 17)}<span>${meta.name}</span></div>
15474
+ <div class="cnt">${meta.total} 个词条 · ${FIELDS[m].length} 项属性</div>
15475
+ <div class="tl">${meta.tagline}</div>
15476
+ </div>`;
15477
+ }).join("");
15478
+ const legend = legendChips();
15479
+ const body = header(null, "我的世界猜谜 · 玩法说明", { big: `${dailyLimit}`, cap: "每日局数上限" }) + `<div class="pad">
15480
+ <div class="sec">
15481
+ <div class="sec-t"><span>三 种 模 式</span></div>
15482
+ <div class="grid3">${modeCards}</div>
15483
+ </div>
15484
+ <div class="sec">
15485
+ <div class="sec-t"><span>怎 么 读 提 示</span></div>
15486
+ <div class="panel">
15487
+ <div class="legend" style="border:0;padding:0;background:transparent;margin-bottom:12px">${legend}</div>
15488
+ <div class="rows">
15489
+ <div class="row"><span class="k">${px("half", 11)}</span><span>部分匹配的格子里,<b>被点亮的标签</b>就是与答案重合的那几项。</span></div>
15490
+ <div class="row"><span class="k">${px("up", 11)}</span><span>数值与版本用箭头指向答案:向上代表答案更大、更晚。</span></div>
15491
+ </div>
15492
+ </div>
15493
+ </div>
15494
+ <div class="sec">
15495
+ <div class="sec-t"><span>指 令</span></div>
15496
+ <div class="panel"><div class="rows">
15497
+ <div class="row"><span class="cmd">mcdle.猜 [名称]</span><span class="m">开始一局,或提交猜测</span></div>
15498
+ <div class="row"><span class="cmd">mcdle.猜</span><span class="m">局中直接使用可回看当前棋盘</span></div>
15499
+ <div class="row"><span class="cmd">mcdle.排行榜</span><span class="m">查看群内战绩</span></div>
15500
+ <div class="row"><span class="cmd">mcdle.词库</span><span class="m">查阅全部候选词条</span></div>
15501
+ </div></div>
15502
+ </div>
15503
+ <div class="sec">
15504
+ <div class="sec-t"><span>规 则 细 则</span></div>
15505
+ <div class="panel"><div class="rows">
15506
+ <div class="row"><span class="k">·</span><span>每个群每日可开始 <b>${dailyLimit}</b> 局,跨零点重置。</span></div>
15507
+ <div class="row"><span class="k">·</span><span>${allowRepeat ? "允许重复提交已经猜过的词条。" : "同一局内不能重复提交已猜过的词条。"}</span></div>
15508
+ <div class="row"><span class="k">·</span><span>词条与数据来自 Minecraft Wiki,版本号按发布先后比较。</span></div>
15509
+ </div></div>
15510
+ </div>
15511
+ </div>` + noteRow("词条数据来自 zh.minecraft.wiki");
15512
+ return shell(body, { accent: "#8ec96a", width: 760 });
15513
+ }
15514
+ __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
+ function rankCard(entries, shown) {
15547
+ if (!entries.length) {
15548
+ 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>`;
15550
+ return shell(body2, { accent: "#e8b53c", width: 560 });
15551
+ }
15552
+ const max = Math.max(...entries.map((e) => e.total), 1);
15553
+ const rows = entries.map((e, i) => {
15554
+ const pos = i + 1;
15555
+ const split = ["mob", "item", "block"].map((m) => {
15556
+ const n = m === "mob" ? e.mob : m === "item" ? e.item : e.block;
15557
+ return `<span class="sp" style="color:${n ? MODES[m].accent : "#5f6857"}">${px(MODES[m].icon, 10)}<span>${n}</span></span>`;
15558
+ }).join("");
15559
+ const w = Math.round(e.total / max * 100);
15560
+ return `<div class="rk p${pos <= 3 ? pos : "n"}">
15561
+ <div class="pos">${pos === 1 ? px("crown", 15) : pos}</div>
15562
+ <div class="who"><div class="nm">${esc(e.username || e.userId)}</div><div class="split">${split}</div>
15563
+ <div class="prog" style="margin-top:8px"><div class="bar" style="height:6px"><div class="fill" style="width:${w}%"></div></div></div>
15564
+ </div>
15565
+ <div class="tot"><div class="n">${e.total}</div><div class="c">猜 中</div></div>
15566
+ </div>`;
15567
+ }).join("");
15568
+ const body = header(null, "群内战绩 · 按累计猜中排序", { big: `${shown}`, cap: "上榜人数" }) + `<div class="pad"><div class="rank">${rows}</div></div>` + noteRow("每猜中一局记一分,按模式分别计数");
15569
+ return shell(body, { accent: "#e8b53c", width: 560 });
15570
+ }
15571
+ __name(rankCard, "rankCard");
15572
+
14621
15573
  // src/index.ts
14622
15574
  var name = "mcdle";
14623
15575
  var usage = `# MCDLE - 我的世界猜谜游戏
@@ -14638,10 +15590,15 @@ var usage = `# MCDLE - 我的世界猜谜游戏
14638
15590
  | 🟥⬆️ | 答案更大 |
14639
15591
  | 🟥⬇️ | 答案更小 |
14640
15592
 
15593
+ ## 图片
15594
+
15595
+ 安装 \`puppeteer\` 服务后,棋盘、开局、揭晓、排行榜都会渲染成像素风卡片;
15596
+ 未安装或渲染失败时自动回退为等价的文本,功能不受影响。
15597
+
14641
15598
  ## QQ 群
14642
15599
 
14643
15600
  956758505`;
14644
- var inject = ["database"];
15601
+ var inject = { required: ["database"], optional: ["puppeteer"] };
14645
15602
  var Config = import_koishi.Schema.object({
14646
15603
  atReply: import_koishi.Schema.boolean().default(false).description("响应时@用户"),
14647
15604
  quoteReply: import_koishi.Schema.boolean().default(true).description("响应时引用消息"),
@@ -14653,7 +15610,7 @@ var Config = import_koishi.Schema.object({
14653
15610
  `撤回上一条消息的等待时间,单位是秒。值为 0 时不启用自动撤回功能。`
14654
15611
  ),
14655
15612
  allowRepeatedGuesses: import_koishi.Schema.boolean().default(false).description("允许重复猜测已猜过的词语(防止撤回时历史不可见)"),
14656
- disableImages: import_koishi.Schema.boolean().default(false).description("不发送图片(解决网络问题导致的图片下载失败)")
15613
+ disableImages: import_koishi.Schema.boolean().default(false).description("不发送图片,全部改用文本(解决网络问题导致的图片下载失败)")
14657
15614
  });
14658
15615
  function apply(ctx, cfg) {
14659
15616
  ctx.model.extend(
@@ -14723,35 +15680,161 @@ function apply(ctx, cfg) {
14723
15680
  ctx.command("mcdle.帮助").action(async ({ session }) => bz(session));
14724
15681
  ctx.command("mcdle.排行榜").action(async ({ session }) => phb(session));
14725
15682
  ctx.command("mcdle.词库").action(async ({ session }) => ck(session));
15683
+ const RULE = "────────────";
15684
+ function textCard(title, ...sections) {
15685
+ const body = sections.map((s) => Array.isArray(s) ? s.filter(Boolean).join("\n") : s).filter((s) => !!s && !!s.trim()).join("\n\n");
15686
+ return [`▍${title}`, RULE, body].filter(Boolean).join("\n");
15687
+ }
15688
+ __name(textCard, "textCard");
15689
+ function modeName(mode) {
15690
+ return MODES[mode].name;
15691
+ }
15692
+ __name(modeName, "modeName");
15693
+ function untilReset(now) {
15694
+ const next = new Date(now);
15695
+ next.setHours(24, 0, 0, 0);
15696
+ const minutes = Math.max(1, Math.round((next.getTime() - now.getTime()) / 6e4));
15697
+ const hh = Math.floor(minutes / 60);
15698
+ const mm = minutes % 60;
15699
+ return hh > 0 ? `${hh} 小时 ${mm} 分` : `${mm} 分`;
15700
+ }
15701
+ __name(untilReset, "untilReset");
15702
+ function encourage(locked, total, attempts) {
15703
+ const ratio = total ? locked / total : 0;
15704
+ if (ratio >= 1) return "只差名字了";
15705
+ if (ratio >= 0.7) return "非常接近";
15706
+ if (ratio >= 0.4) return "范围在收窄";
15707
+ if (attempts >= 6) return "不妨换个方向试试";
15708
+ return "继续缩小范围";
15709
+ }
15710
+ __name(encourage, "encourage");
15711
+ function statusTag(label) {
15712
+ return cfg.addStatusTextAfterEmoji ? `(${label})` : "";
15713
+ }
15714
+ __name(statusTag, "statusTag");
15715
+ function wikiImage(entry) {
15716
+ if (cfg.disableImages || !entry?.wiki_image_url) return null;
15717
+ return `${(0, import_koishi.h)("p", "")}${import_koishi.h.image(entry.wiki_image_url)}${(0, import_koishi.h)("p", "")}`;
15718
+ }
15719
+ __name(wikiImage, "wikiImage");
15720
+ function boardText(mode, guesses, titles) {
15721
+ const last = guesses[guesses.length - 1];
15722
+ const fields = FIELDS[mode];
15723
+ const lines = fields.map((key) => {
15724
+ const status = statusOf(last, key);
15725
+ const meta = STATUS_META[status];
15726
+ const hits = Array.isArray(last[`${key}_hits`]) ? last[`${key}_hits`].map(String) : [];
15727
+ const value = displayValues(key, last[key]).map((v) => status === "mixed" && hits.includes(v.raw) ? `【${v.text}】` : v.text).join("、");
15728
+ return `${meta.emoji}${statusTag(meta.label)} ${fieldLabel(key)} ${value}`;
15729
+ });
15730
+ const locked = fields.filter((k) => statusOf(last, k) === "true").length;
15731
+ const history = titles.length > 1 ? `本局已猜:${titles.join(" › ")}` : null;
15732
+ return textCard(
15733
+ `${modeName(mode)}模式 · 第 ${guesses.length} 次猜测`,
15734
+ wikiImage(last),
15735
+ `${last.chinese_title}${last.title ? ` ${last.title}` : ""}`,
15736
+ lines,
15737
+ `已锁定 ${locked}/${fields.length} 项 · ${encourage(locked, fields.length, guesses.length)}`,
15738
+ history
15739
+ );
15740
+ }
15741
+ __name(boardText, "boardText");
15742
+ function startText(mode, played) {
15743
+ const meta = MODES[mode];
15744
+ return textCard(
15745
+ `新的一局 · ${meta.name}模式`,
15746
+ `${meta.tagline},从 ${meta.total} 个候选中锁定唯一答案。`,
15747
+ `可推理属性:${FIELDS[mode].map((k) => keyMap[k] || k).join(" / ")}`,
15748
+ [`mcdle.猜 [名称] 提交一次猜测`, `今日进度 ${played}/${cfg.dailyPlayLimit}`]
15749
+ );
15750
+ }
15751
+ __name(startText, "startText");
15752
+ function winText(o) {
15753
+ const left = Math.max(0, cfg.dailyPlayLimit - o.played);
15754
+ const profile = FIELDS[o.mode].map(
15755
+ (k) => `${fieldLabel(k)} ${displayValues(k, o.answer[k]).map((v) => v.text).join("、")}`
15756
+ ).join(" · ");
15757
+ return textCard(
15758
+ "猜中了",
15759
+ wikiImage(o.answer),
15760
+ `答案 ${o.answer.chinese_title}${o.answer.title ? ` ${o.answer.title}` : ""}`,
15761
+ [
15762
+ `猜测 ${o.attempts} 次`,
15763
+ `用时 ${formatDuration(o.seconds)}`,
15764
+ `${o.username} 累计猜中 ${o.total} 次`
15765
+ ],
15766
+ `答案档案:${profile}`,
15767
+ left > 0 ? `今日还剩 ${left} 局 · mcdle.排行榜 查看战绩` : "今日额度已用完 · mcdle.排行榜 查看战绩"
15768
+ );
15769
+ }
15770
+ __name(winText, "winText");
15771
+ async function renderCard(html) {
15772
+ if (cfg.disableImages || !ctx.puppeteer) return null;
15773
+ try {
15774
+ return await ctx.puppeteer.render("", async (page, next) => {
15775
+ try {
15776
+ await page.setViewport({ width: 1800, height: 1200, deviceScaleFactor: 2 });
15777
+ try {
15778
+ await page.setContent(html, { waitUntil: "load", timeout: 15e3 });
15779
+ } catch (error) {
15780
+ logger.debug("页面资源加载超时,按当前状态出图:%s", error?.message ?? error);
15781
+ }
15782
+ await page.evaluate(async () => {
15783
+ await document.fonts?.ready;
15784
+ });
15785
+ const card = await page.$("#card");
15786
+ return await next(card ?? void 0);
15787
+ } catch (error) {
15788
+ await page.close().catch(() => {
15789
+ });
15790
+ throw error;
15791
+ }
15792
+ });
15793
+ } catch (error) {
15794
+ logger.warn("图片渲染失败,本次回退为文本:%s", error?.message ?? error);
15795
+ return null;
15796
+ }
15797
+ }
15798
+ __name(renderCard, "renderCard");
15799
+ async function sendCard(session, html, fallback) {
15800
+ const image = await renderCard(html);
15801
+ await sendMsg(session, image ?? fallback);
15802
+ }
15803
+ __name(sendCard, "sendCard");
14726
15804
  async function mcdle(session) {
14727
- await sendMsg(session, [
14728
- "欢迎使用 MCDLE",
14729
- "",
14730
- "猜生物 物品 方块",
14731
- "挑战知识边界",
14732
- "",
14733
- "颜色与箭头指引方向",
14734
- "每日限次 排行榜更新",
14735
- "",
14736
- "指令示例:",
14737
- " mcdle.猜 [名称]",
14738
- " mcdle.帮助",
14739
- " mcdle.排行榜",
14740
- "",
14741
- "愿你洞察世界万象"
14742
- ].join("\n"));
15805
+ await sendCard(session, introCard(cfg.dailyPlayLimit), introText());
14743
15806
  }
14744
15807
  __name(mcdle, "mcdle");
15808
+ function introText() {
15809
+ const total = mobData.length + itemData.length + blockData.length;
15810
+ return textCard(
15811
+ "MCDLE · 我的世界猜谜",
15812
+ `从 ${total} 个词条里,只凭属性提示锁定唯一答案。`,
15813
+ [
15814
+ `壹 mcdle.猜 苦力怕 随便报个名字即可开局`,
15815
+ `贰 对照颜色与箭头缩小范围,每次猜测都会留在棋盘上`,
15816
+ `叁 猜中后自动记入 mcdle.排行榜`
15817
+ ],
15818
+ [
15819
+ `生物 ${mobData.length} · 物品 ${itemData.length} · 方块 ${blockData.length}`,
15820
+ `每日 ${cfg.dailyPlayLimit} 局,跨零点重置`,
15821
+ `完整规则:mcdle.帮助`
15822
+ ]
15823
+ );
15824
+ }
15825
+ __name(introText, "introText");
14745
15826
  async function ck(session) {
15827
+ const sections = [
15828
+ [`生物词库 · ${mobChineseTitle.length} 条`, mobChineseTitle],
15829
+ [`方块词库 · ${blockChineseTitle.length} 条`, blockChineseTitle],
15830
+ [`物品词库 · ${itemChineseTitle.length} 条`, itemChineseTitle]
15831
+ ];
14746
15832
  if (["red", "onebot"].includes(session.platform)) {
14747
- const allContentNodes = [
14748
- (0, import_koishi.h)("message", { userId: session.userId }, `生物词库:
14749
- ${mobChineseTitle.join(" ")}`),
14750
- (0, import_koishi.h)("message", { userId: session.userId }, `方块词库:
14751
- ${blockChineseTitle.join(" ")}`),
14752
- (0, import_koishi.h)("message", { userId: session.userId }, `物品词库:
14753
- ${itemChineseTitle.join(" ")}`)
14754
- ];
15833
+ const allContentNodes = sections.map(
15834
+ ([title, words]) => (0, import_koishi.h)("message", { userId: session.userId }, `▍${title}
15835
+ ${RULE}
15836
+ ${words.join(" ")}`)
15837
+ );
14755
15838
  try {
14756
15839
  await session.send((0, import_koishi.h)("figure", {}, allContentNodes));
14757
15840
  return;
@@ -14759,245 +15842,307 @@ ${itemChineseTitle.join(" ")}`)
14759
15842
  logger.warn("合并转发失败,降级为普通消息:", error);
14760
15843
  }
14761
15844
  }
14762
- const combinedMessage = [
14763
- `生物词库:
14764
- ${mobChineseTitle.join(" ")}`,
14765
- `方块词库:
14766
- ${blockChineseTitle.join(" ")}`,
14767
- `物品词库:
14768
- ${itemChineseTitle.join(" ")}`
14769
- ].join("\n\n");
14770
- await sendMsg(session, combinedMessage);
15845
+ await sendMsg(
15846
+ session,
15847
+ sections.map(([title, words]) => `▍${title}
15848
+ ${RULE}
15849
+ ${words.join(" ")}`).join("\n\n")
15850
+ );
14771
15851
  }
14772
15852
  __name(ck, "ck");
14773
- async function c(session, guess) {
15853
+ const POOLS = {
15854
+ mob: mobData,
15855
+ item: itemData,
15856
+ block: blockData
15857
+ };
15858
+ function modeOf(entry) {
15859
+ if (mobData.includes(entry)) return "mob";
15860
+ if (itemData.includes(entry)) return "item";
15861
+ return "block";
15862
+ }
15863
+ __name(modeOf, "modeOf");
15864
+ function findAnywhere(title) {
15865
+ return mobData.find((d) => d.chinese_title === title) || itemData.find((d) => d.chinese_title === title) || blockData.find((d) => d.chinese_title === title);
15866
+ }
15867
+ __name(findAnywhere, "findAnywhere");
15868
+ async function ensureRecord(session) {
14774
15869
  let records = await ctx.database.get("mcdle", { channelId: session.channelId });
14775
15870
  if (!records || records.length === 0) {
14776
- records = [await ctx.database.create("mcdle", {
14777
- channelId: session.channelId,
14778
- answer: null,
14779
- guesses: [],
14780
- historyChineseTitles: [],
14781
- guessedChineseTitles: [],
14782
- dailyPlayedToday: 0,
14783
- gameEnded: true,
14784
- lastGameStartTime: /* @__PURE__ */ new Date(0),
14785
- gameMode: null
14786
- })];
15871
+ records = [
15872
+ await ctx.database.create("mcdle", {
15873
+ channelId: session.channelId,
15874
+ answer: null,
15875
+ guesses: [],
15876
+ historyChineseTitles: [],
15877
+ guessedChineseTitles: [],
15878
+ dailyPlayedToday: 0,
15879
+ gameEnded: true,
15880
+ lastGameStartTime: /* @__PURE__ */ new Date(0),
15881
+ gameMode: null
15882
+ })
15883
+ ];
14787
15884
  }
14788
- const record = records[0];
14789
- const now = /* @__PURE__ */ new Date();
14790
- const lastDate = new Date(record.lastGameStartTime);
14791
- if (now.getFullYear() !== lastDate.getFullYear() || now.getMonth() !== lastDate.getMonth() || now.getDate() !== lastDate.getDate()) {
15885
+ return records[0];
15886
+ }
15887
+ __name(ensureRecord, "ensureRecord");
15888
+ async function resetDailyIfNeeded(session, record, now) {
15889
+ const last = new Date(record.lastGameStartTime);
15890
+ if (now.getFullYear() !== last.getFullYear() || now.getMonth() !== last.getMonth() || now.getDate() !== last.getDate()) {
14792
15891
  record.dailyPlayedToday = 0;
14793
15892
  await ctx.database.set("mcdle", { channelId: session.channelId }, { dailyPlayedToday: 0 });
14794
15893
  }
15894
+ }
15895
+ __name(resetDailyIfNeeded, "resetDailyIfNeeded");
15896
+ async function pickAnswer(session, record, mode) {
15897
+ const pool = POOLS[mode];
15898
+ let candidates = pool.filter((d) => !record.historyChineseTitles.includes(d.chinese_title));
15899
+ if (candidates.length === 0) {
15900
+ const remaining = record.historyChineseTitles.filter(
15901
+ (title) => !pool.some((d) => d.chinese_title === title)
15902
+ );
15903
+ await ctx.database.set(
15904
+ "mcdle",
15905
+ { channelId: session.channelId },
15906
+ { historyChineseTitles: remaining }
15907
+ );
15908
+ record.historyChineseTitles = remaining;
15909
+ candidates = pool;
15910
+ }
15911
+ return candidates[Math.floor(Math.random() * candidates.length)];
15912
+ }
15913
+ __name(pickAnswer, "pickAnswer");
15914
+ async function c(session, guess) {
15915
+ const record = await ensureRecord(session);
15916
+ const now = /* @__PURE__ */ new Date();
15917
+ await resetDailyIfNeeded(session, record, now);
14795
15918
  if (record.gameEnded) {
14796
- let isInHistory = function(title) {
14797
- return record.historyChineseTitles.includes(title);
14798
- };
14799
- __name(isInHistory, "isInHistory");
14800
- if (record.dailyPlayedToday >= cfg.dailyPlayLimit) {
14801
- await sendMsg(session, `今日游戏次数已达上限,请明日再来!`);
14802
- return;
14803
- }
14804
- let chosenMode;
14805
- let dataSource;
14806
- let matchedData = void 0;
14807
- if (guess) {
14808
- matchedData = mobData.find((d) => d.chinese_title === guess) || itemData.find((d) => d.chinese_title === guess) || blockData.find((d) => d.chinese_title === guess);
14809
- }
14810
- if (!matchedData) {
14811
- const choices = ["mob", "item", "block"];
14812
- chosenMode = choices[Math.floor(Math.random() * choices.length)];
14813
- } else {
14814
- if (mobData.includes(matchedData)) chosenMode = "mob";
14815
- else if (itemData.includes(matchedData)) chosenMode = "item";
14816
- else chosenMode = "block";
14817
- }
14818
- if (!matchedData) {
14819
- if (chosenMode === "mob") dataSource = mobData;
14820
- else if (chosenMode === "item") dataSource = itemData;
14821
- else dataSource = blockData;
14822
- const filtered = dataSource.filter((d) => !isInHistory(d.chinese_title));
14823
- if (filtered.length === 0) {
14824
- const remainingHistory = record.historyChineseTitles.filter((title) => {
14825
- const isMob = mobData.some((mob) => mob.chinese_title === title);
14826
- const isItem = itemData.some((item) => item.chinese_title === title);
14827
- const isBlock = blockData.some((block) => block.chinese_title === title);
14828
- if (chosenMode === "mob") return !isMob;
14829
- if (chosenMode === "item") return !isItem;
14830
- if (chosenMode === "block") return !isBlock;
14831
- return true;
14832
- });
14833
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14834
- historyChineseTitles: remainingHistory
14835
- });
14836
- dataSource = chosenMode === "mob" ? mobData : chosenMode === "item" ? itemData : blockData;
14837
- } else {
14838
- dataSource = filtered;
14839
- }
14840
- const answer = dataSource[Math.floor(Math.random() * dataSource.length)];
14841
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14842
- answer,
14843
- guesses: [],
14844
- guessedChineseTitles: [],
14845
- gameEnded: false,
14846
- lastGameStartTime: now,
14847
- dailyPlayedToday: record.dailyPlayedToday + 1,
14848
- historyChineseTitles: [...record.historyChineseTitles, answer.chinese_title],
14849
- gameMode: chosenMode
14850
- });
14851
- await sendMsg(session, `游戏开始!模式: ${chosenMode === "mob" ? "生物" : chosenMode === "item" ? "物品" : "方块"}`);
14852
- return;
14853
- }
14854
- if (matchedData) {
14855
- if (mobData.includes(matchedData)) chosenMode = "mob";
14856
- else if (itemData.includes(matchedData)) chosenMode = "item";
14857
- else chosenMode = "block";
14858
- if (chosenMode === "mob") dataSource = mobData;
14859
- else if (chosenMode === "item") dataSource = itemData;
14860
- else dataSource = blockData;
14861
- let filtered = dataSource.filter((d) => !isInHistory(d.chinese_title));
14862
- if (filtered.length === 0) {
14863
- const remainingHistory = record.historyChineseTitles.filter((title) => {
14864
- const isMob = mobData.some((mob) => mob.chinese_title === title);
14865
- const isItem = itemData.some((item) => item.chinese_title === title);
14866
- const isBlock = blockData.some((block) => block.chinese_title === title);
14867
- if (chosenMode === "mob") return !isMob;
14868
- if (chosenMode === "item") return !isItem;
14869
- if (chosenMode === "block") return !isBlock;
14870
- return true;
14871
- });
14872
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14873
- historyChineseTitles: remainingHistory
14874
- });
14875
- filtered = chosenMode === "mob" ? mobData : chosenMode === "item" ? itemData : blockData;
14876
- }
14877
- const answer = filtered[Math.floor(Math.random() * filtered.length)];
14878
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14879
- answer,
14880
- guesses: [],
14881
- guessedChineseTitles: [],
14882
- gameEnded: false,
14883
- lastGameStartTime: now,
14884
- dailyPlayedToday: record.dailyPlayedToday + 1,
14885
- historyChineseTitles: [...record.historyChineseTitles, answer.chinese_title],
14886
- gameMode: chosenMode
14887
- });
14888
- const comparisonResult = compareData(matchedData, answer);
14889
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14890
- guesses: [...record.guesses, comparisonResult],
14891
- guessedChineseTitles: [...record.guessedChineseTitles, matchedData.chinese_title]
14892
- });
14893
- await generateAndSendComparison(session, [...record.guesses, comparisonResult]);
14894
- if (matchedData.chinese_title === answer.chinese_title) {
14895
- await sendMsg(session, `恭喜你猜对啦!
14896
- 答案:${matchedData.chinese_title}
14897
- 猜测:${record.guesses.length + 1} 次`);
14898
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14899
- gameEnded: true,
14900
- answer: null,
14901
- guesses: [],
14902
- guessedChineseTitles: [],
14903
- gameMode: null
14904
- });
14905
- await updateRank(session.userId, session.username, chosenMode);
14906
- }
14907
- await sendMsg(session, `游戏开始!模式:${chosenMode === "mob" ? "生物" : chosenMode === "item" ? "物品" : "方块"}`);
14908
- return;
14909
- }
15919
+ await startNewGame(session, record, guess, now);
14910
15920
  } else {
14911
- if (!guess) {
14912
- await sendMsg(session, `请输入 ${record.gameMode === "mob" ? "生物" : record.gameMode === "item" ? "物品" : "方块"}名称 猜测`);
14913
- return;
14914
- }
14915
- let dataSource;
14916
- if (record.gameMode === "mob") dataSource = mobData;
14917
- else if (record.gameMode === "item") dataSource = itemData;
14918
- else if (record.gameMode === "block") dataSource = blockData;
14919
- else {
14920
- await sendMsg(session, `模式数据异常`);
14921
- return;
14922
- }
14923
- if (!cfg.allowRepeatedGuesses && record.guessedChineseTitles.includes(guess)) {
14924
- await sendMsg(session, `${guess} 已猜过`);
14925
- return;
14926
- }
14927
- const matched = dataSource.find((d) => d.chinese_title === guess);
14928
- if (!matched) {
14929
- const fuzzyMatches = dataSource.filter(
14930
- (d) => d.chinese_title.includes(guess) || guess.includes(d.chinese_title)
14931
- ).slice(0, 3);
14932
- if (fuzzyMatches.length > 0) {
14933
- const suggestions = fuzzyMatches.map((d) => d.chinese_title).join("、");
14934
- await sendMsg(session, `${guess} 不在${record.gameMode === "mob" ? "生物" : record.gameMode === "item" ? "物品" : "方块"}词库中
14935
-
14936
- 是否想猜:${suggestions}?`);
14937
- } else {
14938
- await sendMsg(session, `${guess} 不在${record.gameMode === "mob" ? "生物" : record.gameMode === "item" ? "物品" : "方块"}词库中`);
14939
- }
14940
- return;
14941
- }
14942
- const answer = record.answer;
14943
- if (!answer) {
14944
- await sendMsg(session, `数据异常`);
14945
- return;
15921
+ await continueGame(session, record, guess);
15922
+ }
15923
+ }
15924
+ __name(c, "c");
15925
+ async function startNewGame(session, record, guess, now) {
15926
+ if (record.dailyPlayedToday >= cfg.dailyPlayLimit) {
15927
+ await sendMsg(
15928
+ session,
15929
+ textCard(
15930
+ `今日 ${cfg.dailyPlayLimit} 局已经用完`,
15931
+ `跨零点后重置,还要等 ${untilReset(now)}。`,
15932
+ "在那之前可以逛逛 mcdle.排行榜 或 mcdle.词库。"
15933
+ )
15934
+ );
15935
+ return;
15936
+ }
15937
+ const opener = guess ? findAnywhere(guess) : void 0;
15938
+ const mode = opener ? modeOf(opener) : ["mob", "item", "block"][Math.floor(Math.random() * 3)];
15939
+ const answer = await pickAnswer(session, record, mode);
15940
+ const played = record.dailyPlayedToday + 1;
15941
+ await ctx.database.set(
15942
+ "mcdle",
15943
+ { channelId: session.channelId },
15944
+ {
15945
+ answer,
15946
+ guesses: [],
15947
+ guessedChineseTitles: [],
15948
+ gameEnded: false,
15949
+ lastGameStartTime: now,
15950
+ dailyPlayedToday: played,
15951
+ historyChineseTitles: [...record.historyChineseTitles, answer.chinese_title],
15952
+ gameMode: mode
14946
15953
  }
14947
- const comparisonResult = compareData(matched, answer);
14948
- const updatedGuesses = [...record.guesses, comparisonResult];
14949
- const updatedChineseTitles = [...record.guessedChineseTitles, matched.chinese_title];
14950
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14951
- guesses: updatedGuesses,
14952
- guessedChineseTitles: updatedChineseTitles
14953
- });
14954
- await generateAndSendComparison(session, updatedGuesses);
14955
- if (matched.chinese_title === answer.chinese_title) {
14956
- await sendMsg(session, `恭喜你猜对啦!
14957
- 答案:${matched.chinese_title}
14958
- 猜测:${record.guesses.length + 1} 次`);
14959
- await ctx.database.set("mcdle", { channelId: session.channelId }, {
14960
- gameEnded: true,
14961
- answer: null,
14962
- guesses: [],
14963
- guessedChineseTitles: [],
14964
- gameMode: null
14965
- });
14966
- await updateRank(session.userId, session.username, record.gameMode);
15954
+ );
15955
+ await sendCard(session, startCard(mode, played, cfg.dailyPlayLimit), startText(mode, played));
15956
+ if (opener) {
15957
+ await applyGuess(
15958
+ session,
15959
+ { mode, answer, guesses: [], titles: [], startedAt: now, played },
15960
+ opener
15961
+ );
15962
+ }
15963
+ }
15964
+ __name(startNewGame, "startNewGame");
15965
+ async function continueGame(session, record, guess) {
15966
+ const mode = record.gameMode;
15967
+ if (!mode || !MODES[mode] || !record.answer) {
15968
+ await ctx.database.set(
15969
+ "mcdle",
15970
+ { channelId: session.channelId },
15971
+ { gameEnded: true, answer: null, guesses: [], guessedChineseTitles: [], gameMode: null }
15972
+ );
15973
+ await sendMsg(
15974
+ session,
15975
+ textCard("这一局的牌面已经无法还原", "已经把它收掉了,mcdle.猜 [名称] 可以重新开一局。")
15976
+ );
15977
+ return;
15978
+ }
15979
+ const state = {
15980
+ mode,
15981
+ answer: record.answer,
15982
+ guesses: record.guesses,
15983
+ titles: record.guessedChineseTitles,
15984
+ startedAt: new Date(record.lastGameStartTime),
15985
+ played: record.dailyPlayedToday
15986
+ };
15987
+ if (!guess) {
15988
+ if (state.guesses.length) {
15989
+ await sendCard(
15990
+ session,
15991
+ boardCard({ mode, guesses: state.guesses, tip: "轮到你了 · mcdle.猜 [名称]" }),
15992
+ boardText(mode, state.guesses, state.titles)
15993
+ );
15994
+ } else {
15995
+ await sendMsg(
15996
+ session,
15997
+ textCard(
15998
+ `轮到你了 · ${modeName(mode)}模式`,
15999
+ `报一个${modeName(mode)}的名字,比如 mcdle.猜 ${POOLS[mode][0].chinese_title}。`,
16000
+ `拿不准有哪些词条就看 mcdle.词库。`
16001
+ )
16002
+ );
14967
16003
  }
16004
+ return;
16005
+ }
16006
+ if (!cfg.allowRepeatedGuesses && state.titles.includes(guess)) {
16007
+ await sendMsg(
16008
+ session,
16009
+ textCard(
16010
+ `「${guess}」这一局已经猜过了`,
16011
+ `本局已猜 ${state.titles.length} 个:${state.titles.join(" › ")}`,
16012
+ "换一个还没试过的词条吧。"
16013
+ )
16014
+ );
16015
+ return;
16016
+ }
16017
+ const matched = POOLS[mode].find((d) => d.chinese_title === guess);
16018
+ if (!matched) {
16019
+ const near = POOLS[mode].filter((d) => d.chinese_title.includes(guess) || guess.includes(d.chinese_title)).slice(0, 3).map((d) => d.chinese_title);
16020
+ await sendMsg(
16021
+ session,
16022
+ textCard(
16023
+ `「${guess}」不在${modeName(mode)}词库里`,
16024
+ near.length ? `是不是想猜:${near.join("、")}?` : null,
16025
+ `本局共有 ${MODES[mode].total} 个${modeName(mode)}候选,mcdle.词库 可以全部翻看。`
16026
+ )
16027
+ );
16028
+ return;
14968
16029
  }
16030
+ await applyGuess(session, state, matched);
14969
16031
  }
14970
- __name(c, "c");
14971
- function bz(session) {
14972
- return sendMsg(
16032
+ __name(continueGame, "continueGame");
16033
+ async function applyGuess(session, state, matched) {
16034
+ const comparison = compareData(matched, state.answer);
16035
+ const guesses = [...state.guesses, comparison];
16036
+ const titles = [...state.titles, matched.chinese_title];
16037
+ await ctx.database.set(
16038
+ "mcdle",
16039
+ { channelId: session.channelId },
16040
+ { guesses, guessedChineseTitles: titles }
16041
+ );
16042
+ const solved = matched.chinese_title === state.answer.chinese_title;
16043
+ const locked = FIELDS[state.mode].filter((k) => statusOf(comparison, k) === "true").length;
16044
+ await sendCard(
14973
16045
  session,
14974
- [
14975
- "MCDLE 我的世界猜谜(类 Wordle)",
14976
- "玩法核心:根据猜测物品/生物/方块与答案字段对应,标记正确、部分匹配、不匹配。",
14977
- "颜色标识:",
14978
- "🟩 绿色 - 字段完全匹配",
14979
- "🟨 黄色 - 部分匹配",
14980
- "🟥 红色 - 不匹配",
14981
- "🟥⬆️ 红色上箭头 - 答案更大",
14982
- "🟥⬇️ 红色下箭头 - 答案更小",
14983
- "使用指令:mcdle.猜 [名称] 进行开始或猜测,mcdle.排行榜 查看成绩。",
14984
- `每日游玩次数限制:${cfg.dailyPlayLimit}次`,
14985
- "我的世界中文Wiki:https://zh.minecraft.wiki/w/Minecraft_Wiki"
14986
- ].join("\n")
16046
+ boardCard({
16047
+ mode: state.mode,
16048
+ guesses,
16049
+ tip: solved ? "答案已锁定" : encourage(locked, FIELDS[state.mode].length, guesses.length)
16050
+ }),
16051
+ boardText(state.mode, guesses, titles)
14987
16052
  );
16053
+ if (!solved) return;
16054
+ await ctx.database.set(
16055
+ "mcdle",
16056
+ { channelId: session.channelId },
16057
+ { gameEnded: true, answer: null, guesses: [], guessedChineseTitles: [], gameMode: null }
16058
+ );
16059
+ const username = session.username || session.userId || "匿名冒险者";
16060
+ const total = await updateRank(session.userId, username, state.mode);
16061
+ const started = new Date(state.startedAt).getTime();
16062
+ const seconds = started > 0 ? Math.max(0, Math.round((Date.now() - started) / 1e3)) : NaN;
16063
+ const summary = {
16064
+ mode: state.mode,
16065
+ answer: state.answer,
16066
+ attempts: guesses.length,
16067
+ seconds,
16068
+ username,
16069
+ total,
16070
+ played: state.played
16071
+ };
16072
+ await sendCard(
16073
+ session,
16074
+ winCard({ ...summary, limit: cfg.dailyPlayLimit }),
16075
+ winText(summary)
16076
+ );
16077
+ }
16078
+ __name(applyGuess, "applyGuess");
16079
+ function bz(session) {
16080
+ return sendCard(session, helpCard(cfg.dailyPlayLimit, cfg.allowRepeatedGuesses), helpText());
14988
16081
  }
14989
16082
  __name(bz, "bz");
16083
+ function helpText() {
16084
+ const legend = ["true", "mixed", "false", "false_up", "false_down"].map(
16085
+ (s) => `${STATUS_META[s].emoji} ${STATUS_META[s].label}`
16086
+ );
16087
+ return textCard(
16088
+ "MCDLE 玩法说明",
16089
+ "每局随机抽一个生物、物品或方块,你的每次猜测都会逐项与答案比对,用颜色告诉你差在哪里。",
16090
+ legend,
16091
+ [
16092
+ "部分匹配的字段里,【】括起来的就是与答案重合的那几项。",
16093
+ "数值与版本用箭头指向答案:向上代表答案更大、更晚。"
16094
+ ],
16095
+ [
16096
+ `生物 ${mobData.length} 条 · ${FIELDS.mob.length} 项属性`,
16097
+ `物品 ${itemData.length} 条 · ${FIELDS.item.length} 项属性`,
16098
+ `方块 ${blockData.length} 条 · ${FIELDS.block.length} 项属性`
16099
+ ],
16100
+ [
16101
+ "mcdle.猜 [名称] 开始一局,或提交猜测",
16102
+ "mcdle.猜 局中直接使用可回看当前棋盘",
16103
+ "mcdle.排行榜 查看群内战绩",
16104
+ "mcdle.词库 查阅全部候选词条"
16105
+ ],
16106
+ [
16107
+ `每个群每日 ${cfg.dailyPlayLimit} 局,跨零点重置。`,
16108
+ cfg.allowRepeatedGuesses ? "允许重复提交已经猜过的词条。" : "同一局内不能重复提交已猜过的词条。",
16109
+ "词条与数据来自 zh.minecraft.wiki,版本号按发布先后比较。"
16110
+ ]
16111
+ );
16112
+ }
16113
+ __name(helpText, "helpText");
14990
16114
  async function phb(session) {
14991
- const ranks = await ctx.database.get("mcdle_rank", {}, {
14992
- limit: cfg.maxRank,
14993
- sort: { totalSuccessCount: "desc" }
14994
- });
14995
- if (!ranks || ranks.length === 0) {
14996
- await sendMsg(session, "暂无排行榜数据");
16115
+ const ranks = await ctx.database.get(
16116
+ "mcdle_rank",
16117
+ {},
16118
+ { limit: cfg.maxRank, sort: { totalSuccessCount: "desc" } }
16119
+ );
16120
+ const entries = ranks.map((r) => ({
16121
+ username: r.username,
16122
+ userId: r.userId,
16123
+ mob: r.mobSuccessCount,
16124
+ item: r.itemSuccessCount,
16125
+ block: r.blockSuccessCount,
16126
+ total: r.totalSuccessCount
16127
+ }));
16128
+ if (!entries.length) {
16129
+ await sendCard(
16130
+ session,
16131
+ rankCard([], 0),
16132
+ textCard("排行榜还空着", "用 mcdle.猜 开出第一局,这里的第一行就是你。")
16133
+ );
14997
16134
  return;
14998
16135
  }
14999
- const text = "MCDLE 排行榜:\n" + ranks.map((r, i) => `${i + 1}. ${r.username} ${r.totalSuccessCount}`).join("\n") + "\n...";
15000
- await sendMsg(session, text);
16136
+ const lines = entries.map((e, i) => {
16137
+ const medal = ["①", "②", "③"][i] || `${i + 1}.`;
16138
+ const split = `生物 ${e.mob} · 物品 ${e.item} · 方块 ${e.block}`;
16139
+ return `${medal} ${e.username || e.userId} ${e.total} 次 (${split})`;
16140
+ });
16141
+ await sendCard(
16142
+ session,
16143
+ rankCard(entries, entries.length),
16144
+ textCard(`排行榜 · 前 ${entries.length} 名`, lines, "每猜中一局记一分,按模式分别计数。")
16145
+ );
15001
16146
  }
15002
16147
  __name(phb, "phb");
15003
16148
  function compareData(guess, answer) {
@@ -15041,6 +16186,7 @@ ${itemChineseTitle.join(" ")}`
15041
16186
  const guessArr = guess[key];
15042
16187
  const answerArr = answer[key];
15043
16188
  const intersection = guessArr.filter((x) => answerArr.includes(x));
16189
+ result[`${key}_hits`] = intersection;
15044
16190
  if (intersection.length === guessArr.length && intersection.length === answerArr.length) {
15045
16191
  result[`${key}_gui`] = "true";
15046
16192
  } else if (intersection.length > 0) {
@@ -15078,69 +16224,9 @@ ${itemChineseTitle.join(" ")}`
15078
16224
  else if (mode === "item") updates.itemSuccessCount = record.itemSuccessCount + 1;
15079
16225
  else updates.blockSuccessCount = record.blockSuccessCount + 1;
15080
16226
  await ctx.database.set("mcdle_rank", { userId }, updates);
16227
+ return updates.totalSuccessCount;
15081
16228
  }
15082
16229
  __name(updateRank, "updateRank");
15083
- async function generateAndSendComparison(session, guesses) {
15084
- if (guesses.length === 0) {
15085
- await sendMsg(session, "暂无猜测结果");
15086
- return;
15087
- }
15088
- const lastGuess = guesses[guesses.length - 1];
15089
- let resultText = "";
15090
- if (!cfg.disableImages && lastGuess.wiki_image_url) {
15091
- resultText += `${import_koishi.h.image(lastGuess.wiki_image_url)}
15092
- `;
15093
- }
15094
- if (lastGuess.chinese_title) {
15095
- const excludedKeys = ["id_gui", "title_gui", "url_gui", "image_url_gui", "wiki_image_url_gui"];
15096
- const guiKeys = Object.keys(lastGuess).filter(
15097
- (k) => k.endsWith("_gui") && k !== "chinese_title_gui" && !excludedKeys.includes(k)
15098
- );
15099
- const allOtherFieldsTrue = guiKeys.every((k) => lastGuess[k] === "true");
15100
- const chineseTitleGui = lastGuess["chinese_title_gui"];
15101
- let titleLine = `名称:${lastGuess.chinese_title}`;
15102
- if (allOtherFieldsTrue) {
15103
- if (chineseTitleGui === "true") {
15104
- titleLine += " 🟩";
15105
- } else {
15106
- titleLine += " 🟥";
15107
- }
15108
- }
15109
- resultText += titleLine + "\n";
15110
- }
15111
- for (const key in lastGuess) {
15112
- if (key.endsWith("_gui")) continue;
15113
- if (key === "id" || key === "title" || key === "url" || key === "image_url") continue;
15114
- if (key === "wiki_image_url" || key === "chinese_title") continue;
15115
- const guiKey = `${key}_gui`;
15116
- const guiStatus = lastGuess[guiKey];
15117
- const addText = cfg.addStatusTextAfterEmoji;
15118
- let statusEmoji = "❓" + (addText ? "(未知)" : "");
15119
- if (guiStatus === "true") statusEmoji = "🟩" + (addText ? "(完全匹配)" : "");
15120
- else if (guiStatus === "mixed") statusEmoji = "🟨" + (addText ? "(部分匹配)" : "");
15121
- else if (guiStatus === "false") statusEmoji = "🟥" + (addText ? "(不匹配)" : "");
15122
- else if (guiStatus === "false_up") statusEmoji = "🟥⬆️" + (addText ? "(答案更大)" : "");
15123
- else if (guiStatus === "false_down") statusEmoji = "🟥⬇️" + (addText ? "(答案更小)" : "");
15124
- const displayKey = keyMap[key] || key;
15125
- let value = lastGuess[key];
15126
- if (Array.isArray(value)) {
15127
- value = value.map((v) => {
15128
- if (typeof v === "string" && valueMap[key]?.[v]) {
15129
- return valueMap[key][v];
15130
- }
15131
- return v;
15132
- }).join("、");
15133
- } else if (typeof value === "string") {
15134
- if (valueMap[key]?.[value]) {
15135
- value = valueMap[key][value];
15136
- }
15137
- }
15138
- resultText += `${displayKey}: ${value} ${statusEmoji}
15139
- `;
15140
- }
15141
- await sendMsg(session, resultText);
15142
- }
15143
- __name(generateAndSendComparison, "generateAndSendComparison");
15144
16230
  function normalizeVersion(versionString) {
15145
16231
  if (versionString === "pre-alpha") return "0";
15146
16232
  if (versionString.startsWith("Alpha ")) return "0.0.0.0." + versionString.replace("Alpha ", "");
@@ -15183,13 +16269,11 @@ ${itemChineseTitle.join(" ")}`
15183
16269
  const timePassed = Date.now() - prevMessage.timestamp;
15184
16270
  const remainingDelay = cfg.retractDelay * 1e3 - timePassed;
15185
16271
  if (timePassed < 118e3) {
15186
- setTimeout(async () => {
15187
- try {
15188
- await session.bot.deleteMessage(session.channelId, prevMessage.id);
15189
- } catch (error) {
15190
- logger.warn(`Failed to retract message ${prevMessage.id}: ${error.message}`);
15191
- }
15192
- }, remainingDelay);
16272
+ ctx.setTimeout(() => {
16273
+ session.bot.deleteMessage(session.channelId, prevMessage.id).catch((error) => {
16274
+ logger.warn(`撤回消息 ${prevMessage.id} 失败:${error.message}`);
16275
+ });
16276
+ }, Math.max(0, remainingDelay));
15193
16277
  }
15194
16278
  }
15195
16279
  lastMessageInfo.set(session.channelId, { id: messageId, timestamp: Date.now() });