@sjtdev/koishi-plugin-dota2tracker 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +32 -0
- package/dist/index.js +1 -1
- package/lib/index.js +128 -94
- package/{queries → lib/queries}/MatchInfo.graphql +13 -0
- package/lib/templates/common/components/building_icons.ejs +20 -0
- package/lib/templates/common/styles/normalize.min.css +1 -0
- package/lib/templates/hero/hero_1.ejs +69 -0
- package/lib/templates/images/7.38_simple_minimap.png +0 -0
- package/lib/templates/item/item/recipe.ejs +9 -0
- package/lib/templates/item/item/style.css +1 -0
- package/lib/templates/item/item.ejs +52 -0
- package/lib/templates/item/itemlist.ejs +11 -0
- package/lib/templates/match/match_1/base.css +1 -0
- package/lib/templates/match/match_1/item.ejs +1 -0
- package/lib/templates/match/match_1/main.ejs +8 -0
- package/lib/templates/match/match_1/player.ejs +1 -0
- package/lib/templates/match/match_1/style.css +1 -0
- package/lib/templates/match/match_1.ejs +18 -0
- package/lib/templates/match/match_2/original.css +1 -0
- package/lib/templates/match/match_2/original.ejs +10 -0
- package/lib/templates/match/match_2+/charts.ejs +1 -0
- package/lib/templates/match/match_2+/extra.css +1 -0
- package/lib/templates/match/match_2+/lane_outcome.ejs +56 -0
- package/lib/templates/match/match_2+/map.ejs +160 -0
- package/lib/templates/match/match_2+.ejs +1 -0
- package/lib/templates/match/match_2.ejs +1 -0
- package/lib/templates/player/player_1/base.css +1 -0
- package/lib/templates/player/player_1/private.ejs +1 -0
- package/lib/templates/player/player_1.ejs +78 -0
- package/lib/templates/rank/rank_fun.ejs +1 -0
- package/lib/templates/report/daily/base.css +1 -0
- package/lib/templates/report/daily.ejs +29 -0
- package/package.json +2 -2
- package/template/hero/hero_1.ejs +0 -900
- package/template/item/item/recipe.ejs +0 -51
- package/template/item/item/style.css +0 -244
- package/template/item/item.ejs +0 -140
- package/template/item/itemlist.ejs +0 -99
- package/template/match/match_1/item.ejs +0 -11
- package/template/match/match_1/main.ejs +0 -37
- package/template/match/match_1/player.ejs +0 -154
- package/template/match/match_1/style.css +0 -764
- package/template/match/match_1.ejs +0 -56
- package/template/match/match_2/original.css +0 -463
- package/template/match/match_2/original.ejs +0 -192
- package/template/match/match_2+/charts.ejs +0 -261
- package/template/match/match_2+/extra.css +0 -143
- package/template/match/match_2+/lane_outcome.ejs +0 -157
- package/template/match/match_2+.ejs +0 -27
- package/template/match/match_2.ejs +0 -18
- package/template/player/player_1/private.ejs +0 -5
- package/template/player/player_1.ejs +0 -654
- package/template/rank/rank_fun.ejs +0 -131
- package/template/report/daily.ejs +0 -191
- /package/{queries → lib/queries}/Constants.graphql +0 -0
- /package/{queries → lib/queries}/GetWeeklyMetaByPosition.graphql +0 -0
- /package/{queries → lib/queries}/PlayerExtraInfo.graphql +0 -0
- /package/{queries → lib/queries}/PlayerInfoWith25Matches.graphql +0 -0
- /package/{queries → lib/queries}/PlayerPerformanceForHeroRecommendation.graphql +0 -0
- /package/{queries → lib/queries}/PlayersInfoWith10MatchesForGuild.graphql +0 -0
- /package/{queries → lib/queries}/PlayersLastmatchRankinfo.graphql +0 -0
- /package/{queries → lib/queries}/PlayersMatchesForDaily.graphql +0 -0
- /package/{queries → lib/queries}/RequestMatchDataAnalysis.graphql +0 -0
- /package/{queries → lib/queries}/VerifyingPlayer.graphql +0 -0
- /package/{template → lib/templates}/images/bei.jpg +0 -0
- /package/{template → lib/templates}/images/disconnected.png +0 -0
- /package/{template → lib/templates}/images/flag_dire.png +0 -0
- /package/{template → lib/templates}/images/flag_radiant.png +0 -0
- /package/{template → lib/templates}/images/hero_badge_1.png +0 -0
- /package/{template → lib/templates}/images/hero_badge_2.png +0 -0
- /package/{template → lib/templates}/images/hero_badge_3.png +0 -0
- /package/{template → lib/templates}/images/hero_badge_4.png +0 -0
- /package/{template → lib/templates}/images/hero_badge_5.png +0 -0
- /package/{template → lib/templates}/images/hero_badge_6.png +0 -0
- /package/{template → lib/templates}/images/lane_fail.svg +0 -0
- /package/{template → lib/templates}/images/lane_jungle.svg +0 -0
- /package/{template → lib/templates}/images/lane_stomp.svg +0 -0
- /package/{template → lib/templates}/images/lane_stomped.svg +0 -0
- /package/{template → lib/templates}/images/lane_tie.svg +0 -0
- /package/{template → lib/templates}/images/lane_victory.svg +0 -0
- /package/{template → lib/templates}/images/logo_dire.png +0 -0
- /package/{template → lib/templates}/images/logo_radiant.png +0 -0
- /package/{template → lib/templates}/images/medal_0.png +0 -0
- /package/{template → lib/templates}/images/medal_1.png +0 -0
- /package/{template → lib/templates}/images/medal_2.png +0 -0
- /package/{template → lib/templates}/images/medal_3.png +0 -0
- /package/{template → lib/templates}/images/medal_4.png +0 -0
- /package/{template → lib/templates}/images/medal_5.png +0 -0
- /package/{template → lib/templates}/images/medal_6.png +0 -0
- /package/{template → lib/templates}/images/medal_7.png +0 -0
- /package/{template → lib/templates}/images/medal_8.png +0 -0
- /package/{template → lib/templates}/images/medal_8b.png +0 -0
- /package/{template → lib/templates}/images/medal_8c.png +0 -0
- /package/{template → lib/templates}/images/scepter.png +0 -0
- /package/{template → lib/templates}/images/scepter_0.png +0 -0
- /package/{template → lib/templates}/images/scepter_1.png +0 -0
- /package/{template → lib/templates}/images/shard.png +0 -0
- /package/{template → lib/templates}/images/shard_0.png +0 -0
- /package/{template → lib/templates}/images/shard_1.png +0 -0
- /package/{template → lib/templates}/images/star_0.png +0 -0
- /package/{template → lib/templates}/images/star_1.png +0 -0
- /package/{template → lib/templates}/images/star_2.png +0 -0
- /package/{template → lib/templates}/images/star_3.png +0 -0
- /package/{template → lib/templates}/images/star_4.png +0 -0
- /package/{template → lib/templates}/images/star_5.png +0 -0
- /package/{template → lib/templates}/images/xi.jpg +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<% const match = data; %> <%
|
|
2
|
+
const BUILDINGS = [
|
|
3
|
+
// ================= 天辉 Radiant (左下) =================
|
|
4
|
+
// 上路
|
|
5
|
+
{ id: 16, name: 'Rad Top T1', type: 'tower', lane: "top", bit: 0, team: 'radiant', x: 25, y: 93 },
|
|
6
|
+
{ id: 19, name: 'Rad Top T2', type: 'tower', lane: "top", bit: 1, team: 'radiant', x: 23, y: 135 },
|
|
7
|
+
{ id: 22, name: 'Rad Top T3', type: 'tower', lane: "top", bit: 2, team: 'radiant', x: 21, y: 174 },
|
|
8
|
+
{ id: 38, name: 'Rad Top Melee', type: 'rax', lane: "top", bit: 0, team: 'radiant', x: 18, y: 181 },
|
|
9
|
+
{ id: 41, name: 'Rad Top Range', type: 'rax', lane: "top", bit: 1, team: 'radiant', x: 26, y: 181 },
|
|
10
|
+
{ id: 38, name: 'Rad Top HG Time', type: 'rax_time', lane: "top", bit: 0, team: 'radiant', x: 27, y: 184 },
|
|
11
|
+
|
|
12
|
+
// 中路
|
|
13
|
+
{ id: 17, name: 'Rad Mid T1', type: 'tower', lane: "mid", bit: 3, team: 'radiant', x: 98, y: 143 },
|
|
14
|
+
{ id: 20, name: 'Rad Mid T2', type: 'tower', lane: "mid", bit: 4, team: 'radiant', x: 73, y: 166 },
|
|
15
|
+
{ id: 23, name: 'Rad Mid T3', type: 'tower', lane: "mid", bit: 5, team: 'radiant', x: 51, y: 185 },
|
|
16
|
+
{ id: 39, name: 'Rad Mid Melee', type: 'rax', lane: "mid", bit: 2, team: 'radiant', x: 46, y: 187 },
|
|
17
|
+
{ id: 42, name: 'Rad Mid Range', type: 'rax', lane: "mid", bit: 3, team: 'radiant', x: 52, y: 193 },
|
|
18
|
+
{ id: 39, name: 'Rad Mid HG Time', type: 'rax_time', lane: "mid", bit: 2, team: 'radiant', x: 54, y: 194 },
|
|
19
|
+
|
|
20
|
+
// 下路
|
|
21
|
+
{ id: 18, name: 'Rad Bot T1', type: 'tower', lane: "bot", bit: 6, team: 'radiant', x: 197, y: 217 },
|
|
22
|
+
{ id: 21, name: 'Rad Bot T2', type: 'tower', lane: "bot", bit: 7, team: 'radiant', x: 116, y: 218 },
|
|
23
|
+
{ id: 24, name: 'Rad Bot T3', type: 'tower', lane: "bot", bit: 8, team: 'radiant', x: 61, y: 215 },
|
|
24
|
+
{ id: 40, name: 'Rad Bot Melee', type: 'rax', lane: "bot", bit: 4, team: 'radiant', x: 58, y: 220 },
|
|
25
|
+
{ id: 43, name: 'Rad Bot Range', type: 'rax', lane: "bot", bit: 5, team: 'radiant', x: 58, y: 212 },
|
|
26
|
+
{ id: 40, name: 'Rad Bot HG Time', type: 'rax_time', lane: "bot", bit: 4, team: 'radiant', x: 61, y: 220 },
|
|
27
|
+
|
|
28
|
+
// 门牙 & 基地
|
|
29
|
+
{ id: 25, name: 'Rad T4 Left', type: 'tower', lane: "mid", bit: 9, team: 'radiant', x: 34, y: 197 },
|
|
30
|
+
{ id: 25, name: 'Rad T4 Right', type: 'tower', lane: "mid", bit: 10, team: 'radiant', x: 39, y: 201 },
|
|
31
|
+
{ id: 25, name: 'Rad T4 Time', type: 't4_time', lane: "mid", bit: [9,10], team: 'radiant', x: 43, y: 204 },
|
|
32
|
+
{ id: 50, name: 'Rad Ancient', type: 'fort', lane: "", bit: -1, team: 'radiant', x: 29, y: 202 },
|
|
33
|
+
{ id: 50, name: 'Rad Ancient Time', type: 'fort_time', lane: "", bit: -1, team: 'radiant', x: 35, y: 213 },
|
|
34
|
+
|
|
35
|
+
// ================= 夜魔 Dire (右上) =================
|
|
36
|
+
// 上路
|
|
37
|
+
{ id: 26, name: 'Dire Top T1', type: 'tower', lane: "top", bit: 0, team: 'dire', x: 41, y: 32 },
|
|
38
|
+
{ id: 29, name: 'Dire Top T2', type: 'tower', lane: "top", bit: 1, team: 'dire', x: 120, y: 31 },
|
|
39
|
+
{ id: 32, name: 'Dire Top T3', type: 'tower', lane: "top", bit: 2, team: 'dire', x: 176, y: 34 },
|
|
40
|
+
{ id: 44, name: 'Dire Top Melee', type: 'rax', lane: "top", bit: 0, team: 'dire', x: 182, y: 40 },
|
|
41
|
+
{ id: 47, name: 'Dire Top Range', type: 'rax', lane: "top", bit: 1, team: 'dire', x: 182, y: 32 },
|
|
42
|
+
{ id: 44, name: 'Dire Top HG Time', type: 'rax_time', lane: "top", bit: 0, team: 'dire', x: 185, y: 40 },
|
|
43
|
+
|
|
44
|
+
// 中路
|
|
45
|
+
{ id: 27, name: 'Dire Mid T1', type: 'tower', lane: "mid", bit: 3, team: 'dire', x: 130, y: 112 },
|
|
46
|
+
{ id: 30, name: 'Dire Mid T2', type: 'tower', lane: "mid", bit: 4, team: 'dire', x: 160, y: 90 },
|
|
47
|
+
{ id: 33, name: 'Dire Mid T3', type: 'tower', lane: "mid", bit: 5, team: 'dire', x: 187, y: 65 },
|
|
48
|
+
{ id: 45, name: 'Dire Mid Melee', type: 'rax', lane: "mid", bit: 2, team: 'dire', x: 189, y: 60 },
|
|
49
|
+
{ id: 48, name: 'Dire Mid Range', type: 'rax', lane: "mid", bit: 3, team: 'dire', x: 195, y: 64 },
|
|
50
|
+
{ id: 45, name: 'Dire Mid HG Time', type: 'rax_time', lane: "mid", bit: 2, team: 'dire', x: 196, y: 68 },
|
|
51
|
+
|
|
52
|
+
// 下路
|
|
53
|
+
{ id: 28, name: 'Dire Bot T1', type: 'tower', lane: "bot", bit: 6, team: 'dire', x: 218, y: 157 },
|
|
54
|
+
{ id: 31, name: 'Dire Bot T2', type: 'tower', lane: "bot", bit: 7, team: 'dire', x: 220, y: 117 },
|
|
55
|
+
{ id: 34, name: 'Dire Bot T3', type: 'tower', lane: "bot", bit: 8, team: 'dire', x: 219, y: 76 },
|
|
56
|
+
{ id: 46, name: 'Dire Bot Melee', type: 'rax', lane: "bot", bit: 4, team: 'dire', x: 224, y: 72 },
|
|
57
|
+
{ id: 49, name: 'Dire Bot Range', type: 'rax', lane: "bot", bit: 5, team: 'dire', x: 216, y: 72 },
|
|
58
|
+
{ id: 46, name: 'Dire Bot HG Time', type: 'rax_time', lane: "bot", bit: 4, team: 'dire', x: 225, y: 78 },
|
|
59
|
+
|
|
60
|
+
// 门牙 & 基地
|
|
61
|
+
{ id: 35, name: 'Dire T4 Left', type: 'tower', lane: "mid", bit: 9, team: 'dire', x: 197, y: 49 },
|
|
62
|
+
{ id: 35, name: 'Dire T4 Right', type: 'tower', lane: "mid", bit: 10, team: 'dire', x: 203, y: 54 },
|
|
63
|
+
{ id: 35, name: 'Dire T4 Time', type: 't4_time', lane: "mid", bit: 10, team: 'dire', x: 207, y: 57 },
|
|
64
|
+
{ id: 51, name: 'Dire Ancient', type: 'fort', lane: "", bit: -1, team: 'dire', x: 204, y: 44 },
|
|
65
|
+
{ id: 51, name: 'Dire Ancient Time', type: 'fort_time', lane: "", bit: -1, team: 'dire', x: 215, y: 48 },
|
|
66
|
+
];
|
|
67
|
+
%> <%
|
|
68
|
+
// 1. 算法:提取摧毁时间 (Last Event Algorithm)
|
|
69
|
+
const destructionTimes = {};
|
|
70
|
+
if (match.playbackData && match.playbackData.buildingEvents) {
|
|
71
|
+
match.playbackData.buildingEvents.forEach(evt => {
|
|
72
|
+
if (evt.time > 0) {
|
|
73
|
+
destructionTimes[evt.npcId] = evt.time;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. 存活判定 (逻辑保持不变,确保正确引用 mask)
|
|
79
|
+
const checkAlive = (b) => {
|
|
80
|
+
let mask = 0;
|
|
81
|
+
if (b.team === 'radiant') {
|
|
82
|
+
mask = b.type.includes('tower') ? match.towerStatusRadiant : match.barracksStatusRadiant;
|
|
83
|
+
} else {
|
|
84
|
+
mask = b.type.includes('tower') ? match.towerStatusDire : match.barracksStatusDire;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// T4 时间锚点 (只要有一个活着,整体就算活)
|
|
88
|
+
if (b.type === 't4_time') {
|
|
89
|
+
const t1Alive = (mask & (1 << b.bit[0])) !== 0;
|
|
90
|
+
const t2Alive = (mask & (1 << b.bit[1])) !== 0;
|
|
91
|
+
return t1Alive || t2Alive;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 基地 (胜负即生死)
|
|
95
|
+
// 逻辑:如果是天辉建筑,天辉赢了=活;如果是夜魇建筑,天辉赢了=死(即 !match.didRadiantWin)
|
|
96
|
+
if (b.type === 'fort_time' || b.type === 'fort') {
|
|
97
|
+
return b.team === 'radiant' ? match.didRadiantWin : !match.didRadiantWin;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 常规单位
|
|
101
|
+
return (mask & (1 << b.bit)) !== 0;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// 3. 辅助:时间格式化
|
|
105
|
+
const fmtTime = (s) => {
|
|
106
|
+
const absS = Math.abs(s ?? 0);
|
|
107
|
+
const m = Math.floor(absS / 60);
|
|
108
|
+
const ss = (absS % 60).toString().padStart(2, '0');
|
|
109
|
+
// 如果原数据确实是负数(极少数情况),手动加上符号
|
|
110
|
+
return (s < 0 ? "-" : "") + `${m}:${ss}`;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 4. 定义图标尺寸与权重
|
|
114
|
+
const buildingProps = {
|
|
115
|
+
rax: { size: 10, z: 1 },
|
|
116
|
+
tower: { size: 12, z: 2 },
|
|
117
|
+
fort: { size: 16, z: 3 },
|
|
118
|
+
// 时间层级最高,确保覆盖在死掉的图标上
|
|
119
|
+
rax_time: { size: 0, z: 10 },
|
|
120
|
+
t4_time: { size: 0, z: 10 },
|
|
121
|
+
fort_time: { size: 0, z: 10 },
|
|
122
|
+
tower_time: { size: 0, z: 10 },
|
|
123
|
+
_default: { size: 12, z: 0 }
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const getProps = (type) => buildingProps[type] || buildingProps._default;
|
|
127
|
+
%> <% const images = { minimapBase: "7.38_simple_minimap" } %> <section class="map"> <%- include("../../common/components/building_icons") %> <svg id="map" viewBox="0 0 255 255" width="250" height="250"><rect x="0" y="0" width="255" height="255" fill="#f5f5f5" rx="12" ry="12"/><image href="<%= getImageUrl(images.minimapBase) %>" width="255" height="255"/> <%
|
|
128
|
+
[...BUILDINGS].sort((a, b) => {
|
|
129
|
+
return getProps(a.type).z - getProps(b.type).z;
|
|
130
|
+
}).forEach(b => {
|
|
131
|
+
const isAlive = checkAlive(b);
|
|
132
|
+
const deathTime = destructionTimes[b.id];
|
|
133
|
+
|
|
134
|
+
// 基础参数
|
|
135
|
+
const props = getProps(b.type);
|
|
136
|
+
const size = props.size;
|
|
137
|
+
|
|
138
|
+
// 中心校准 (用于文字渲染)
|
|
139
|
+
const centerX = b.x + (size / 2);
|
|
140
|
+
const centerY = b.y + (size / 2);
|
|
141
|
+
|
|
142
|
+
const rotation = b.lane === "mid" ? "_angle" : "";
|
|
143
|
+
const iconRef = "#" + b.type + rotation;
|
|
144
|
+
const statusClass = isAlive ? b.team : "dead";
|
|
145
|
+
|
|
146
|
+
// ================= 极简白名单逻辑 =================
|
|
147
|
+
|
|
148
|
+
// 1. 自渲染时间的塔:只有 T1 和 T2
|
|
149
|
+
// 逻辑:类型是 tower, 且名字里不含 T3, 不含 T4
|
|
150
|
+
const isOuterTower = b.type === 'tower' &&
|
|
151
|
+
!b.name.includes('T3') &&
|
|
152
|
+
!b.name.includes('T4');
|
|
153
|
+
|
|
154
|
+
// 2. 虚拟时间锚点:数据表里留下的所有 _time 对象
|
|
155
|
+
const isTimeAnchor = b.type.endsWith('_time');
|
|
156
|
+
|
|
157
|
+
// 3. 最终开关
|
|
158
|
+
const shouldRenderTime = (isOuterTower || isTimeAnchor) && !isAlive && deathTime;
|
|
159
|
+
|
|
160
|
+
%> <% /* 1. 图标渲染 (排除 _time 类型的纯虚拟点) */ %> <% if (!b.type.endsWith('_time')) { %> <use href="<%= iconRef %>" class="<%= statusClass %>" x="<%= b.x %>" y="<%= b.y %>" width="<%= size %>" height="<%= size %>"/> <% } %> <% /* 2. 时间文字渲染 (无旋转,自然居中) */ %> <% if (shouldRenderTime) { %> <text x="<%= centerX %>" y="<%= centerY %>" fill="#ccc" font-weight="bold" text-anchor="middle" dominant-baseline="middle"> <%= fmtTime(deathTime) %> </text> <% } %> <% }) %> </svg></section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="<%= languageTag %>"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Match</title> <%- "<style>" %> <%- include("../common/styles/normalize.min.css") %> <%- include("./match_2/original.css") %> <%- include("./match_2+/extra.css") %> <% if (fontFamily) { %> <%- `body { font-family: ${fontFamily}; }` %> <% } %> <%- "</style>" %> </head><body><section id="regular"><%- include('./match_2/original') %></section><section id="extra"> <% if (data.parsedDateTime && data.radiantNetworthLeads && data.radiantExperienceLeads) { %> <%- include('./match_2+/charts') %> <div class="container"> <%- include('./match_2+/map') %> <%- include('./match_2+/lane_outcome') %> </div> <% } else { %> <div class="tip"><%= $t("dota2tracker.template.empty_extra_info") %></div> <% } %> </section></body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="<%= languageTag %>"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Match</title> <%- "<style>" %> <%- include("../common/styles/normalize.min.css") %> <%- include("./match_2/original.css") %> <% if (fontFamily) { %> <%- `body { font-family: ${fontFamily}; }` %> <% } %> <%- "</style>" %> </head><body> <%- include('./match_2/original') %> </body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
html,body{background-color:#000;color:#fff;width:800px}.wrapper>*{margin:5px;box-shadow:0 0 5px #fff;width:790px;border-radius:5px;overflow:hidden;position:relative}img{width:auto;vertical-align:middle}p{margin:0}.player{display:flex;position:relative;height:128px}.player .avatar{width:128px;height:128px;position:absolute}.player .avatar img{width:100%;border-radius:5px}.player .name{font-size:24px}.player .name .guild:before{content:"[";color:#fff}.player .name .guild:after{content:"]";color:#fff}.player .name .hero_name:before{content:">";color:#fff}.player .name .hero_name:after{content:"<";color:#fff}.player .info{width:100%;display:flex;flex-direction:column;align-items:center;justify-content:space-around}.player .info .guild.Copper{color:#b4775f}.player .info .guild.Silver{color:#9a9593}.player .info .guild.Gold{color:#bda97f}.player .info .guild.Diamond{color:#a5cbcf}.player .info .matches span.win{color:#007a00}.player .info .matches span.lose{color:#b30000}.player .info .matches span.victory{color:#90ee90}.player .info .matches span.stomp{color:green}.player .info .matches span.fail{color:#ff6961}.player .info .matches span.stomped{color:red}.player .info .matches.blur{filter:blur(10px)}.player .rank{width:64px;height:64px;flex-grow:1;position:absolute;top:0;right:0}.player .rank.estimated{filter:grayscale(100%)}.player .rank .medal{z-index:1}.player .rank .star{z-index:2}.player .rank p{z-index:4}.player .rank div{height:100%;width:100%;top:0;right:0;position:absolute}.player .rank img{top:0;right:0;position:absolute;height:64px}.player .rank p{font-size:12px;line-height:1;position:absolute;text-align:center;width:64px;bottom:4px;right:0;box-sizing:border-box;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}.hero_winrate .heroes{display:grid;grid-template-columns:32px auto auto auto auto auto;justify-content:start;align-items:center}.hero_winrate .heroes .hero{width:100%;display:flex;align-items:center}.hero_winrate .heroes img{width:100%;border-radius:6px;overflow:hidden}.hero_winrate .heroes span.imp{margin:0 4px}.hero_winrate .heroes span{text-align:center}.hero_winrate .heroes .tip.row{grid-column:1/-1}.hero_winrate .heroes .win{text-align:right;padding-right:8px;background-color:#006400;border-radius:16px 0 0 16px;height:16px;justify-self:end}.hero_winrate .heroes .lose{text-align:left;padding-left:8px;background-color:#8b0000;border-radius:0 16px 16px 0;height:16px;justify-self:start}table.matches{table-layout:fixed;width:100%}.matches .match.win{background-color:#006400}.matches .match.lose{background-color:#8b0000}.matches .match td{text-align:center;height:40px;overflow:hidden}.matches .match .player_lane{width:100%;height:100%;justify-content:center;align-items:center;display:flex}.matches .match .player_lane svg{width:36px;height:36px}.player_lane.victory svg path{fill:#90ee90}.player_lane.stomp svg path{fill:green}.player_lane.stomped svg path{fill:red}.plus{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.plus .hero{width:190px;height:190px;border-radius:5px;position:relative;display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:118.75px auto;justify-items:center;align-items:center}.plus .hero img{width:100%;grid-column:1/-1}.plus .level{position:absolute;width:50px;height:50px;left:calc(50% - 25px);top:93.75px}.plus .level span{position:absolute;width:100%;text-align:center;left:0;bottom:18px;font-size:14px;text-shadow:-1px -1px 1px #000,1px -1px 1px #000,-1px 1px 1px #000,1px 1px 1px #000}.blur-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:#00000050;backdrop-filter:blur(10px);display:flex;flex-direction:column;justify-content:center;align-items:center}.lock-icon{font-size:24px;margin-bottom:10px}.lock-text{font-size:18px;font-weight:700}.lock-sub{line-height:3;font-size:14px;color:#aaa}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="blur-overlay"><span class="lock-icon">🔒</span> <span class="lock-text"><%= $t("dota2tracker.template.anonymous_player_1") %></span><span class="lock-sub"><%= $t("dota2tracker.template.anonymous_player_2",{player:player.steamAccount.name}) %></span></div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<% const p = data; %> <% const isAnon = p.steamAccount.isAnonymous; %> <% const getWinRateColor = (val) => {
|
|
2
|
+
val = Math.max(0, Math.min(100, val * 100));
|
|
3
|
+
let r, g, b;
|
|
4
|
+
if (val <= 50) {
|
|
5
|
+
const s = Math.round(255 * (val / 50));
|
|
6
|
+
r = 255; g = s; b = s;
|
|
7
|
+
} else {
|
|
8
|
+
const s = Math.round(255 * ((val - 50) / 50));
|
|
9
|
+
r = 255 - s; g = 255; b = 255 - s;
|
|
10
|
+
}
|
|
11
|
+
const hex = c => c.toString(16).padStart(2, "0").toUpperCase();
|
|
12
|
+
return `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
13
|
+
};
|
|
14
|
+
%> <% const getGuildClass = pct => pct <= 25 ? "Copper" : pct <= 50 ? "Silver" : pct <= 75 ? "Gold" : "Diamond"; %> <% const outcomes = { victory: 0, stomp: 0, fail: 0, stomped: 0, tie: 0 }; %> <% const laneMap = { RADIANT_VICTORY:['victory','fail'], RADIANT_STOMP:['stomp','stomped'], DIRE_VICTORY:['fail','victory'], DIRE_STOMP:['stomped','stomp'] }; %> <% let nearWin = 0, streak = 0; %> <% const nearCount = 25; %> <% const matchesData = p.matches.map(m => {
|
|
15
|
+
const inner = m.players.find(ip => p.steamAccount.id == ip.steamAccount.id);
|
|
16
|
+
const isRad = inner.isRadiant;
|
|
17
|
+
const didWin = m.didRadiantWin === isRad;
|
|
18
|
+
|
|
19
|
+
// 连胜计算
|
|
20
|
+
if (!p.streak) {
|
|
21
|
+
if (streak !== 0) {
|
|
22
|
+
if (didWin && streak > 0) streak++;
|
|
23
|
+
else if (!didWin && streak < 0) streak--;
|
|
24
|
+
else p.streak = streak;
|
|
25
|
+
} else streak = didWin ? 1 : -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 近期胜率
|
|
29
|
+
nearWin += didWin ? 1 : 0;
|
|
30
|
+
|
|
31
|
+
// 分路结果处理
|
|
32
|
+
const resRaw = laneMap[m.topLaneOutcome] || ['tie','tie'];
|
|
33
|
+
// 这里简化分路判定逻辑,只关注 outcome 计数,具体每局的 laneResult 下面处理
|
|
34
|
+
let laneRes = "tie";
|
|
35
|
+
if (inner.lane === "JUNGLE") laneRes = "jungle";
|
|
36
|
+
else {
|
|
37
|
+
const laneKey = inner.lane === "MID_LANE" ? "mid" : (inner.lane === "SAFE_LANE" ? (isRad?"bottom":"top") : (isRad?"top":"bottom"));
|
|
38
|
+
// 注意:原逻辑 processLaneOutcome 对三路都做了处理,这里为了性能只取玩家所在路
|
|
39
|
+
const outcomeKey = laneKey === 'mid' ? m.midLaneOutcome : (laneKey === 'top' ? m.topLaneOutcome : m.bottomLaneOutcome);
|
|
40
|
+
const pair = laneMap[outcomeKey] || ['tie', 'tie'];
|
|
41
|
+
laneRes = pair[isRad ? 0 : 1];
|
|
42
|
+
}
|
|
43
|
+
if (outcomes[laneRes] !== undefined) outcomes[laneRes]++;
|
|
44
|
+
|
|
45
|
+
// 击杀参与率 KDA
|
|
46
|
+
const teamKills = m.parsedDateTime
|
|
47
|
+
? (m[(isRad ? "radiant" : "dire") + "Kills"]?.reduce((a, b) => a + b, 0) ?? 0)
|
|
48
|
+
: m.players.filter(pl => pl.isRadiant === isRad).reduce((a, pl) => a + pl.kills, 0);
|
|
49
|
+
const kp = ((inner.kills + inner.assists) / (teamKills || 1) * 100).toFixed(0);
|
|
50
|
+
const kda = ((inner.kills + inner.assists) / Math.max(1, inner.deaths)).toFixed(2);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...m,
|
|
54
|
+
inner,
|
|
55
|
+
laneRes,
|
|
56
|
+
didWin,
|
|
57
|
+
kda,
|
|
58
|
+
kp,
|
|
59
|
+
timeStr: DateTime.fromSeconds(m.startDateTime ?? 0).toFormat("yyyy-MM-dd HH:mm:ss").slice(2)
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
%> <% const getScale = (list, isNear) => {
|
|
63
|
+
if (!list || list.length === 0) return 0;
|
|
64
|
+
const maxWin = Math.max(...list.map(i => i.winCount));
|
|
65
|
+
const maxLose = Math.max(...list.map(i => i.matchCount - i.winCount));
|
|
66
|
+
// 800 - (5 * 40) = 600px available
|
|
67
|
+
const totalW = 600;
|
|
68
|
+
const baseScale = totalW / (maxWin + maxLose || 1);
|
|
69
|
+
// 近期数据需要调整因子
|
|
70
|
+
return isNear ? baseScale * Math.min(maxWin/(maxWin+maxLose), maxLose/(maxWin+maxLose)) : baseScale;
|
|
71
|
+
};
|
|
72
|
+
%> <% const heroList = p.genHero ? p.heroesPerformanceTop10 : (p.heroesPerformance?.filter(h => h.matchCount > 1) || []); %> <% const heroScale = getScale(heroList, !p.genHero); %> <% const posIcons = ["damage","nuke","armor","speed","healing"]; %> <% p.positionPerformance = []; %> <% for (let i = 0; i < 5; i++) {
|
|
73
|
+
const posMatches = p.matches.filter(m => m.players.find(ip => ip.steamAccount.id == p.steamAccount.id).position == ("POSITION_" + (i + 1)));
|
|
74
|
+
const wins = posMatches.filter(m => m.didRadiantWin == m.players.find(ip => ip.steamAccount.id == p.steamAccount.id).isRadiant).length;
|
|
75
|
+
const impAvg = posMatches.length > 0 ? Math.round(posMatches.reduce((acc, m) => acc + m.players.find(ip => ip.steamAccount.id == p.steamAccount.id).imp, 0) / posMatches.length) : "-";
|
|
76
|
+
p.positionPerformance.push({ pos: i + 1, icon: posIcons[i], matchCount: posMatches.length, winCount: wins, imp: impAvg });
|
|
77
|
+
}
|
|
78
|
+
%> <% const posScale = getScale(p.positionPerformance.filter(pos => pos.matchCount > 1), true); %> <!DOCTYPE html><html lang="<%= languageTag %>"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Player</title> <%- `<style>` %> <%- include("../common/styles/normalize.min.css") %> <%- include(`./player_1/base.css`) %> <% if (fontFamily) { %><%- `body { font-family: ${fontFamily}; }` %><% } %> <%- `</style>` %> </head><body><div class="wrapper"><div class="player"><div class="avatar"><img src="<%= p.steamAccount?.avatar %>" alt=""/></div><div class="info"><p class="name"> <%= p.steamAccount.name %> <% if (p.guildMember) { %> <span class="guild <%= getGuildClass(p.guildMember.guild.currentPercentile) %>"><%= p.guildMember.guild.tag %></span> <% } %> <% if (p.genHero) { %> <span class="hero_name"><%= p.genHero.name %></span> <% } %> </p><p class="matches"> <%= $t("dota2tracker.template.match_count_") %><%= p.matchCount %> (<span class="win"><%= p.winCount %></span>/<span class="lose"><%= p.matchCount - p.winCount %></span>) <%= $t("dota2tracker.template.winrate_") %> <span <%- `style="color:${getWinRateColor(p.winCount / p.matchCount)};"` %>><%= ((p.winCount / p.matchCount) * 100).toFixed(2) %>%</span></p><p class="matches<%= isAnon ? " blur" : "" %>"><span><%= $t("dota2tracker.template.last25matches_") %> <span class="win"><%= nearWin %></span>/<span class="lose"><%= nearCount - nearWin %></span></span> <span><%= $t("dota2tracker.template.winrate_") %> <span <%- `style="color:${getWinRateColor(nearWin / nearCount)};"` %>><%= ((nearWin / nearCount) * 100).toFixed(2) %>%</span> </span> <span><%= $t("dota2tracker.template.imp_") %><%= isAnon ? "?" : ((p.performance?.imp > 0 ? "+" : "") + p.performance?.imp) %></span></p><p class="matches<%= isAnon ? " blur" : "" %>"><span><%= $t("dota2tracker.template.lane") %> <span class="victory"><%= outcomes.victory + outcomes.stomp %>(<span class="stomp"><%= outcomes.stomp %></span>)</span>-<span class="tie"><%= outcomes.tie %></span>-<span class="fail"><%= outcomes.fail + outcomes.stomped %>(<span class="stomped"><%= outcomes.stomped %></span>)</span> </span> <% const laneTotal = outcomes.victory + outcomes.stomp + outcomes.tie + outcomes.fail + outcomes.stomped; %> <% const laneScore = (outcomes.victory + outcomes.stomp + outcomes.tie / 2) / laneTotal; %> <span><%= $t("dota2tracker.template.lane_advantage_rate_") %> <span <%- `style="color:${getWinRateColor(laneScore)};"` %>><%= isAnon ? "?" : (laneScore * 100).toFixed(2) %>%</span></span></p></div><div class="rank<%= p.isEstimatedRank ? " estimated" : "" %>"><img class="medal" src="<%= getImageUrl('medal_' + (p.rank.inTop100 ?? p.rank.medal)) %>" alt=""/> <img class="star" src="<%= getImageUrl('star_' + p.rank.star) %>" alt=""/><p><%= p.steamAccount.seasonLeaderboardRank ?? "" %></p></div></div><div class="hero_winrate"><div class="heroes"><p class="tip row total"><%= p.genHero ? $t("dota2tracker.template.all_matches_") : $t("dota2tracker.template.top10_") %></p><span class="tip"><%= $t("dota2tracker.template.hero") %></span><span class="tip" style="margin:0 4px"><%= $t("dota2tracker.template.match_count") %></span><span class="tip" style="margin:0 4px"><%= $t("dota2tracker.template.winrate") %></span><span class="tip" style="margin:0 4px"><%= $t("dota2tracker.template.imp") %></span><span class="tip win_count" style="justify-self:end;margin-right:2px"><%= $t("dota2tracker.template.win_count") %></span><span class="tip lose_count" style="justify-self:start;margin-left:2px"><%= $t("dota2tracker.template.lose_count") %></span> <% heroList.forEach(hero => { %> <span><img alt="" src="<%= getImageUrl(hero.hero.shortName, ImageType.HeroIcons) %>"/></span><span class="count"><%= hero.matchCount %></span><span class="win_rate"><%= ((hero.winCount / hero.matchCount) * 100).toFixed(0) %>%</span> <span class="imp"><%= (hero.imp > 0 ? "+" : "") + hero.imp %></span><span class="win" <%- `style="${hero.winCount == 0 ? "visibility:hidden;" : ""}width: ${hero.winCount * heroScale}px"` %>><%= hero.winCount %></span><span class="lose" <%- `style="${hero.matchCount - hero.winCount == 0 ? "visibility:hidden;" : ""}width: ${(hero.matchCount - hero.winCount) * heroScale}px"` %>><%= hero.matchCount - hero.winCount %></span> <% }); %> <p class="tip row near"><%= $t("dota2tracker.template.recently_positions") %></p><span class="tip"><%= $t("dota2tracker.template.position") %></span><span class="tip" style="margin:0 4px"><%= $t("dota2tracker.template.match_count") %></span><span class="tip" style="margin:0 4px"><%= $t("dota2tracker.template.winrate") %></span><span class="tip" style="margin:0 4px"><%= $t("dota2tracker.template.imp") %></span><span class="tip win_count" style="justify-self:end;margin-right:2px"><%= $t("dota2tracker.template.win_count") %></span><span class="tip lose_count" style="justify-self:start;margin-left:2px"><%= $t("dota2tracker.template.lose_count") %></span> <% p.positionPerformance.forEach(pos => { %> <span><img src="<%= getImageUrl(pos.icon, ImageType.IconsFacets) %>"></span><span class="count"><%= pos.matchCount %></span><span class="win_rate"><%= pos.matchCount > 0 ? ((pos.winCount / pos.matchCount) * 100).toFixed(0) : "-" %>%</span> <span class="imp"><%= (pos.imp > 0 ? "+" : "") + pos.imp %></span><span class="win" <%- `style="${pos.winCount == 0 ? "visibility:hidden;" : ""}width: ${pos.winCount * posScale}px"` %>><%= pos.winCount %></span><span class="lose" <%- `style="${pos.matchCount - pos.winCount == 0 ? "visibility:hidden;" : ""}width: ${(pos.matchCount - pos.winCount) * posScale}px"` %>><%= pos.matchCount - pos.winCount %></span> <% }); %> </div> <% if (isAnon) { %> <%- include("player_1/private", {$t, player: p}) %> <% } %> </div> <% if (Math.abs(p.streak) > 1 && !isAnon) { %> <div class="streak" <%- `style="box-shadow:none;color:${getWinRateColor((p.streak + 10) / 20)};"` %>> <%= Math.abs(p.streak) + (p.streak > 0 ? $t("dota2tracker.template.winning_streak") : $t("dota2tracker.template.losing_streak")) %> </div> <% } %> <div><table class="matches"><colgroup><col style="width:auto"/><col style="width:auto"/><col style="width:40px"/><col style="width:auto"/><col style="width:40px"/><col style="width:auto"/><col style="width:auto"/><col style="width:auto"/><col style="width:40px"/></colgroup><thead><tr><th><%= $t("dota2tracker.template.id") %></th><th><%= $t("dota2tracker.template.mode") %></th><th><%= $t("dota2tracker.template.hero") %></th><th><%= $t("dota2tracker.template.kda_kc") %></th><th><%= $t("dota2tracker.template.lane") %></th><th><%= $t("dota2tracker.template.time") %></th><th><%= $t("dota2tracker.template.duration") %></th><th><%= $t("dota2tracker.template.imp") %></th><th><%= $t("dota2tracker.template.rank") %></th></tr></thead><tbody> <% matchesData.forEach(m => { %> <tr class="match <%= m.didWin ? "win" : "lose" %>"><td><%- m.parsedDateTime ? m.id : `<p>${m.id}</p><p>${$t("dota2tracker.template.un_parsed")}</p>` %></td><td><p><%= $t("dota2tracker.template.lobby_types." + m.lobbyType) || m.lobbyType %></p><p><%= $t("dota2tracker.template.game_modes." + m.gameMode) || m.gameMode %></p></td><td><img alt="" src="<%= getImageUrl(m.inner.hero.shortName, ImageType.HeroIcons) %>"/></td><td style="line-height:20px"><p><%= m.kda %> (<%= (m.parsedDateTime ? "" : "≈") + m.kp %>%)</p><p><%= m.inner.kills %>/<%= m.inner.deaths %>/<%= m.inner.assists %></p></td><td><div class="player_lane <%= m.laneRes %>"><%- getImageUrl("lane_" + m.laneRes, undefined, ImageFormat.svg) %></div></td><td style="line-height:20px"><%= m.timeStr %></td><td><%= m.durationTime %></td><td><%= m.inner.imp != null ? ((m.inner.imp >= 0 ? "+" : "") + m.inner.imp) : "?" %></td><td><img class="medal" src="<%= getImageUrl("medal_" + m.rank?.toString().split("")[0]) %>" style="width:100%"/></td></tr> <% }); %> </tbody></table> <% if (isAnon) { %> <%- include("player_1/private", {$t, player: p}) %> <% } %> </div><div class="plus"> <% p.dotaPlus.forEach(hero => { %> <div class="hero"><img src="<%= getImageUrl(hero.shortName, ImageType.Heroes) %>" alt=""/><div class="level"><img src="<%= getImageUrl("hero_badge_" + Math.ceil((hero.level + 1) / 6)) %>" alt=""/><span><%= hero.level %></span></div><span><%= ((hero.winCount / hero.matchCount) * 100).toFixed(2) %>%</span> <span><%= hero.matchCount %></span></div> <% }); %> <% if (isAnon) { %> <%- include("player_1/private", {$t, player: p}) %> <% } %> </div></div></body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<% const { name, avatar, isRising, prevRank, currRank, date } = data; %> <% const kind = isRising ? "xi" : "bei"; %> <% const avatarHtml = `<img src="${avatar}" />`; %> <% const prevHtml = `<span class="rank prev">${$t("dota2tracker.template.ranks." + prevRank.medal)}${prevRank.leader ?? prevRank.star}</span>`; %> <% const currHtml = `<span class="rank curr">${$t("dota2tracker.template.ranks." + currRank.medal)}${currRank.leader ?? currRank.star}</span>`; %> <% const msgKey = isRising ? "rank_fun_up_message" : "rank_fun_down_message"; %> <% const rawMsg = $t("dota2tracker.template." + msgKey, { name: name }); %> <% const finalMessage = rawMsg.replace('AVATAR_PLACEHOLDER', avatarHtml).replace('PREV_PLACEHOLDER', prevHtml).replace('CURR_PLACEHOLDER', currHtml); %> <!DOCTYPE html><html><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Rank Change</title> <%- `<style>` %> * { margin: 0; padding: 0; } html { width: 1024px; height: 768px; } body { background: no-repeat center center / cover; } .wrapper { padding: 80px; padding-top: 180px; } .wrapper > p { font-size: 32px; line-height: 48px; height: 48px; text-align: center; } body.xi p.bei, body.bei p.xi { display: none; } .wrapper > p > span { margin: 0 10px; } .wrapper > p > span.prev { color: gray; } .wrapper > p > span.curr { font-weight: bold; color: red; } .wrapper > p > img { width: 48px; height: 48px; vertical-align: middle; line-height: 48px; } .ranks { margin-top: 40px; display: flex; width: 100%; height: 256px; justify-content: space-evenly; } div.rank { position: relative; width: 256px; height: 256px; } div.rank > img { position: absolute; } div.rank.prev { filter: grayscale(1); } div.rank.curr { transform: scale(1.25); } div.rank p { font-size: 36px; bottom: 20px; width: 100%; text-align: center; color: #fff; position: absolute; } <% if (fontFamily) { %><%- `body { font-family: ${fontFamily}; }` %><% } %> <%- `</style>` %> </head><body class="<%= kind %>" <%- `style="background-image: url(${getImageUrl(kind, undefined, "jpg")})"` %>><div class="wrapper"><p class="<%= kind %>"> <%- finalMessage %> </p><p></p><div class="ranks"><div class="rank prev"><img src="<%- getImageUrl('medal_' + (prevRank.inTop100 ?? prevRank.medal)) %>" alt=""/> <img src="<%- getImageUrl('star_' + prevRank.star) %>" alt=""/><p><%= prevRank.leader ?? "" %></p></div><div class="rank curr"><img src="<%- getImageUrl('medal_' + (currRank.inTop100 ?? currRank.medal)) %>" alt=""/> <img src="<%- getImageUrl('star_' + currRank.star) %>" alt=""/><p><%= currRank.leader ?? "" %></p></div></div><p style="text-align:right;margin-top:40px">—— <%= date %></p></div></body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
html,body{background-color:#000;color:#fff;width:600px;font-size:14px}body>*{width:100%;border:1px #fff solid;border-radius:5px;box-sizing:border-box}img{vertical-align:middle}span{min-width:0;white-space:nowrap}.title{margin:0}.players{display:grid}.player{display:grid;grid-template-columns:24px 120px 128px 88px 160px;grid-template-rows:1fr;width:100%;height:24px;align-items:center;justify-items:center;justify-content:space-between}.avatar{width:24px;height:24px;border-radius:50%;border:#fff 1px solid;box-sizing:border-box}.player>.name{display:block;width:100%;text-align:center;overflow:hidden;text-overflow:ellipsis}.player>.count{display:grid;grid-template-columns:1fr 1fr 1.8fr;width:100%}.player>.count>span{text-align:left}.player>.count>span:last-child{text-align:right}.player>.performance{display:flex;align-items:center}.player>.performance>.score_bar{display:flex;align-items:center;justify-content:center;height:10px;width:51px;border-radius:1px;background-color:#333}.player>.performance>.score_bar>*{height:100%}.player>.performance>.score_bar.neg.over{justify-content:flex-start}.player>.performance>.score_bar.pos.over{justify-content:flex-end}.player>.performance>.score_bar.neg>.left{background:linear-gradient(to right,#777,#888,#888,#777)}.player>.performance>.score_bar.pos>.right{background:linear-gradient(to right,#6cf,#7df,#6cf)}.player>.performance>.score_bar>.pipe{height:12px;width:1px;background-color:#fff;box-shadow:0 0 0 1px #00000080;position:relative}.player>.performance>.score_value{display:block;margin-left:4px;width:28px;text-align:center}.combinations{display:grid;grid-template-columns:repeat(4,auto);justify-content:start;align-items:center}.combinations>.players{display:flex}.combinations span:not(:first-child){margin-left:4px}span.win{color:#007a00}span.lose{color:#b30000}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<% const { title, players, combinations, showCombi } = data; %> <% const getImpInfo = (impVal) => {
|
|
2
|
+
const abs = Math.abs(impVal);
|
|
3
|
+
const isPos = impVal > 0;
|
|
4
|
+
const isOver = abs > 25;
|
|
5
|
+
|
|
6
|
+
// 宽度计算:如果溢出(>25),则填满单侧(25px);否则按比例
|
|
7
|
+
// 原逻辑中 .score_bar 宽 51px (中心各 25px?)。
|
|
8
|
+
// 原逻辑:
|
|
9
|
+
// if (abs > 25) { right = abs (if pos), left = abs (if neg) }
|
|
10
|
+
// 这里的逻辑有点奇怪,如果 abs=30,width=30px,父容器只有25px空间?
|
|
11
|
+
// 让我们严格遵循原逻辑:
|
|
12
|
+
// width 赋值直接是 absValue。CSS里父容器 overflow 没写 hidden,可能靠 flex 布局撑开?
|
|
13
|
+
// 无论如何,保持原值逻辑:
|
|
14
|
+
let left = 0, right = 0;
|
|
15
|
+
if (isOver) {
|
|
16
|
+
if (isPos) right = abs;
|
|
17
|
+
else left = abs;
|
|
18
|
+
} else {
|
|
19
|
+
left = right = abs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
valStr: (isPos ? "+" : "") + impVal,
|
|
24
|
+
barClass: `score_bar ${isPos ? "pos" : "neg"}${isOver ? " over" : ""}`,
|
|
25
|
+
leftStyle: `width: ${left}px`,
|
|
26
|
+
rightStyle: `width: ${right}px`
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
%> <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Daily Report</title> <%- `<style>` %> <%- include("../common/styles/normalize.min.css") %> <%- include("./daily/base.css") %> <% if (fontFamily) { %><%- `body { font-family: ${fontFamily}; }` %><% } %> <%- `</style>` %> </head><body><h3 class="title"><%= title %></h3><div class="players"> <% players.forEach(p => { %> <% const winRate = Math.round((p.winCount / p.matches.length) * 100); %> <% const imp = getImpInfo(p.avgImp); %> <div class="player"><img src="<%= p.steamAccount.avatar %>" class="avatar"/> <span class="name"><%= p.name %></span><span class="count"><span class="win"><%= $t("dota2tracker.template.report_won") %><%= p.winCount %></span><span class="lose"><%= $t("dota2tracker.template.report_lost") %><%= p.loseCount %></span><span><%= $t("dota2tracker.template.report_winrate") %> <%= winRate %>%</span></span><div class="performance"><div class="<%= imp.barClass %>"><div class="left" <%- `style="${imp.leftStyle}"` %>></div><div class="pipe"></div><div class="right" <%- `style="${imp.rightStyle}"` %>></div></div><span class="score_value"><%= imp.valStr %></span></div><span class="kda"><%= p.avgKills %>/<%= p.avgDeaths %>/<%= p.avgAssists %> (<%= p.avgKDA %>)</span></div> <% }); %> </div><div class="combinations" <%- !showCombi ? `style="display:none;"` : "" %>><span style="grid-column:1/-1"><%= $t("dota2tracker.template.combined_win_loss_summary") %></span> <% combinations.forEach(combi => { %> <% const combiRate = Math.round((combi.winCount / combi.matches.length) * 100); %> <div class="players"> <% combi.players.forEach(p => { %> <img src="<%= p.steamAccount.avatar %>" class="avatar"/> <% }); %> </div><span class="win"><%= $t("dota2tracker.template.report_won") %><%= combi.winCount %></span><span class="lose"><%= $t("dota2tracker.template.report_lost") %><%= combi.matches.length - combi.winCount %></span><span><%= $t("dota2tracker.template.report_winrate") %> <%= combiRate %>%</span> <% }); %> </div></body></html>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjtdev/koishi-plugin-dota2tracker",
|
|
3
3
|
"description": "koishi插件-追踪群友的DOTA2对局",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"axios": "^1.13.2",
|
|
31
|
-
"dotaconstants": "^
|
|
31
|
+
"dotaconstants": "^10.7.0",
|
|
32
32
|
"ejs": "^3.1.10",
|
|
33
33
|
"https-proxy-agent": "^7.0.6",
|
|
34
34
|
"luxon": "^3.8.0-alpha.1"
|