v0.4.0: Cloudflare Pages deploy tooling, items search in URL, spirit bear patch lookup

This commit is contained in:
voson
2026-07-27 16:28:31 +08:00
parent 1d0428b4a3
commit adb8f303c2
13 changed files with 674 additions and 27 deletions
+40 -19
View File
@@ -397,8 +397,11 @@ function withAbilityDetail(ent, ab) {
return ent;
}
/** Build icon-bar entries: base skills + shard/scepter upgrades (dota2.com style). */
/** Build icon-bar entries: base skills + shard/scepter upgrades (dota2.com style).
* Cached per hero key; invalidated when data reloads. */
const _skillEntriesCache = new Map();
function skillEntriesFor(heroKey) {
if (_skillEntriesCache.has(heroKey)) return _skillEntriesCache.get(heroKey);
const cell = heroAbilityCell(heroKey);
if (cell == null) return null;
const entries = [];
@@ -473,7 +476,9 @@ function skillEntriesFor(heroKey) {
);
}
}
return { entries, talents: cell.talents };
const result = { entries, talents: cell.talents };
_skillEntriesCache.set(heroKey, result);
return result;
}
const DISPEL_LABEL = {
@@ -1029,14 +1034,17 @@ function fmtRegen(v) {
return `+${fmtNum(v, 2)}`;
}
/** Shared HP/Mana bar scale: max base resource across all heroes. */
/** Shared HP/Mana bar scale: max base resource across all heroes (cached). */
let _resourceBarScaleCache = null;
function resourceBarScale() {
if (_resourceBarScaleCache != null) return _resourceBarScaleCache;
let max = 0;
for (const h of state.data?.heroes || []) {
if (h.health != null) max = Math.max(max, Number(h.health) || 0);
if (h.mana != null) max = Math.max(max, Number(h.mana) || 0);
}
return max > 0 ? max : 1000;
_resourceBarScaleCache = max > 0 ? max : 1000;
return _resourceBarScaleCache;
}
/** Fill width % on shared scale; clamp so tiny values stay visible. */
@@ -1637,7 +1645,7 @@ function renderItemEntry(entry, lookup) {
function renderHeroEntry(hero, lookup) {
const meta = (lookup.heroes || {})[String(hero.hero_id)];
const key = meta ? meta.key : "";
const name = meta ? meta.name_loc : "# " + hero.hero_id;
const name = meta ? meta.name_loc : "其它单位";
const portrait = key
? `<img class="patch-hero-icon" src="portrait/${encodeURIComponent(key)}.png?v=wide" alt="" onerror="this.style.visibility='hidden'">`
: "";
@@ -1695,6 +1703,14 @@ function buildPatchChangesPanel(heroKey) {
return wrap;
}
function ensureDefaultPatch() {
const patches = state.data?.patches || [];
if (!patches.length) return;
if (!state.selectedPatch || !patches.some((p) => p.version === state.selectedPatch)) {
state.selectedPatch = patches[0].version;
}
}
function renderPatches() {
if (!state.data) return;
const select = $("#patch-select");
@@ -1710,9 +1726,7 @@ function renderPatches() {
return;
}
if (!state.selectedPatch || !patches.some((p) => p.version === state.selectedPatch)) {
state.selectedPatch = patches[0].version; // default: latest
}
ensureDefaultPatch();
if (select) {
select.innerHTML = patches
@@ -1823,8 +1837,9 @@ function render() {
}
}
// Search-box URL sync timer (debounced replace so typing does not spam history).
let searchSyncTimer = 0;
// Search-box URL sync timers (debounced replace so typing does not spam history).
let heroSearchSyncTimer = 0;
let itemSearchSyncTimer = 0;
const SEARCH_SYNC_DEBOUNCE_MS = 300;
/**
@@ -1885,6 +1900,11 @@ function applyPatch(patch) {
const inp = $("#q");
if (inp && inp.value !== state.query) inp.value = state.query;
}
if (typeof patch.itemQuery === "string") {
state.itemQuery = patch.itemQuery;
const inp = $("#q-item");
if (inp && inp.value !== state.itemQuery) inp.value = state.itemQuery;
}
render();
}
@@ -1895,10 +1915,9 @@ function bindSearch() {
input.addEventListener("input", () => {
state.query = input.value;
if (state.page === "heroes") renderBoard();
// Debounced replace so each keystroke does not push a history entry.
if (searchSyncTimer) clearTimeout(searchSyncTimer);
searchSyncTimer = setTimeout(() => {
searchSyncTimer = 0;
if (heroSearchSyncTimer) clearTimeout(heroSearchSyncTimer);
heroSearchSyncTimer = setTimeout(() => {
heroSearchSyncTimer = 0;
syncStateToUrl({ replace: true });
}, SEARCH_SYNC_DEBOUNCE_MS);
});
@@ -1909,9 +1928,9 @@ function bindSearch() {
itemInput.addEventListener("input", () => {
state.itemQuery = itemInput.value;
if (state.page === "items") renderItemShop();
if (searchSyncTimer) clearTimeout(searchSyncTimer);
searchSyncTimer = setTimeout(() => {
searchSyncTimer = 0;
if (itemSearchSyncTimer) clearTimeout(itemSearchSyncTimer);
itemSearchSyncTimer = setTimeout(() => {
itemSearchSyncTimer = 0;
syncStateToUrl({ replace: true });
}, SEARCH_SYNC_DEBOUNCE_MS);
});
@@ -1983,6 +2002,7 @@ async function main() {
bindSearch();
bindPageChrome();
const res = await fetch("data.json");
if (!res.ok) throw new Error(`data.json: HTTP ${res.status}`);
state.data = await res.json();
state.data.relations = state.data.relations || { counters: [], synergies: [] };
state.data.hero_items = state.data.hero_items || { items: {}, by_hero: {} };
@@ -1997,8 +2017,9 @@ 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 || {};
// Install hash router now that state.data is loaded (validation needs it),
// then apply the initial URL (deep-link support) which also renders.
_skillEntriesCache.clear();
_resourceBarScaleCache = null;
_innateAbilityKeys = null;
installRouter({ getState: () => state, applyPatch });
applyUrlToState();
}