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",
|
||||
|
||||
Reference in New Issue
Block a user