koishi-plugin-cchess 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -14,19 +14,15 @@ yarn add koishi-plugin-cchess
|
|
|
14
14
|
|
|
15
15
|
| 指令 | 说明 |
|
|
16
16
|
| --- | --- |
|
|
17
|
-
| `cchess
|
|
18
|
-
| `cchess
|
|
19
|
-
| `cchess
|
|
20
|
-
| `cchess.移动 <着法>` | 落子 |
|
|
21
|
-
| `cchess.悔棋` | 请求悔棋 |
|
|
22
|
-
| `cchess.悔棋 <同意/拒绝>` | 表决悔棋 |
|
|
17
|
+
| `cchess.开始 [红/黑] [人人/人机]` | 入座并开局,双方就位后自动开战 |
|
|
18
|
+
| `cchess.开始 <FEN>` | 摆谱,导入局面后再 `cchess.开始` 即可 |
|
|
19
|
+
| `cchess.悔棋 [同意/拒绝]` | 请求悔棋,也可直接回复同意或拒绝表决 |
|
|
23
20
|
| `cchess.认输` | 认输 |
|
|
24
21
|
| `cchess.结束` | 强制结束棋局 |
|
|
25
|
-
| `cchess
|
|
26
|
-
| `cchess.编辑棋盘.导出` | 导出局面 |
|
|
22
|
+
| `cchess.棋绩 [@某人/榜] [胜场/输场] [人数]` | 查询战绩与排行榜 |
|
|
27
23
|
| `cchess.查看云库残局 [DTM/DTC]` | 云库残局统计 |
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
|
|
25
|
+
发送 `cchess.开始` 即入座,双方就位后自动开局,无需单独的加入与退出指令。
|
|
30
26
|
|
|
31
27
|
着法可直接发送,例如 `炮二平五` 或 `b2e2`。
|
|
32
28
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
2
|
export declare const name = "cchess";
|
|
3
|
-
export declare const usage = "## \u4F7F\u7528\n\n`cchess.\
|
|
3
|
+
export declare const usage = "## \u4F7F\u7528\n\n`cchess.\u5F00\u59CB` \u5373\u53EF\u5165\u5EA7\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u5C40\uFF1B`cchess.\u5F00\u59CB \u4EBA\u673A` \u76F4\u63A5\u6311\u6218\u76AE\u5361\u9C7C\u3002\u7740\u6CD5\u53EF\u76F4\u63A5\u53D1\u9001\uFF0C\u4F8B\u5982 `\u70AE\u4E8C\u5E73\u4E94` \u6216 `b2e2`\u3002\n\n\u6094\u68CB\u8BF7\u6C42\u9001\u51FA\u540E\uFF0C\u5BF9\u65B9\u65E0\u9700\u5E26\u6307\u4EE4\u524D\u7F00\uFF0C\u76F4\u63A5\u56DE\u590D `\u540C\u610F` \u6216 `\u62D2\u7EDD` \u5373\u53EF\u8868\u51B3\u3002\n\n## \u6307\u4EE4\n\n| \u6307\u4EE4 | \u8BF4\u660E |\n| --- | --- |\n| `cchess.\u5F00\u59CB [\u7EA2/\u9ED1] [\u4EBA\u4EBA/\u4EBA\u673A]` | \u5165\u5EA7\u5E76\u5F00\u5C40\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u6218 |\n| `cchess.\u5F00\u59CB <FEN>` | \u6446\u8C31\uFF0C\u5BFC\u5165\u5C40\u9762\u540E\u518D `cchess.\u5F00\u59CB` \u5373\u53EF |\n| `cchess.\u6094\u68CB [\u540C\u610F/\u62D2\u7EDD]` | \u8BF7\u6C42\u6094\u68CB\uFF0C\u4E5F\u53EF\u76F4\u63A5\u56DE\u590D\u540C\u610F\u6216\u62D2\u7EDD\u8868\u51B3 |\n| `cchess.\u8BA4\u8F93` | \u8BA4\u8F93 |\n| `cchess.\u7ED3\u675F` | \u5F3A\u5236\u7ED3\u675F\u68CB\u5C40 |\n| `cchess.\u68CB\u7EE9 [@\u67D0\u4EBA/\u699C] [\u80DC\u573A/\u8F93\u573A] [\u4EBA\u6570]` | \u67E5\u8BE2\u6218\u7EE9\u4E0E\u6392\u884C\u699C |\n| `cchess.\u67E5\u770B\u4E91\u5E93\u6B8B\u5C40 [DTM/DTC]` | \u4E91\u5E93\u6B8B\u5C40\u7EDF\u8BA1 |";
|
|
4
4
|
export declare const inject: string[];
|
|
5
5
|
export interface Config {
|
|
6
6
|
boardSkin: string;
|
package/lib/index.js
CHANGED
|
@@ -38,12 +38,215 @@ __export(src_exports, {
|
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(src_exports);
|
|
40
40
|
var import_koishi = require("koishi");
|
|
41
|
+
|
|
42
|
+
// src/m3.ts
|
|
43
|
+
var WHITE_X = 95.047;
|
|
44
|
+
var WHITE_Y = 100;
|
|
45
|
+
var WHITE_Z = 108.883;
|
|
46
|
+
function labToXyz(l, a, b) {
|
|
47
|
+
const fy = (l + 16) / 116;
|
|
48
|
+
const fx = fy + a / 500;
|
|
49
|
+
const fz = fy - b / 200;
|
|
50
|
+
const f = /* @__PURE__ */ __name((t) => t > 6 / 29 ? t * t * t : 3 * (6 / 29) ** 2 * (t - 4 / 29), "f");
|
|
51
|
+
return [f(fx) * WHITE_X, f(fy) * WHITE_Y, f(fz) * WHITE_Z];
|
|
52
|
+
}
|
|
53
|
+
__name(labToXyz, "labToXyz");
|
|
54
|
+
function gamma(value) {
|
|
55
|
+
return value <= 31308e-7 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;
|
|
56
|
+
}
|
|
57
|
+
__name(gamma, "gamma");
|
|
58
|
+
function xyzToRgb(x, y, z) {
|
|
59
|
+
x /= 100;
|
|
60
|
+
y /= 100;
|
|
61
|
+
z /= 100;
|
|
62
|
+
return [
|
|
63
|
+
gamma(3.2406 * x - 1.5372 * y - 0.4986 * z),
|
|
64
|
+
gamma(-0.9689 * x + 1.8758 * y + 0.0415 * z),
|
|
65
|
+
gamma(0.0557 * x - 0.204 * y + 1.057 * z)
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
__name(xyzToRgb, "xyzToRgb");
|
|
69
|
+
var EPSILON = 1 / 512;
|
|
70
|
+
function lch(tone, chroma, hue) {
|
|
71
|
+
const radian = hue * Math.PI / 180;
|
|
72
|
+
const at = /* @__PURE__ */ __name((c) => xyzToRgb(...labToXyz(tone, Math.cos(radian) * c, Math.sin(radian) * c)), "at");
|
|
73
|
+
const inGamut = /* @__PURE__ */ __name((rgb2) => rgb2.every((value) => value >= -EPSILON && value <= 1 + EPSILON), "inGamut");
|
|
74
|
+
let rgb = at(chroma);
|
|
75
|
+
if (!inGamut(rgb)) {
|
|
76
|
+
let low = 0;
|
|
77
|
+
let high = chroma;
|
|
78
|
+
while (high - low > 0.05) {
|
|
79
|
+
const mid = (low + high) / 2;
|
|
80
|
+
if (inGamut(at(mid))) low = mid;
|
|
81
|
+
else high = mid;
|
|
82
|
+
}
|
|
83
|
+
rgb = at(low);
|
|
84
|
+
}
|
|
85
|
+
return "#" + rgb.map((value) => Math.round(Math.min(1, Math.max(0, value)) * 255).toString(16).padStart(2, "0")).join("");
|
|
86
|
+
}
|
|
87
|
+
__name(lch, "lch");
|
|
88
|
+
function degamma(value) {
|
|
89
|
+
value /= 255;
|
|
90
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
91
|
+
}
|
|
92
|
+
__name(degamma, "degamma");
|
|
93
|
+
function lchOf(hex) {
|
|
94
|
+
const value = parseInt(hex.replace("#", "").slice(0, 6), 16) || 0;
|
|
95
|
+
const r = degamma(value >> 16 & 255);
|
|
96
|
+
const g = degamma(value >> 8 & 255);
|
|
97
|
+
const b = degamma(value & 255);
|
|
98
|
+
const x = (0.4124 * r + 0.3576 * g + 0.1805 * b) * 100 / WHITE_X;
|
|
99
|
+
const y = (0.2126 * r + 0.7152 * g + 0.0722 * b) * 100 / WHITE_Y;
|
|
100
|
+
const z = (0.0193 * r + 0.1192 * g + 0.9505 * b) * 100 / WHITE_Z;
|
|
101
|
+
const f = /* @__PURE__ */ __name((t) => t > (6 / 29) ** 3 ? Math.cbrt(t) : t / (3 * (6 / 29) ** 2) + 4 / 29, "f");
|
|
102
|
+
const [fx, fy, fz] = [f(x), f(y), f(z)];
|
|
103
|
+
const a = 500 * (fx - fy);
|
|
104
|
+
const bb = 200 * (fy - fz);
|
|
105
|
+
return {
|
|
106
|
+
tone: 116 * fy - 16,
|
|
107
|
+
chroma: Math.hypot(a, bb),
|
|
108
|
+
hue: (Math.atan2(bb, a) * 180 / Math.PI + 360) % 360
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
__name(lchOf, "lchOf");
|
|
112
|
+
function harmonize(hex, tone, chroma = 48, fallbackHue = 0) {
|
|
113
|
+
const source = lchOf(hex);
|
|
114
|
+
const hue = source.chroma < 4 ? fallbackHue : source.hue;
|
|
115
|
+
return lch(tone, source.chroma < 4 ? Math.min(chroma, 6) : chroma, hue);
|
|
116
|
+
}
|
|
117
|
+
__name(harmonize, "harmonize");
|
|
118
|
+
function palette(hue, chroma) {
|
|
119
|
+
const cache = /* @__PURE__ */ new Map();
|
|
120
|
+
return (tone) => {
|
|
121
|
+
const hit = cache.get(tone);
|
|
122
|
+
if (hit) return hit;
|
|
123
|
+
const value = lch(tone, chroma, hue);
|
|
124
|
+
cache.set(tone, value);
|
|
125
|
+
return value;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
__name(palette, "palette");
|
|
129
|
+
function palettesOf(hue, options = {}) {
|
|
130
|
+
const { chroma = 56, tertiaryShift = 60 } = options;
|
|
131
|
+
return {
|
|
132
|
+
primary: palette(hue, chroma),
|
|
133
|
+
secondary: palette(hue, Math.max(16, chroma / 3.2)),
|
|
134
|
+
tertiary: palette(hue + tertiaryShift, Math.max(28, chroma / 1.8)),
|
|
135
|
+
neutral: palette(hue, 4),
|
|
136
|
+
neutralVariant: palette(hue, 9),
|
|
137
|
+
error: palette(36, 68)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
__name(palettesOf, "palettesOf");
|
|
141
|
+
function scheme(hue, dark = false, options = {}) {
|
|
142
|
+
const p = palettesOf(hue, options);
|
|
143
|
+
const { primary: P, secondary: S, tertiary: T, neutral: N, neutralVariant: V, error: E } = p;
|
|
144
|
+
if (dark) {
|
|
145
|
+
return {
|
|
146
|
+
primary: P(80),
|
|
147
|
+
onPrimary: P(20),
|
|
148
|
+
primaryContainer: P(30),
|
|
149
|
+
onPrimaryContainer: P(90),
|
|
150
|
+
secondary: S(80),
|
|
151
|
+
onSecondary: S(20),
|
|
152
|
+
secondaryContainer: S(30),
|
|
153
|
+
onSecondaryContainer: S(90),
|
|
154
|
+
tertiary: T(80),
|
|
155
|
+
onTertiary: T(20),
|
|
156
|
+
tertiaryContainer: T(30),
|
|
157
|
+
onTertiaryContainer: T(90),
|
|
158
|
+
error: E(80),
|
|
159
|
+
onError: E(20),
|
|
160
|
+
errorContainer: E(30),
|
|
161
|
+
onErrorContainer: E(90),
|
|
162
|
+
background: N(6),
|
|
163
|
+
onBackground: N(90),
|
|
164
|
+
surface: N(6),
|
|
165
|
+
onSurface: N(90),
|
|
166
|
+
surfaceVariant: V(30),
|
|
167
|
+
onSurfaceVariant: V(80),
|
|
168
|
+
surfaceDim: N(6),
|
|
169
|
+
surfaceBright: N(24),
|
|
170
|
+
surfaceContainerLowest: N(4),
|
|
171
|
+
surfaceContainerLow: N(10),
|
|
172
|
+
surfaceContainer: N(12),
|
|
173
|
+
surfaceContainerHigh: N(17),
|
|
174
|
+
surfaceContainerHighest: N(22),
|
|
175
|
+
outline: V(60),
|
|
176
|
+
outlineVariant: V(30),
|
|
177
|
+
inverseSurface: N(90),
|
|
178
|
+
inverseOnSurface: N(20),
|
|
179
|
+
inversePrimary: P(40),
|
|
180
|
+
scrim: N(0),
|
|
181
|
+
shadow: N(0)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
primary: P(40),
|
|
186
|
+
onPrimary: P(100),
|
|
187
|
+
primaryContainer: P(90),
|
|
188
|
+
onPrimaryContainer: P(30),
|
|
189
|
+
secondary: S(40),
|
|
190
|
+
onSecondary: S(100),
|
|
191
|
+
secondaryContainer: S(90),
|
|
192
|
+
onSecondaryContainer: S(30),
|
|
193
|
+
tertiary: T(40),
|
|
194
|
+
onTertiary: T(100),
|
|
195
|
+
tertiaryContainer: T(90),
|
|
196
|
+
onTertiaryContainer: T(30),
|
|
197
|
+
error: E(40),
|
|
198
|
+
onError: E(100),
|
|
199
|
+
errorContainer: E(90),
|
|
200
|
+
onErrorContainer: E(30),
|
|
201
|
+
background: N(98),
|
|
202
|
+
onBackground: N(10),
|
|
203
|
+
surface: N(98),
|
|
204
|
+
onSurface: N(10),
|
|
205
|
+
surfaceVariant: V(90),
|
|
206
|
+
onSurfaceVariant: V(30),
|
|
207
|
+
surfaceDim: N(87),
|
|
208
|
+
surfaceBright: N(98),
|
|
209
|
+
surfaceContainerLowest: N(100),
|
|
210
|
+
surfaceContainerLow: N(96),
|
|
211
|
+
surfaceContainer: N(94),
|
|
212
|
+
surfaceContainerHigh: N(92),
|
|
213
|
+
surfaceContainerHighest: N(90),
|
|
214
|
+
outline: V(50),
|
|
215
|
+
outlineVariant: V(80),
|
|
216
|
+
inverseSurface: N(20),
|
|
217
|
+
inverseOnSurface: N(95),
|
|
218
|
+
inversePrimary: P(80),
|
|
219
|
+
scrim: N(0),
|
|
220
|
+
shadow: N(0)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
__name(scheme, "scheme");
|
|
224
|
+
var SHAPE = {
|
|
225
|
+
none: 0,
|
|
226
|
+
extraSmall: 4,
|
|
227
|
+
small: 8,
|
|
228
|
+
medium: 12,
|
|
229
|
+
large: 16,
|
|
230
|
+
largeIncreased: 20,
|
|
231
|
+
extraLarge: 28,
|
|
232
|
+
extraLargeIncreased: 32,
|
|
233
|
+
extraExtraLarge: 48,
|
|
234
|
+
full: 9999
|
|
235
|
+
};
|
|
236
|
+
var FONT_STACK = '"Roboto Flex", "Roboto", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", system-ui, sans-serif';
|
|
237
|
+
var MEDAL = {
|
|
238
|
+
gold: lch(58, 52, 85),
|
|
239
|
+
silver: lch(66, 5, 260),
|
|
240
|
+
bronze: lch(48, 36, 48)
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/index.ts
|
|
41
244
|
var path = __toESM(require("path"));
|
|
42
245
|
var fs = __toESM(require("fs"));
|
|
43
246
|
var name = "cchess";
|
|
44
247
|
var usage = `## 使用
|
|
45
248
|
|
|
46
|
-
\`cchess
|
|
249
|
+
\`cchess.开始\` 即可入座,双方就位后自动开局;\`cchess.开始 人机\` 直接挑战皮卡鱼。着法可直接发送,例如 \`炮二平五\` 或 \`b2e2\`。
|
|
47
250
|
|
|
48
251
|
悔棋请求送出后,对方无需带指令前缀,直接回复 \`同意\` 或 \`拒绝\` 即可表决。
|
|
49
252
|
|
|
@@ -51,26 +254,20 @@ var usage = `## 使用
|
|
|
51
254
|
|
|
52
255
|
| 指令 | 说明 |
|
|
53
256
|
| --- | --- |
|
|
54
|
-
| \`cchess
|
|
55
|
-
| \`cchess
|
|
56
|
-
| \`cchess
|
|
57
|
-
| \`cchess.移动 <着法>\` | 落子 |
|
|
58
|
-
| \`cchess.悔棋\` | 请求悔棋 |
|
|
59
|
-
| \`cchess.悔棋 <同意/拒绝>\` | 表决悔棋,也可直接回复同意或拒绝 |
|
|
257
|
+
| \`cchess.开始 [红/黑] [人人/人机]\` | 入座并开局,双方就位后自动开战 |
|
|
258
|
+
| \`cchess.开始 <FEN>\` | 摆谱,导入局面后再 \`cchess.开始\` 即可 |
|
|
259
|
+
| \`cchess.悔棋 [同意/拒绝]\` | 请求悔棋,也可直接回复同意或拒绝表决 |
|
|
60
260
|
| \`cchess.认输\` | 认输 |
|
|
61
261
|
| \`cchess.结束\` | 强制结束棋局 |
|
|
62
|
-
| \`cchess
|
|
63
|
-
| \`cchess
|
|
64
|
-
| \`cchess.查看云库残局 [DTM/DTC]\` | 云库残局统计 |
|
|
65
|
-
| \`cchess.排行榜 [胜场/输场] [人数]\` | 排行榜 |
|
|
66
|
-
| \`cchess.查询玩家记录 [@某人]\` | 战绩 |`;
|
|
262
|
+
| \`cchess.棋绩 [@某人/榜] [胜场/输场] [人数]\` | 查询战绩与排行榜 |
|
|
263
|
+
| \`cchess.查看云库残局 [DTM/DTC]\` | 云库残局统计 |`;
|
|
67
264
|
var inject = ["database", "canvas"];
|
|
68
265
|
var pieceSkins = ["棋弈无限图形棋子", "云库木制棋子", "刘炳森红黑牛角隶书棋子", "木制棋子", "本纹隶书棋子", "银灰将军体棋子", "棋弈无限红绿正棋子", "棋弈无限红绿棋子", "棋天大圣棋子", "棋者象棋棋子", "楷书玉石棋子", "水墨青瓷棋子", "牛皮纸华康金文棋子", "皮卡鱼棋子", "红蓝祥隶棋子", "红黑精典无阴影棋子", "红黑精典棋子", "行书玉石棋子", "象甲棋子", "金属棋子篆体棋子", "镌刻华彩棋子", "鹏飞红黑棋子", "鹏飞经典棋子", "鹏飞绿龙棋子", "龙腾四海棋子"];
|
|
69
266
|
var boardSkins = ["棋弈无限红绿棋盘", "一鸣惊人棋盘", "三星堆棋盘", "云库木制棋盘", "云心鹤眼棋盘", "兰亭集序无字棋盘", "兰亭集序棋盘", "凤舞九天棋盘", "卯兔添福棋盘", "叱咤风云棋盘", "君临天下棋盘", "壁画古梦棋盘", "大闹天宫棋盘", "天山共色棋盘", "女神之约棋盘", "小吕飞刀棋盘", "山海绘卷棋盘", "护眼绿棋盘", "星河璀璨棋盘", "木制棋盘", "本纹隶书白棋盘", "本纹隶书黑棋盘", "桃花源记棋盘", "棋天大圣棋盘", "棋天无大圣棋盘", "武侠江湖棋盘", "水墨青瓷棋盘", "清悠茶道棋盘", "游园惊梦棋盘", "牛转乾坤棋盘", "玉石太极棋盘", "皓月无痕棋盘", "皮卡鱼棋盘", "盲棋神迹棋盘", "空城计棋盘", "竹波烟雨棋盘", "经典木棋盘棋盘", "绿色棋盘", "象甲2023棋盘", "象甲征程棋盘", "象甲重燃棋盘", "金牛降世棋盘", "金虎贺岁棋盘", "金虎贺岁(王天一签名版)棋盘", "鎏金岁月棋盘", "霸王别姬棋盘", "鸿门宴棋盘", "鹏飞红黑棋盘", "鹏飞经典棋盘", "鹏飞绿龙棋盘", "龙腾天坛棋盘"];
|
|
70
267
|
var Config = import_koishi.Schema.object({
|
|
71
268
|
boardSkin: import_koishi.Schema.union(boardSkins).default("象甲2023棋盘").description(`棋盘皮肤。`),
|
|
72
269
|
pieceSkin: import_koishi.Schema.union(pieceSkins).default("象甲棋子").description(`棋子皮肤。`),
|
|
73
|
-
allowFreePieceMovementInHumanMachineMode: import_koishi.Schema.boolean().default(false).description(
|
|
270
|
+
allowFreePieceMovementInHumanMachineMode: import_koishi.Schema.boolean().default(false).description(`是否允许在人机模式下所有用户都可以自由移动棋子,开启后可以不入座直接开始人机对局。`),
|
|
74
271
|
defaultEngineThinkingDepth: import_koishi.Schema.number().min(0).max(100).default(10).description(`默认引擎思考深度,越高 AI 棋力越强,耗时也越长(小于 1 时按 1 计算)。由于 Nodejs 不支持 SIMD,所以不建议设置过高。`),
|
|
75
272
|
defaultMaxLeaderboardEntries: import_koishi.Schema.number().min(0).default(10).description(`显示排行榜时默认的最大人数。`),
|
|
76
273
|
retractDelay: import_koishi.Schema.number().min(0).default(0).description(`自动撤回等待的时间,单位是秒。值为 0 时不启用自动撤回功能。`),
|
|
@@ -121,6 +318,22 @@ function apply(ctx, config) {
|
|
|
121
318
|
}
|
|
122
319
|
});
|
|
123
320
|
});
|
|
321
|
+
const FRAME = scheme(60);
|
|
322
|
+
function roundRect(context, x, y, width, height, radius) {
|
|
323
|
+
const r = Math.max(0, Math.min(radius, width / 2, height / 2));
|
|
324
|
+
context.beginPath();
|
|
325
|
+
context.moveTo(x + r, y);
|
|
326
|
+
context.lineTo(x + width - r, y);
|
|
327
|
+
context.arcTo(x + width, y, x + width, y + r, r);
|
|
328
|
+
context.lineTo(x + width, y + height - r);
|
|
329
|
+
context.arcTo(x + width, y + height, x + width - r, y + height, r);
|
|
330
|
+
context.lineTo(x + r, y + height);
|
|
331
|
+
context.arcTo(x, y + height, x, y + height - r, r);
|
|
332
|
+
context.lineTo(x, y + r);
|
|
333
|
+
context.arcTo(x, y, x + r, y, r);
|
|
334
|
+
context.closePath();
|
|
335
|
+
}
|
|
336
|
+
__name(roundRect, "roundRect");
|
|
124
337
|
const CELL_WIDTH = 54;
|
|
125
338
|
const ARROW_OFFSET = 33;
|
|
126
339
|
const BOARD_PADDING = 33;
|
|
@@ -138,8 +351,14 @@ function apply(ctx, config) {
|
|
|
138
351
|
const thinkingDepth = Math.max(1, Math.round(config.defaultEngineThinkingDepth) || 1);
|
|
139
352
|
const ENGINE_TIMEOUT = 120 * 1e3;
|
|
140
353
|
const piecesImgResources = loadPiecesImageResources(config.pieceSkin);
|
|
141
|
-
const outerFrameImg = fs.readFileSync(path.join(__dirname, "assets", "棋盘皮肤", `外框.png`));
|
|
142
354
|
const boardSkinImg = fs.readFileSync(path.join(__dirname, "assets", "棋盘皮肤", `${config.boardSkin}.webp`));
|
|
355
|
+
const FRAME_WIDTH = 620;
|
|
356
|
+
const FRAME_HEIGHT = 780;
|
|
357
|
+
const FRAME_BOARD = { x: 36.5, y: 90, width: 550, height: 605 };
|
|
358
|
+
const FRAME_ZOOM = FRAME_BOARD.width / BOARD_WIDTH;
|
|
359
|
+
const FILES = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];
|
|
360
|
+
const RED_FILES = ["九", "八", "七", "六", "五", "四", "三", "二", "一"];
|
|
361
|
+
const ARROW_COLOR = harmonize("#80ab45", 58, 56);
|
|
143
362
|
const possibleFigureNames = ["将", "士", "象", "车", "马", "炮", "卒", "帅", "仕", "相", "兵"];
|
|
144
363
|
ctx.model.extend("cchess_game_records", {
|
|
145
364
|
id: "unsigned",
|
|
@@ -238,131 +457,82 @@ function apply(ctx, config) {
|
|
|
238
457
|
if (playerRecord.length !== 0 && playerRecord[0].side !== convertTurnToString(gameRecord.turn)) {
|
|
239
458
|
return next();
|
|
240
459
|
}
|
|
241
|
-
await session
|
|
460
|
+
await handleMove(session, moveOperation);
|
|
242
461
|
});
|
|
243
462
|
ctx.command("cchess", "中国象棋游戏指令帮助").action(async ({ session }) => {
|
|
244
463
|
await session.execute(`cchess -h`);
|
|
245
464
|
});
|
|
246
|
-
ctx.command("cchess
|
|
465
|
+
ctx.command("cchess.开始 [args:text]", "入座并开局;可选「红 / 黑」「人人 / 人机」,也可直接粘贴 FEN 摆谱").action(async ({ session }, args) => {
|
|
247
466
|
const { username, userId, channelId } = session;
|
|
248
467
|
await updateNameInPlayerRecord(userId, username);
|
|
249
468
|
const gameRecord = await getGameRecord(channelId);
|
|
250
469
|
if (gameRecord.isStarted) {
|
|
470
|
+
return await sendMessage(session, alreadyStartedPanel(username));
|
|
471
|
+
}
|
|
472
|
+
const text = (args ?? "").trim();
|
|
473
|
+
if (text && isValidateFen(text)) {
|
|
474
|
+
const startFen = text.includes("moves") ? text.split("moves")[0].trim() : text;
|
|
475
|
+
await ctx.database.set("cchess_game_records", { channelId }, {
|
|
476
|
+
turn: fenToTurn(text),
|
|
477
|
+
board: fenToBoard(text),
|
|
478
|
+
moveList: [],
|
|
479
|
+
movesAfterLastEat: [],
|
|
480
|
+
boardAfterLastEat: fenToBoard(text),
|
|
481
|
+
turnAfterLastEat: fenToTurn(text),
|
|
482
|
+
lastMove: null,
|
|
483
|
+
isHistoryMode: false,
|
|
484
|
+
originalState: {},
|
|
485
|
+
startFen
|
|
486
|
+
});
|
|
487
|
+
await parseFen(channelId, text);
|
|
488
|
+
const record = await getGameRecord(channelId);
|
|
251
489
|
return await sendMessage(session, panel({
|
|
252
|
-
icon: "
|
|
253
|
-
title: "
|
|
490
|
+
icon: "✅",
|
|
491
|
+
title: "棋盘摆放完毕",
|
|
254
492
|
at: username,
|
|
255
|
-
body: ["
|
|
256
|
-
tips: ["
|
|
493
|
+
body: [field("轮走方", withSideIcon(convertTurnToString(record.turn)))],
|
|
494
|
+
tips: ["发送「cchess.开始」由此局面开战"],
|
|
257
495
|
image: await renderBoard(channelId)
|
|
258
496
|
}));
|
|
259
497
|
}
|
|
260
|
-
|
|
261
|
-
if (choice?.includes("红") && choice.includes("黑")) {
|
|
498
|
+
if (text.includes("红") && text.includes("黑")) {
|
|
262
499
|
return await sendMessage(session, panel({
|
|
263
500
|
icon: "⚠️",
|
|
264
501
|
title: "一次只能选一方",
|
|
265
502
|
at: username,
|
|
266
503
|
body: ["请只选择红方或黑方。"],
|
|
267
|
-
tips: ["「cchess
|
|
268
|
-
}));
|
|
269
|
-
}
|
|
270
|
-
if (choice?.includes("红")) choice = "红方";
|
|
271
|
-
else if (choice?.includes("黑")) choice = "黑方";
|
|
272
|
-
else choice = Math.random() < 0.5 ? "红方" : "黑方";
|
|
273
|
-
const selfRecords = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
|
|
274
|
-
const allRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
275
|
-
const playersNum = allRecords.length;
|
|
276
|
-
if (selfRecords.length === 0) {
|
|
277
|
-
await ctx.database.create("cchess_gaming_player_records", { channelId, userId, username, side: choice });
|
|
278
|
-
return await sendMessage(session, panel({
|
|
279
|
-
icon: "✅",
|
|
280
|
-
title: "入座成功",
|
|
281
|
-
at: username,
|
|
282
|
-
body: [field("阵营", withSideIcon(choice)), field("当前人数", `${playersNum + 1} 人`)],
|
|
283
|
-
tips: ["再次发送「cchess.加入 红 / 黑」即可换边", "人齐后发送「cchess.开始 人人」开局"]
|
|
284
|
-
}));
|
|
285
|
-
} else {
|
|
286
|
-
await ctx.database.set("cchess_gaming_player_records", { channelId, userId }, { side: choice });
|
|
287
|
-
return await sendMessage(session, panel({
|
|
288
|
-
icon: "✅",
|
|
289
|
-
title: "换边成功",
|
|
290
|
-
at: username,
|
|
291
|
-
body: [field("新的阵营", withSideIcon(choice)), field("当前人数", `${playersNum} 人`)]
|
|
504
|
+
tips: ["「cchess.开始 红」执红先行", "「cchess.开始 黑」执黑应招"]
|
|
292
505
|
}));
|
|
293
506
|
}
|
|
294
|
-
|
|
295
|
-
ctx.command("cchess.退出", "退出游戏").action(async ({ session }) => {
|
|
296
|
-
const { username, userId, channelId } = session;
|
|
297
|
-
await updateNameInPlayerRecord(userId, username);
|
|
298
|
-
const gameRecord = await getGameRecord(channelId);
|
|
299
|
-
if (gameRecord.isStarted) {
|
|
300
|
-
return await sendMessage(session, panel({
|
|
301
|
-
icon: "⚠️",
|
|
302
|
-
title: "棋局已经开战",
|
|
303
|
-
at: username,
|
|
304
|
-
body: ["对局进行中,无法退出。"],
|
|
305
|
-
tips: ["可发送「cchess.认输」结束本局"]
|
|
306
|
-
}));
|
|
307
|
-
}
|
|
308
|
-
const selfRecords = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
|
|
309
|
-
const allRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
310
|
-
const playersNum = allRecords.length;
|
|
311
|
-
if (selfRecords.length === 0) {
|
|
312
|
-
return await sendMessage(session, notJoinedPanel(username));
|
|
313
|
-
} else {
|
|
314
|
-
await ctx.database.remove("cchess_gaming_player_records", { channelId, userId });
|
|
315
|
-
return await sendMessage(session, panel({
|
|
316
|
-
icon: "✅",
|
|
317
|
-
title: "已离席",
|
|
318
|
-
at: username,
|
|
319
|
-
body: ["已退出本局。", field("剩余人数", `${playersNum - 1} 人`)]
|
|
320
|
-
}));
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
ctx.command("cchess.开始 [mode:string]", "开始对局,mode 可选「人人」或「人机」").action(async ({ session }, mode) => {
|
|
324
|
-
const { username, userId, channelId } = session;
|
|
325
|
-
await updateNameInPlayerRecord(userId, username);
|
|
326
|
-
const gameRecord = await getGameRecord(channelId);
|
|
327
|
-
if (gameRecord.isStarted) {
|
|
328
|
-
return await sendMessage(session, alreadyStartedPanel(username));
|
|
329
|
-
}
|
|
330
|
-
const playerRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
331
|
-
const playersNum = playerRecords.length;
|
|
332
|
-
if (/机|电脑|ai|engine|pve/i.test(mode ?? "")) {
|
|
333
|
-
return await startVersusEngine(session, gameRecord, playerRecords);
|
|
334
|
-
}
|
|
507
|
+
const chosenSide = text.includes("红") ? "红方" : text.includes("黑") ? "黑方" : "";
|
|
335
508
|
void checkEngine(channelId);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
509
|
+
if (/机|电脑|ai|engine|pve/i.test(text)) {
|
|
510
|
+
const players2 = await seatPlayer(channelId, userId, username, chosenSide);
|
|
511
|
+
return await startVersusEngine(session, gameRecord, players2);
|
|
512
|
+
}
|
|
513
|
+
const players = await seatPlayer(channelId, userId, username, chosenSide);
|
|
514
|
+
const redPlayers = players.filter((player) => player.side === "红方");
|
|
515
|
+
const blackPlayers = players.filter((player) => player.side === "黑方");
|
|
516
|
+
const selfSide = players.find((player) => player.userId === userId)?.side ?? "";
|
|
517
|
+
if (!redPlayers.length || !blackPlayers.length) {
|
|
340
518
|
return await sendMessage(session, panel({
|
|
341
|
-
icon: "
|
|
342
|
-
title: "
|
|
519
|
+
icon: "⏳",
|
|
520
|
+
title: "已入座,静候对手",
|
|
343
521
|
at: username,
|
|
344
|
-
body: [
|
|
345
|
-
|
|
522
|
+
body: [
|
|
523
|
+
field("阵营", withSideIcon(selfSide)),
|
|
524
|
+
field("当前人数", `${players.length} 人`),
|
|
525
|
+
!blackPlayers.length ? "黑方尚无人。" : "红方尚无人。"
|
|
526
|
+
],
|
|
527
|
+
tips: ["好友发送「cchess.开始」补位,双方就位即自动开局", "「cchess.开始 黑」可指定阵营"]
|
|
346
528
|
}));
|
|
347
529
|
}
|
|
348
|
-
if (redPlayers.length !== 1 || blackPlayers.length !== 1) {
|
|
349
|
-
const randomPlayer = playerRecords[Math.floor(Math.random() * playerRecords.length)];
|
|
350
|
-
randomPlayer.side = redPlayers.length < 1 ? "红方" : "黑方";
|
|
351
|
-
assignNotice = `🎲 @${randomPlayer.username} 被随机分配至 ${withSideIcon(randomPlayer.side)}`;
|
|
352
|
-
await ctx.database.set("cchess_gaming_player_records", {
|
|
353
|
-
channelId,
|
|
354
|
-
userId: randomPlayer.userId
|
|
355
|
-
}, { side: randomPlayer.side });
|
|
356
|
-
redPlayers = playerRecords.filter((player) => player.side === "红方");
|
|
357
|
-
blackPlayers = playerRecords.filter((player) => player.side === "黑方");
|
|
358
|
-
}
|
|
359
530
|
await ctx.database.set("cchess_game_records", { channelId }, { isStarted: true });
|
|
360
531
|
const sideString = convertTurnToString(gameRecord.turn);
|
|
361
532
|
return await sendMessage(session, panel({
|
|
362
533
|
icon: "✅",
|
|
363
534
|
title: "楚河汉界,对局开始",
|
|
364
535
|
body: [
|
|
365
|
-
...assignNotice ? [assignNotice, ""] : [],
|
|
366
536
|
`${withSideIcon("红方")}(${redPlayers.length})`,
|
|
367
537
|
...redPlayers.map((player) => ` @${player.username}`),
|
|
368
538
|
"",
|
|
@@ -385,7 +555,7 @@ function apply(ctx, config) {
|
|
|
385
555
|
title: "棋盘尚且空空",
|
|
386
556
|
at: username,
|
|
387
557
|
body: ["当前没有进行中的对局,无需收拾残局。"],
|
|
388
|
-
tips: ["发送「cchess
|
|
558
|
+
tips: ["发送「cchess.开始」入座,再开一局"]
|
|
389
559
|
}));
|
|
390
560
|
}
|
|
391
561
|
await endGame(channelId);
|
|
@@ -394,10 +564,10 @@ function apply(ctx, config) {
|
|
|
394
564
|
title: "对局已强制结束",
|
|
395
565
|
at: username,
|
|
396
566
|
body: ["棋盘已收,胜负不计。"],
|
|
397
|
-
tips: ["发送「cchess
|
|
567
|
+
tips: ["发送「cchess.开始」重整旗鼓"]
|
|
398
568
|
}));
|
|
399
569
|
});
|
|
400
|
-
|
|
570
|
+
async function handleMove(session, moveOperation) {
|
|
401
571
|
const { username, userId, channelId } = session;
|
|
402
572
|
await updateNameInPlayerRecord(userId, username);
|
|
403
573
|
moveOperation = moveOperation?.trim();
|
|
@@ -519,7 +689,8 @@ function apply(ctx, config) {
|
|
|
519
689
|
} finally {
|
|
520
690
|
busyChannels.delete(channelId);
|
|
521
691
|
}
|
|
522
|
-
}
|
|
692
|
+
}
|
|
693
|
+
__name(handleMove, "handleMove");
|
|
523
694
|
ctx.command("cchess.悔棋 [decision:string]", "请求悔棋;待决时由对方以「同意 / 拒绝」表决").action(async ({ session }, decision) => {
|
|
524
695
|
const { username, userId, channelId } = session;
|
|
525
696
|
await updateNameInPlayerRecord(userId, username);
|
|
@@ -635,109 +806,30 @@ function apply(ctx, config) {
|
|
|
635
806
|
body: isDtc ? ["DTC:距吃子的步数统计。", "https://www.chessdb.cn/egtb_info.html"] : ["DTM:距将死的步数统计。", "https://www.chessdb.cn/egtb_info_dtm.html"]
|
|
636
807
|
}));
|
|
637
808
|
});
|
|
638
|
-
ctx.command("cchess
|
|
639
|
-
const { username, userId, channelId } = session;
|
|
640
|
-
await updateNameInPlayerRecord(userId, username);
|
|
641
|
-
if (!fen) {
|
|
642
|
-
return await sendMessage(session, panel({
|
|
643
|
-
icon: "📋",
|
|
644
|
-
title: "FEN 串使用方法",
|
|
645
|
-
at: username,
|
|
646
|
-
body: ["中国象棋 FEN 格式规范:", "https://www.xqbase.com/protocol/cchess_fen.htm"]
|
|
647
|
-
}));
|
|
648
|
-
}
|
|
649
|
-
const gameRecord = await getGameRecord(channelId);
|
|
650
|
-
if (gameRecord.isStarted) {
|
|
651
|
-
return await sendMessage(session, panel({
|
|
652
|
-
icon: "⚠️",
|
|
653
|
-
title: "棋局进行中",
|
|
654
|
-
at: username,
|
|
655
|
-
body: ["对弈期间不可改动棋盘。"],
|
|
656
|
-
tips: ["发送「cchess.结束」收局后再行摆谱"]
|
|
657
|
-
}));
|
|
658
|
-
}
|
|
659
|
-
fen = fen?.trim();
|
|
660
|
-
if (!fen || !isValidateFen(fen)) {
|
|
661
|
-
return await sendMessage(session, panel({
|
|
662
|
-
icon: "❌",
|
|
663
|
-
title: "FEN 串无法解析",
|
|
664
|
-
at: username,
|
|
665
|
-
body: ["请检查局面串的格式是否完整。"],
|
|
666
|
-
tips: [
|
|
667
|
-
"示例:rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w",
|
|
668
|
-
"发送「cchess.编辑棋盘」查看规范"
|
|
669
|
-
]
|
|
670
|
-
}));
|
|
671
|
-
}
|
|
672
|
-
const newStartFen = fen.includes("moves") ? fen.split("moves")[0].trim() : fen;
|
|
673
|
-
await ctx.database.set("cchess_game_records", { channelId }, {
|
|
674
|
-
turn: fenToTurn(fen),
|
|
675
|
-
board: fenToBoard(fen),
|
|
676
|
-
moveList: [],
|
|
677
|
-
movesAfterLastEat: [],
|
|
678
|
-
boardAfterLastEat: fenToBoard(fen),
|
|
679
|
-
turnAfterLastEat: fenToTurn(fen),
|
|
680
|
-
lastMove: null,
|
|
681
|
-
isHistoryMode: false,
|
|
682
|
-
originalState: {},
|
|
683
|
-
startFen: newStartFen
|
|
684
|
-
});
|
|
685
|
-
await parseFen(channelId, fen);
|
|
686
|
-
const record = await getGameRecord(channelId);
|
|
687
|
-
return await sendMessage(session, panel({
|
|
688
|
-
icon: "✅",
|
|
689
|
-
title: "棋盘摆放完毕",
|
|
690
|
-
at: username,
|
|
691
|
-
body: [field("轮走方", withSideIcon(convertTurnToString(record.turn)))],
|
|
692
|
-
tips: ["发送「cchess.开始 人人」或「cchess.开始 人机」由此局面开战"],
|
|
693
|
-
image: await renderBoard(channelId)
|
|
694
|
-
}));
|
|
695
|
-
});
|
|
696
|
-
ctx.command("cchess.编辑棋盘.导出", "导出FEN串").action(async ({ session }) => {
|
|
697
|
-
const { username, userId, channelId } = session;
|
|
698
|
-
await updateNameInPlayerRecord(userId, username);
|
|
699
|
-
const gameRecord = await getGameRecord(channelId);
|
|
700
|
-
if (!gameRecord.isStarted) {
|
|
701
|
-
return await sendMessage(session, notStartedPanel(username));
|
|
702
|
-
}
|
|
703
|
-
if (gameRecord.isAnalyzing || busyChannels.has(channelId)) {
|
|
704
|
-
return await sendMessage(session, analyzingPanel(username));
|
|
705
|
-
}
|
|
706
|
-
const fenWithFullMove = await getFenWithFullMove(channelId);
|
|
707
|
-
return await sendMessage(session, panel({
|
|
708
|
-
icon: "📋",
|
|
709
|
-
title: "当前局面 FEN",
|
|
710
|
-
at: username,
|
|
711
|
-
body: [fenWithFullMove],
|
|
712
|
-
tips: ["可用「cchess.编辑棋盘 <FEN>」还原此局面"]
|
|
713
|
-
}));
|
|
714
|
-
});
|
|
715
|
-
ctx.command("cchess.排行榜 [type:string] [number:number]", "查看玩家排行榜").action(async ({ session }, type, number = config.defaultMaxLeaderboardEntries) => {
|
|
716
|
-
const { userId, username } = session;
|
|
717
|
-
await updateNameInPlayerRecord(userId, username);
|
|
718
|
-
if (type && /^\d+$/.test(type)) {
|
|
719
|
-
number = Number(type);
|
|
720
|
-
type = "";
|
|
721
|
-
}
|
|
722
|
-
if (typeof number !== "number" || isNaN(number) || number < 0) {
|
|
723
|
-
return await sendMessage(session, invalidLeaderboardSizePanel(username));
|
|
724
|
-
}
|
|
725
|
-
const byLose = !!type && /负|输|lose/i.test(type);
|
|
726
|
-
return await getLeaderboard(
|
|
727
|
-
session,
|
|
728
|
-
byLose ? "lose" : "win",
|
|
729
|
-
byLose ? "总输场排行榜" : "总胜场排行榜",
|
|
730
|
-
byLose ? "🍂" : "🏆",
|
|
731
|
-
number
|
|
732
|
-
);
|
|
733
|
-
});
|
|
734
|
-
ctx.command("cchess.查询玩家记录 [targetUser:text]", "查询玩家记录").action(async ({ session }, targetUser) => {
|
|
809
|
+
ctx.command("cchess.棋绩 [args:text]", "查询战绩;带「榜」看排行榜,@某人 可查看对方战绩").action(async ({ session }, args) => {
|
|
735
810
|
let { userId, username } = session;
|
|
736
811
|
await updateNameInPlayerRecord(userId, username);
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
812
|
+
const text = (args ?? "").trim();
|
|
813
|
+
const byLose = /负|输|lose/i.test(text);
|
|
814
|
+
const numberMatch = text.match(/\d+/);
|
|
815
|
+
const hasTarget = text.includes("<at");
|
|
816
|
+
const isLeaderboard = !hasTarget && (/榜|排行|排名|leaderboard/i.test(text) || byLose || !!numberMatch);
|
|
817
|
+
if (isLeaderboard) {
|
|
818
|
+
const size = numberMatch ? Number(numberMatch[0]) : config.defaultMaxLeaderboardEntries;
|
|
819
|
+
if (isNaN(size) || size < 0) {
|
|
820
|
+
return await sendMessage(session, invalidLeaderboardSizePanel(username));
|
|
821
|
+
}
|
|
822
|
+
return await getLeaderboard(
|
|
823
|
+
session,
|
|
824
|
+
byLose ? "lose" : "win",
|
|
825
|
+
byLose ? "总输场排行榜" : "总胜场排行榜",
|
|
826
|
+
byLose ? "🍂" : "🏆",
|
|
827
|
+
size
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
if (hasTarget) {
|
|
831
|
+
const replaced = await replaceAtTags(session, text);
|
|
832
|
+
const match = replaced.match(/<at id="([^"]+)"(?: name="([^"]+)")?\/>/);
|
|
741
833
|
userId = match?.[1] ?? userId;
|
|
742
834
|
username = match?.[2] ?? username;
|
|
743
835
|
}
|
|
@@ -754,7 +846,7 @@ function apply(ctx, config) {
|
|
|
754
846
|
title: "棋士档案",
|
|
755
847
|
at: session.username,
|
|
756
848
|
body: [field("查询对象", username), "尚无对局记录,静待首战。"],
|
|
757
|
-
tips: ["发送「cchess
|
|
849
|
+
tips: ["发送「cchess.开始」开启第一局"]
|
|
758
850
|
}));
|
|
759
851
|
}
|
|
760
852
|
const { win, lose } = targetUserRecord[0];
|
|
@@ -779,7 +871,7 @@ function apply(ctx, config) {
|
|
|
779
871
|
title: "棋局尚未开始",
|
|
780
872
|
at: username,
|
|
781
873
|
body: ["当前频道还没有进行中的对局。"],
|
|
782
|
-
tips: ["「cchess
|
|
874
|
+
tips: ["「cchess.开始」入座并对弈", "「cchess.开始 人机」挑战皮卡鱼"]
|
|
783
875
|
});
|
|
784
876
|
}
|
|
785
877
|
__name(notStartedPanel, "notStartedPanel");
|
|
@@ -798,8 +890,8 @@ function apply(ctx, config) {
|
|
|
798
890
|
icon: "⚠️",
|
|
799
891
|
title: "你尚未入座",
|
|
800
892
|
at: username,
|
|
801
|
-
body: ["
|
|
802
|
-
tips: ["发送「cchess
|
|
893
|
+
body: ["请先入座再落子。"],
|
|
894
|
+
tips: ["发送「cchess.开始」入座"]
|
|
803
895
|
});
|
|
804
896
|
}
|
|
805
897
|
__name(notJoinedPanel, "notJoinedPanel");
|
|
@@ -838,7 +930,7 @@ function apply(ctx, config) {
|
|
|
838
930
|
title: "皮卡鱼尚未就绪",
|
|
839
931
|
at: username,
|
|
840
932
|
body: ["引擎启动失败,暂时无法进行人机对战。"],
|
|
841
|
-
tips: ["稍后重试,或改用「cchess
|
|
933
|
+
tips: ["稍后重试,或改用「cchess.开始」人人对战"]
|
|
842
934
|
});
|
|
843
935
|
}
|
|
844
936
|
__name(engineUnavailablePanel, "engineUnavailablePanel");
|
|
@@ -848,7 +940,7 @@ function apply(ctx, config) {
|
|
|
848
940
|
title: "榜单人数有误",
|
|
849
941
|
at: username,
|
|
850
942
|
body: ["请输入不小于 0 的整数。"],
|
|
851
|
-
tips: ["例如:cchess
|
|
943
|
+
tips: ["例如:cchess.棋绩 榜 10"]
|
|
852
944
|
});
|
|
853
945
|
}
|
|
854
946
|
__name(invalidLeaderboardSizePanel, "invalidLeaderboardSizePanel");
|
|
@@ -874,7 +966,7 @@ function apply(ctx, config) {
|
|
|
874
966
|
field("胜方", withSideIcon(convertTurnToString(winSide))),
|
|
875
967
|
field("棋手", winners.length ? winners.map((player) => `@${player.username}`).join("、") : "皮卡鱼")
|
|
876
968
|
],
|
|
877
|
-
tips: ["发送「cchess
|
|
969
|
+
tips: ["发送「cchess.开始」再战一局"],
|
|
878
970
|
image
|
|
879
971
|
});
|
|
880
972
|
}
|
|
@@ -884,15 +976,6 @@ function apply(ctx, config) {
|
|
|
884
976
|
const playersNum = playerRecords.length;
|
|
885
977
|
const redPlayers = playerRecords.filter((player) => player.side === "红方");
|
|
886
978
|
const blackPlayers = playerRecords.filter((player) => player.side === "黑方");
|
|
887
|
-
if (playersNum < 1 && !config.allowFreePieceMovementInHumanMachineMode) {
|
|
888
|
-
return await sendMessage(session, panel({
|
|
889
|
-
icon: "⚠️",
|
|
890
|
-
title: "尚无棋手入座",
|
|
891
|
-
at: username,
|
|
892
|
-
body: ["人机对战至少需要 1 位棋手。"],
|
|
893
|
-
tips: ["发送「cchess.加入」即可入座挑战皮卡鱼"]
|
|
894
|
-
}));
|
|
895
|
-
}
|
|
896
979
|
if (!await checkEngine(channelId)) {
|
|
897
980
|
return await sendMessage(session, engineUnavailablePanel(username));
|
|
898
981
|
}
|
|
@@ -1027,7 +1110,7 @@ function apply(ctx, config) {
|
|
|
1027
1110
|
icon,
|
|
1028
1111
|
title,
|
|
1029
1112
|
body: topPlayers.length ? topPlayers.map((player, index) => `${MEDALS[index] ?? `${index + 1}.`} ${player.username} ${player[sortField]} 局`) : ["榜上尚无名姓,快去手谈一局。"],
|
|
1030
|
-
tips: topPlayers.length ? [] : ["发送「cchess
|
|
1113
|
+
tips: topPlayers.length ? [] : ["发送「cchess.开始」开启对局"]
|
|
1031
1114
|
}));
|
|
1032
1115
|
}
|
|
1033
1116
|
__name(getLeaderboard, "getLeaderboard");
|
|
@@ -1080,18 +1163,29 @@ function apply(ctx, config) {
|
|
|
1080
1163
|
return false;
|
|
1081
1164
|
}
|
|
1082
1165
|
__name(requestEngineMove, "requestEngineMove");
|
|
1083
|
-
async function
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1166
|
+
async function seatPlayer(channelId, userId, username, chosenSide) {
|
|
1167
|
+
const players = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
1168
|
+
const self = players.find((player) => player.userId === userId);
|
|
1169
|
+
if (!self) {
|
|
1170
|
+
await ctx.database.create("cchess_gaming_player_records", {
|
|
1171
|
+
channelId,
|
|
1172
|
+
userId,
|
|
1173
|
+
username,
|
|
1174
|
+
side: chosenSide || balancedSide(players)
|
|
1175
|
+
});
|
|
1176
|
+
} else if (chosenSide && self.side !== chosenSide) {
|
|
1177
|
+
await ctx.database.set("cchess_gaming_player_records", { channelId, userId }, { side: chosenSide });
|
|
1091
1178
|
}
|
|
1092
|
-
return
|
|
1179
|
+
return await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
1180
|
+
}
|
|
1181
|
+
__name(seatPlayer, "seatPlayer");
|
|
1182
|
+
function balancedSide(players) {
|
|
1183
|
+
const redNum = players.filter((player) => player.side === "红方").length;
|
|
1184
|
+
const blackNum = players.length - redNum;
|
|
1185
|
+
if (redNum === blackNum) return Math.random() < 0.5 ? "红方" : "黑方";
|
|
1186
|
+
return redNum < blackNum ? "红方" : "黑方";
|
|
1093
1187
|
}
|
|
1094
|
-
__name(
|
|
1188
|
+
__name(balancedSide, "balancedSide");
|
|
1095
1189
|
function fenToTurn(t) {
|
|
1096
1190
|
let e = t.split(" ");
|
|
1097
1191
|
return "b" == e[1] ? "b" : "w";
|
|
@@ -1520,12 +1614,42 @@ function apply(ctx, config) {
|
|
|
1520
1614
|
}
|
|
1521
1615
|
__name(convertTurnToString, "convertTurnToString");
|
|
1522
1616
|
async function createImage(buffer) {
|
|
1523
|
-
const canvas = await ctx.canvas.createCanvas(
|
|
1617
|
+
const canvas = await ctx.canvas.createCanvas(FRAME_WIDTH * scale, FRAME_HEIGHT * scale);
|
|
1524
1618
|
const context = canvas.getContext("2d");
|
|
1525
|
-
|
|
1526
|
-
context.
|
|
1619
|
+
context.scale(scale, scale);
|
|
1620
|
+
context.fillStyle = FRAME.surfaceContainer;
|
|
1621
|
+
roundRect(context, 0, 0, FRAME_WIDTH, FRAME_HEIGHT, SHAPE.extraLarge);
|
|
1622
|
+
context.fill();
|
|
1527
1623
|
const boardImg = await ctx.canvas.loadImage(buffer);
|
|
1528
|
-
context.drawImage(
|
|
1624
|
+
context.drawImage(
|
|
1625
|
+
boardImg,
|
|
1626
|
+
FRAME_BOARD.x,
|
|
1627
|
+
FRAME_BOARD.y,
|
|
1628
|
+
FRAME_BOARD.width,
|
|
1629
|
+
FRAME_BOARD.height
|
|
1630
|
+
);
|
|
1631
|
+
const columnAt = /* @__PURE__ */ __name((col) => FRAME_BOARD.x + (col * CELL_WIDTH + BOARD_PADDING) * FRAME_ZOOM, "columnAt");
|
|
1632
|
+
const rowAt = /* @__PURE__ */ __name((row) => FRAME_BOARD.y + (row * CELL_WIDTH + BOARD_PADDING) * FRAME_ZOOM, "rowAt");
|
|
1633
|
+
context.textAlign = "center";
|
|
1634
|
+
context.textBaseline = "middle";
|
|
1635
|
+
const label = /* @__PURE__ */ __name((text, x, y, size, color) => {
|
|
1636
|
+
context.font = `600 ${size}px ${FONT_STACK}`;
|
|
1637
|
+
context.fillStyle = color;
|
|
1638
|
+
context.fillText(text, x, y);
|
|
1639
|
+
}, "label");
|
|
1640
|
+
for (let col = 0; col < 9; col++) {
|
|
1641
|
+
const x = columnAt(isFlipBoard ? 8 - col : col);
|
|
1642
|
+
label(FILES[col], x, 30, 22, FRAME.onSurfaceVariant);
|
|
1643
|
+
label(String(col + 1), x, 64, 22, FRAME.outline);
|
|
1644
|
+
label(RED_FILES[col], x, FRAME_HEIGHT - 62, 24, FRAME.onSurfaceVariant);
|
|
1645
|
+
label(FILES[col], x, FRAME_HEIGHT - 26, 22, FRAME.outline);
|
|
1646
|
+
}
|
|
1647
|
+
for (let row = 0; row < 10; row++) {
|
|
1648
|
+
const y = rowAt(isFlipBoard ? 9 - row : row);
|
|
1649
|
+
const text = String(9 - row);
|
|
1650
|
+
label(text, 19, y, 22, FRAME.outline);
|
|
1651
|
+
label(text, FRAME_WIDTH - 19, y, 22, FRAME.outline);
|
|
1652
|
+
}
|
|
1529
1653
|
return canvas.toBuffer(imageMimeType);
|
|
1530
1654
|
}
|
|
1531
1655
|
__name(createImage, "createImage");
|
|
@@ -1995,7 +2119,7 @@ function apply(ctx, config) {
|
|
|
1995
2119
|
let dist = Math.sqrt((startX - endX) ** 2 + (startY - endY) ** 2);
|
|
1996
2120
|
let adjustedEndX = startX + (endX - startX) * (dist - 30) / dist;
|
|
1997
2121
|
let adjustedEndY = startY + (endY - startY) * (dist - 30) / dist;
|
|
1998
|
-
await drawLineArrow(context, startX * scale, startY * scale, adjustedEndX * scale, adjustedEndY * scale,
|
|
2122
|
+
await drawLineArrow(context, startX * scale, startY * scale, adjustedEndX * scale, adjustedEndY * scale, ARROW_COLOR + "b3", 15 * scale);
|
|
1999
2123
|
}
|
|
2000
2124
|
if (moveList.length > 0) {
|
|
2001
2125
|
const currentMoveId = moveList[moveList.length - 1].moveId;
|
package/lib/m3.d.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Material 3 Expressive 设计系统。
|
|
3
|
+
*
|
|
4
|
+
* 只依赖标准库,输出纯 CSS 变量与常量,任何渲染后端(puppeteer / canvas)都能用。
|
|
5
|
+
* 色板按 M3 的做法从一个源色推出六套色调板,再按角色映射成明暗两套配色。
|
|
6
|
+
*/
|
|
7
|
+
/** M3 的「色调」就是 CIE L*,0 为黑、100 为白。 */
|
|
8
|
+
export type Tone = number;
|
|
9
|
+
/** LCh -> #rrggbb。超出 sRGB 色域时按二分法降低彩度,色调与色相保持不变。 */
|
|
10
|
+
export declare function lch(tone: Tone, chroma: number, hue: number): string;
|
|
11
|
+
/** `#rrggbb` -> LCh,`lch()` 的逆运算。 */
|
|
12
|
+
export declare function lchOf(hex: string): {
|
|
13
|
+
tone: number;
|
|
14
|
+
chroma: number;
|
|
15
|
+
hue: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* 把一个任意来源的颜色(头像主色、用户自选色)收进本设计系统。
|
|
19
|
+
*
|
|
20
|
+
* 只保留它的色相,色调与彩度一律换成设计系统里的取值——于是每个人都还有
|
|
21
|
+
* 自己的颜色,整张图的明度节奏却是齐的,不会因为某个头像特别暗就糊成一团。
|
|
22
|
+
* 彩度太低的灰色头像没有可用的色相,退回主色。
|
|
23
|
+
*/
|
|
24
|
+
export declare function harmonize(hex: string, tone: Tone, chroma?: number, fallbackHue?: number): string;
|
|
25
|
+
/** 一套色调板:固定色相与彩度,按色调取色。 */
|
|
26
|
+
export type Palette = (tone: Tone) => string;
|
|
27
|
+
export declare function palette(hue: number, chroma: number): Palette;
|
|
28
|
+
export interface Palettes {
|
|
29
|
+
primary: Palette;
|
|
30
|
+
secondary: Palette;
|
|
31
|
+
tertiary: Palette;
|
|
32
|
+
neutral: Palette;
|
|
33
|
+
neutralVariant: Palette;
|
|
34
|
+
error: Palette;
|
|
35
|
+
}
|
|
36
|
+
export interface SourceOptions {
|
|
37
|
+
/** 主色彩度,默认 56,够鲜艳又不至于在浅色调上大面积溢出色域。 */
|
|
38
|
+
chroma?: number;
|
|
39
|
+
/**
|
|
40
|
+
* 第三色相对主色的相位差,默认 +60°。
|
|
41
|
+
* 这个值规范里是个经验值,并非永远合适:暖橙 +60° 会落到发闷的橄榄绿,
|
|
42
|
+
* 这种时候改成 -60° 取玫红,强调色才跳得出来。
|
|
43
|
+
*/
|
|
44
|
+
tertiaryShift?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 按 M3 的推导规则从源色相生成六套色调板。
|
|
48
|
+
*
|
|
49
|
+
* Expressive 相比基础风格更敢用彩度:第三色比规范里的保守取值更高,
|
|
50
|
+
* 中性色也留了一点源色的味道,成片的表面不会灰得发死。
|
|
51
|
+
*/
|
|
52
|
+
export declare function palettesOf(hue: number, options?: SourceOptions): Palettes;
|
|
53
|
+
export interface Scheme {
|
|
54
|
+
primary: string;
|
|
55
|
+
onPrimary: string;
|
|
56
|
+
primaryContainer: string;
|
|
57
|
+
onPrimaryContainer: string;
|
|
58
|
+
secondary: string;
|
|
59
|
+
onSecondary: string;
|
|
60
|
+
secondaryContainer: string;
|
|
61
|
+
onSecondaryContainer: string;
|
|
62
|
+
tertiary: string;
|
|
63
|
+
onTertiary: string;
|
|
64
|
+
tertiaryContainer: string;
|
|
65
|
+
onTertiaryContainer: string;
|
|
66
|
+
error: string;
|
|
67
|
+
onError: string;
|
|
68
|
+
errorContainer: string;
|
|
69
|
+
onErrorContainer: string;
|
|
70
|
+
background: string;
|
|
71
|
+
onBackground: string;
|
|
72
|
+
surface: string;
|
|
73
|
+
onSurface: string;
|
|
74
|
+
surfaceVariant: string;
|
|
75
|
+
onSurfaceVariant: string;
|
|
76
|
+
surfaceDim: string;
|
|
77
|
+
surfaceBright: string;
|
|
78
|
+
surfaceContainerLowest: string;
|
|
79
|
+
surfaceContainerLow: string;
|
|
80
|
+
surfaceContainer: string;
|
|
81
|
+
surfaceContainerHigh: string;
|
|
82
|
+
surfaceContainerHighest: string;
|
|
83
|
+
outline: string;
|
|
84
|
+
outlineVariant: string;
|
|
85
|
+
inverseSurface: string;
|
|
86
|
+
inverseOnSurface: string;
|
|
87
|
+
inversePrimary: string;
|
|
88
|
+
scrim: string;
|
|
89
|
+
shadow: string;
|
|
90
|
+
}
|
|
91
|
+
/** 生成一套完整的角色配色。`dark` 为真时返回暗色方案。 */
|
|
92
|
+
export declare function scheme(hue: number, dark?: boolean, options?: SourceOptions): Scheme;
|
|
93
|
+
/**
|
|
94
|
+
* M3 Expressive 形状刻度(px)。
|
|
95
|
+
* Expressive 把圆角整体推大了一档,容器之间靠「圆角差」而不是分隔线来分层。
|
|
96
|
+
*/
|
|
97
|
+
export declare const SHAPE: {
|
|
98
|
+
readonly none: 0;
|
|
99
|
+
readonly extraSmall: 4;
|
|
100
|
+
readonly small: 8;
|
|
101
|
+
readonly medium: 12;
|
|
102
|
+
readonly large: 16;
|
|
103
|
+
readonly largeIncreased: 20;
|
|
104
|
+
readonly extraLarge: 28;
|
|
105
|
+
readonly extraLargeIncreased: 32;
|
|
106
|
+
readonly extraExtraLarge: 48;
|
|
107
|
+
readonly full: 9999;
|
|
108
|
+
};
|
|
109
|
+
/** 正文与标题字体栈,覆盖 Windows / macOS / Linux 与随包字体三种情况。 */
|
|
110
|
+
export declare const FONT_STACK = "\"Roboto Flex\", \"Roboto\", \"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", \"Source Han Sans SC\", system-ui, sans-serif";
|
|
111
|
+
/** 等宽字体栈,用于比分、倒计时这类需要对齐的数字。 */
|
|
112
|
+
export declare const MONO_STACK = "\"Roboto Mono\", \"JetBrains Mono\", \"SF Mono\", \"Cascadia Mono\", Consolas, \"Noto Sans Mono\", monospace";
|
|
113
|
+
export interface TypeStyle {
|
|
114
|
+
size: number;
|
|
115
|
+
line: number;
|
|
116
|
+
weight: number;
|
|
117
|
+
tracking: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* M3 Expressive 字阶。`emphasized` 档提高字重,用在需要被一眼看到的地方,
|
|
121
|
+
* 这是 Expressive 与基础风格最直观的差别之一。
|
|
122
|
+
*/
|
|
123
|
+
export declare const TYPE: {
|
|
124
|
+
displayLarge: {
|
|
125
|
+
size: number;
|
|
126
|
+
line: number;
|
|
127
|
+
weight: number;
|
|
128
|
+
tracking: number;
|
|
129
|
+
};
|
|
130
|
+
displayMedium: {
|
|
131
|
+
size: number;
|
|
132
|
+
line: number;
|
|
133
|
+
weight: number;
|
|
134
|
+
tracking: number;
|
|
135
|
+
};
|
|
136
|
+
displaySmall: {
|
|
137
|
+
size: number;
|
|
138
|
+
line: number;
|
|
139
|
+
weight: number;
|
|
140
|
+
tracking: number;
|
|
141
|
+
};
|
|
142
|
+
headlineLarge: {
|
|
143
|
+
size: number;
|
|
144
|
+
line: number;
|
|
145
|
+
weight: number;
|
|
146
|
+
tracking: number;
|
|
147
|
+
};
|
|
148
|
+
headlineMedium: {
|
|
149
|
+
size: number;
|
|
150
|
+
line: number;
|
|
151
|
+
weight: number;
|
|
152
|
+
tracking: number;
|
|
153
|
+
};
|
|
154
|
+
headlineSmall: {
|
|
155
|
+
size: number;
|
|
156
|
+
line: number;
|
|
157
|
+
weight: number;
|
|
158
|
+
tracking: number;
|
|
159
|
+
};
|
|
160
|
+
titleLarge: {
|
|
161
|
+
size: number;
|
|
162
|
+
line: number;
|
|
163
|
+
weight: number;
|
|
164
|
+
tracking: number;
|
|
165
|
+
};
|
|
166
|
+
titleMedium: {
|
|
167
|
+
size: number;
|
|
168
|
+
line: number;
|
|
169
|
+
weight: number;
|
|
170
|
+
tracking: number;
|
|
171
|
+
};
|
|
172
|
+
titleSmall: {
|
|
173
|
+
size: number;
|
|
174
|
+
line: number;
|
|
175
|
+
weight: number;
|
|
176
|
+
tracking: number;
|
|
177
|
+
};
|
|
178
|
+
bodyLarge: {
|
|
179
|
+
size: number;
|
|
180
|
+
line: number;
|
|
181
|
+
weight: number;
|
|
182
|
+
tracking: number;
|
|
183
|
+
};
|
|
184
|
+
bodyMedium: {
|
|
185
|
+
size: number;
|
|
186
|
+
line: number;
|
|
187
|
+
weight: number;
|
|
188
|
+
tracking: number;
|
|
189
|
+
};
|
|
190
|
+
bodySmall: {
|
|
191
|
+
size: number;
|
|
192
|
+
line: number;
|
|
193
|
+
weight: number;
|
|
194
|
+
tracking: number;
|
|
195
|
+
};
|
|
196
|
+
labelLarge: {
|
|
197
|
+
size: number;
|
|
198
|
+
line: number;
|
|
199
|
+
weight: number;
|
|
200
|
+
tracking: number;
|
|
201
|
+
};
|
|
202
|
+
labelMedium: {
|
|
203
|
+
size: number;
|
|
204
|
+
line: number;
|
|
205
|
+
weight: number;
|
|
206
|
+
tracking: number;
|
|
207
|
+
};
|
|
208
|
+
labelSmall: {
|
|
209
|
+
size: number;
|
|
210
|
+
line: number;
|
|
211
|
+
weight: number;
|
|
212
|
+
tracking: number;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Expressive 的强调字重。取 600 而非规范里的 500:很多机器上只装了 400 / 700 两档,
|
|
217
|
+
* 500 会回落成常规字重,强调就白做了;600 在这种字体集下会匹配到 700,效果才稳定。
|
|
218
|
+
* 正文维持 400,字重拉高反而伤可读性。
|
|
219
|
+
*/
|
|
220
|
+
export declare const EMPHASIZED_WEIGHT: {
|
|
221
|
+
readonly display: 600;
|
|
222
|
+
readonly headline: 600;
|
|
223
|
+
readonly title: 600;
|
|
224
|
+
readonly label: 600;
|
|
225
|
+
};
|
|
226
|
+
/** 阴影高度(dp -> box-shadow),M3 只有 0 ~ 5 六档。 */
|
|
227
|
+
export declare const ELEVATION: readonly ["none", "0 1px 2px 0 rgba(0,0,0,.30), 0 1px 3px 1px rgba(0,0,0,.15)", "0 1px 2px 0 rgba(0,0,0,.30), 0 2px 6px 2px rgba(0,0,0,.15)", "0 1px 3px 0 rgba(0,0,0,.30), 0 4px 8px 3px rgba(0,0,0,.15)", "0 2px 3px 0 rgba(0,0,0,.30), 0 6px 10px 4px rgba(0,0,0,.15)", "0 4px 4px 0 rgba(0,0,0,.30), 0 8px 12px 6px rgba(0,0,0,.15)"];
|
|
228
|
+
/** 把配色方案铺成 `--md-sys-color-*` 变量。 */
|
|
229
|
+
export declare function colorVars(s: Scheme): string;
|
|
230
|
+
/** 形状、字阶、高度的变量,与配色一起构成完整的令牌集。 */
|
|
231
|
+
export declare function systemVars(): string;
|
|
232
|
+
/** 一段可直接塞进 `<style>` 的基础样式:令牌 + 排版重置。 */
|
|
233
|
+
export declare function baseline(s: Scheme): string;
|
|
234
|
+
/**
|
|
235
|
+
* 通用组件样式。各插件按需挑用,命名统一为 `m3-` 前缀。
|
|
236
|
+
*
|
|
237
|
+
* Expressive 的取向体现在三处:圆角整体偏大且同一张图里刻意拉开层次、
|
|
238
|
+
* 强调元素用高彩度的容器色而不是描边、以及成组元素之间用间距而不是分隔线来断句。
|
|
239
|
+
*/
|
|
240
|
+
/** 金银铜。色调依次 58 / 66 / 48,深浅本身就排出了名次。 */
|
|
241
|
+
export declare const MEDAL: {
|
|
242
|
+
readonly gold: string;
|
|
243
|
+
readonly silver: string;
|
|
244
|
+
readonly bronze: string;
|
|
245
|
+
};
|
|
246
|
+
export declare function components(): string;
|
|
247
|
+
/**
|
|
248
|
+
* 浏览器端的配色小库,序列化进页面脚本用。
|
|
249
|
+
*
|
|
250
|
+
* 有些图是在页面里用 canvas 画的,那边拿不到这个模块。与其在客户端另写一套
|
|
251
|
+
* HSL 近似,不如把同一套 LCh 运算原样送过去——服务端与浏览器端算出来的颜色
|
|
252
|
+
* 必须一致,否则「同一支色相」的承诺在两边会对不上。
|
|
253
|
+
*
|
|
254
|
+
* 暴露的接口与本模块同名:`M3.lch` / `M3.lchOf` / `M3.harmonize`。
|
|
255
|
+
*/
|
|
256
|
+
export declare function clientColorScript(): string;
|
package/package.json
CHANGED