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:
voson
2026-07-31 10:06:09 +08:00
co-authored by Cursor
parent a257f96d94
commit 30218790ce
18 changed files with 2169 additions and 63 deletions
+141 -10
View File
@@ -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",