koishi-plugin-dew-bot 1.0.241 → 1.0.242
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 +252 -17
- 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/shared/face/index.d.ts +19 -0
- package/package.json +1 -1
- package/src/plugin/appearanceCardTemplate.ts +16 -0
- package/src/plugin/cardLayout.ts +16 -0
- package/src/plugin/characterCardTemplate.ts +64 -0
- package/src/plugin/serverHandel/image.ts +2 -2
- package/src/plugin/skillCard.ts +13 -4
- 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, frame2) {
|
|
81
|
+
const slice = /* @__PURE__ */ __name((y, height) => `<svg class="frame" viewBox="0 ${y} 760 ${height}" preserveAspectRatio="none" aria-hidden="true"><image href="${escape(frame2)}" width="760" height="1107" preserveAspectRatio="none"/></svg>`, "slice");
|
|
82
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><style>
|
|
83
|
+
*{box-sizing:border-box}html,body{margin:0;width:760px;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#f5edd6;background:#141c26}
|
|
84
|
+
#capture{width:760px}.section{position:relative}.frame{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.content{position:relative}
|
|
85
|
+
.heading{height:205px}.title{position:absolute;left:58px;top:46px;width:644px;height:74px;margin:0;display:flex;align-items:center;font-size:30px;font-weight:800;line-height:1.15;text-shadow:0 2px 3px #152a24;overflow-wrap:anywhere;overflow:hidden}
|
|
86
|
+
.meta{position:absolute;left:92px;right:91px;top:151px;display:flex;justify-content:space-between;font-size:21px}
|
|
87
|
+
.portrait{height:957px}.art{position:relative;margin-left:93px;width:574px;height:957px;border:3px solid #bea874;background:#e9e5d9;overflow:hidden}.item-artwork{display:block;width:100%;height:100%;object-fit:contain}
|
|
88
|
+
.footer{min-height:328px;padding:58px 62px 73px;color:#202c24}.description{margin:0;font-size:23px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}.details{display:flex;justify-content:space-between;gap:24px;border-top:1px solid #52665366;margin-top:16px;padding-top:12px;font-size:18px;line-height:1.5;overflow-wrap:anywhere}.serial{position:absolute;top:9px;right:94px;font-size:14px;color:#23382e;letter-spacing:2px}
|
|
89
|
+
</style></head><body><article id="capture"><header class="section heading">${slice(0, 205)}<h1 class="title">${escape(card.name)}</h1><div class="meta"><span>【形象卡】</span><span>使用后更换外观</span></div></header><div class="section portrait">${slice(205, 574)}<div class="art"><img class="item-artwork" src="${escape(artwork)}" alt="角色形象"></div></div><footer class="section footer">${slice(779, 328)}<div class="serial">DEW · APPEARANCE</div><div class="content"><p class="description">${escape(card.description)}</p><div class="details"><span>数量:${escape(card.count)}</span><span>参考价格:${escape(card.referencePrice)}</span></div></div></footer></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");
|
|
@@ -143,10 +245,10 @@ async function layoutCard(document = globalThis.document) {
|
|
|
143
245
|
__name(layoutCard, "layoutCard");
|
|
144
246
|
|
|
145
247
|
// src/plugin/leaderboardTemplate.ts
|
|
146
|
-
var
|
|
248
|
+
var escape4 = /* @__PURE__ */ __name((s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]), "escape");
|
|
147
249
|
function leaderboardHtml(data, _headerImage, backgroundImage) {
|
|
148
250
|
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>${
|
|
251
|
+
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
252
|
const personal = data.mine ? row(data.mine, true) : "";
|
|
151
253
|
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><style>
|
|
152
254
|
*{box-sizing:border-box}body{margin:0;width:760px;color:#242725;font-family:"Microsoft YaHei","Noto Sans CJK SC",sans-serif}
|
|
@@ -164,7 +266,7 @@ function leaderboardHtml(data, _headerImage, backgroundImage) {
|
|
|
164
266
|
.value{font-size:23px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
165
267
|
#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
268
|
.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="' +
|
|
269
|
+
</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
270
|
}
|
|
169
271
|
__name(leaderboardHtml, "leaderboardHtml");
|
|
170
272
|
|
|
@@ -173,16 +275,16 @@ var import_promises = __toESM(require("node:fs/promises"));
|
|
|
173
275
|
var import_node_path = __toESM(require("node:path"));
|
|
174
276
|
|
|
175
277
|
// src/plugin/skillCardTemplate.ts
|
|
176
|
-
var
|
|
278
|
+
var escape5 = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]), "escape");
|
|
177
279
|
var colors = { F: "#b6c3c9", E: "#82cf88", D: "#5fbeed", C: "#9b8cfa", B: "#d97eee", A: "#f9b25f", S: "#f2dc88", SS: "#ff9e78", SSS: "#fff0b1" };
|
|
178
280
|
function skillCardHtml(card, frame2, artwork) {
|
|
179
281
|
const quality = card.quality.replace(/[+-]$/, ""), color = colors[quality] ?? colors.F;
|
|
180
282
|
const desc = card.description || "暂无描述";
|
|
181
283
|
return `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
182
284
|
*{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">${
|
|
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">${
|
|
185
|
-
<div class="number">DEW · SKILL / ${
|
|
285
|
+
</style></head><body><div id="capture"><div id="card"><div class="name" id="name">${escape5(card.quality)}级 ${escape5(card.name)}</div><div class="meta"><span>【${card.passive ? "被动" : "主动"}技能】</span><span>Lv.${card.level} / ${card.maxLevel}</span></div>
|
|
286
|
+
<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">${escape5(card.passive ? "被 动 铭 刻" : "技 能 术 式")}</div>${artwork ? '<img class="skill-artwork" alt="" src="' + escape5(artwork) + '">' : ""}</div>
|
|
287
|
+
<div class="number">DEW · SKILL / ${escape5(card.quality)}</div><div class="effect" id="effect"><p>${escape5(desc)}</p></div></div></div></body></html>`;
|
|
186
288
|
}
|
|
187
289
|
__name(skillCardHtml, "skillCardHtml");
|
|
188
290
|
|
|
@@ -227,6 +329,16 @@ __name(loadSkillArtwork, "loadSkillArtwork");
|
|
|
227
329
|
// src/plugin/skillCard.ts
|
|
228
330
|
var running = 0;
|
|
229
331
|
var waiting = [];
|
|
332
|
+
async function characterAsset(file) {
|
|
333
|
+
for (const base of ["../assets/character-card", "../../assets/character-card"]) {
|
|
334
|
+
try {
|
|
335
|
+
return "data:image/webp;base64," + (await import_promises.default.readFile(import_node_path.default.resolve(__dirname, base, file))).toString("base64");
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
__name(characterAsset, "characterAsset");
|
|
230
342
|
async function itemPaper() {
|
|
231
343
|
for (const base of ["../assets/item-card", "../../assets/item-card"]) {
|
|
232
344
|
try {
|
|
@@ -260,9 +372,9 @@ async function leaderboardHeader() {
|
|
|
260
372
|
return void 0;
|
|
261
373
|
}
|
|
262
374
|
__name(leaderboardHeader, "leaderboardHeader");
|
|
263
|
-
async function renderSkillCard(ctx, card, leaderboard, itemCard) {
|
|
375
|
+
async function renderSkillCard(ctx, card, leaderboard, itemCard, characterCard) {
|
|
264
376
|
if (!ctx.puppeteer) throw new Error("Puppeteer 未启用");
|
|
265
|
-
if (JSON.stringify(itemCard ?? card ?? leaderboard ?? {}).length > 24e3) throw new Error("
|
|
377
|
+
if (JSON.stringify(characterCard ?? itemCard ?? card ?? leaderboard ?? {}).length > 24e3) throw new Error("卡片内容过长");
|
|
266
378
|
if (running >= 2) {
|
|
267
379
|
if (waiting.length >= 16) throw new Error("绘图繁忙");
|
|
268
380
|
await new Promise((resolve) => waiting.push(resolve));
|
|
@@ -272,9 +384,9 @@ async function renderSkillCard(ctx, card, leaderboard, itemCard) {
|
|
|
272
384
|
let expired = false;
|
|
273
385
|
try {
|
|
274
386
|
const work = /* @__PURE__ */ __name(async () => {
|
|
275
|
-
const art = await loadSkillArtwork(itemCard?.artworkUrl ?? leaderboard?.backgroundUrl ?? card?.artworkUrl);
|
|
387
|
+
const art = await loadSkillArtwork(characterCard?.artworkUrl ?? itemCard?.artworkUrl ?? leaderboard?.backgroundUrl ?? card?.artworkUrl);
|
|
276
388
|
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);
|
|
389
|
+
const html = characterCard ? characterCardHtml(characterCard, art, await characterAsset("paper.webp"), await characterAsset("default-female.webp")) : itemCard ? itemCard.type === "形象卡" ? appearanceCardHtml(itemCard, art, await frame(false)) : itemCardHtml(itemCard, art, await itemPaper()) : leaderboard ? leaderboardHtml(leaderboard, await leaderboardHeader(), art) : skillCardHtml(card, await frame(card.passive), art);
|
|
278
390
|
page = await ctx.puppeteer.page();
|
|
279
391
|
if (expired) {
|
|
280
392
|
await page.close();
|
|
@@ -377,8 +489,8 @@ var ImageHandler = class extends handel_base {
|
|
|
377
489
|
let content;
|
|
378
490
|
let rendered = false;
|
|
379
491
|
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();
|
|
492
|
+
if (!data.characterCard && !data.itemCard && !data.card && !data.leaderboard) throw new Error("缺少卡片数据");
|
|
493
|
+
content = import_koishi.h.image(await renderSkillCard(this.ctx, data.card, data.leaderboard, data.itemCard, data.characterCard), "image/png").toString();
|
|
382
494
|
rendered = true;
|
|
383
495
|
} catch (error) {
|
|
384
496
|
this.ctx.logger.warn("卡片绘制失败,使用文字详情", error instanceof Error ? error.message : "unknown");
|
|
@@ -448,7 +560,7 @@ var import_tsrpc = require("tsrpc");
|
|
|
448
560
|
|
|
449
561
|
// src/shared/bot/serviceProto.ts
|
|
450
562
|
var serviceProto = {
|
|
451
|
-
"version":
|
|
563
|
+
"version": 7,
|
|
452
564
|
"services": [
|
|
453
565
|
{
|
|
454
566
|
"id": 0,
|
|
@@ -529,6 +641,15 @@ var serviceProto = {
|
|
|
529
641
|
"target": "messageFace/MessageHandleType"
|
|
530
642
|
}
|
|
531
643
|
},
|
|
644
|
+
{
|
|
645
|
+
"id": 5,
|
|
646
|
+
"name": "characterCard",
|
|
647
|
+
"type": {
|
|
648
|
+
"type": "Reference",
|
|
649
|
+
"target": "messageFace/CharacterCardData"
|
|
650
|
+
},
|
|
651
|
+
"optional": true
|
|
652
|
+
},
|
|
532
653
|
{
|
|
533
654
|
"id": 2,
|
|
534
655
|
"name": "card",
|
|
@@ -582,6 +703,120 @@ var serviceProto = {
|
|
|
582
703
|
}
|
|
583
704
|
]
|
|
584
705
|
},
|
|
706
|
+
"messageFace/CharacterCardData": {
|
|
707
|
+
"type": "Interface",
|
|
708
|
+
"properties": [
|
|
709
|
+
{
|
|
710
|
+
"id": 0,
|
|
711
|
+
"name": "artworkUrl",
|
|
712
|
+
"type": {
|
|
713
|
+
"type": "String"
|
|
714
|
+
},
|
|
715
|
+
"optional": true
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
"id": 1,
|
|
719
|
+
"name": "name",
|
|
720
|
+
"type": {
|
|
721
|
+
"type": "String"
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
"id": 2,
|
|
726
|
+
"name": "level",
|
|
727
|
+
"type": {
|
|
728
|
+
"type": "Number"
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
"id": 3,
|
|
733
|
+
"name": "experiencePercent",
|
|
734
|
+
"type": {
|
|
735
|
+
"type": "Number"
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
"id": 4,
|
|
740
|
+
"name": "power",
|
|
741
|
+
"type": {
|
|
742
|
+
"type": "Number"
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"id": 5,
|
|
747
|
+
"name": "hp",
|
|
748
|
+
"type": {
|
|
749
|
+
"type": "Number"
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"id": 6,
|
|
754
|
+
"name": "maxHp",
|
|
755
|
+
"type": {
|
|
756
|
+
"type": "Number"
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
"id": 7,
|
|
761
|
+
"name": "attributes",
|
|
762
|
+
"type": {
|
|
763
|
+
"type": "Array",
|
|
764
|
+
"elementType": {
|
|
765
|
+
"type": "Interface",
|
|
766
|
+
"properties": [
|
|
767
|
+
{
|
|
768
|
+
"id": 0,
|
|
769
|
+
"name": "id",
|
|
770
|
+
"type": {
|
|
771
|
+
"type": "String"
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
"id": 1,
|
|
776
|
+
"name": "name",
|
|
777
|
+
"type": {
|
|
778
|
+
"type": "String"
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
"id": 2,
|
|
783
|
+
"name": "value",
|
|
784
|
+
"type": {
|
|
785
|
+
"type": "Number"
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
"id": 8,
|
|
794
|
+
"name": "bloodlines",
|
|
795
|
+
"type": {
|
|
796
|
+
"type": "Array",
|
|
797
|
+
"elementType": {
|
|
798
|
+
"type": "Interface",
|
|
799
|
+
"properties": [
|
|
800
|
+
{
|
|
801
|
+
"id": 0,
|
|
802
|
+
"name": "name",
|
|
803
|
+
"type": {
|
|
804
|
+
"type": "String"
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
"id": 1,
|
|
809
|
+
"name": "quality",
|
|
810
|
+
"type": {
|
|
811
|
+
"type": "String"
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
]
|
|
819
|
+
},
|
|
585
820
|
"messageFace/SkillCardData": {
|
|
586
821
|
"type": "Interface",
|
|
587
822
|
"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,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
|
+
/** Reuse the skill frame, extending only its illustration and description sections. */
|
|
6
|
+
export function appearanceCardHtml(card: ItemCardData, artwork: string, frame: string): string {
|
|
7
|
+
const slice = (y: number, height: number) => `<svg class="frame" viewBox="0 ${y} 760 ${height}" preserveAspectRatio="none" aria-hidden="true"><image href="${escape(frame)}" width="760" height="1107" preserveAspectRatio="none"/></svg>`;
|
|
8
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><style>
|
|
9
|
+
*{box-sizing:border-box}html,body{margin:0;width:760px;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#f5edd6;background:#141c26}
|
|
10
|
+
#capture{width:760px}.section{position:relative}.frame{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.content{position:relative}
|
|
11
|
+
.heading{height:205px}.title{position:absolute;left:58px;top:46px;width:644px;height:74px;margin:0;display:flex;align-items:center;font-size:30px;font-weight:800;line-height:1.15;text-shadow:0 2px 3px #152a24;overflow-wrap:anywhere;overflow:hidden}
|
|
12
|
+
.meta{position:absolute;left:92px;right:91px;top:151px;display:flex;justify-content:space-between;font-size:21px}
|
|
13
|
+
.portrait{height:957px}.art{position:relative;margin-left:93px;width:574px;height:957px;border:3px solid #bea874;background:#e9e5d9;overflow:hidden}.item-artwork{display:block;width:100%;height:100%;object-fit:contain}
|
|
14
|
+
.footer{min-height:328px;padding:58px 62px 73px;color:#202c24}.description{margin:0;font-size:23px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}.details{display:flex;justify-content:space-between;gap:24px;border-top:1px solid #52665366;margin-top:16px;padding-top:12px;font-size:18px;line-height:1.5;overflow-wrap:anywhere}.serial{position:absolute;top:9px;right:94px;font-size:14px;color:#23382e;letter-spacing:2px}
|
|
15
|
+
</style></head><body><article id="capture"><header class="section heading">${slice(0, 205)}<h1 class="title">${escape(card.name)}</h1><div class="meta"><span>【形象卡】</span><span>使用后更换外观</span></div></header><div class="section portrait">${slice(205, 574)}<div class="art"><img class="item-artwork" src="${escape(artwork)}" alt="角色形象"></div></div><footer class="section footer">${slice(779, 328)}<div class="serial">DEW · APPEARANCE</div><div class="content"><p class="description">${escape(card.description)}</p><div class="details"><span>数量:${escape(card.count)}</span><span>参考价格:${escape(card.referencePrice)}</span></div></div></footer></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')
|
|
@@ -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,8 +8,8 @@ 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')
|
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 frame(false)) : 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 })
|
|
@@ -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;
|