Add 7.41e patch notes with a hand-authored reading sidebar.
Ship Climperor Web 0.6.13: latest-patch summary panel, icon/innate fallbacks, and layout/scroll fixes; keep shared ability badges in git. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+141
-10
@@ -1175,18 +1175,24 @@ function wireTalentTrigger(btn, talents) {
|
||||
* badge — many innate keys 404 on Steam CDN, matching dota2.com.
|
||||
* Other abilities load `ability/{key}.png` with a quiet miss state on error.
|
||||
*/
|
||||
function markAbilityIconMissing(img) {
|
||||
img.onerror = null;
|
||||
img.removeAttribute("src");
|
||||
img.alt = "";
|
||||
img.classList.add("missing");
|
||||
}
|
||||
|
||||
function setAbilityIcon(img, abilityKey, isInnate) {
|
||||
if (isInnate) {
|
||||
img.src = innateIconSrc();
|
||||
img.onerror = null;
|
||||
img.onerror = () => markAbilityIconMissing(img);
|
||||
return;
|
||||
}
|
||||
img.src = abilityIconSrc(abilityKey);
|
||||
img.onerror = () => {
|
||||
img.onerror = null;
|
||||
img.removeAttribute("src");
|
||||
img.alt = "";
|
||||
img.classList.add("missing");
|
||||
// Many innates / renamed keys 404 on CDN — shared badge, then quiet miss.
|
||||
img.onerror = () => markAbilityIconMissing(img);
|
||||
img.src = innateIconSrc();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3668,13 +3674,25 @@ function innateAbilityKeys() {
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Inline onerror for patch HTML icons: optional innate fallback, then remove (no empty box). */
|
||||
function patchIconOnErrorAttr(tryInnate) {
|
||||
if (tryInnate) {
|
||||
const src = innateIconSrc().replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||
return (
|
||||
`this.onerror=null;this.src='${src}';` +
|
||||
"this.onerror=function(){this.onerror=null;this.remove()}"
|
||||
);
|
||||
}
|
||||
return "this.onerror=null;this.remove()";
|
||||
}
|
||||
|
||||
function patchAbilityBlockHtml(ab, lookup) {
|
||||
const ameta = (lookup.abilities || {})[String(ab.ability_id)];
|
||||
const akey = ameta ? ameta.key : "";
|
||||
const aname = ameta ? ameta.name_loc : "# " + ab.ability_id;
|
||||
const innate = akey && innateAbilityKeys().has(akey);
|
||||
const aicon = akey
|
||||
? `<img class="patch-ability-icon" src="${innate ? innateIconSrc() : abilityIconSrc(akey)}" alt="" onerror="this.style.visibility='hidden'">`
|
||||
? `<img class="patch-ability-icon" src="${innate ? innateIconSrc() : abilityIconSrc(akey)}" alt="" onerror="${patchIconOnErrorAttr(!innate)}">`
|
||||
: "";
|
||||
return `<div class="patch-ability">${aicon}<div class="patch-entry-body"><span class="patch-name">${escapeHtml(aname)}</span>${renderNotesList(ab.ability_notes)}</div></div>`;
|
||||
}
|
||||
@@ -3684,7 +3702,7 @@ function renderItemEntry(entry, lookup) {
|
||||
const key = meta ? meta.key : "";
|
||||
const name = meta ? meta.name_loc : "# " + entry.ability_id;
|
||||
const icon = key
|
||||
? `<img class="patch-item-icon" src="${itemIconSrc(key)}" alt="" onerror="this.style.visibility='hidden'">`
|
||||
? `<img class="patch-item-icon" src="${itemIconSrc(key)}" alt="" onerror="${patchIconOnErrorAttr(false)}">`
|
||||
: "";
|
||||
return `<div class="patch-entry-row">${icon}<div class="patch-entry-body"><span class="patch-name">${escapeHtml(name)}</span>${renderNotesList(entry.ability_notes)}</div></div>`;
|
||||
}
|
||||
@@ -3694,7 +3712,7 @@ function renderHeroEntry(hero, lookup) {
|
||||
const key = meta ? meta.key : "";
|
||||
const name = meta ? meta.name_loc : "其它单位";
|
||||
const portrait = key
|
||||
? `<img class="patch-hero-icon" src="${portraitSrc(key)}" alt="" onerror="this.style.visibility='hidden'">`
|
||||
? `<img class="patch-hero-icon" src="${portraitSrc(key)}" alt="" onerror="${patchIconOnErrorAttr(false)}">`
|
||||
: "";
|
||||
let html = `<div class="patch-hero"><div class="patch-hero-head">${portrait}<span class="patch-name">${escapeHtml(name)}</span></div>`;
|
||||
html += renderHeroNotes(hero.hero_notes);
|
||||
@@ -3762,6 +3780,112 @@ function ensureDefaultPatch() {
|
||||
}
|
||||
}
|
||||
|
||||
function patchSummaryFor(version) {
|
||||
const pack = state.data?.patch_summaries || {};
|
||||
const by = pack.by_version || {};
|
||||
return version && by[version] ? by[version] : null;
|
||||
}
|
||||
|
||||
function patchSummaryEntityLabel(kind, key) {
|
||||
if (kind === "hero") {
|
||||
const hero = heroByKey(key);
|
||||
return (hero && (hero.name_loc || hero.key)) || key;
|
||||
}
|
||||
const meta = (state.data.items_meta || {})[key];
|
||||
if (meta && meta.name_loc) return meta.name_loc;
|
||||
const shop = ((state.data.item_shop || {}).items || {})[key];
|
||||
if (shop && shop.name_loc) return shop.name_loc;
|
||||
return key;
|
||||
}
|
||||
|
||||
function renderPatchSummaryChip(kind, row) {
|
||||
const key = row && row.key ? String(row.key) : "";
|
||||
if (!key) return "";
|
||||
const note = row.note ? String(row.note) : "";
|
||||
const label = patchSummaryEntityLabel(kind, key);
|
||||
const img =
|
||||
kind === "hero"
|
||||
? `<img class="patch-summary-icon patch-summary-icon--hero" src="${portraitSrc(key)}" alt="" onerror="this.style.visibility='hidden'">`
|
||||
: `<img class="patch-summary-icon patch-summary-icon--item" src="${itemIconSrc(key)}" alt="" onerror="this.style.visibility='hidden'">`;
|
||||
const clickable = kind === "hero" && heroByKey(key);
|
||||
const tag = clickable ? "button" : "div";
|
||||
const attrs = clickable
|
||||
? ` type="button" class="patch-summary-chip is-clickable" data-hero="${escapeHtml(key)}"`
|
||||
: ` class="patch-summary-chip"`;
|
||||
return `<${tag}${attrs}>${img}<span class="patch-summary-chip-body"><span class="patch-summary-chip-name">${escapeHtml(label)}</span>${note ? `<span class="patch-summary-chip-note">${escapeHtml(note)}</span>` : ""}</span></${tag}>`;
|
||||
}
|
||||
|
||||
function renderPatchSummarySection(title, html) {
|
||||
if (!html) return "";
|
||||
return `<section class="patch-summary-section"><h4 class="patch-summary-section-title">${escapeHtml(title)}</h4>${html}</section>`;
|
||||
}
|
||||
|
||||
function renderPatchAside(version) {
|
||||
const aside = $("#patches-aside");
|
||||
if (!aside) return;
|
||||
const summary = patchSummaryFor(version);
|
||||
if (!summary) {
|
||||
aside.hidden = true;
|
||||
aside.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const themes = (summary.themes || [])
|
||||
.filter((t) => typeof t === "string" && t.trim())
|
||||
.map((t) => `<li>${escapeHtml(t)}</li>`)
|
||||
.join("");
|
||||
const buffs = (summary.buffs || []).map((r) => renderPatchSummaryChip("hero", r)).join("");
|
||||
const nerfs = (summary.nerfs || []).map((r) => renderPatchSummaryChip("hero", r)).join("");
|
||||
const items = (summary.items || []).map((r) => renderPatchSummaryChip("item", r)).join("");
|
||||
const takeaways = (summary.takeaways || [])
|
||||
.filter((t) => typeof t === "string" && t.trim())
|
||||
.map((t) => `<li>${escapeHtml(t)}</li>`)
|
||||
.join("");
|
||||
|
||||
let body = "";
|
||||
if (summary.headline) {
|
||||
body += `<p class="patch-summary-headline">${escapeHtml(String(summary.headline))}</p>`;
|
||||
}
|
||||
if (themes) {
|
||||
body += renderPatchSummarySection("本版方向", `<ul class="patch-summary-list">${themes}</ul>`);
|
||||
}
|
||||
if (buffs) {
|
||||
body += renderPatchSummarySection("明显加强", `<div class="patch-summary-chips">${buffs}</div>`);
|
||||
}
|
||||
if (nerfs) {
|
||||
body += renderPatchSummarySection("明显削弱", `<div class="patch-summary-chips">${nerfs}</div>`);
|
||||
}
|
||||
if (items) {
|
||||
body += renderPatchSummarySection("装备要点", `<div class="patch-summary-chips">${items}</div>`);
|
||||
}
|
||||
if (takeaways) {
|
||||
body += renderPatchSummarySection("选将提示", `<ul class="patch-summary-list">${takeaways}</ul>`);
|
||||
}
|
||||
|
||||
aside.hidden = false;
|
||||
aside.innerHTML = `
|
||||
<div class="patch-summary ui-panel">
|
||||
<div class="patch-summary-head">
|
||||
<h3 class="patch-summary-title">版本解读</h3>
|
||||
<p class="patch-summary-disclaimer">非官方 · 社区摘要</p>
|
||||
</div>
|
||||
${body}
|
||||
</div>`;
|
||||
|
||||
aside.querySelectorAll("button.patch-summary-chip[data-hero]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const key = btn.dataset.hero;
|
||||
if (!key || !heroByKey(key)) return;
|
||||
state.page = "heroes";
|
||||
state.selectedKey = key;
|
||||
state.detailTab = "patches";
|
||||
state.inspect = null;
|
||||
syncStateToUrl();
|
||||
render();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderPatches() {
|
||||
if (!state.data) return;
|
||||
const select = $("#patch-select");
|
||||
@@ -3774,6 +3898,7 @@ function renderPatches() {
|
||||
if (select) select.innerHTML = "";
|
||||
if (verEl) verEl.textContent = "";
|
||||
root.innerHTML = '<div class="patches-empty">暂无版本数据</div>';
|
||||
renderPatchAside(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3803,6 +3928,8 @@ function renderPatches() {
|
||||
const cur = patches.find((p) => p.version === state.selectedPatch) || patches[0];
|
||||
if (verEl) verEl.textContent = (cur && cur.version) || "";
|
||||
|
||||
renderPatchAside(state.selectedPatch);
|
||||
|
||||
const lookup = state.data.patch_lookup || { items: {}, abilities: {}, heroes: {} };
|
||||
const details = state.data.patch_details || {};
|
||||
const det = details[state.selectedPatch];
|
||||
@@ -5721,8 +5848,11 @@ function renderMechanics() {
|
||||
abImg.alt = "";
|
||||
abImg.loading = "lazy";
|
||||
abImg.onerror = function () {
|
||||
this.onerror = null;
|
||||
this.src = assetUrl("ability/innate.png");
|
||||
this.onerror = function () {
|
||||
this.onerror = null;
|
||||
this.remove();
|
||||
};
|
||||
this.src = innateIconSrc();
|
||||
};
|
||||
const textWrap = document.createElement("span");
|
||||
textWrap.className = "mechanics-text";
|
||||
@@ -6201,6 +6331,7 @@ async function main() {
|
||||
state.data.patches = state.data.patches || [];
|
||||
state.data.patch_lookup = state.data.patch_lookup || { items: {}, abilities: {}, heroes: {} };
|
||||
state.data.patch_details = state.data.patch_details || {};
|
||||
state.data.patch_summaries = state.data.patch_summaries || { by_version: {} };
|
||||
state.data.leaderboards = state.data.leaderboards || {
|
||||
fetched_at: null,
|
||||
source: "valve",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Local defaults; production export overwrites via export_relations_site.py. */
|
||||
var SITE_VERSION = "0.6.9";
|
||||
var SITE_VERSION = "0.6.13";
|
||||
var SITE_ORIGIN = "";
|
||||
var ABILITY_VIDEO_BASE = "";
|
||||
var STATIC_ASSET_BASE = "";
|
||||
|
||||
+19
-12
@@ -43,8 +43,8 @@
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
|
||||
<link rel="stylesheet" href="/style.css?v=0.6.9" />
|
||||
<script src="/mobile-gate.js?v=0.6.9"></script>
|
||||
<link rel="stylesheet" href="/style.css?v=0.6.13" />
|
||||
<script src="/mobile-gate.js?v=0.6.13"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="sr-only">DOTA2 上分帝</h1>
|
||||
@@ -218,14 +218,21 @@
|
||||
</main>
|
||||
|
||||
<main id="patches-view" class="board patches-board hidden">
|
||||
<div class="patches-header">
|
||||
<h2 class="patches-title">
|
||||
<span class="patches-kicker page-kicker">游戏性更新</span>
|
||||
<span class="patches-ver" id="patch-ver"></span>
|
||||
</h2>
|
||||
<select id="patch-select" aria-label="选择版本"></select>
|
||||
<div class="patches-cluster">
|
||||
<div class="patches-center-wrap">
|
||||
<div class="patches-main">
|
||||
<div class="patches-header">
|
||||
<h2 class="patches-title">
|
||||
<span class="patches-kicker page-kicker">游戏性更新</span>
|
||||
<span class="patches-ver" id="patch-ver"></span>
|
||||
</h2>
|
||||
<select id="patch-select" aria-label="选择版本"></select>
|
||||
</div>
|
||||
<div class="patches-detail" id="patches-detail"></div>
|
||||
</div>
|
||||
<aside class="patches-aside" id="patches-aside" hidden></aside>
|
||||
</div>
|
||||
</div>
|
||||
<div class="patches-detail" id="patches-detail"></div>
|
||||
</main>
|
||||
|
||||
<div id="detail-drawer" class="detail-drawer is-collapsed hidden" aria-hidden="true">
|
||||
@@ -233,8 +240,8 @@
|
||||
</div>
|
||||
<footer class="heroes-site-foot" id="heroes-site-foot" aria-hidden="true"></footer>
|
||||
|
||||
<script src="/config.js?v=0.6.9"></script>
|
||||
<script src="/router.js?v=0.6.9"></script>
|
||||
<script src="/app.js?v=0.6.9"></script>
|
||||
<script src="/config.js?v=0.6.13"></script>
|
||||
<script src="/router.js?v=0.6.13"></script>
|
||||
<script src="/app.js?v=0.6.13"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+184
-6
@@ -1675,7 +1675,7 @@ body.detail-drawer-open {
|
||||
display: block;
|
||||
}
|
||||
.skill-icon img.missing {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
/* Circular triggers: talent tree + innate (official dota2.com order). */
|
||||
.skill-icon.talent-trigger,
|
||||
@@ -2833,17 +2833,32 @@ body:has(#items-view:not(.hidden)) {
|
||||
}
|
||||
|
||||
.patches-board {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* Center only the changelog column; summary hangs in the right gutter. */
|
||||
.patches-cluster {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.patches-center-wrap {
|
||||
position: relative;
|
||||
width: min(var(--content-read), calc(100% - 48px));
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.patches-main {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.patches-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-lg);
|
||||
width: min(var(--content-read), 92%);
|
||||
width: 100%;
|
||||
padding: var(--space-xl) 0 10px;
|
||||
}
|
||||
.patches-title {
|
||||
@@ -2881,9 +2896,169 @@ body:has(#items-view:not(.hidden)) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.patches-detail {
|
||||
width: min(var(--content-read), 92%);
|
||||
width: 100%;
|
||||
padding: 6px 0 64px;
|
||||
}
|
||||
.patches-aside {
|
||||
position: absolute;
|
||||
left: calc(100% + var(--aside-gap));
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.patches-aside[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/* Cap to the visible board (below .topbar), not raw 100vh — otherwise the
|
||||
sticky box hangs past the viewport and the last lines stay unreachable. */
|
||||
.patch-summary {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
align-self: start;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
max-height: calc(100dvh - 9rem);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(var(--space-lg) + 8px);
|
||||
}
|
||||
.patch-summary-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.patch-summary-title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-body-lg);
|
||||
font-weight: var(--fw-black);
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
line-height: var(--lh-tight);
|
||||
}
|
||||
.patch-summary-disclaimer {
|
||||
margin: 0;
|
||||
font-size: var(--fs-label);
|
||||
font-weight: var(--fw-medium);
|
||||
color: var(--muted);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.patch-summary-headline {
|
||||
margin: 0;
|
||||
font-size: var(--fs-body-sm);
|
||||
font-weight: var(--fw-medium);
|
||||
color: var(--text);
|
||||
line-height: var(--lh-body);
|
||||
}
|
||||
.patch-summary-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.patch-summary-section-title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-label);
|
||||
font-weight: var(--fw-semibold);
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.08em;
|
||||
line-height: var(--lh-title);
|
||||
}
|
||||
.patch-summary-list {
|
||||
margin: 0;
|
||||
padding-left: 1.15em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: var(--fs-body-sm);
|
||||
color: var(--text);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.patch-summary-chips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.patch-summary-chip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
button.patch-summary-chip.is-clickable {
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
button.patch-summary-chip.is-clickable:hover .patch-summary-chip-name {
|
||||
color: var(--sel);
|
||||
}
|
||||
.patch-summary-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
background: var(--panel);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.patch-summary-icon--hero {
|
||||
width: 32px;
|
||||
height: 18px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.patch-summary-icon--item {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.patch-summary-chip-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.patch-summary-chip-name {
|
||||
font-size: var(--fs-body-sm);
|
||||
font-weight: var(--fw-semibold);
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
}
|
||||
.patch-summary-chip-note {
|
||||
font-size: var(--fs-label);
|
||||
font-weight: var(--fw-medium);
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* Need ~820 main + 300 aside gutter on each side of optical center. */
|
||||
@media (max-width: 1440px) {
|
||||
.patches-center-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(var(--content-read), 100%);
|
||||
}
|
||||
.patches-aside {
|
||||
position: static;
|
||||
width: 100%;
|
||||
pointer-events: auto;
|
||||
order: -1;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
.patch-summary {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
.patches-empty,
|
||||
.rankings-empty,
|
||||
.streamers-empty {
|
||||
@@ -3384,6 +3559,9 @@ body:has(#items-view:not(.hidden)) {
|
||||
height: 48px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.patch-ability-icon.missing {
|
||||
display: none;
|
||||
}
|
||||
.patch-name {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
|
||||
Reference in New Issue
Block a user