v0.5.59: item counter evidence for fears, catch up Web features to site version.

Ship OpenDota counter-stats reordering for feared items, finalize SITE_VERSION/docs for rankings/streamers/trends/matches/mechanics and draft archetypes, and ignore regenerable Web data caches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-29 02:11:49 +08:00
co-authored by Cursor
parent 37769580f5
commit 3ec8007077
72 changed files with 20669 additions and 2105 deletions
+94 -7
View File
@@ -1,11 +1,20 @@
/* global window, history, location */
/**
* Hash-based router for the relations preview (web/relations).
* Hash-based router for the Climperor web site (web/relations).
*
* Synchronizes the browser URL with app state across these dimensions:
* - page: heroes | items | patches (top-level tab)
* - hero: selected hero key + detail sub-tab (skills|core|fears|patches)
* - page: heroes | rankings | streamers | trends | mechanics | items | patches (top-level tab)
* - hero: selected hero key + detail sub-tab
* (skills|core|fears|trends|matchups|matches|streamers|patches; legacy stats → trends)
* - rankings: Immortal leaderboard region
* #/rankings[/region] (legacy #/rankings/meta[/bracket] → players / china)
* - streamers: curated Douyin streamer directory
* #/streamers
* - trends: medal bracket for the 8-week win/pick board
* #/trends[/bracket][?sort=pr] (default bracket legend, sort wr omitted)
* - mechanics: applies-effect query (dispel / CC)
* #/mechanics[/{effect}] (default effect basic_dispel omitted)
* - item: selected shop item key (items page)
* - patch: selected version string (patches page; latest when absent)
* - query params: tags=csv (heroes), q=search text (heroes + items)
@@ -21,8 +30,44 @@
*/
const ROUTE_DEFAULT = "#/heroes";
const VALID_PAGES = ["heroes", "items", "patches"];
const VALID_DETAIL_TABS = ["skills", "core", "fears", "patches"];
const VALID_PAGES = ["heroes", "rankings", "streamers", "trends", "mechanics", "items", "patches"];
const VALID_DETAIL_TABS = [
"skills",
"core",
"fears",
"trends",
"stats",
"matchups",
"matches",
"streamers",
"patches",
];
const DEFAULT_RANKING_REGION = "china";
const DEFAULT_TRENDS_BRACKET = "legend";
const DEFAULT_TRENDS_SORT = "wr_end";
const VALID_TRENDS_SORTS = ["wr_end", "pr_end"];
const TRENDS_SORT_URL = { wr_end: "wr", pr_end: "pr" };
const TRENDS_SORT_FROM_URL = { wr: "wr_end", pr: "pr_end", wr_end: "wr_end", pr_end: "pr_end" };
const DEFAULT_MECHANIC_EFFECT = "basic_dispel";
const VALID_MECHANIC_EFFECTS = [
"basic_dispel",
"strong_dispel",
"root",
"disarm",
"silence",
"mute",
"stun",
"hex",
"break",
"sleep",
"fear",
"taunt",
"blind",
"leash",
"invis",
"ethereal",
"cyclone",
];
let _deps = null;
@@ -46,6 +91,10 @@ function parseHash(hash) {
detailTab: null,
itemKey: null,
patchVersion: null,
rankingRegion: null,
trendsBracket: null,
trendsSort: null,
mechanicEffect: null,
tags: null,
query: null,
itemQuery: null,
@@ -64,6 +113,15 @@ function parseHash(hash) {
if (page === "heroes") {
if (segs[1]) out.heroKey = safeDecode(segs[1]);
if (segs[2]) out.detailTab = safeDecode(segs[2]);
} else if (page === "rankings") {
// Legacy #/rankings/meta[/bracket] → Immortal players board (default region).
if (segs[1] && segs[1] !== "meta") {
out.rankingRegion = safeDecode(segs[1]);
}
} else if (page === "trends") {
if (segs[1]) out.trendsBracket = safeDecode(segs[1]);
} else if (page === "mechanics") {
if (segs[1]) out.mechanicEffect = safeDecode(segs[1]);
} else if (page === "items") {
if (segs[1]) out.itemKey = safeDecode(segs[1]);
} else if (page === "patches") {
@@ -81,6 +139,12 @@ function parseHash(hash) {
if (page === "items") out.itemQuery = qParam;
else out.query = qParam;
}
if (page === "trends") {
const sortParam = params.get("sort");
if (sortParam && TRENDS_SORT_FROM_URL[sortParam]) {
out.trendsSort = TRENDS_SORT_FROM_URL[sortParam];
}
}
return out;
}
@@ -99,10 +163,33 @@ function serializeHash(state) {
if (state.page === "heroes") {
if (state.selectedKey) {
hash += "/" + encodeURIComponent(state.selectedKey);
if (state.detailTab && VALID_DETAIL_TABS.includes(state.detailTab)) {
hash += "/" + encodeURIComponent(state.detailTab);
let tab = state.detailTab;
if (tab === "stats") tab = "trends"; // normalize legacy alias in URL
if (tab && VALID_DETAIL_TABS.includes(tab) && tab !== "stats") {
hash += "/" + encodeURIComponent(tab);
}
}
} else if (state.page === "rankings") {
// Omit region when it is the default (china) — bare #/rankings means China.
const region = state.rankingRegion || DEFAULT_RANKING_REGION;
if (region && region !== DEFAULT_RANKING_REGION) {
hash += "/" + encodeURIComponent(region);
}
} else if (state.page === "trends") {
// Omit bracket when it is the default (legend) — bare #/trends means legend.
const bracket = state.trendsBracket || DEFAULT_TRENDS_BRACKET;
if (bracket && bracket !== DEFAULT_TRENDS_BRACKET) {
hash += "/" + encodeURIComponent(bracket);
}
const sort = state.trendsSort || DEFAULT_TRENDS_SORT;
if (VALID_TRENDS_SORTS.includes(sort) && sort !== DEFAULT_TRENDS_SORT) {
hash += "?sort=" + encodeURIComponent(TRENDS_SORT_URL[sort] || sort);
}
} else if (state.page === "mechanics") {
const effect = state.mechanicEffect || DEFAULT_MECHANIC_EFFECT;
if (effect && effect !== DEFAULT_MECHANIC_EFFECT) {
hash += "/" + encodeURIComponent(effect);
}
} else if (state.page === "items") {
if (state.selectedItemKey) {
hash += "/" + encodeURIComponent(state.selectedItemKey);