v0.4.0: Cloudflare Pages deploy tooling, items search in URL, spirit bear patch lookup
This commit is contained in:
+40
-19
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user