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.
@@ -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
package/lib/index.js CHANGED
@@ -75,12 +75,27 @@ var AnalysisMsg = class {
75
75
  }
76
76
  };
77
77
 
78
- // src/plugin/itemCardTemplate.ts
78
+ // src/plugin/appearanceCardTemplate.ts
79
79
  var escape = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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('${escape(paper)}');background-size:100% 100%;` : ""}box-shadow:inset 0 0 55px #85796228;position:relative}
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">${escape(card.name)}</h1><div class="quality"><span class="quality-label">品质</span><span class="quality-seal">${escape(card.quality)}</span></div></header><div class="item-type">类型:${escape(card.type)}</div><div class="item-art-frame"><img class="item-artwork" alt="道具插画" src="${escape(artwork)}"></div><div class="item-count">数量:${escape(card.count)}</div><div class="description-title">描述</div><p class="item-description">${escape(card.description)}</p><footer class="item-price">参考价格:${escape(card.referencePrice)}</footer></article></body></html>`;
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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 escape2 = /* @__PURE__ */ __name((s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]), "escape");
249
+ var escape4 = /* @__PURE__ */ __name((s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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>${escape2(r.rank)}</b></div><div class="name">${escape2(r.name)}</div><div class="value">${escape2(r.value.toLocaleString("zh-CN", { maximumFractionDigits: 2 }))}</div></div>`, "row");
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="' + escape2(backgroundImage) + '">' : ""}<h1 class="board-title">${escape2(data.title)}</h1><main class="sheet">${rows.length ? rows.map((r) => row(r)).join("") : '<div class="empty">暂无玩家上榜</div>'}${personal}</main></div></body></html>`;
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 escape3 = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]), "escape");
177
- var colors = { F: "#b6c3c9", E: "#82cf88", D: "#5fbeed", C: "#9b8cfa", B: "#d97eee", A: "#f9b25f", S: "#f2dc88", SS: "#ff9e78", SSS: "#fff0b1" };
178
- function skillCardHtml(card, frame2, artwork) {
179
- const quality = card.quality.replace(/[+-]$/, ""), color = colors[quality] ?? colors.F;
180
- const desc = card.description || "暂无描述";
279
+ var escape5 = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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}html,body{margin:0;width:760px;background:#141c26;color:#15252b;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif}body{padding:0}#capture{width:760px}#card{width:760px;height:1107px;position:relative;background-image:url('${frame2}');background-size:100% 100%}.name{position:absolute;left:58px;top:46px;width:644px;height:74px;display:flex;align-items:center;font-size:36px;font-weight:800;color:#f5f7e9;text-shadow:1px 2px 2px #163838;overflow:hidden}.badge{position:absolute;right:57px;top:54px;width:68px;height:62px;border:2px solid ${color};background:#163338;border-radius:50%;color:${color};display:grid;place-items:center;font:bold 25px Georgia}.meta{position:absolute;left:92px;right:91px;top:151px;display:flex;justify-content:space-between;color:#f9f1cf;font-size:21px;font-weight:700}.art{position:absolute;left:93px;top:205px;width:574px;height:574px;overflow:hidden;background:radial-gradient(ellipse at 50% 43%,#284951 0,#12222f 42%,#070d19 90%);border:3px solid #bea874}.skill-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:1}.art svg{position:absolute;width:100%;height:100%;color:${color}}.glyph{position:absolute;top:157px;width:100%;text-align:center;font-size:147px;filter:drop-shadow(0 0 30px ${color})}.art-title{position:absolute;bottom:113px;left:25px;right:25px;text-align:center;font-size:18px;letter-spacing:6px;color:${color}}.number{position:absolute;top:786px;right:94px;color:#e2eade;font:14px Georgia;letter-spacing:2px}.effect{position:absolute;top:837px;left:62px;width:636px;height:197px;overflow:hidden;font-size:23px;line-height:1.48}.effect b{font-size:23px}.effect p{margin:8px 0;white-space:pre-wrap;overflow-wrap:anywhere}
183
- </style></head><body><div id="capture"><div id="card"><div class="name" id="name">${escape3(card.quality)}级 ${escape3(card.name)}</div><div class="meta"><span>【${card.passive ? "被动" : "主动"}技能】</span><span>Lv.${card.level} / ${card.maxLevel}</span></div>
184
- <div class="art"><svg viewBox="0 0 574 574" xmlns="http://www.w3.org/2000/svg"><defs><radialGradient id="glow"><stop stop-color="${color}" stop-opacity=".25"/><stop offset="1" stop-color="${color}" stop-opacity="0"/></radialGradient></defs><circle cx="287" cy="244" r="220" fill="url(#glow)"/><g stroke="currentColor" fill="none"><circle cx="287" cy="244" r="191" opacity=".35"/><circle cx="287" cy="244" r="179" stroke-dasharray="2 13" opacity=".8"/><circle cx="287" cy="244" r="158" opacity=".6"/><path d="M287 65 442 334 132 334Z M287 423 132 155 442 155Z" opacity=".3"/><path d="M60 244H130M444 244H514M287 18V85M287 403V470" stroke-width="2" opacity=".6"/><path d="M80 440 491 72M102 457 505 95" opacity=".13" stroke-width="16"/></g></svg><div class="art-title">${escape3(card.passive ? "被 动 铭 刻" : "技 能 术 式")}</div>${artwork ? '<img class="skill-artwork" alt="" src="' + escape3(artwork) + '">' : ""}</div>
185
- <div class="number">DEW · SKILL / ${escape3(card.quality)}</div><div class="effect" id="effect"><p>${escape3(desc)}</p></div></div></div></body></html>`;
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": 6,
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 { ItemCardData } from '../shared/face';
2
+ /** Hanging scroll shared by bot screenshots and editor previews. */
3
+ export declare function appearanceCardHtml(card: ItemCardData, artwork: string, paper?: string): string;
@@ -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
- export declare function skillCardHtml(card: SkillCardData, frame: string, artwork?: string): string;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-dew-bot",
3
3
  "description": "连游戏服,把聊天消息交给服务器处理",
4
- "version": "1.0.241",
4
+ "version": "1.0.243",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -0,0 +1,16 @@
1
+ import type { ItemCardData } from '../shared/face';
2
+
3
+ const escape = (value: unknown) => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
+ }
@@ -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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
  }
@@ -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
- const escape = (value: unknown) => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!))
3
- const colors: Record<string,string> = {F:'#b6c3c9',E:'#82cf88',D:'#5fbeed',C:'#9b8cfa',B:'#d97eee',A:'#f9b25f',S:'#f2dc88',SS:'#ff9e78',SSS:'#fff0b1'}
4
- export function skillCardHtml(card: SkillCardData, frame: string, artwork?: string): string {
5
- const quality = card.quality.replace(/[+-]$/, ''), color = colors[quality] ?? colors.F
6
- const desc = card.description || '暂无描述'
7
- return `<!doctype html><html><head><meta charset="utf-8"><style>
8
- *{box-sizing:border-box}html,body{margin:0;width:760px;background:#141c26;color:#15252b;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif}body{padding:0}#capture{width:760px}#card{width:760px;height:1107px;position:relative;background-image:url('${frame}');background-size:100% 100%}.name{position:absolute;left:58px;top:46px;width:644px;height:74px;display:flex;align-items:center;font-size:36px;font-weight:800;color:#f5f7e9;text-shadow:1px 2px 2px #163838;overflow:hidden}.badge{position:absolute;right:57px;top:54px;width:68px;height:62px;border:2px solid ${color};background:#163338;border-radius:50%;color:${color};display:grid;place-items:center;font:bold 25px Georgia}.meta{position:absolute;left:92px;right:91px;top:151px;display:flex;justify-content:space-between;color:#f9f1cf;font-size:21px;font-weight:700}.art{position:absolute;left:93px;top:205px;width:574px;height:574px;overflow:hidden;background:radial-gradient(ellipse at 50% 43%,#284951 0,#12222f 42%,#070d19 90%);border:3px solid #bea874}.skill-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:1}.art svg{position:absolute;width:100%;height:100%;color:${color}}.glyph{position:absolute;top:157px;width:100%;text-align:center;font-size:147px;filter:drop-shadow(0 0 30px ${color})}.art-title{position:absolute;bottom:113px;left:25px;right:25px;text-align:center;font-size:18px;letter-spacing:6px;color:${color}}.number{position:absolute;top:786px;right:94px;color:#e2eade;font:14px Georgia;letter-spacing:2px}.effect{position:absolute;top:837px;left:62px;width:636px;height:197px;overflow:hidden;font-size:23px;line-height:1.48}.effect b{font-size:23px}.effect p{margin:8px 0;white-space:pre-wrap;overflow-wrap:anywhere}
9
- </style></head><body><div id="capture"><div id="card"><div class="name" id="name">${escape(card.quality)}级 ${escape(card.name)}</div><div class="meta"><span>【${card.passive ? '被动' : '主动'}技能】</span><span>Lv.${card.level} / ${card.maxLevel}</span></div>
10
- <div class="art"><svg viewBox="0 0 574 574" xmlns="http://www.w3.org/2000/svg"><defs><radialGradient id="glow"><stop stop-color="${color}" stop-opacity=".25"/><stop offset="1" stop-color="${color}" stop-opacity="0"/></radialGradient></defs><circle cx="287" cy="244" r="220" fill="url(#glow)"/><g stroke="currentColor" fill="none"><circle cx="287" cy="244" r="191" opacity=".35"/><circle cx="287" cy="244" r="179" stroke-dasharray="2 13" opacity=".8"/><circle cx="287" cy="244" r="158" opacity=".6"/><path d="M287 65 442 334 132 334Z M287 423 132 155 442 155Z" opacity=".3"/><path d="M60 244H130M444 244H514M287 18V85M287 403V470" stroke-width="2" opacity=".6"/><path d="M80 440 491 72M102 457 505 95" opacity=".13" stroke-width="16"/></g></svg><div class="art-title">${escape(card.passive ? '被 动 铭 刻' : '技 能 术 式')}</div>${artwork ? '<img class="skill-artwork" alt="" src="' + escape(artwork) + '">' : ''}</div>
11
- <div class="number">DEW · SKILL / ${escape(card.quality)}</div><div class="effect" id="effect"><p>${escape(desc)}</p></div></div></div></body></html>`
1
+ import type { SkillCardData } from '../shared/face'
2
+
3
+ const escape = (value: unknown) => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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": 6,
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": [
@@ -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;