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();
|
||||
}
|
||||
|
||||
+12
-5
@@ -8,7 +8,7 @@
|
||||
* - hero: selected hero key + detail sub-tab (skills|core|fears|patches)
|
||||
* - item: selected shop item key (items page)
|
||||
* - patch: selected version string (patches page; latest when absent)
|
||||
* - query params (heroes page only): tags=csv, q=search text
|
||||
* - query params: tags=csv (heroes), q=search text (heroes + items)
|
||||
*
|
||||
* Two-way binding without feedback loops:
|
||||
* - App calls syncStateToUrl() after each state mutation. We use
|
||||
@@ -48,6 +48,7 @@ function parseHash(hash) {
|
||||
patchVersion: null,
|
||||
tags: null,
|
||||
query: null,
|
||||
itemQuery: null,
|
||||
};
|
||||
let raw = hash || "";
|
||||
if (raw.startsWith("#")) raw = raw.slice(1);
|
||||
@@ -76,7 +77,10 @@ function parseHash(hash) {
|
||||
);
|
||||
}
|
||||
const qParam = params.get("q");
|
||||
if (qParam != null) out.query = qParam;
|
||||
if (qParam != null) {
|
||||
if (page === "items") out.itemQuery = qParam;
|
||||
else out.query = qParam;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -91,7 +95,7 @@ function safeDecode(s) {
|
||||
/** Serialize current app state into a hash string (with leading '#'). */
|
||||
function serializeHash(state) {
|
||||
if (!state || !VALID_PAGES.includes(state.page)) return ROUTE_DEFAULT;
|
||||
let hash = "#" + state.page;
|
||||
let hash = "#/" + state.page;
|
||||
if (state.page === "heroes") {
|
||||
if (state.selectedKey) {
|
||||
hash += "/" + encodeURIComponent(state.selectedKey);
|
||||
@@ -117,13 +121,16 @@ function serializeHash(state) {
|
||||
if (state.page === "heroes") {
|
||||
const qs = new URLSearchParams();
|
||||
if (state.tagFilters && state.tagFilters.size) {
|
||||
// URLSearchParams.set already percent-encodes the value, so join raw
|
||||
// tag names with "," — do NOT pre-encodeURIComponent (double encoding).
|
||||
qs.set("tags", Array.from(state.tagFilters).join(","));
|
||||
}
|
||||
if (state.query) qs.set("q", state.query);
|
||||
const s = qs.toString();
|
||||
if (s) hash += "?" + s;
|
||||
} else if (state.page === "items") {
|
||||
const qs = new URLSearchParams();
|
||||
if (state.itemQuery) qs.set("q", state.itemQuery);
|
||||
const s = qs.toString();
|
||||
if (s) hash += "?" + s;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user