koishi-plugin-dew-bot 1.0.241 → 1.0.243
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/character-card/README.md +8 -0
- package/assets/character-card/default-female.webp +0 -0
- package/assets/character-card/paper.webp +0 -0
- package/lib/index.js +264 -23
- package/lib/plugin/appearanceCardTemplate.d.ts +3 -0
- package/lib/plugin/characterCardTemplate.d.ts +3 -0
- package/lib/plugin/skillCard.d.ts +2 -1
- package/lib/plugin/skillCardTemplate.d.ts +2 -1
- package/lib/shared/face/index.d.ts +19 -0
- package/package.json +1 -1
- package/src/plugin/appearanceCardTemplate.ts +16 -0
- package/src/plugin/cardLayout.ts +18 -0
- package/src/plugin/characterCardTemplate.ts +64 -0
- package/src/plugin/serverHandel/image.ts +2 -3
- package/src/plugin/skillCard.ts +13 -4
- package/src/plugin/skillCardTemplate.ts +20 -11
- package/src/shared/bot/serviceProto.ts +124 -1
- package/src/shared/face/index.ts +12 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# 角色卡素材
|
|
2
|
+
|
|
3
|
+
- `paper.webp`:水墨宣纸背景。
|
|
4
|
+
- `default-female.webp`:默认女性角色,600×1000,展示区域 300×500。没有专属角色形象或专属图不可用时使用;专属形象优先。素材随机器人安装包分发,后台预览也使用同一张图片。
|
|
5
|
+
|
|
6
|
+
默认女性形象通过内置 ImageGen 生成,再压缩为 WebP。生成提示词:
|
|
7
|
+
|
|
8
|
+
> Generate a standalone default female character portrait asset for a Chinese ink-wash fantasy RPG game character panel. Exact portrait aspect ratio WIDTH:HEIGHT 3:5, intended display 300x500 pixels, ideally source 600x1000 or larger same ratio. One clearly adult female adventurer in her mid twenties, elegant composed expression, long black hair partly tied with an understated pale jade hair ornament, dark charcoal and ivory layered wuxia traveling robes, fitted practical leather belt and subtle antique brass fastenings, fully clothed. Three-quarter to full body composition, head fully within frame with comfortable margin, show body down to boots if possible. Natural relaxed standing pose, both hands anatomically natural. A high quality detailed Chinese fantasy game illustration with delicate ink brush edges, restrained watercolor shading, beautiful expressive face, refined fabric detail. Background warm pale rice paper, soft ink wash mountains and mist, very subtle desaturated jade accents. Character is the clear focus. Suitable as a neutral starter character, understated equipment, not a royal or legendary figure. No text, no lettering, no watermark, no UI, no borders or frame, no additional characters. Entire rectangular image includes its background, not a cutout.
|
|
Binary file
|
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -75,12 +75,27 @@ var AnalysisMsg = class {
|
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
// src/plugin/
|
|
78
|
+
// src/plugin/appearanceCardTemplate.ts
|
|
79
79
|
var escape = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]), "escape");
|
|
80
|
+
function appearanceCardHtml(card, artwork, paper) {
|
|
81
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><style>
|
|
82
|
+
*{box-sizing:border-box}html,body{margin:0;width:760px;background:#ece8df;color:#625a49;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif}
|
|
83
|
+
#capture{width:760px;padding:25px 18px 29px;background:radial-gradient(ellipse at center,#f6f2e9,#e4ded2)}
|
|
84
|
+
.scroll{position:relative;padding:18px 0}.sheet{position:relative;margin:0 29px;padding:0 23px;background:#eee5cf;border-left:1px solid #b8a37b;border-right:1px solid #b8a37b;box-shadow:0 7px 16px #49381c24;overflow:hidden}
|
|
85
|
+
.paper{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.22;pointer-events:none}.sheet:after{content:'';position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 18px 20px -18px #49351cb0,inset 0 -18px 20px -18px #49351c90}
|
|
86
|
+
.title{position:relative;margin:0;padding:29px 0 23px;text-align:center;font-family:'KaiTi','STKaiti','SimSun',serif;font-size:34px;font-weight:400;line-height:1.4;letter-spacing:8px;color:#514630}.portrait{position:relative;padding:5px;border:1px solid #ae9360;background:#e8dec5}.item-artwork{display:block;width:100%;height:auto;aspect-ratio:3/5;object-fit:contain}
|
|
87
|
+
.details{position:relative;display:flex;justify-content:flex-end;gap:24px;padding:24px 8px 29px;font-size:17px;line-height:1.6;letter-spacing:1px}.details span{min-width:0;overflow-wrap:anywhere}.details span:last-child{text-align:right}.details b{font-weight:400;color:#403c31;margin-left:10px}
|
|
88
|
+
.roller{position:absolute;left:12px;right:12px;height:30px;z-index:2;border:1px solid #665033;border-radius:5px;background:linear-gradient(180deg,#695136 0%,#aa8b5f 16%,#dfc89a 38%,#b99a67 56%,#80613c 84%,#503a26 100%);box-shadow:0 4px 6px #44311933}.roller.top{top:0}.roller.bottom{bottom:0;height:34px}.roller:before,.roller:after{content:'';position:absolute;top:-4px;width:22px;height:calc(100% + 8px);border:1px solid #443321;border-radius:5px;background:linear-gradient(180deg,#493727,#987b50 28%,#bda071 42%,#745533 65%,#3d2d20);box-shadow:inset 3px 0 2px #d2b88166,inset -3px 0 2px #2c201c66}.roller:before{left:-14px}.roller:after{right:-14px}
|
|
89
|
+
</style></head><body><article id="capture"><div class="scroll"><div class="roller top" aria-hidden="true"></div><div class="sheet">${paper ? `<img class="paper" src="${escape(paper)}" alt="" aria-hidden="true">` : ""}<h1 class="title">形象卡</h1><div class="portrait"><img class="item-artwork" src="${escape(artwork)}" alt="${escape(card.name)}"></div><div class="details"><span>参考价格<b>${escape(card.referencePrice)}</b></span></div></div><div class="roller bottom" aria-hidden="true"></div></div></article></body></html>`;
|
|
90
|
+
}
|
|
91
|
+
__name(appearanceCardHtml, "appearanceCardHtml");
|
|
92
|
+
|
|
93
|
+
// src/plugin/itemCardTemplate.ts
|
|
94
|
+
var escape2 = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]), "escape");
|
|
80
95
|
function itemCardHtml(card, artwork, paper) {
|
|
81
96
|
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><style>
|
|
82
97
|
*{box-sizing:border-box}html,body{margin:0;width:760px;color:#29271f;font-family:SimSun,'Noto Serif CJK SC','Microsoft YaHei',serif}
|
|
83
|
-
#capture{width:760px;min-height:1140px;padding:30px 48px 38px;background-color:#f3eddf;${paper ? `background-image:url('${
|
|
98
|
+
#capture{width:760px;min-height:1140px;padding:30px 48px 38px;background-color:#f3eddf;${paper ? `background-image:url('${escape2(paper)}');background-size:100% 100%;` : ""}box-shadow:inset 0 0 55px #85796228;position:relative}
|
|
84
99
|
.catalog-label{display:flex;align-items:center;justify-content:center;gap:22px;font-size:25px;letter-spacing:8px;margin-bottom:32px}.catalog-label:before,.catalog-label:after{content:'';width:106px;height:1px;background:#69625288}
|
|
85
100
|
.item-heading{display:grid;grid-template-columns:140px minmax(0,1fr) 140px;align-items:center;margin-bottom:10px;min-height:76px}.item-title{font-family:KaiTi,STKaiti,'Noto Serif CJK SC',serif;font-weight:700;font-size:51px;line-height:1.3;overflow-wrap:anywhere;grid-column:2;text-align:center;margin:0}
|
|
86
101
|
.quality{grid-column:3;grid-row:1;justify-self:end;display:flex;align-items:center;gap:8px;flex-shrink:0}.quality-label{writing-mode:vertical-rl;font-size:18px;letter-spacing:4px}.quality-seal{border:3px double #a33f31;padding:8px 5px;min-width:68px;max-width:116px;text-align:center;color:#a33f31;font-family:KaiTi,STKaiti,serif;font-size:33px;font-weight:bold;box-shadow:inset 0 0 0 2px #a33f3126;overflow-wrap:anywhere}
|
|
@@ -91,13 +106,100 @@ function itemCardHtml(card, artwork, paper) {
|
|
|
91
106
|
.description-title{display:flex;align-items:center;gap:22px;font-family:KaiTi,STKaiti,serif;font-size:34px;font-weight:bold;margin-bottom:16px}.description-title:after{content:'';height:1px;background:#655e5088;flex:1}
|
|
92
107
|
.item-description{font-size:25px;line-height:1.75;white-space:pre-wrap;overflow-wrap:anywhere;margin:0 0 42px;min-height:88px}
|
|
93
108
|
.item-price{border-top:1px solid #736b5b88;padding-top:22px;font-size:24px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
94
|
-
</style></head><body><article id="capture"><div class="catalog-label">道具图鉴</div><header class="item-heading"><h1 class="item-title">${
|
|
109
|
+
</style></head><body><article id="capture"><div class="catalog-label">道具图鉴</div><header class="item-heading"><h1 class="item-title">${escape2(card.name)}</h1><div class="quality"><span class="quality-label">品质</span><span class="quality-seal">${escape2(card.quality)}</span></div></header><div class="item-type">类型:${escape2(card.type)}</div><div class="item-art-frame"><img class="item-artwork" alt="道具插画" src="${escape2(artwork)}"></div><div class="item-count">数量:${escape2(card.count)}</div><div class="description-title">描述</div><p class="item-description">${escape2(card.description)}</p><footer class="item-price">参考价格:${escape2(card.referencePrice)}</footer></article></body></html>`;
|
|
95
110
|
}
|
|
96
111
|
__name(itemCardHtml, "itemCardHtml");
|
|
97
112
|
|
|
113
|
+
// src/plugin/characterCardTemplate.ts
|
|
114
|
+
var escape3 = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]), "escape");
|
|
115
|
+
var finite = /* @__PURE__ */ __name((value) => Number.isFinite(value) ? value : 0, "finite");
|
|
116
|
+
var number = /* @__PURE__ */ __name((value) => finite(value).toLocaleString("zh-CN", { maximumFractionDigits: 2 }), "number");
|
|
117
|
+
var ratio = /* @__PURE__ */ __name((value, max) => max > 0 ? Math.max(0, Math.min(1, finite(value) / max)) : 0, "ratio");
|
|
118
|
+
var qualityColors = { F: "#99958b", E: "#bac1ab", D: "#75b889", C: "#75a8cb", B: "#ab88cc", A: "#e5bd64", S: "#d8a2f0", SS: "#edb76e", SSS: "#f09783" };
|
|
119
|
+
function radar(attributes) {
|
|
120
|
+
const order = ["力量", "敏捷", "体质", "智力", "精神力", "抗性"];
|
|
121
|
+
const attrs = [...attributes].sort((a, b) => {
|
|
122
|
+
const rank = /* @__PURE__ */ __name((name2) => {
|
|
123
|
+
const i = order.indexOf(name2);
|
|
124
|
+
return i < 0 ? order.length : i;
|
|
125
|
+
}, "rank");
|
|
126
|
+
return rank(a.name) - rank(b.name);
|
|
127
|
+
});
|
|
128
|
+
if (attrs.length < 3) return `<div class="empty">${attrs.map((a) => `${escape3(a.name)} ${number(a.value)}`).join("<br>") || "暂无一级属性"}</div>`;
|
|
129
|
+
const max = Math.max(1, ...attrs.map((a) => Math.max(0, finite(a.value))));
|
|
130
|
+
const ceiling = Math.ceil(max / Math.pow(10, Math.floor(Math.log10(max)))) * Math.pow(10, Math.floor(Math.log10(max)));
|
|
131
|
+
const point = /* @__PURE__ */ __name((index, radius) => {
|
|
132
|
+
const angle = -Math.PI / 2 + index * Math.PI * 2 / attrs.length;
|
|
133
|
+
return [350 + Math.cos(angle) * radius, 205 + Math.sin(angle) * radius];
|
|
134
|
+
}, "point");
|
|
135
|
+
const polygon = /* @__PURE__ */ __name((scale) => attrs.map((_, i) => point(i, 128 * scale).join(",")).join(" "), "polygon");
|
|
136
|
+
const data = attrs.map((a, i) => point(i, 128 * ratio(a.value, ceiling)));
|
|
137
|
+
return `<svg class="radar" viewBox="0 0 700 410" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="一级属性雷达图">
|
|
138
|
+
${[0.2, 0.4, 0.6, 0.8, 1].map((scale) => `<polygon points="${polygon(scale)}" fill="none" stroke="#8b897c" stroke-opacity=".5"/>`).join("")}
|
|
139
|
+
${attrs.map((_, i) => `<path d="M350 205 L${point(i, 128).join(" ")}" stroke="#777a71" stroke-opacity=".6"/>`).join("")}
|
|
140
|
+
<polygon points="${data.map((p) => p.join(",")).join(" ")}" fill="#438e7a" fill-opacity=".4" stroke="#245a50" stroke-width="2.5"/>
|
|
141
|
+
${data.map((p) => `<circle cx="${p[0]}" cy="${p[1]}" r="4.5" fill="#174e45"/>`).join("")}
|
|
142
|
+
${attrs.map((a, i) => {
|
|
143
|
+
const [x, y] = point(i, 172);
|
|
144
|
+
return `<g transform="translate(${x} ${y})"><path d="M-44 -18 H44 L49 -6 L44 6 H-44 L-49 -6 Z" fill="#222827" stroke="#ab8b4f"/><text y="0" text-anchor="middle" fill="#f3e7c9" font-size="17">${escape3(a.name)}</text><text y="32" text-anchor="middle" fill="#1c5048" font-size="${number(a.value).length > 12 ? 17 : 24}" font-weight="bold">${number(a.value)}</text></g>`;
|
|
145
|
+
}).join("")}
|
|
146
|
+
</svg><div class="scale">各轴同尺度 · 满刻度 ${number(ceiling)}</div>`;
|
|
147
|
+
}
|
|
148
|
+
__name(radar, "radar");
|
|
149
|
+
function characterCardHtml(card, artwork, paper, defaultArtwork) {
|
|
150
|
+
const portrait = artwork || defaultArtwork;
|
|
151
|
+
const hpPercent = ratio(card.hp, card.maxHp) * 100;
|
|
152
|
+
const exp = Math.max(0, Math.min(100, finite(card.experiencePercent)));
|
|
153
|
+
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><style>
|
|
154
|
+
*{box-sizing:border-box}body{margin:0;width:760px;font-family:"Microsoft YaHei","Noto Sans CJK SC",sans-serif;color:#262b27}
|
|
155
|
+
#capture{width:760px;padding:22px;background:#eee5d2;position:relative;border:7px solid #252a27;box-shadow:inset 0 0 0 2px #b3975a}
|
|
156
|
+
.paper{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.8}.content{position:relative}.top{display:grid;grid-template-columns:300px minmax(0,1fr);gap:18px}
|
|
157
|
+
.portrait{width:300px;height:500px;position:relative;background:linear-gradient(160deg,#ebe5d7,#bdc6bb);outline:3px solid #252a27;box-shadow:0 0 0 5px #b59657;overflow:hidden}
|
|
158
|
+
.portrait img{width:300px;height:500px;object-fit:cover;object-position:center top;display:block}.portrait:after{content:"";position:absolute;inset:7px;border:1px solid #d7b971;pointer-events:none}
|
|
159
|
+
.placeholder{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at 70% 75%,#546b6150,transparent 60%);color:#46564e;gap:20px}.placeholder .seal{font:76px KaiTi,STKaiti,serif;border:2px solid #647266;padding:28px 17px}.placeholder small{font-size:15px;letter-spacing:3px}
|
|
160
|
+
.info{display:flex;flex-direction:column;gap:16px;min-width:0}.dark{background:linear-gradient(115deg,#171e1d,#303431);border:2px solid #ad8d50;box-shadow:0 0 0 2px #252a27,inset 0 0 0 4px #252a27;color:#eee0b9;position:relative;padding:18px}
|
|
161
|
+
.dark:before,.dark:after{content:"◆";position:absolute;color:#c1a367;font-size:13px}.dark:before{left:3px;top:0}.dark:after{right:3px;bottom:0}
|
|
162
|
+
.title{font:28px KaiTi,STKaiti,serif;letter-spacing:4px;text-align:center;padding:16px 8px}.identity{padding:5px 3px;flex:1;display:flex;flex-direction:column;justify-content:center}.eyebrow{font-size:14px;color:#696854;letter-spacing:3px}.identity-row{display:flex;align-items:center;gap:12px;margin:12px 0}.character-name{font: bold 27px KaiTi,STKaiti,"Microsoft YaHei",serif;overflow-wrap:anywhere;min-width:0;flex:1;margin:0;line-height:1.4}.level{flex-shrink:0;border:1px solid #b28c44;background:#252b27;color:#eddbab;padding:7px 10px;font:22px Georgia,serif}
|
|
163
|
+
.experience-label{font-size:13px;color:#676954;margin-top:6px}.experience{height:4px;background:#b5b19e;margin-top:8px}.experience span{height:100%;display:block;background:#7e8c70}
|
|
164
|
+
.power-label{font-size:19px;letter-spacing:2px}.power-value{font:bold 43px Georgia,serif;margin-top:10px;color:#edd69a;overflow-wrap:anywhere}.power-value.long{font-size:29px}.health{padding:15px 13px}.health-label{font-size:18px;margin-bottom:9px}.bar{position:relative;height:30px;border:1px solid #bea365;background:#101817;box-shadow:inset 0 2px 4px #000}.fill{height:100%;background:linear-gradient(#df4d39,#9a241d);border-right:1px solid #f19b67}.bar b{position:absolute;inset:0;display:grid;place-items:center;color:#fff7e6;font:16px Georgia,serif;text-shadow:0 1px 3px #000}
|
|
165
|
+
.panel{margin-top:22px;border:3px solid #252a27;box-shadow:0 0 0 1px #b19559,inset 0 0 0 1px #b19559;background:#f9f1df65;padding:10px 12px 14px}.heading{display:inline-block;margin:-10px 0 0 -12px;padding:9px 25px 10px 20px;background:#222927;border-right:3px solid #b3975a;border-bottom:1px solid #b3975a;color:#eee1bb;font:25px KaiTi,STKaiti,serif;letter-spacing:4px;clip-path:polygon(0 0,94% 0,100% 50%,94% 100%,0 100%)}
|
|
166
|
+
.radar{display:block;width:100%;height:390px;margin-top:-12px;overflow:visible}.scale{text-align:center;font-size:12px;color:#767565}.bloodlines{display:grid;gap:11px;margin-top:15px}.bloodline{display:flex;align-items:center;gap:14px;padding:12px;background:linear-gradient(100deg,#1e2624,#363b34);border:1px solid #b59961;box-shadow:0 0 0 2px #292f29;color:#f2e5c5;min-height:68px}.blood-icon{width:40px;height:40px;border:1px solid #b79a59;display:grid;place-items:center;color:#ddc180;font:26px KaiTi,serif;flex-shrink:0}.blood-name{font-size:21px;flex:1;overflow-wrap:anywhere}.quality{min-width:54px;padding:6px 9px;text-align:center;border:1px solid currentColor;font:bold 27px Georgia,serif;background:#111916;box-shadow:inset 0 0 10px #ffffff12}.empty{text-align:center;padding:25px;color:#6d7266;font-size:18px}.foot{font-size:13px;text-align:center;margin-top:18px;color:#626454;letter-spacing:1px}
|
|
167
|
+
</style><body><main id="capture">${paper ? `<img class="paper" src="${escape3(paper)}" alt="">` : ""}<div class="content"><div class="top">
|
|
168
|
+
<div class="portrait">${portrait ? `<img class="character-artwork" src="${escape3(portrait)}" data-fallback="${escape3(defaultArtwork ?? "")}" alt="角色形象">` : '<div class="placeholder"><div class="seal">轮回</div><small>形象尚未生成</small></div>'}</div>
|
|
169
|
+
<div class="info"><div class="dark title">✦ 角色档案</div><div class="identity"><div class="eyebrow">轮回者</div><div class="identity-row"><h1 class="character-name">${escape3(card.name)}</h1><span class="level">Lv. ${number(card.level)}</span></div><div class="experience-label">升级进度 ${number(exp)}%</div><div class="experience"><span style="width:${exp}%"></span></div></div>
|
|
170
|
+
<div class="dark"><div class="power-label">⚔ 综合战力</div><div class="power-value ${number(card.power).length > 11 ? "long" : ""}">${number(card.power)}</div></div>
|
|
171
|
+
<div class="dark health"><div class="health-label">✚ 生命</div><div class="bar"><div class="fill" style="width:${hpPercent}%"></div><b>${number(card.hp)} / ${number(card.maxHp)}</b></div></div></div></div>
|
|
172
|
+
<section class="panel"><h2 class="heading">一级属性</h2>${radar(card.attributes)}</section>
|
|
173
|
+
<section class="panel"><h2 class="heading">血统</h2><div class="bloodlines">${card.bloodlines.length ? card.bloodlines.map((b) => `<div class="bloodline"><span class="blood-icon">血</span><span class="blood-name">${escape3(b.name)}</span><span class="quality" style="color:${qualityColors[b.quality] ?? "#bdbbae"}">${escape3(b.quality)}</span></div>`).join("") : '<div class="empty">尚未融合血统</div>'}</div></section>
|
|
174
|
+
<div class="foot">[详细属性] 查看二级属性 · [指令] 查看操作</div></div></main></body></html>`;
|
|
175
|
+
}
|
|
176
|
+
__name(characterCardHtml, "characterCardHtml");
|
|
177
|
+
|
|
98
178
|
// src/plugin/cardLayout.ts
|
|
99
179
|
async function layoutCard(document = globalThis.document) {
|
|
100
180
|
await document.fonts.ready;
|
|
181
|
+
const portrait = document.querySelector(".character-artwork");
|
|
182
|
+
if (portrait) {
|
|
183
|
+
try {
|
|
184
|
+
await portrait.decode();
|
|
185
|
+
} catch {
|
|
186
|
+
let recovered = false;
|
|
187
|
+
if (portrait.dataset.fallback && portrait.getAttribute("src") !== portrait.dataset.fallback) {
|
|
188
|
+
portrait.src = portrait.dataset.fallback;
|
|
189
|
+
try {
|
|
190
|
+
await portrait.decode();
|
|
191
|
+
recovered = true;
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!recovered) {
|
|
196
|
+
const placeholder = document.createElement("div");
|
|
197
|
+
placeholder.className = "placeholder";
|
|
198
|
+
placeholder.textContent = "形象暂不可用";
|
|
199
|
+
portrait.replaceWith(placeholder);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
101
203
|
const itemArtwork = document.querySelector(".item-artwork");
|
|
102
204
|
if (itemArtwork) await itemArtwork.decode();
|
|
103
205
|
const artwork = document.querySelector(".skill-artwork");
|
|
@@ -123,6 +225,7 @@ async function layoutCard(document = globalThis.document) {
|
|
|
123
225
|
if (avatar.scrollWidth <= avatar.clientWidth) break;
|
|
124
226
|
}
|
|
125
227
|
}
|
|
228
|
+
if (document.getElementById("capture")?.classList.contains("skill-sheet")) return;
|
|
126
229
|
const name2 = document.getElementById("name");
|
|
127
230
|
if (!name2) return;
|
|
128
231
|
for (let size = 36; size >= 20; size--) {
|
|
@@ -143,10 +246,10 @@ async function layoutCard(document = globalThis.document) {
|
|
|
143
246
|
__name(layoutCard, "layoutCard");
|
|
144
247
|
|
|
145
248
|
// src/plugin/leaderboardTemplate.ts
|
|
146
|
-
var
|
|
249
|
+
var escape4 = /* @__PURE__ */ __name((s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]), "escape");
|
|
147
250
|
function leaderboardHtml(data, _headerImage, backgroundImage) {
|
|
148
251
|
const rows = data.rows.slice(0, 20);
|
|
149
|
-
const row = /* @__PURE__ */ __name((r, mine = false) => `<div class="row rank-${r.rank} ${mine ? "personal" : ""}"><div class="rank">${mine ? "<small>我的排名</small>" : ""}<b>${
|
|
252
|
+
const row = /* @__PURE__ */ __name((r, mine = false) => `<div class="row rank-${r.rank} ${mine ? "personal" : ""}"><div class="rank">${mine ? "<small>我的排名</small>" : ""}<b>${escape4(r.rank)}</b></div><div class="name">${escape4(r.name)}</div><div class="value">${escape4(r.value.toLocaleString("zh-CN", { maximumFractionDigits: 2 }))}</div></div>`, "row");
|
|
150
253
|
const personal = data.mine ? row(data.mine, true) : "";
|
|
151
254
|
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><style>
|
|
152
255
|
*{box-sizing:border-box}body{margin:0;width:760px;color:#242725;font-family:"Microsoft YaHei","Noto Sans CJK SC",sans-serif}
|
|
@@ -164,7 +267,7 @@ function leaderboardHtml(data, _headerImage, backgroundImage) {
|
|
|
164
267
|
.value{font-size:23px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
165
268
|
#capture.has-background{padding:90px 80px}.has-background .sheet{background:rgba(250,247,238,.20);border:0}.has-background .row{grid-template-columns:48px minmax(0,1fr) 142px;gap:6px;padding:10px 6px}.has-background .name{font-size:20px}
|
|
166
269
|
.has-background .row{background-color:rgba(255,253,246,.38)}
|
|
167
|
-
</style><body><div id="capture" class="${backgroundImage ? "has-background" : ""}">${backgroundImage ? '<img class="board-background" alt="" src="' +
|
|
270
|
+
</style><body><div id="capture" class="${backgroundImage ? "has-background" : ""}">${backgroundImage ? '<img class="board-background" alt="" src="' + escape4(backgroundImage) + '">' : ""}<h1 class="board-title">${escape4(data.title)}</h1><main class="sheet">${rows.length ? rows.map((r) => row(r)).join("") : '<div class="empty">暂无玩家上榜</div>'}${personal}</main></div></body></html>`;
|
|
168
271
|
}
|
|
169
272
|
__name(leaderboardHtml, "leaderboardHtml");
|
|
170
273
|
|
|
@@ -173,16 +276,22 @@ var import_promises = __toESM(require("node:fs/promises"));
|
|
|
173
276
|
var import_node_path = __toESM(require("node:path"));
|
|
174
277
|
|
|
175
278
|
// src/plugin/skillCardTemplate.ts
|
|
176
|
-
var
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const desc = card.description || "暂无描述";
|
|
279
|
+
var escape5 = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]), "escape");
|
|
280
|
+
function skillCardHtml(card, _frame, artwork) {
|
|
281
|
+
const description = escape5((card.description || "暂无技能效果说明").replace(/\s*\n+\s*/g, ""));
|
|
282
|
+
const grain = "data:image/svg+xml," + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180"><filter id="paper"><feTurbulence type="fractalNoise" baseFrequency=".72" numOctaves="3" stitchTiles="stitch"/></filter><rect width="100%" height="100%" opacity=".15" filter="url(#paper)"/></svg>');
|
|
181
283
|
return `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
182
|
-
*{box-sizing:border-box}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
284
|
+
*{box-sizing:border-box}
|
|
285
|
+
html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#34372f}
|
|
286
|
+
#capture{position:relative;isolation:isolate;width:760px;min-height:1060px;padding:57px 58px 38px;background:radial-gradient(ellipse at 0 0,#ddd9c744,transparent 48%),radial-gradient(ellipse at 100% 90%,#c9cdb938,transparent 45%),#f4f1e8;border:1px solid #dedbcf}
|
|
287
|
+
#capture:before{content:'';position:absolute;inset:0;z-index:-1;background-image:url('${grain}');opacity:.42;pointer-events:none}
|
|
288
|
+
#capture:after{content:'';position:absolute;inset:17px;border:1px solid #8c8b6f22;pointer-events:none}
|
|
289
|
+
header{position:relative}.heading{display:flex;align-items:center;gap:24px}.heading-copy{flex:1;min-width:0}.overline{font-size:14px;letter-spacing:6px;color:#868a76;margin-bottom:13px}.name{font-family:'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;font-size:43px;font-weight:700;line-height:1.35;margin:0;color:#303a31;overflow-wrap:anywhere}
|
|
290
|
+
.seal{flex-shrink:0;width:66px;min-height:74px;transform:rotate(3deg);padding:8px 3px;color:#a04b40;border:2px solid #a04b40;outline:1px solid #a04b40;outline-offset:-6px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border-radius:3px 1px 4px 2px}.seal strong{font:700 24px Georgia;letter-spacing:-1px}.seal span{font-family:'KaiTi','SimSun',serif;font-size:16px;letter-spacing:4px;padding-left:4px}
|
|
291
|
+
.meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 22px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta span{overflow-wrap:anywhere;max-width:100%}.meta small{font-size:16px;color:#92917f;margin-right:7px}.meta .kind{color:#915344;font-family:'KaiTi','SimSun',serif;font-size:21px}
|
|
292
|
+
.art{position:relative;width:calc(100% + 44px);height:570px;margin:14px -22px -8px;display:grid;place-items:center;isolation:isolate}.art:before{content:'';position:absolute;inset:15% 10%;border-radius:48% 52% 56% 44%;background:radial-gradient(ellipse,#aab4a443,transparent 69%);transform:rotate(-12deg)}.skill-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 43%;filter:saturate(.68) contrast(.94);mix-blend-mode:multiply;mask-image:linear-gradient(to bottom,transparent 0,#000 9%,#000 72%,transparent 100%),linear-gradient(to right,transparent 0,#000 8%,#000 92%,transparent 100%);mask-composite:intersect}.art-fallback{position:relative;z-index:-1;width:300px;height:300px;border:2px solid #7c887d33;border-radius:48% 52% 50% 46%;transform:rotate(-12deg);display:grid;place-items:center;color:#8c948177;font:70px 'KaiTi','SimSun',serif}.art-fallback span{transform:rotate(12deg)}.art:has(.skill-artwork) .art-fallback{visibility:hidden}
|
|
293
|
+
.effect{position:relative;margin-top:0}h2{display:flex;align-items:center;gap:15px;margin:0 0 17px;font:22px 'KaiTi','SimSun',serif;letter-spacing:5px;color:#626e5e}h2:before{content:'';width:5px;height:17px;background:#a04b40;transform:rotate(8deg);opacity:.75}h2:after{content:'';flex:1;height:1px;background:linear-gradient(to right,#7f897c55,transparent)}p{margin:0;font-size:25px;line-height:1.85;white-space:pre-wrap;overflow-wrap:anywhere;text-align:justify}.footer{margin-top:24px;padding-top:17px;border-top:1px solid #8a8a7128}.footer p{font-size:18px;color:#7d8070;line-height:1.75;text-align:left}.edition{display:flex;align-items:center;gap:12px;justify-content:center;margin-top:36px;color:#a6a38e;font:14px 'KaiTi','SimSun',serif;letter-spacing:5px}.edition:before,.edition:after{content:'';height:1px;width:32px;background:#9b9c833d}
|
|
294
|
+
</style></head><body><div id="capture" class="skill-sheet ink-skill-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">技 艺 图 录</div><h1 class="name">${escape5(card.name)}</h1></div><div class="seal"><strong>${escape5(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="kind">${card.passive ? "被动" : "主动"}</span><span><small>等级</small>${escape5(card.level)} / ${escape5(card.maxLevel)}</span><span><small>消耗</small>${escape5(card.cost || "无消耗")}</span><span><small>冷却</small>${escape5(card.cooldown || "无冷却")}</span></div></header><div class="art"><div class="art-fallback"><span>${card.passive ? "静" : "势"}</span></div>${artwork ? `<img class="skill-artwork" alt="" src="${escape5(artwork)}">` : ""}</div><section class="effect"><h2>技能效果</h2><p>${description}</p></section>${card.sources?.length || card.note ? `<div class="footer">${card.sources?.length ? `<p>获取来源:${escape5(card.sources.join("、"))}</p>` : ""}${card.note ? `<p>${escape5(card.note)}</p>` : ""}</div>` : ""}<div class="edition">露 · 技艺录</div></div></body></html>`;
|
|
186
295
|
}
|
|
187
296
|
__name(skillCardHtml, "skillCardHtml");
|
|
188
297
|
|
|
@@ -227,6 +336,16 @@ __name(loadSkillArtwork, "loadSkillArtwork");
|
|
|
227
336
|
// src/plugin/skillCard.ts
|
|
228
337
|
var running = 0;
|
|
229
338
|
var waiting = [];
|
|
339
|
+
async function characterAsset(file) {
|
|
340
|
+
for (const base of ["../assets/character-card", "../../assets/character-card"]) {
|
|
341
|
+
try {
|
|
342
|
+
return "data:image/webp;base64," + (await import_promises.default.readFile(import_node_path.default.resolve(__dirname, base, file))).toString("base64");
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
__name(characterAsset, "characterAsset");
|
|
230
349
|
async function itemPaper() {
|
|
231
350
|
for (const base of ["../assets/item-card", "../../assets/item-card"]) {
|
|
232
351
|
try {
|
|
@@ -260,9 +379,9 @@ async function leaderboardHeader() {
|
|
|
260
379
|
return void 0;
|
|
261
380
|
}
|
|
262
381
|
__name(leaderboardHeader, "leaderboardHeader");
|
|
263
|
-
async function renderSkillCard(ctx, card, leaderboard, itemCard) {
|
|
382
|
+
async function renderSkillCard(ctx, card, leaderboard, itemCard, characterCard) {
|
|
264
383
|
if (!ctx.puppeteer) throw new Error("Puppeteer 未启用");
|
|
265
|
-
if (JSON.stringify(itemCard ?? card ?? leaderboard ?? {}).length > 24e3) throw new Error("
|
|
384
|
+
if (JSON.stringify(characterCard ?? itemCard ?? card ?? leaderboard ?? {}).length > 24e3) throw new Error("卡片内容过长");
|
|
266
385
|
if (running >= 2) {
|
|
267
386
|
if (waiting.length >= 16) throw new Error("绘图繁忙");
|
|
268
387
|
await new Promise((resolve) => waiting.push(resolve));
|
|
@@ -272,9 +391,9 @@ async function renderSkillCard(ctx, card, leaderboard, itemCard) {
|
|
|
272
391
|
let expired = false;
|
|
273
392
|
try {
|
|
274
393
|
const work = /* @__PURE__ */ __name(async () => {
|
|
275
|
-
const art = await loadSkillArtwork(itemCard?.artworkUrl ?? leaderboard?.backgroundUrl ?? card?.artworkUrl);
|
|
394
|
+
const art = await loadSkillArtwork(characterCard?.artworkUrl ?? itemCard?.artworkUrl ?? leaderboard?.backgroundUrl ?? card?.artworkUrl);
|
|
276
395
|
if (itemCard && !art) throw new Error("道具图片读取失败");
|
|
277
|
-
const html = itemCard ? itemCardHtml(itemCard, art, await itemPaper()) : leaderboard ? leaderboardHtml(leaderboard, await leaderboardHeader(), art) : skillCardHtml(card, await frame(card.passive), art);
|
|
396
|
+
const html = characterCard ? characterCardHtml(characterCard, art, await characterAsset("paper.webp"), await characterAsset("default-female.webp")) : itemCard ? itemCard.type === "形象卡" ? appearanceCardHtml(itemCard, art, await characterAsset("paper.webp")) : itemCardHtml(itemCard, art, await itemPaper()) : leaderboard ? leaderboardHtml(leaderboard, await leaderboardHeader(), art) : skillCardHtml(card, await frame(card.passive), art);
|
|
278
397
|
page = await ctx.puppeteer.page();
|
|
279
398
|
if (expired) {
|
|
280
399
|
await page.close();
|
|
@@ -377,15 +496,14 @@ var ImageHandler = class extends handel_base {
|
|
|
377
496
|
let content;
|
|
378
497
|
let rendered = false;
|
|
379
498
|
try {
|
|
380
|
-
if (!data.itemCard && !data.card && !data.leaderboard) throw new Error("缺少卡片数据");
|
|
381
|
-
content = import_koishi.h.image(await renderSkillCard(this.ctx, data.card, data.leaderboard, data.itemCard), "image/png").toString();
|
|
499
|
+
if (!data.characterCard && !data.itemCard && !data.card && !data.leaderboard) throw new Error("缺少卡片数据");
|
|
500
|
+
content = import_koishi.h.image(await renderSkillCard(this.ctx, data.card, data.leaderboard, data.itemCard, data.characterCard), "image/png").toString();
|
|
382
501
|
rendered = true;
|
|
383
502
|
} catch (error) {
|
|
384
503
|
this.ctx.logger.warn("卡片绘制失败,使用文字详情", error instanceof Error ? error.message : "unknown");
|
|
385
504
|
content = new AnalysisMsg().parse(data);
|
|
386
505
|
}
|
|
387
506
|
await this.send(data, content, rendered ? "image" : "text");
|
|
388
|
-
if (rendered && data.itemCard) await this.send(data, new AnalysisMsg().parse(data));
|
|
389
507
|
}
|
|
390
508
|
};
|
|
391
509
|
|
|
@@ -448,7 +566,7 @@ var import_tsrpc = require("tsrpc");
|
|
|
448
566
|
|
|
449
567
|
// src/shared/bot/serviceProto.ts
|
|
450
568
|
var serviceProto = {
|
|
451
|
-
"version":
|
|
569
|
+
"version": 7,
|
|
452
570
|
"services": [
|
|
453
571
|
{
|
|
454
572
|
"id": 0,
|
|
@@ -529,6 +647,15 @@ var serviceProto = {
|
|
|
529
647
|
"target": "messageFace/MessageHandleType"
|
|
530
648
|
}
|
|
531
649
|
},
|
|
650
|
+
{
|
|
651
|
+
"id": 5,
|
|
652
|
+
"name": "characterCard",
|
|
653
|
+
"type": {
|
|
654
|
+
"type": "Reference",
|
|
655
|
+
"target": "messageFace/CharacterCardData"
|
|
656
|
+
},
|
|
657
|
+
"optional": true
|
|
658
|
+
},
|
|
532
659
|
{
|
|
533
660
|
"id": 2,
|
|
534
661
|
"name": "card",
|
|
@@ -582,6 +709,120 @@ var serviceProto = {
|
|
|
582
709
|
}
|
|
583
710
|
]
|
|
584
711
|
},
|
|
712
|
+
"messageFace/CharacterCardData": {
|
|
713
|
+
"type": "Interface",
|
|
714
|
+
"properties": [
|
|
715
|
+
{
|
|
716
|
+
"id": 0,
|
|
717
|
+
"name": "artworkUrl",
|
|
718
|
+
"type": {
|
|
719
|
+
"type": "String"
|
|
720
|
+
},
|
|
721
|
+
"optional": true
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
"id": 1,
|
|
725
|
+
"name": "name",
|
|
726
|
+
"type": {
|
|
727
|
+
"type": "String"
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
"id": 2,
|
|
732
|
+
"name": "level",
|
|
733
|
+
"type": {
|
|
734
|
+
"type": "Number"
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
"id": 3,
|
|
739
|
+
"name": "experiencePercent",
|
|
740
|
+
"type": {
|
|
741
|
+
"type": "Number"
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
"id": 4,
|
|
746
|
+
"name": "power",
|
|
747
|
+
"type": {
|
|
748
|
+
"type": "Number"
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
"id": 5,
|
|
753
|
+
"name": "hp",
|
|
754
|
+
"type": {
|
|
755
|
+
"type": "Number"
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"id": 6,
|
|
760
|
+
"name": "maxHp",
|
|
761
|
+
"type": {
|
|
762
|
+
"type": "Number"
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"id": 7,
|
|
767
|
+
"name": "attributes",
|
|
768
|
+
"type": {
|
|
769
|
+
"type": "Array",
|
|
770
|
+
"elementType": {
|
|
771
|
+
"type": "Interface",
|
|
772
|
+
"properties": [
|
|
773
|
+
{
|
|
774
|
+
"id": 0,
|
|
775
|
+
"name": "id",
|
|
776
|
+
"type": {
|
|
777
|
+
"type": "String"
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
"id": 1,
|
|
782
|
+
"name": "name",
|
|
783
|
+
"type": {
|
|
784
|
+
"type": "String"
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
"id": 2,
|
|
789
|
+
"name": "value",
|
|
790
|
+
"type": {
|
|
791
|
+
"type": "Number"
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
]
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
"id": 8,
|
|
800
|
+
"name": "bloodlines",
|
|
801
|
+
"type": {
|
|
802
|
+
"type": "Array",
|
|
803
|
+
"elementType": {
|
|
804
|
+
"type": "Interface",
|
|
805
|
+
"properties": [
|
|
806
|
+
{
|
|
807
|
+
"id": 0,
|
|
808
|
+
"name": "name",
|
|
809
|
+
"type": {
|
|
810
|
+
"type": "String"
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
"id": 1,
|
|
815
|
+
"name": "quality",
|
|
816
|
+
"type": {
|
|
817
|
+
"type": "String"
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
]
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
]
|
|
825
|
+
},
|
|
585
826
|
"messageFace/SkillCardData": {
|
|
586
827
|
"type": "Interface",
|
|
587
828
|
"properties": [
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CharacterCardData } from '../shared/face';
|
|
2
|
+
/** Portrait alone is replaceable artwork; every label, bar and chart is live game data. */
|
|
3
|
+
export declare function characterCardHtml(card: CharacterCardData, artwork?: string, paper?: string, defaultArtwork?: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { CharacterCardData } from '../shared/face';
|
|
1
2
|
import type { ItemCardData } from '../shared/face';
|
|
2
3
|
import type { LeaderboardImageData } from '../shared/face';
|
|
3
4
|
import type { Context } from 'koishi';
|
|
4
5
|
import type { SkillCardData } from '../shared/face';
|
|
5
|
-
export declare function renderSkillCard(ctx: Context, card?: SkillCardData, leaderboard?: LeaderboardImageData, itemCard?: ItemCardData): Promise<Buffer>;
|
|
6
|
+
export declare function renderSkillCard(ctx: Context, card?: SkillCardData, leaderboard?: LeaderboardImageData, itemCard?: ItemCardData, characterCard?: CharacterCardData): Promise<Buffer>;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { SkillCardData } from '../shared/face';
|
|
2
|
-
|
|
2
|
+
/** One continuous rice-paper sheet; the old frame parameter remains API-compatible. */
|
|
3
|
+
export declare function skillCardHtml(card: SkillCardData, _frame: string, artwork?: string): string;
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
export interface CharacterCardData {
|
|
2
|
+
artworkUrl?: string;
|
|
3
|
+
name: string;
|
|
4
|
+
level: number;
|
|
5
|
+
experiencePercent: number;
|
|
6
|
+
power: number;
|
|
7
|
+
hp: number;
|
|
8
|
+
maxHp: number;
|
|
9
|
+
attributes: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
value: number;
|
|
13
|
+
}>;
|
|
14
|
+
bloodlines: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
quality: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
1
19
|
export interface ItemCardData {
|
|
2
20
|
artworkUrl: string;
|
|
3
21
|
name: string;
|
|
@@ -73,6 +91,7 @@ export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
|
|
|
73
91
|
/** 下发给客户端的消息主体 **/
|
|
74
92
|
export interface IMessage {
|
|
75
93
|
handleType: MessageHandleType;
|
|
94
|
+
characterCard?: CharacterCardData;
|
|
76
95
|
card?: SkillCardData;
|
|
77
96
|
itemCard?: ItemCardData;
|
|
78
97
|
leaderboard?: LeaderboardImageData;
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ItemCardData } from '../shared/face';
|
|
2
|
+
|
|
3
|
+
const escape = (value: unknown) => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!));
|
|
4
|
+
|
|
5
|
+
/** Hanging scroll shared by bot screenshots and editor previews. */
|
|
6
|
+
export function appearanceCardHtml(card: ItemCardData, artwork: string, paper?: string): string {
|
|
7
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><style>
|
|
8
|
+
*{box-sizing:border-box}html,body{margin:0;width:760px;background:#ece8df;color:#625a49;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif}
|
|
9
|
+
#capture{width:760px;padding:25px 18px 29px;background:radial-gradient(ellipse at center,#f6f2e9,#e4ded2)}
|
|
10
|
+
.scroll{position:relative;padding:18px 0}.sheet{position:relative;margin:0 29px;padding:0 23px;background:#eee5cf;border-left:1px solid #b8a37b;border-right:1px solid #b8a37b;box-shadow:0 7px 16px #49381c24;overflow:hidden}
|
|
11
|
+
.paper{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.22;pointer-events:none}.sheet:after{content:'';position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 18px 20px -18px #49351cb0,inset 0 -18px 20px -18px #49351c90}
|
|
12
|
+
.title{position:relative;margin:0;padding:29px 0 23px;text-align:center;font-family:'KaiTi','STKaiti','SimSun',serif;font-size:34px;font-weight:400;line-height:1.4;letter-spacing:8px;color:#514630}.portrait{position:relative;padding:5px;border:1px solid #ae9360;background:#e8dec5}.item-artwork{display:block;width:100%;height:auto;aspect-ratio:3/5;object-fit:contain}
|
|
13
|
+
.details{position:relative;display:flex;justify-content:flex-end;gap:24px;padding:24px 8px 29px;font-size:17px;line-height:1.6;letter-spacing:1px}.details span{min-width:0;overflow-wrap:anywhere}.details span:last-child{text-align:right}.details b{font-weight:400;color:#403c31;margin-left:10px}
|
|
14
|
+
.roller{position:absolute;left:12px;right:12px;height:30px;z-index:2;border:1px solid #665033;border-radius:5px;background:linear-gradient(180deg,#695136 0%,#aa8b5f 16%,#dfc89a 38%,#b99a67 56%,#80613c 84%,#503a26 100%);box-shadow:0 4px 6px #44311933}.roller.top{top:0}.roller.bottom{bottom:0;height:34px}.roller:before,.roller:after{content:'';position:absolute;top:-4px;width:22px;height:calc(100% + 8px);border:1px solid #443321;border-radius:5px;background:linear-gradient(180deg,#493727,#987b50 28%,#bda071 42%,#745533 65%,#3d2d20);box-shadow:inset 3px 0 2px #d2b88166,inset -3px 0 2px #2c201c66}.roller:before{left:-14px}.roller:after{right:-14px}
|
|
15
|
+
</style></head><body><article id="capture"><div class="scroll"><div class="roller top" aria-hidden="true"></div><div class="sheet">${paper ? `<img class="paper" src="${escape(paper)}" alt="" aria-hidden="true">` : ''}<h1 class="title">形象卡</h1><div class="portrait"><img class="item-artwork" src="${escape(artwork)}" alt="${escape(card.name)}"></div><div class="details"><span>参考价格<b>${escape(card.referencePrice)}</b></span></div></div><div class="roller bottom" aria-hidden="true"></div></div></article></body></html>`;
|
|
16
|
+
}
|
package/src/plugin/cardLayout.ts
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
export async function layoutCard(document: Document = globalThis.document) {
|
|
3
3
|
|
|
4
4
|
await document.fonts.ready
|
|
5
|
+
const portrait = document.querySelector<HTMLImageElement>('.character-artwork')
|
|
6
|
+
if (portrait) {
|
|
7
|
+
try { await portrait.decode() } catch {
|
|
8
|
+
let recovered = false
|
|
9
|
+
if (portrait.dataset.fallback && portrait.getAttribute('src') !== portrait.dataset.fallback) {
|
|
10
|
+
portrait.src = portrait.dataset.fallback
|
|
11
|
+
try { await portrait.decode(); recovered = true } catch {}
|
|
12
|
+
}
|
|
13
|
+
if (!recovered) {
|
|
14
|
+
const placeholder = document.createElement('div')
|
|
15
|
+
placeholder.className = 'placeholder'
|
|
16
|
+
placeholder.textContent = '形象暂不可用'
|
|
17
|
+
portrait.replaceWith(placeholder)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
5
21
|
const itemArtwork = document.querySelector<HTMLImageElement>('.item-artwork')
|
|
6
22
|
if (itemArtwork) await itemArtwork.decode()
|
|
7
23
|
const artwork = document.querySelector<HTMLImageElement>('.skill-artwork')
|
|
@@ -15,6 +31,8 @@ export async function layoutCard(document: Document = globalThis.document) {
|
|
|
15
31
|
for (const avatar of document.querySelectorAll<HTMLElement>('.avatar')) {
|
|
16
32
|
for (let size = 30; size >= 16; size--) { avatar.style.fontSize = size + 'px'; if (avatar.scrollWidth <= avatar.clientWidth) break }
|
|
17
33
|
}
|
|
34
|
+
// Skill details use normal document flow and a fixed readable font size.
|
|
35
|
+
if (document.getElementById('capture')?.classList.contains('skill-sheet')) return;
|
|
18
36
|
const name = document.getElementById('name');
|
|
19
37
|
if (!name) return;
|
|
20
38
|
for (let size=36; size>=20; size--) { name.style.fontSize=size+'px'; if(name.scrollWidth<=name.clientWidth && name.scrollHeight<=name.clientHeight) break }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CharacterCardData } from '../shared/face'
|
|
2
|
+
|
|
3
|
+
const escape = (value: unknown) => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!))
|
|
4
|
+
const finite = (value: number) => Number.isFinite(value) ? value : 0
|
|
5
|
+
const number = (value: number) => finite(value).toLocaleString('zh-CN', { maximumFractionDigits: 2 })
|
|
6
|
+
const ratio = (value: number, max: number) => max > 0 ? Math.max(0, Math.min(1, finite(value) / max)) : 0
|
|
7
|
+
const qualityColors: Record<string, string> = { F: '#99958b', E: '#bac1ab', D: '#75b889', C: '#75a8cb', B: '#ab88cc', A: '#e5bd64', S: '#d8a2f0', SS: '#edb76e', SSS: '#f09783' }
|
|
8
|
+
|
|
9
|
+
function radar(attributes: CharacterCardData['attributes']): string {
|
|
10
|
+
const order = ['力量', '敏捷', '体质', '智力', '精神力', '抗性']
|
|
11
|
+
const attrs = [...attributes].sort((a, b) => {
|
|
12
|
+
const rank = (name: string) => { const i = order.indexOf(name); return i < 0 ? order.length : i }
|
|
13
|
+
return rank(a.name) - rank(b.name)
|
|
14
|
+
})
|
|
15
|
+
if (attrs.length < 3) return `<div class="empty">${attrs.map(a => `${escape(a.name)} ${number(a.value)}`).join('<br>') || '暂无一级属性'}</div>`
|
|
16
|
+
const max = Math.max(1, ...attrs.map(a => Math.max(0, finite(a.value))))
|
|
17
|
+
// Shared scale within this character; values remain visible so shape is never mistaken for absolute strength.
|
|
18
|
+
const ceiling = Math.ceil(max / Math.pow(10, Math.floor(Math.log10(max)))) * Math.pow(10, Math.floor(Math.log10(max)))
|
|
19
|
+
const point = (index: number, radius: number) => {
|
|
20
|
+
const angle = -Math.PI / 2 + index * Math.PI * 2 / attrs.length
|
|
21
|
+
return [350 + Math.cos(angle) * radius, 205 + Math.sin(angle) * radius]
|
|
22
|
+
}
|
|
23
|
+
const polygon = (scale: number) => attrs.map((_, i) => point(i, 128 * scale).join(',')).join(' ')
|
|
24
|
+
const data = attrs.map((a, i) => point(i, 128 * ratio(a.value, ceiling)))
|
|
25
|
+
return `<svg class="radar" viewBox="0 0 700 410" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="一级属性雷达图">
|
|
26
|
+
${[.2, .4, .6, .8, 1].map(scale => `<polygon points="${polygon(scale)}" fill="none" stroke="#8b897c" stroke-opacity=".5"/>`).join('')}
|
|
27
|
+
${attrs.map((_, i) => `<path d="M350 205 L${point(i, 128).join(' ')}" stroke="#777a71" stroke-opacity=".6"/>`).join('')}
|
|
28
|
+
<polygon points="${data.map(p => p.join(',')).join(' ')}" fill="#438e7a" fill-opacity=".4" stroke="#245a50" stroke-width="2.5"/>
|
|
29
|
+
${data.map(p => `<circle cx="${p[0]}" cy="${p[1]}" r="4.5" fill="#174e45"/>`).join('')}
|
|
30
|
+
${attrs.map((a, i) => {
|
|
31
|
+
const [x, y] = point(i, 172)
|
|
32
|
+
return `<g transform="translate(${x} ${y})"><path d="M-44 -18 H44 L49 -6 L44 6 H-44 L-49 -6 Z" fill="#222827" stroke="#ab8b4f"/><text y="0" text-anchor="middle" fill="#f3e7c9" font-size="17">${escape(a.name)}</text><text y="32" text-anchor="middle" fill="#1c5048" font-size="${number(a.value).length > 12 ? 17 : 24}" font-weight="bold">${number(a.value)}</text></g>`
|
|
33
|
+
}).join('')}
|
|
34
|
+
</svg><div class="scale">各轴同尺度 · 满刻度 ${number(ceiling)}</div>`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Portrait alone is replaceable artwork; every label, bar and chart is live game data. */
|
|
38
|
+
export function characterCardHtml(card: CharacterCardData, artwork?: string, paper?: string, defaultArtwork?: string): string {
|
|
39
|
+
const portrait = artwork || defaultArtwork;
|
|
40
|
+
const hpPercent = ratio(card.hp, card.maxHp) * 100
|
|
41
|
+
const exp = Math.max(0, Math.min(100, finite(card.experiencePercent)))
|
|
42
|
+
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><style>
|
|
43
|
+
*{box-sizing:border-box}body{margin:0;width:760px;font-family:"Microsoft YaHei","Noto Sans CJK SC",sans-serif;color:#262b27}
|
|
44
|
+
#capture{width:760px;padding:22px;background:#eee5d2;position:relative;border:7px solid #252a27;box-shadow:inset 0 0 0 2px #b3975a}
|
|
45
|
+
.paper{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.8}.content{position:relative}.top{display:grid;grid-template-columns:300px minmax(0,1fr);gap:18px}
|
|
46
|
+
.portrait{width:300px;height:500px;position:relative;background:linear-gradient(160deg,#ebe5d7,#bdc6bb);outline:3px solid #252a27;box-shadow:0 0 0 5px #b59657;overflow:hidden}
|
|
47
|
+
.portrait img{width:300px;height:500px;object-fit:cover;object-position:center top;display:block}.portrait:after{content:"";position:absolute;inset:7px;border:1px solid #d7b971;pointer-events:none}
|
|
48
|
+
.placeholder{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at 70% 75%,#546b6150,transparent 60%);color:#46564e;gap:20px}.placeholder .seal{font:76px KaiTi,STKaiti,serif;border:2px solid #647266;padding:28px 17px}.placeholder small{font-size:15px;letter-spacing:3px}
|
|
49
|
+
.info{display:flex;flex-direction:column;gap:16px;min-width:0}.dark{background:linear-gradient(115deg,#171e1d,#303431);border:2px solid #ad8d50;box-shadow:0 0 0 2px #252a27,inset 0 0 0 4px #252a27;color:#eee0b9;position:relative;padding:18px}
|
|
50
|
+
.dark:before,.dark:after{content:"◆";position:absolute;color:#c1a367;font-size:13px}.dark:before{left:3px;top:0}.dark:after{right:3px;bottom:0}
|
|
51
|
+
.title{font:28px KaiTi,STKaiti,serif;letter-spacing:4px;text-align:center;padding:16px 8px}.identity{padding:5px 3px;flex:1;display:flex;flex-direction:column;justify-content:center}.eyebrow{font-size:14px;color:#696854;letter-spacing:3px}.identity-row{display:flex;align-items:center;gap:12px;margin:12px 0}.character-name{font: bold 27px KaiTi,STKaiti,"Microsoft YaHei",serif;overflow-wrap:anywhere;min-width:0;flex:1;margin:0;line-height:1.4}.level{flex-shrink:0;border:1px solid #b28c44;background:#252b27;color:#eddbab;padding:7px 10px;font:22px Georgia,serif}
|
|
52
|
+
.experience-label{font-size:13px;color:#676954;margin-top:6px}.experience{height:4px;background:#b5b19e;margin-top:8px}.experience span{height:100%;display:block;background:#7e8c70}
|
|
53
|
+
.power-label{font-size:19px;letter-spacing:2px}.power-value{font:bold 43px Georgia,serif;margin-top:10px;color:#edd69a;overflow-wrap:anywhere}.power-value.long{font-size:29px}.health{padding:15px 13px}.health-label{font-size:18px;margin-bottom:9px}.bar{position:relative;height:30px;border:1px solid #bea365;background:#101817;box-shadow:inset 0 2px 4px #000}.fill{height:100%;background:linear-gradient(#df4d39,#9a241d);border-right:1px solid #f19b67}.bar b{position:absolute;inset:0;display:grid;place-items:center;color:#fff7e6;font:16px Georgia,serif;text-shadow:0 1px 3px #000}
|
|
54
|
+
.panel{margin-top:22px;border:3px solid #252a27;box-shadow:0 0 0 1px #b19559,inset 0 0 0 1px #b19559;background:#f9f1df65;padding:10px 12px 14px}.heading{display:inline-block;margin:-10px 0 0 -12px;padding:9px 25px 10px 20px;background:#222927;border-right:3px solid #b3975a;border-bottom:1px solid #b3975a;color:#eee1bb;font:25px KaiTi,STKaiti,serif;letter-spacing:4px;clip-path:polygon(0 0,94% 0,100% 50%,94% 100%,0 100%)}
|
|
55
|
+
.radar{display:block;width:100%;height:390px;margin-top:-12px;overflow:visible}.scale{text-align:center;font-size:12px;color:#767565}.bloodlines{display:grid;gap:11px;margin-top:15px}.bloodline{display:flex;align-items:center;gap:14px;padding:12px;background:linear-gradient(100deg,#1e2624,#363b34);border:1px solid #b59961;box-shadow:0 0 0 2px #292f29;color:#f2e5c5;min-height:68px}.blood-icon{width:40px;height:40px;border:1px solid #b79a59;display:grid;place-items:center;color:#ddc180;font:26px KaiTi,serif;flex-shrink:0}.blood-name{font-size:21px;flex:1;overflow-wrap:anywhere}.quality{min-width:54px;padding:6px 9px;text-align:center;border:1px solid currentColor;font:bold 27px Georgia,serif;background:#111916;box-shadow:inset 0 0 10px #ffffff12}.empty{text-align:center;padding:25px;color:#6d7266;font-size:18px}.foot{font-size:13px;text-align:center;margin-top:18px;color:#626454;letter-spacing:1px}
|
|
56
|
+
</style><body><main id="capture">${paper ? `<img class="paper" src="${escape(paper)}" alt="">` : ''}<div class="content"><div class="top">
|
|
57
|
+
<div class="portrait">${portrait ? `<img class="character-artwork" src="${escape(portrait)}" data-fallback="${escape(defaultArtwork ?? '')}" alt="角色形象">` : '<div class="placeholder"><div class="seal">轮回</div><small>形象尚未生成</small></div>'}</div>
|
|
58
|
+
<div class="info"><div class="dark title">✦ 角色档案</div><div class="identity"><div class="eyebrow">轮回者</div><div class="identity-row"><h1 class="character-name">${escape(card.name)}</h1><span class="level">Lv. ${number(card.level)}</span></div><div class="experience-label">升级进度 ${number(exp)}%</div><div class="experience"><span style="width:${exp}%"></span></div></div>
|
|
59
|
+
<div class="dark"><div class="power-label">⚔ 综合战力</div><div class="power-value ${number(card.power).length > 11 ? 'long' : ''}">${number(card.power)}</div></div>
|
|
60
|
+
<div class="dark health"><div class="health-label">✚ 生命</div><div class="bar"><div class="fill" style="width:${hpPercent}%"></div><b>${number(card.hp)} / ${number(card.maxHp)}</b></div></div></div></div>
|
|
61
|
+
<section class="panel"><h2 class="heading">一级属性</h2>${radar(card.attributes)}</section>
|
|
62
|
+
<section class="panel"><h2 class="heading">血统</h2><div class="bloodlines">${card.bloodlines.length ? card.bloodlines.map(b => `<div class="bloodline"><span class="blood-icon">血</span><span class="blood-name">${escape(b.name)}</span><span class="quality" style="color:${qualityColors[b.quality] ?? '#bdbbae'}">${escape(b.quality)}</span></div>`).join('') : '<div class="empty">尚未融合血统</div>'}</div></section>
|
|
63
|
+
<div class="foot">[详细属性] 查看二级属性 · [指令] 查看操作</div></div></main></body></html>`
|
|
64
|
+
}
|
|
@@ -8,14 +8,13 @@ export default class ImageHandler extends handel_base {
|
|
|
8
8
|
let content: string
|
|
9
9
|
let rendered = false
|
|
10
10
|
try {
|
|
11
|
-
if (!data.itemCard && !data.card && !data.leaderboard) throw new Error('缺少卡片数据')
|
|
12
|
-
content = h.image(await renderSkillCard(this.ctx, data.card, data.leaderboard, data.itemCard), 'image/png').toString()
|
|
11
|
+
if (!data.characterCard && !data.itemCard && !data.card && !data.leaderboard) throw new Error('缺少卡片数据')
|
|
12
|
+
content = h.image(await renderSkillCard(this.ctx, data.card, data.leaderboard, data.itemCard, data.characterCard), 'image/png').toString()
|
|
13
13
|
rendered = true
|
|
14
14
|
} catch (error) {
|
|
15
15
|
this.ctx.logger.warn('卡片绘制失败,使用文字详情', error instanceof Error ? error.message : 'unknown')
|
|
16
16
|
content = new AnalysisMsg().parse(data)
|
|
17
17
|
}
|
|
18
18
|
await this.send(data, content, rendered ? 'image' : 'text')
|
|
19
|
-
if (rendered && data.itemCard) await this.send(data, new AnalysisMsg().parse(data))
|
|
20
19
|
}
|
|
21
20
|
}
|
package/src/plugin/skillCard.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { appearanceCardHtml } from './appearanceCardTemplate'
|
|
1
2
|
import { itemCardHtml } from './itemCardTemplate'
|
|
3
|
+
import { characterCardHtml } from './characterCardTemplate'
|
|
4
|
+
import type { CharacterCardData } from '../shared/face'
|
|
2
5
|
import type { ItemCardData } from '../shared/face'
|
|
3
6
|
import { layoutCard } from './cardLayout'
|
|
4
7
|
import { leaderboardHtml } from './leaderboardTemplate';
|
|
@@ -12,6 +15,12 @@ import { skillCardHtml } from './skillCardTemplate'
|
|
|
12
15
|
import { loadSkillArtwork } from './skillArtwork'
|
|
13
16
|
let running = 0
|
|
14
17
|
const waiting: Array<() => void> = []
|
|
18
|
+
async function characterAsset(file: 'paper.webp' | 'default-female.webp') {
|
|
19
|
+
for (const base of ['../assets/character-card', '../../assets/character-card']) {
|
|
20
|
+
try { return 'data:image/webp;base64,' + (await fs.readFile(path.resolve(__dirname, base, file))).toString('base64') } catch {}
|
|
21
|
+
}
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
15
24
|
async function itemPaper() {
|
|
16
25
|
for (const base of ['../assets/item-card', '../../assets/item-card']) {
|
|
17
26
|
try { return 'data:image/webp;base64,' + (await fs.readFile(path.resolve(__dirname, base, 'paper.webp'))).toString('base64') } catch {}
|
|
@@ -34,18 +43,18 @@ async function leaderboardHeader(): Promise<string | undefined> {
|
|
|
34
43
|
}
|
|
35
44
|
return undefined
|
|
36
45
|
}
|
|
37
|
-
export async function renderSkillCard(ctx: Context, card?: SkillCardData, leaderboard?: LeaderboardImageData, itemCard?: ItemCardData): Promise<Buffer> {
|
|
46
|
+
export async function renderSkillCard(ctx: Context, card?: SkillCardData, leaderboard?: LeaderboardImageData, itemCard?: ItemCardData, characterCard?: CharacterCardData): Promise<Buffer> {
|
|
38
47
|
if (!ctx.puppeteer) throw new Error('Puppeteer 未启用')
|
|
39
|
-
if (JSON.stringify(itemCard ?? card ?? leaderboard ?? {}).length > 24000) throw new Error('
|
|
48
|
+
if (JSON.stringify(characterCard ?? itemCard ?? card ?? leaderboard ?? {}).length > 24000) throw new Error('卡片内容过长')
|
|
40
49
|
if (running >= 2) { if (waiting.length >= 16) throw new Error('绘图繁忙'); await new Promise<void>(resolve => waiting.push(resolve)) } else running++
|
|
41
50
|
let page: Awaited<ReturnType<typeof ctx.puppeteer.page>> | undefined
|
|
42
51
|
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
43
52
|
let expired = false
|
|
44
53
|
try {
|
|
45
54
|
const work = async () => {
|
|
46
|
-
const art = await loadSkillArtwork(itemCard?.artworkUrl ?? leaderboard?.backgroundUrl ?? card?.artworkUrl)
|
|
55
|
+
const art = await loadSkillArtwork(characterCard?.artworkUrl ?? itemCard?.artworkUrl ?? leaderboard?.backgroundUrl ?? card?.artworkUrl)
|
|
47
56
|
if (itemCard && !art) throw new Error('道具图片读取失败')
|
|
48
|
-
const html = itemCard ? itemCardHtml(itemCard, art!, await itemPaper()) : leaderboard ? leaderboardHtml(leaderboard, await leaderboardHeader(), art) : skillCardHtml(card!, await frame(card!.passive), art)
|
|
57
|
+
const html = characterCard ? characterCardHtml(characterCard, art, await characterAsset('paper.webp'), await characterAsset('default-female.webp')) : itemCard ? (itemCard.type === '形象卡' ? appearanceCardHtml(itemCard, art!, await characterAsset('paper.webp')) : itemCardHtml(itemCard, art!, await itemPaper())) : leaderboard ? leaderboardHtml(leaderboard, await leaderboardHeader(), art) : skillCardHtml(card!, await frame(card!.passive), art)
|
|
49
58
|
page = await ctx.puppeteer.page()
|
|
50
59
|
if (expired) { await page.close(); throw new Error('绘图超时') }
|
|
51
60
|
await page.setViewport({ width:760,height:1107,deviceScaleFactor:1 })
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import type { SkillCardData } from '../shared/face'
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { SkillCardData } from '../shared/face'
|
|
2
|
+
|
|
3
|
+
const escape = (value: unknown) => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!))
|
|
4
|
+
|
|
5
|
+
/** One continuous rice-paper sheet; the old frame parameter remains API-compatible. */
|
|
6
|
+
export function skillCardHtml(card: SkillCardData, _frame: string, artwork?: string): string {
|
|
7
|
+
const description = escape((card.description || '暂无技能效果说明').replace(/\s*\n+\s*/g, ''))
|
|
8
|
+
const grain = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180"><filter id="paper"><feTurbulence type="fractalNoise" baseFrequency=".72" numOctaves="3" stitchTiles="stitch"/></filter><rect width="100%" height="100%" opacity=".15" filter="url(#paper)"/></svg>')
|
|
9
|
+
return `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
10
|
+
*{box-sizing:border-box}
|
|
11
|
+
html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#34372f}
|
|
12
|
+
#capture{position:relative;isolation:isolate;width:760px;min-height:1060px;padding:57px 58px 38px;background:radial-gradient(ellipse at 0 0,#ddd9c744,transparent 48%),radial-gradient(ellipse at 100% 90%,#c9cdb938,transparent 45%),#f4f1e8;border:1px solid #dedbcf}
|
|
13
|
+
#capture:before{content:'';position:absolute;inset:0;z-index:-1;background-image:url('${grain}');opacity:.42;pointer-events:none}
|
|
14
|
+
#capture:after{content:'';position:absolute;inset:17px;border:1px solid #8c8b6f22;pointer-events:none}
|
|
15
|
+
header{position:relative}.heading{display:flex;align-items:center;gap:24px}.heading-copy{flex:1;min-width:0}.overline{font-size:14px;letter-spacing:6px;color:#868a76;margin-bottom:13px}.name{font-family:'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;font-size:43px;font-weight:700;line-height:1.35;margin:0;color:#303a31;overflow-wrap:anywhere}
|
|
16
|
+
.seal{flex-shrink:0;width:66px;min-height:74px;transform:rotate(3deg);padding:8px 3px;color:#a04b40;border:2px solid #a04b40;outline:1px solid #a04b40;outline-offset:-6px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border-radius:3px 1px 4px 2px}.seal strong{font:700 24px Georgia;letter-spacing:-1px}.seal span{font-family:'KaiTi','SimSun',serif;font-size:16px;letter-spacing:4px;padding-left:4px}
|
|
17
|
+
.meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 22px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta span{overflow-wrap:anywhere;max-width:100%}.meta small{font-size:16px;color:#92917f;margin-right:7px}.meta .kind{color:#915344;font-family:'KaiTi','SimSun',serif;font-size:21px}
|
|
18
|
+
.art{position:relative;width:calc(100% + 44px);height:570px;margin:14px -22px -8px;display:grid;place-items:center;isolation:isolate}.art:before{content:'';position:absolute;inset:15% 10%;border-radius:48% 52% 56% 44%;background:radial-gradient(ellipse,#aab4a443,transparent 69%);transform:rotate(-12deg)}.skill-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 43%;filter:saturate(.68) contrast(.94);mix-blend-mode:multiply;mask-image:linear-gradient(to bottom,transparent 0,#000 9%,#000 72%,transparent 100%),linear-gradient(to right,transparent 0,#000 8%,#000 92%,transparent 100%);mask-composite:intersect}.art-fallback{position:relative;z-index:-1;width:300px;height:300px;border:2px solid #7c887d33;border-radius:48% 52% 50% 46%;transform:rotate(-12deg);display:grid;place-items:center;color:#8c948177;font:70px 'KaiTi','SimSun',serif}.art-fallback span{transform:rotate(12deg)}.art:has(.skill-artwork) .art-fallback{visibility:hidden}
|
|
19
|
+
.effect{position:relative;margin-top:0}h2{display:flex;align-items:center;gap:15px;margin:0 0 17px;font:22px 'KaiTi','SimSun',serif;letter-spacing:5px;color:#626e5e}h2:before{content:'';width:5px;height:17px;background:#a04b40;transform:rotate(8deg);opacity:.75}h2:after{content:'';flex:1;height:1px;background:linear-gradient(to right,#7f897c55,transparent)}p{margin:0;font-size:25px;line-height:1.85;white-space:pre-wrap;overflow-wrap:anywhere;text-align:justify}.footer{margin-top:24px;padding-top:17px;border-top:1px solid #8a8a7128}.footer p{font-size:18px;color:#7d8070;line-height:1.75;text-align:left}.edition{display:flex;align-items:center;gap:12px;justify-content:center;margin-top:36px;color:#a6a38e;font:14px 'KaiTi','SimSun',serif;letter-spacing:5px}.edition:before,.edition:after{content:'';height:1px;width:32px;background:#9b9c833d}
|
|
20
|
+
</style></head><body><div id="capture" class="skill-sheet ink-skill-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">技 艺 图 录</div><h1 class="name">${escape(card.name)}</h1></div><div class="seal"><strong>${escape(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="kind">${card.passive ? '被动' : '主动'}</span><span><small>等级</small>${escape(card.level)} / ${escape(card.maxLevel)}</span><span><small>消耗</small>${escape(card.cost || '无消耗')}</span><span><small>冷却</small>${escape(card.cooldown || '无冷却')}</span></div></header><div class="art"><div class="art-fallback"><span>${card.passive ? '静' : '势'}</span></div>${artwork ? `<img class="skill-artwork" alt="" src="${escape(artwork)}">` : ''}</div><section class="effect"><h2>技能效果</h2><p>${description}</p></section>${card.sources?.length || card.note ? `<div class="footer">${card.sources?.length ? `<p>获取来源:${escape(card.sources.join('、'))}</p>` : ''}${card.note ? `<p>${escape(card.note)}</p>` : ''}</div>` : ''}<div class="edition">露 · 技艺录</div></div></body></html>`
|
|
12
21
|
}
|
|
@@ -20,7 +20,7 @@ export interface ServiceType {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export const serviceProto: ServiceProto<ServiceType> = {
|
|
23
|
-
"version":
|
|
23
|
+
"version": 7,
|
|
24
24
|
"services": [
|
|
25
25
|
{
|
|
26
26
|
"id": 0,
|
|
@@ -101,6 +101,15 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
|
|
101
101
|
"target": "messageFace/MessageHandleType"
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
"id": 5,
|
|
106
|
+
"name": "characterCard",
|
|
107
|
+
"type": {
|
|
108
|
+
"type": "Reference",
|
|
109
|
+
"target": "messageFace/CharacterCardData"
|
|
110
|
+
},
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
104
113
|
{
|
|
105
114
|
"id": 2,
|
|
106
115
|
"name": "card",
|
|
@@ -154,6 +163,120 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
|
|
154
163
|
}
|
|
155
164
|
]
|
|
156
165
|
},
|
|
166
|
+
"messageFace/CharacterCardData": {
|
|
167
|
+
"type": "Interface",
|
|
168
|
+
"properties": [
|
|
169
|
+
{
|
|
170
|
+
"id": 0,
|
|
171
|
+
"name": "artworkUrl",
|
|
172
|
+
"type": {
|
|
173
|
+
"type": "String"
|
|
174
|
+
},
|
|
175
|
+
"optional": true
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"id": 1,
|
|
179
|
+
"name": "name",
|
|
180
|
+
"type": {
|
|
181
|
+
"type": "String"
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"id": 2,
|
|
186
|
+
"name": "level",
|
|
187
|
+
"type": {
|
|
188
|
+
"type": "Number"
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"id": 3,
|
|
193
|
+
"name": "experiencePercent",
|
|
194
|
+
"type": {
|
|
195
|
+
"type": "Number"
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"id": 4,
|
|
200
|
+
"name": "power",
|
|
201
|
+
"type": {
|
|
202
|
+
"type": "Number"
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": 5,
|
|
207
|
+
"name": "hp",
|
|
208
|
+
"type": {
|
|
209
|
+
"type": "Number"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": 6,
|
|
214
|
+
"name": "maxHp",
|
|
215
|
+
"type": {
|
|
216
|
+
"type": "Number"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": 7,
|
|
221
|
+
"name": "attributes",
|
|
222
|
+
"type": {
|
|
223
|
+
"type": "Array",
|
|
224
|
+
"elementType": {
|
|
225
|
+
"type": "Interface",
|
|
226
|
+
"properties": [
|
|
227
|
+
{
|
|
228
|
+
"id": 0,
|
|
229
|
+
"name": "id",
|
|
230
|
+
"type": {
|
|
231
|
+
"type": "String"
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"id": 1,
|
|
236
|
+
"name": "name",
|
|
237
|
+
"type": {
|
|
238
|
+
"type": "String"
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": 2,
|
|
243
|
+
"name": "value",
|
|
244
|
+
"type": {
|
|
245
|
+
"type": "Number"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": 8,
|
|
254
|
+
"name": "bloodlines",
|
|
255
|
+
"type": {
|
|
256
|
+
"type": "Array",
|
|
257
|
+
"elementType": {
|
|
258
|
+
"type": "Interface",
|
|
259
|
+
"properties": [
|
|
260
|
+
{
|
|
261
|
+
"id": 0,
|
|
262
|
+
"name": "name",
|
|
263
|
+
"type": {
|
|
264
|
+
"type": "String"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"id": 1,
|
|
269
|
+
"name": "quality",
|
|
270
|
+
"type": {
|
|
271
|
+
"type": "String"
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
},
|
|
157
280
|
"messageFace/SkillCardData": {
|
|
158
281
|
"type": "Interface",
|
|
159
282
|
"properties": [
|
package/src/shared/face/index.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export interface CharacterCardData {
|
|
2
|
+
artworkUrl?: string;
|
|
3
|
+
name: string;
|
|
4
|
+
level: number;
|
|
5
|
+
experiencePercent: number;
|
|
6
|
+
power: number;
|
|
7
|
+
hp: number;
|
|
8
|
+
maxHp: number;
|
|
9
|
+
attributes: Array<{ id: string; name: string; value: number }>;
|
|
10
|
+
bloodlines: Array<{ name: string; quality: string }>;
|
|
11
|
+
}
|
|
1
12
|
export interface ItemCardData { artworkUrl: string; name: string; quality: string; type: string; description: string; count: string; referencePrice: string; }
|
|
2
13
|
export interface LeaderboardImageData {
|
|
3
14
|
backgroundUrl?: string;
|
|
@@ -61,6 +72,7 @@ export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
|
|
|
61
72
|
/** 下发给客户端的消息主体 **/
|
|
62
73
|
export interface IMessage {
|
|
63
74
|
handleType: MessageHandleType;
|
|
75
|
+
characterCard?: CharacterCardData;
|
|
64
76
|
card?: SkillCardData;
|
|
65
77
|
itemCard?: ItemCardData;
|
|
66
78
|
leaderboard?: LeaderboardImageData;
|