koishi-plugin-ll-bns-fortune 0.1.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/assets/bg.png ADDED
Binary file
package/lib/data.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 默认文案池(全部可以在 Koishi 设置页里覆盖)
3
+ *
4
+ * 宜忌对支持三种写法,从简到繁:
5
+ * '打团本|单飞' 只给词,标签自动生成
6
+ * '打团本|单飞|3' 带权重
7
+ * '睡觉|熬夜|1|快睡吧,今天的梦里什么都有|今天或许值得再给好运一次机会。'
8
+ * 第 4/5 段是「详细文案」,
9
+ * 单独占一行输出(宜睡觉:/ 快睡吧…)
10
+ * 第 3 段是纯数字时按权重解析,否则按文案处理。
11
+ *
12
+ * 成对的意义:宜和忌永远来自同一条,结构上不可能出现「宜睡觉/忌睡觉」。
13
+ * 所有池子都只是「默认值」,用户在设置页改过之后以设置页为准。
14
+ */
15
+
16
+ /** 24 对宜忌(游戏 10 对 + 日常 14 对) */
17
+ export const DEFAULT_PAIRS = [
18
+ // ===== 游戏 =====
19
+ '合卡|炸卡',
20
+ '合印章|洗属性',
21
+ '做装备|强化',
22
+ '打团本|单飞',
23
+ '打极限本|硬抗',
24
+ '寻宝|贪宝箱',
25
+ '比武|上头',
26
+ '挂机刷怪|硬肝',
27
+ '逛拍卖行|冲动消费',
28
+ '刷副本日常|重复死亡',
29
+ // ===== 日常 =====
30
+ '睡觉|熬夜|1|快睡吧,今天的梦里什么都有|今天或许值得再给好运一次机会。',
31
+ '早起早睡|通宵',
32
+ '喝奶茶|空腹喝冰的',
33
+ '听歌放松|拆歌单',
34
+ '出门走走|宅到发霉',
35
+ '洗个热水澡|拖到半夜',
36
+ '整理背包|攒着不清',
37
+ '看攻略|盲打',
38
+ '陪门派水群|单机自闭',
39
+ '收邮件领奖|鸽日常',
40
+ '交委托任务|拖进度',
41
+ '给好友点赞|阴阳怪气',
42
+ '陪人打本|拒绝组队',
43
+ '抽卡|玄学十连',
44
+ ]
45
+
46
+ /** 剑灵怀旧服地图(经典四大区域,共 32 条)
47
+ * 想在某个地图下面配一句专属文案,就在后面加一段(顺序是「地图名|权重|文案」,权重可省):
48
+ * '御龙林 · 竹林海岸|烟花从未停歇,今晚的梦里似乎藏着一个好结果。'
49
+ * '御龙林 · 竹林海岸|3|烟花从未停歇…' ← 带权重
50
+ */
51
+ export const DEFAULT_MAPS = [
52
+ // ===== 御龙林 =====
53
+ '御龙林 · 竹林海岸|1|烟花从未停歇,今晚的梦里似乎藏着一个好结果。',
54
+ '御龙林 · 绿明村',
55
+ '御龙林 · 松岩岛',
56
+ '御龙林 · 亡者森林',
57
+ '御龙林 · 焚尸岗',
58
+ '御龙林 · 冲角团平南舰队',
59
+ '御龙林 · 黑龙寨',
60
+ '御龙林 · 恶魔洞',
61
+ // ===== 大漠 =====
62
+ '大漠 · 土门客栈',
63
+ '大漠 · 孤村',
64
+ '大漠 · 五彩岩岛',
65
+ '大漠 · 帝王陵墓',
66
+ '大漠 · 咆哮寺院',
67
+ '大漠 · 蝎子洞窟',
68
+ '大漠 · 红莲墓地',
69
+ // ===== 水月平原 =====
70
+ '水月平原 · 江流市',
71
+ '水月平原 · 大鱼坊',
72
+ '水月平原 · 半月湖',
73
+ '水月平原 · 鬼都',
74
+ '水月平原 · 冲角团本部',
75
+ '水月平原 · 血猪族巢穴',
76
+ '水月平原 · 永灵族遗迹',
77
+ '水月平原 · 蜘蛛巢穴',
78
+ '水月平原 · 灵石矿山',
79
+ // ===== 白青山脉 =====
80
+ '白青山脉 · 白青草原',
81
+ '白青山脉 · 雪域宫',
82
+ '白青山脉 · 浮游岛',
83
+ '白青山脉 · 蛇鳞峡谷',
84
+ '白青山脉 · 分裂迷宫',
85
+ '白青山脉 · 雪人洞窟',
86
+ '白青山脉 · 雷狱',
87
+ '白青山脉 · 悲叹的雪玉宫',
88
+ ]
89
+
90
+ /**
91
+ * 排版模板:{xxx} 会被替换成实际内容。
92
+ *
93
+ * 行首可以加的标记:
94
+ * [left] / [center] 指定这一行的对齐方式(默认跟随 panel.align)
95
+ * [size=0.85] 指定这一行的字号倍率(默认 1)
96
+ *
97
+ * 字号层级(size 标记 = 相对整体字号的倍率):
98
+ * 五角星 —— 由 panel.starSize 单独控制
99
+ * 标题行 0.86 —— 比正文大一点,作为抬头
100
+ * 正文 0.74 —— 比标题小,避免抢戏
101
+ * 所以整体字号(panel.charsPerLine 决定的那个)应当按「最大的一行」去调。
102
+ */
103
+ export const DEFAULT_FORMAT = [
104
+ '[center][size=0.86]您今日的运势指数为:',
105
+ '[center]{stars}',
106
+ '[size=0.74]今日幸运频道:{channel}',
107
+ '[size=0.74]宜{good}:{goodText}',
108
+ '[size=0.74]忌{bad}:{badText}',
109
+ '[size=0.74]今日最佳玄学地图是:{map}',
110
+ '[size=0.74]{mapText}',
111
+ ].join('\n')
112
+
113
+ /** 地图下面的那句补充说明。留空则模板里的 {mapText} 一行自动省略 */
114
+ export const DEFAULT_MAP_TEXT = ''
115
+
116
+ /** 运势指数权重:1~5 星,可自行调整(越大越容易抽到) */
117
+ export const DEFAULT_STAR_WEIGHTS = '1:5,2:15,3:35,4:30,5:15'
118
+
119
+ /** 频道号范围(含两端) */
120
+ export const CHANNEL_MIN = 1
121
+ export const CHANNEL_MAX = 99
package/lib/fonts.js ADDED
@@ -0,0 +1,320 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * 字体解析:把「字体名 / 文件路径 / 下载地址」统一变成可注册的字体文件。
5
+ *
6
+ * 为什么要能下载:Q 版圆体(站酷快乐体、得意黑之类)不是每台机器都装,
7
+ * 插件目录里也没有。所以这里内置一个字体目录,填个名字就能自动下到插件里缓存,
8
+ * 下次直接用本地文件,不用重复下载。
9
+ *
10
+ * 优先级:本地缓存 → 系统已装字体 → 下载 → 放弃(退回默认字体)
11
+ */
12
+
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+
16
+ /** 内置字体目录:直接填名字(fontPath 认这些名字,大小写、中英文都行) */
17
+ const FONT_CATALOG = {
18
+ '站酷快乐体': {
19
+ family: 'ZCOOL KuaiLe',
20
+ file: 'ZCOOLKuaiLe-Regular.ttf',
21
+ urls: [
22
+ 'https://cdn.jsdelivr.net/gh/googlefonts/zcool-kuaile@main/fonts/ttf/ZCOOLKuaiLe-Regular.ttf',
23
+ 'https://raw.githubusercontent.com/google/fonts/main/ofl/zcoolkuaile/ZCOOLKuaiLe-Regular.ttf',
24
+ ],
25
+ note: '圆头圆脑、笔画粗,最接近「Q 版」(推荐)',
26
+ },
27
+ '得意黑': {
28
+ family: 'Smiley Sans',
29
+ file: 'SmileySans-Oblique.ttf',
30
+ zip: true,
31
+ urls: ['https://github.com/atelier-anchor/smiley-sans/releases/latest/download/smiley-sans-v2.0.1.zip'],
32
+ note: '倾斜、活泼,很有冲击力',
33
+ },
34
+ '霞鹜文楷': {
35
+ family: 'LXGW WenKai',
36
+ file: 'LXGWWenKai-Regular.ttf',
37
+ urls: [
38
+ 'https://raw.githubusercontent.com/lxgw/LxgwWenKai/master/fonts/TTF/LXGWWenKai-Regular.ttf',
39
+ ],
40
+ note: '手写感楷体,温柔耐看',
41
+ },
42
+ '站酷小薇体': {
43
+ family: 'ZCOOL XiaoWei',
44
+ file: 'ZCOOLXiaoWei-Regular.ttf',
45
+ urls: [
46
+ 'https://raw.githubusercontent.com/google/fonts/main/ofl/zcoolxiaowei/ZCOOLXiaoWei-Regular.ttf',
47
+ ],
48
+ note: '清秀偏楷,安静一些',
49
+ },
50
+ '马善政毛笔楷': {
51
+ family: 'Ma Shan Zheng',
52
+ file: 'MaShanZheng-Regular.ttf',
53
+ urls: [
54
+ 'https://raw.githubusercontent.com/google/fonts/main/ofl/mashanzheng/MaShanZheng-Regular.ttf',
55
+ ],
56
+ note: '毛笔楷书,适合国风/古风',
57
+ },
58
+ }
59
+
60
+ /** 系统常见圆体/Q 版体(已装就直接用,不用下载) */
61
+ const SYSTEM_ROUND_FONTS = [
62
+ 'YouYuan', '幼圆', 'SIMYOU',
63
+ 'STHupo', '华文琥珀',
64
+ 'STCaiyun', '华文彩云',
65
+ 'STXingkai', '华文行楷',
66
+ 'FZYaoti', '方正姚体',
67
+ 'FZZhunYuan-M02S', '方正准圆',
68
+ 'ZCOOL KuaiLe', '站酷快乐体',
69
+ 'Smiley Sans', '得意黑',
70
+ ]
71
+
72
+ /** 系统字体名 → 字体文件(canvas 不一定会自己扫系统目录,指到文件最稳) */
73
+ const SYSTEM_FONT_FILES = {
74
+ 'YouYuan': ['YouYuan.ttf', 'SIMYOU.TTF'],
75
+ '幼圆': ['YouYuan.ttf', 'SIMYOU.TTF'],
76
+ 'SIMYOU': ['SIMYOU.TTF', 'YouYuan.ttf'],
77
+ 'STHupo': ['STHUPO.TTF'],
78
+ '华文琥珀': ['STHUPO.TTF'],
79
+ 'STCaiyun': ['STCAIYUN.TTF'],
80
+ '华文彩云': ['STCAIYUN.TTF'],
81
+ 'STXingkai': ['STXINGKA.TTF'],
82
+ '华文行楷': ['STXINGKA.TTF'],
83
+ 'STKaiti': ['STKAITI.TTF'],
84
+ '华文楷体': ['STKAITI.TTF'],
85
+ 'STXihei': ['STXIHEI.TTF'],
86
+ '华文细黑': ['STXIHEI.TTF'],
87
+ 'STZhongsong': ['STZHONGS.TTF'],
88
+ '华文中宋': ['STZHONGS.TTF'],
89
+ 'STSong': ['STSONG.TTF'],
90
+ '华文宋体': ['STSONG.TTF'],
91
+ 'FZYaoti': ['FZYTK.TTF'],
92
+ '方正姚体': ['FZYTK.TTF'],
93
+ 'FZShuTi': ['FZSTK.TTF'],
94
+ '方正舒体': ['FZSTK.TTF'],
95
+ 'KaiTi': ['simkai.ttf'],
96
+ '楷体': ['simkai.ttf'],
97
+ 'FangSong': ['simfang.ttf'],
98
+ '仿宋': ['simfang.ttf'],
99
+ 'SimHei': ['simhei.ttf'],
100
+ '黑体': ['simhei.ttf'],
101
+ 'SimSun': ['simsun.ttc'],
102
+ '宋体': ['simsun.ttc'],
103
+ 'Microsoft YaHei': ['msyh.ttc'],
104
+ '微软雅黑': ['msyh.ttc'],
105
+ 'Microsoft YaHei UI': ['msyh.ttc'],
106
+ '华文新魏': ['STXINWEI.TTF'],
107
+ 'STXinwei': ['STXINWEI.TTF'],
108
+ '华文隶书': ['STLITI.TTF'],
109
+ 'STLiti': ['STLITI.TTF'],
110
+ }
111
+
112
+ /** 常见系统字体目录 */
113
+ const SYSTEM_FONT_DIRS = [
114
+ 'C:\\Windows\\Fonts',
115
+ '/usr/share/fonts',
116
+ '/usr/local/share/fonts',
117
+ '/System/Library/Fonts',
118
+ '/Library/Fonts',
119
+ ]
120
+
121
+ /** 在系统字体目录里找这几个文件名,找到返回绝对路径 */
122
+ function findSystemFontFile (names) {
123
+ for (const dir of SYSTEM_FONT_DIRS) {
124
+ for (const name of names) {
125
+ const file = path.join(dir, name)
126
+ try {
127
+ if (fs.existsSync(file)) return file
128
+ } catch (error) { /* 忽略 */ }
129
+ }
130
+ }
131
+ return null
132
+ }
133
+
134
+ /**
135
+ * 把系统字体名解析成字体文件。
136
+ * canvas(尤指 @napi-rs/canvas)不保证会扫描系统字体目录,
137
+ * 所以用户填「幼圆」这类名字时,这里直接把对应文件找出来注册最保险。
138
+ */
139
+ function resolveSystemFont (name) {
140
+ const key = String(name || '').trim()
141
+ if (!key) return null
142
+ const names = SYSTEM_FONT_FILES[key]
143
+ if (names) {
144
+ const file = findSystemFontFile(names)
145
+ return file ? { file, family: key } : null
146
+ }
147
+ return null
148
+ }
149
+
150
+ function fontDir (ctx) {
151
+ // 优先放在 Koishi 的数据目录里(升级插件不会丢),拿不到就用插件目录
152
+ try {
153
+ if (ctx && ctx.baseDir) return path.join(ctx.baseDir, 'data', 'll-fortune', 'fonts')
154
+ } catch (error) { /* 忽略 */ }
155
+ return path.join(__dirname, '..', 'assets', 'fonts')
156
+ }
157
+
158
+ /** 查表:把「站酷快乐体」这类名字换成目录里的定义 */
159
+ function lookup (name) {
160
+ const key = String(name || '').trim()
161
+ if (!key) return null
162
+ if (FONT_CATALOG[key]) return FONT_CATALOG[key]
163
+ const lower = key.toLowerCase()
164
+ for (const [alias, entry] of Object.entries(FONT_CATALOG)) {
165
+ if (entry.family.toLowerCase() === lower || alias.toLowerCase() === lower) return entry
166
+ }
167
+ return null
168
+ }
169
+
170
+ /**
171
+ * 从 ZIP 里取出第一个匹配扩展名的文件(极简实现,只认 stored / deflate,
172
+ * 只用来解压字体包,不追求覆盖所有 ZIP 变体)。
173
+ */
174
+ function firstZipEntry (buffer, pattern) {
175
+ const zlib = require('zlib')
176
+ let offset = 0
177
+ while (offset < buffer.length - 4) {
178
+ const signature = buffer.readUInt32LE(offset)
179
+ if (signature !== 0x04034b50) break
180
+ const method = buffer.readUInt16LE(offset + 8)
181
+ const compressedSize = buffer.readUInt32LE(offset + 18)
182
+ const nameLength = buffer.readUInt16LE(offset + 26)
183
+ const extraLength = buffer.readUInt16LE(offset + 28)
184
+ const nameStart = offset + 30
185
+ const name = buffer.slice(nameStart, nameStart + nameLength).toString('utf8')
186
+ const dataStart = nameStart + nameLength + extraLength
187
+ const data = buffer.slice(dataStart, dataStart + compressedSize)
188
+ if (pattern.test(name)) {
189
+ if (method === 0) return data
190
+ if (method === 8) return zlib.inflateRawSync(data)
191
+ return null
192
+ }
193
+ if (!compressedSize) break
194
+ offset = dataStart + compressedSize
195
+ }
196
+ return null
197
+ }
198
+
199
+ /**
200
+ * 把配置里的字体设置解析成 { files: [绝对路径], families: [字体族名] }。
201
+ * 返回的文件都保证存在;下载不成功就静默跳过,调用方退回默认字体。
202
+ */
203
+ async function resolveFonts (ctx, options) {
204
+ const opts = options || {}
205
+ const wanted = String(opts.fontName || '').trim()
206
+ const extraFiles = []
207
+ const extraFamilies = []
208
+ const messages = []
209
+
210
+ const entry = lookup(wanted)
211
+ const hasUrl = /^https?:\/\//i.test(wanted)
212
+ const isFile = wanted && (/[\\/]/.test(wanted) || /\.(ttf|ttc|otf)$/i.test(wanted))
213
+
214
+ const dir = fontDir(ctx)
215
+ const fileExists = (file) => {
216
+ try { return !!file && fs.existsSync(file) } catch (error) { return false }
217
+ }
218
+
219
+ const tryDownload = async (urls, targetName, options) => {
220
+ const opts = options || {}
221
+ if (!ctx || !ctx.http) {
222
+ messages.push(`想用「${wanted}」但当前没有网络服务,跳过`)
223
+ return null
224
+ }
225
+ const target = path.join(dir, targetName)
226
+ if (fileExists(target)) return target
227
+
228
+ let lastError = null
229
+ for (const url of urls) {
230
+ try {
231
+ fs.mkdirSync(dir, { recursive: true })
232
+ const response = await ctx.http.get(url, { responseType: 'arraybuffer', timeout: 60000 })
233
+ const buffer = Buffer.from(response)
234
+ if (buffer.length < 1024) throw new Error('文件太小,可能不是字体')
235
+
236
+ let payload = buffer
237
+ if (opts.zip || buffer.slice(0, 2).toString('latin1') === 'PK') {
238
+ const entry = firstZipEntry(buffer, /\.(ttf|otf|ttc)$/i)
239
+ if (!entry) throw new Error('压缩包里没有字体文件')
240
+ payload = entry
241
+ }
242
+
243
+ fs.writeFileSync(target, payload)
244
+ fs.chmodSync(target, 0o644)
245
+ messages.push(`字体已下载:${targetName}(${Math.round(payload.length / 1024)} KB)`)
246
+ return target
247
+ } catch (error) {
248
+ lastError = error
249
+ }
250
+ }
251
+ messages.push(`字体下载失败(${targetName}):${lastError && lastError.message}`)
252
+ return null
253
+ }
254
+
255
+ if (entry) {
256
+ const local = path.join(dir, entry.file)
257
+ if (fileExists(local)) {
258
+ extraFiles.push(local)
259
+ extraFamilies.push(entry.family)
260
+ } else {
261
+ const downloaded = await tryDownload(entry.urls || [entry.url], entry.file, { zip: entry.zip })
262
+ if (downloaded) {
263
+ extraFiles.push(downloaded)
264
+ extraFamilies.push(entry.family)
265
+ }
266
+ }
267
+ } else if (hasUrl) {
268
+ const name = decodeURIComponent(wanted.split('/').pop().split('?')[0] || 'downloaded.ttf')
269
+ const downloaded = await tryDownload([wanted], name)
270
+ if (downloaded) {
271
+ extraFiles.push(downloaded)
272
+ extraFamilies.push(path.parse(name).name)
273
+ }
274
+ } else if (isFile) {
275
+ if (fileExists(wanted)) extraFiles.push(wanted)
276
+ else messages.push(`找不到字体文件:${wanted}`)
277
+ } else if (wanted) {
278
+ // 系统字体名:先尝试直接定位到字体文件(最稳),定位不到就只给族名让 canvas 自己找
279
+ const system = resolveSystemFont(wanted)
280
+ if (system) {
281
+ extraFiles.push(system.file)
282
+ extraFamilies.push(system.family)
283
+ messages.push(`使用系统字体「${wanted}」(${system.file})`)
284
+ } else {
285
+ extraFamilies.push(wanted)
286
+ }
287
+ }
288
+
289
+ return { files: extraFiles, families: extraFamilies, messages, dir }
290
+ }
291
+
292
+ /** 探测系统里有没有这几个圆体,给设置页和日志用 */
293
+ function detectSystemRoundFonts () {
294
+ const found = []
295
+ if (process.platform !== 'win32') return found
296
+ for (const name of SYSTEM_ROUND_FONTS) {
297
+ const candidates = [
298
+ path.join('C:\\Windows\\Fonts', name + '.ttf'),
299
+ path.join('C:\\Windows\\Fonts', name + '.ttc'),
300
+ path.join('C:\\Windows\\Fonts', name + '.otf'),
301
+ ]
302
+ for (const file of candidates) {
303
+ try {
304
+ if (fs.existsSync(file)) { found.push(name); break }
305
+ } catch (error) { /* 忽略 */ }
306
+ }
307
+ }
308
+ return found
309
+ }
310
+
311
+ module.exports = {
312
+ FONT_CATALOG,
313
+ SYSTEM_ROUND_FONTS,
314
+ SYSTEM_FONT_FILES,
315
+ resolveFonts,
316
+ resolveSystemFont,
317
+ detectSystemRoundFonts,
318
+ fontDir,
319
+ lookup,
320
+ }