Wire map images and Steam brand icon
This commit is contained in:
120
ai/tasks/done/TASK-249-wire-map-images-and-steam-brand-icon.md
Normal file
120
ai/tasks/done/TASK-249-wire-map-images-and-steam-brand-icon.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# TASK-249 - Wire map images and Steam brand icon
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This task connects the new local map images to current match and historical match detail using a shared frontend resolver, and wires the new Steam brand icon into the external profile buttons.
|
||||||
|
|
||||||
|
No map or brand asset files were modified.
|
||||||
|
|
||||||
|
## Files Read First
|
||||||
|
|
||||||
|
- `frontend/partida-actual.html`
|
||||||
|
- `frontend/assets/js/partida-actual.js`
|
||||||
|
- `frontend/historico-partida.html`
|
||||||
|
- `frontend/assets/js/historico-partida.js`
|
||||||
|
- `frontend/assets/css/historico.css`
|
||||||
|
- `frontend/assets/img/maps/`
|
||||||
|
- `frontend/assets/img/brands/`
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The frontend was still using narrow map-name heuristics and did not know how to resolve the new map image naming pattern consistently across:
|
||||||
|
|
||||||
|
- current match
|
||||||
|
- historical match detail
|
||||||
|
|
||||||
|
The Steam external profile button also had no brand icon wired even though the asset already existed locally.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Create one shared client-side resolver instead of duplicating ad hoc logic in each page.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- normalize map ids, layer ids and pretty names aggressively
|
||||||
|
- resolve to local assets using the `mapid-environment.webp` pattern when possible
|
||||||
|
- keep a safe fallback image path
|
||||||
|
- do not modify physical assets
|
||||||
|
|
||||||
|
## Asset Coverage Detected
|
||||||
|
|
||||||
|
Detected local coverage in `frontend/assets/img/maps/`:
|
||||||
|
|
||||||
|
- `carentan`: day, dusk, night, rain
|
||||||
|
- `driel`: dawn, day, night
|
||||||
|
- `elalamein`: day, dusk
|
||||||
|
- `elsenbornridge`: dawn, day, dusk, night
|
||||||
|
- `foy`: day, night
|
||||||
|
- `hill400`: day, dusk, night
|
||||||
|
- `hurtgenforest`: day, night
|
||||||
|
- `junobeach`: dawn, day, night
|
||||||
|
- `kharkov`: day, night
|
||||||
|
- `kursk`: day, night
|
||||||
|
- `mortain`: day, dusk, night, overcast
|
||||||
|
- `omahabeach`: day, dusk
|
||||||
|
- `purpleheartlane`: dawn, day, night, rain
|
||||||
|
- `remagen`: day, night
|
||||||
|
- `smolensk`: day, dusk, night
|
||||||
|
- `stalingrad`: day, dusk, night, overcast
|
||||||
|
- `stmariedumont`: day, night, rain
|
||||||
|
- `stmereeglise`: dawn, day, night
|
||||||
|
- `tobruk`: dawn, day, dusk
|
||||||
|
- `utahbeach`: day, night
|
||||||
|
- fallback assets: `unknown-day.webp`, `unknown.webp`
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Shared map resolver
|
||||||
|
|
||||||
|
Added `frontend/assets/js/map-image-resolver.js` with:
|
||||||
|
|
||||||
|
- canonical map aliases
|
||||||
|
- environment aliases
|
||||||
|
- compact normalization of layer ids and display names
|
||||||
|
- deterministic fallback selection
|
||||||
|
|
||||||
|
Examples resolved correctly:
|
||||||
|
|
||||||
|
- `junobeachwarfare` -> `./assets/img/maps/junobeach-day.webp`
|
||||||
|
- `Juno Beach Warfare` -> `./assets/img/maps/junobeach-day.webp`
|
||||||
|
- `utahbeach` -> `./assets/img/maps/utahbeach-day.webp`
|
||||||
|
- `Driel` -> `./assets/img/maps/driel-day.webp`
|
||||||
|
- `st marie du mont warfare` -> `./assets/img/maps/stmariedumont-day.webp`
|
||||||
|
|
||||||
|
### Current match
|
||||||
|
|
||||||
|
- `partida-actual.html` now loads the shared resolver before the page script.
|
||||||
|
- `partida-actual.js` now resolves map assets from layer id, map id and pretty names instead of a short hardcoded map list.
|
||||||
|
|
||||||
|
### Historical match detail
|
||||||
|
|
||||||
|
- `historico-partida.html` now loads the shared resolver.
|
||||||
|
- `historico-partida.js` now resolves map images from `match_id`, `map.id`, `map.name` and `map.pretty_name`.
|
||||||
|
- It keeps the existing fallback behavior if no match is found.
|
||||||
|
|
||||||
|
### Steam brand icon
|
||||||
|
|
||||||
|
- Wired the Steam profile button to `./assets/img/brands/steam.png`.
|
||||||
|
- Added `onerror="this.remove();"` to brand icons so a missing file does not leave a broken image.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Executed:
|
||||||
|
|
||||||
|
- `node --check frontend/assets/js/map-image-resolver.js`
|
||||||
|
- `node --check frontend/assets/js/partida-actual.js`
|
||||||
|
- `node --check frontend/assets/js/historico-partida.js`
|
||||||
|
|
||||||
|
Static/runtime validation performed:
|
||||||
|
|
||||||
|
- confirmed `historico-partida.html` does not load `current-match-weapon-icons.js`
|
||||||
|
- confirmed `partida-actual.html` still loads `current-match-weapon-icons.js`
|
||||||
|
- confirmed resolver maps `junobeachwarfare` to `junobeach-day.webp`
|
||||||
|
- confirmed resolver maps `utahbeach`, `driel` and `st marie du mont warfare` to existing local assets
|
||||||
|
- confirmed Steam button now points to the brand icon asset when present
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- No backend changes were required for this task.
|
||||||
|
- No asset files were moved, renamed or modified.
|
||||||
|
- No weapon or clan asset path was touched.
|
||||||
@@ -54,7 +54,7 @@ const EXTERNAL_PROFILE_BRANDS = Object.freeze({
|
|||||||
}),
|
}),
|
||||||
steam: Object.freeze({
|
steam: Object.freeze({
|
||||||
label: "Steam",
|
label: "Steam",
|
||||||
logoSrc: "",
|
logoSrc: "./assets/img/brands/steam.png",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -601,7 +601,7 @@ function renderExternalProfilesSection(player) {
|
|||||||
<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">
|
<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">
|
||||||
${
|
${
|
||||||
brand.logoSrc
|
brand.logoSrc
|
||||||
? `<img class="historical-player-profile-link__brand" src="${escapeHtml(brand.logoSrc)}" alt="" aria-hidden="true" decoding="async" loading="lazy" />`
|
? `<img class="historical-player-profile-link__brand" src="${escapeHtml(brand.logoSrc)}" alt="" aria-hidden="true" decoding="async" loading="lazy" onerror="this.remove();" />`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
<span>${escapeHtml(brand.label)}</span>
|
<span>${escapeHtml(brand.label)}</span>
|
||||||
@@ -807,33 +807,21 @@ function resolveMatchFactions(item, mapName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveMapImagePath(item, mapName) {
|
function resolveMapImagePath(item, mapName) {
|
||||||
const normalizedMap = normalizeLookupText(
|
const resolver = globalThis.HLL_VIETNAM_MAP_IMAGES?.resolveMapImageAsset;
|
||||||
`${item.map?.name || ""} ${item.map?.pretty_name || ""} ${mapName || ""}`,
|
if (typeof resolver !== "function") {
|
||||||
).replaceAll(" ", "");
|
return "";
|
||||||
const mapAssetByKey = {
|
}
|
||||||
carentan: "carentan-day.webp",
|
return (
|
||||||
driel: "driel-day.webp",
|
resolver({
|
||||||
elalamein: "elalamein-day.webp",
|
candidates: [
|
||||||
elsenbornridge: "elsenbornridge-day.webp",
|
item.match_id,
|
||||||
foy: "foy-day.webp",
|
item.map?.id,
|
||||||
hill400: "hill400-day.webp",
|
item.map?.name,
|
||||||
hurtgenforest: "hurtgenforest-day.webp",
|
item.map?.pretty_name,
|
||||||
kharkov: "kharkov-day.webp",
|
mapName,
|
||||||
kursk: "kursk-day.webp",
|
],
|
||||||
mortain: "mortain-day.webp",
|
})?.src || ""
|
||||||
omahabeach: "omahabeach-day.webp",
|
|
||||||
purpleheartlane: "purpleheartlane-rain.webp",
|
|
||||||
smolensk: "smolensk-day.webp",
|
|
||||||
stmariedumont: "stmariedumont-day.webp",
|
|
||||||
stmereeglise: "stmereeglise-day.webp",
|
|
||||||
tobrukdawn: "tobruk-dawn.webp",
|
|
||||||
tobruk: "tobruk-day.webp",
|
|
||||||
utahbeach: "utahbeach-day.webp",
|
|
||||||
};
|
|
||||||
const matchedKey = Object.keys(mapAssetByKey).find((key) =>
|
|
||||||
normalizedMap.includes(key),
|
|
||||||
);
|
);
|
||||||
return matchedKey ? `./assets/img/maps/${mapAssetByKey[matchedKey]}` : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLookupText(value) {
|
function normalizeLookupText(value) {
|
||||||
|
|||||||
182
frontend/assets/js/map-image-resolver.js
Normal file
182
frontend/assets/js/map-image-resolver.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
(() => {
|
||||||
|
const MAP_IMAGE_VARIANTS = Object.freeze({
|
||||||
|
carentan: Object.freeze(["day", "dusk", "night", "rain"]),
|
||||||
|
driel: Object.freeze(["dawn", "day", "night"]),
|
||||||
|
elalamein: Object.freeze(["day", "dusk"]),
|
||||||
|
elsenbornridge: Object.freeze(["dawn", "day", "dusk", "night"]),
|
||||||
|
foy: Object.freeze(["day", "night"]),
|
||||||
|
hill400: Object.freeze(["day", "dusk", "night"]),
|
||||||
|
hurtgenforest: Object.freeze(["day", "night"]),
|
||||||
|
junobeach: Object.freeze(["dawn", "day", "night"]),
|
||||||
|
kharkov: Object.freeze(["day", "night"]),
|
||||||
|
kursk: Object.freeze(["day", "night"]),
|
||||||
|
mortain: Object.freeze(["day", "dusk", "night", "overcast"]),
|
||||||
|
omahabeach: Object.freeze(["day", "dusk"]),
|
||||||
|
purpleheartlane: Object.freeze(["dawn", "day", "night", "rain"]),
|
||||||
|
remagen: Object.freeze(["day", "night"]),
|
||||||
|
smolensk: Object.freeze(["day", "dusk", "night"]),
|
||||||
|
stalingrad: Object.freeze(["day", "dusk", "night", "overcast"]),
|
||||||
|
stmariedumont: Object.freeze(["day", "night", "rain"]),
|
||||||
|
stmereeglise: Object.freeze(["dawn", "day", "night"]),
|
||||||
|
tobruk: Object.freeze(["dawn", "day", "dusk"]),
|
||||||
|
utahbeach: Object.freeze(["day", "night"]),
|
||||||
|
unknown: Object.freeze(["day"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const MAP_ALIASES = Object.freeze({
|
||||||
|
carentan: Object.freeze(["carentan"]),
|
||||||
|
driel: Object.freeze(["driel"]),
|
||||||
|
elalamein: Object.freeze(["elalamein", "el alamein"]),
|
||||||
|
elsenbornridge: Object.freeze(["elsenbornridge", "elsenborn ridge"]),
|
||||||
|
foy: Object.freeze(["foy"]),
|
||||||
|
hill400: Object.freeze(["hill400", "hill 400"]),
|
||||||
|
hurtgenforest: Object.freeze(["hurtgenforest", "hurtgen forest", "hurtgen"]),
|
||||||
|
junobeach: Object.freeze(["junobeach", "juno beach"]),
|
||||||
|
kharkov: Object.freeze(["kharkov"]),
|
||||||
|
kursk: Object.freeze(["kursk"]),
|
||||||
|
mortain: Object.freeze(["mortain"]),
|
||||||
|
omahabeach: Object.freeze(["omahabeach", "omaha beach"]),
|
||||||
|
purpleheartlane: Object.freeze(["purpleheartlane", "purple heart lane"]),
|
||||||
|
remagen: Object.freeze(["remagen"]),
|
||||||
|
smolensk: Object.freeze(["smolensk"]),
|
||||||
|
stalingrad: Object.freeze(["stalingrad"]),
|
||||||
|
stmariedumont: Object.freeze([
|
||||||
|
"stmariedumont",
|
||||||
|
"st marie du mont",
|
||||||
|
"saint marie du mont",
|
||||||
|
"sainte marie du mont",
|
||||||
|
]),
|
||||||
|
stmereeglise: Object.freeze([
|
||||||
|
"stmereeglise",
|
||||||
|
"st mere eglise",
|
||||||
|
"saint mere eglise",
|
||||||
|
"sainte mere eglise",
|
||||||
|
]),
|
||||||
|
tobruk: Object.freeze(["tobruk"]),
|
||||||
|
utahbeach: Object.freeze(["utahbeach", "utah beach"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ENVIRONMENT_ALIASES = Object.freeze({
|
||||||
|
day: Object.freeze(["day"]),
|
||||||
|
night: Object.freeze(["night"]),
|
||||||
|
dawn: Object.freeze(["dawn"]),
|
||||||
|
dusk: Object.freeze(["dusk"]),
|
||||||
|
overcast: Object.freeze(["overcast"]),
|
||||||
|
rain: Object.freeze(["rain"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const DEFAULT_ENVIRONMENT_PRIORITY = Object.freeze([
|
||||||
|
"day",
|
||||||
|
"dawn",
|
||||||
|
"dusk",
|
||||||
|
"overcast",
|
||||||
|
"rain",
|
||||||
|
"night",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ORDERED_MAP_MATCHERS = Object.freeze(
|
||||||
|
Object.entries(MAP_ALIASES)
|
||||||
|
.flatMap(([mapId, aliases]) =>
|
||||||
|
aliases.map((alias) => ({
|
||||||
|
mapId,
|
||||||
|
compactAlias: compactLookup(alias),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.sort((left, right) => right.compactAlias.length - left.compactAlias.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
function normalizeLookup(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactLookup(value) {
|
||||||
|
return normalizeLookup(value).replace(/\s+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMapId(candidates) {
|
||||||
|
const normalizedCandidates = candidates
|
||||||
|
.map((candidate) => compactLookup(candidate))
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const candidate of normalizedCandidates) {
|
||||||
|
for (const matcher of ORDERED_MAP_MATCHERS) {
|
||||||
|
if (candidate.includes(matcher.compactAlias)) {
|
||||||
|
return matcher.mapId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEnvironment(candidates) {
|
||||||
|
const combined = candidates
|
||||||
|
.map((candidate) => normalizeLookup(candidate))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
if (!combined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (const [environment, aliases] of Object.entries(ENVIRONMENT_ALIASES)) {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
if (combined.includes(alias)) {
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveVariant(mapId, requestedEnvironment) {
|
||||||
|
const variants = MAP_IMAGE_VARIANTS[mapId];
|
||||||
|
if (!Array.isArray(variants) || variants.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (requestedEnvironment && variants.includes(requestedEnvironment)) {
|
||||||
|
return requestedEnvironment;
|
||||||
|
}
|
||||||
|
for (const candidate of DEFAULT_ENVIRONMENT_PRIORITY) {
|
||||||
|
if (variants.includes(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return variants[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUnknownResult() {
|
||||||
|
const environment = resolveVariant("unknown", "day") || "day";
|
||||||
|
return {
|
||||||
|
mapId: "unknown",
|
||||||
|
environment,
|
||||||
|
src: `./assets/img/maps/unknown-${environment}.webp`,
|
||||||
|
matched: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMapImageAsset(options = {}) {
|
||||||
|
const candidates = Array.isArray(options.candidates) ? options.candidates : [];
|
||||||
|
const mapId = resolveMapId(candidates);
|
||||||
|
if (!mapId) {
|
||||||
|
return buildUnknownResult();
|
||||||
|
}
|
||||||
|
const requestedEnvironment = resolveEnvironment(candidates);
|
||||||
|
const environment = resolveVariant(mapId, requestedEnvironment);
|
||||||
|
if (!environment) {
|
||||||
|
return buildUnknownResult();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mapId,
|
||||||
|
environment,
|
||||||
|
src: `./assets/img/maps/${mapId}-${environment}.webp`,
|
||||||
|
matched: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.HLL_VIETNAM_MAP_IMAGES = Object.freeze({
|
||||||
|
normalizeLookup,
|
||||||
|
resolveMapImageAsset,
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -600,33 +600,21 @@ function renderMapHero(data, mapName, nodes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveMapImagePath(data, mapName) {
|
function resolveMapImagePath(data, mapName) {
|
||||||
const normalizedMap = normalizeLookupText(
|
const resolver = globalThis.HLL_VIETNAM_MAP_IMAGES?.resolveMapImageAsset;
|
||||||
`${data.map_id || ""} ${data.map || ""} ${data.map_pretty_name || ""} ${mapName || ""}`,
|
if (typeof resolver !== "function") {
|
||||||
).replaceAll(" ", "");
|
return "";
|
||||||
const mapAssetByKey = {
|
}
|
||||||
carentan: "carentan-day.webp",
|
return (
|
||||||
driel: "driel-day.webp",
|
resolver({
|
||||||
elalamein: "elalamein-day.webp",
|
candidates: [
|
||||||
elsenbornridge: "elsenbornridge-day.webp",
|
data.layer_id,
|
||||||
foy: "foy-day.webp",
|
data.map_id,
|
||||||
hill400: "hill400-day.webp",
|
data.map,
|
||||||
hurtgenforest: "hurtgenforest-day.webp",
|
data.map_pretty_name,
|
||||||
kharkov: "kharkov-day.webp",
|
mapName,
|
||||||
kursk: "kursk-day.webp",
|
],
|
||||||
mortain: "mortain-day.webp",
|
})?.src || ""
|
||||||
omahabeach: "omahabeach-day.webp",
|
|
||||||
purpleheartlane: "purpleheartlane-rain.webp",
|
|
||||||
smolensk: "smolensk-day.webp",
|
|
||||||
stmariedumont: "stmariedumont-day.webp",
|
|
||||||
stmereeglise: "stmereeglise-day.webp",
|
|
||||||
tobrukdawn: "tobruk-dawn.webp",
|
|
||||||
tobruk: "tobruk-day.webp",
|
|
||||||
utahbeach: "utahbeach-day.webp",
|
|
||||||
};
|
|
||||||
const matchedKey = Object.keys(mapAssetByKey).find((key) =>
|
|
||||||
normalizedMap.includes(key),
|
|
||||||
);
|
);
|
||||||
return matchedKey ? `./assets/img/maps/${mapAssetByKey[matchedKey]}` : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTrustedScoreboardUrl(data) {
|
function resolveTrustedScoreboardUrl(data) {
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./assets/js/config.js"></script>
|
<script src="./assets/js/config.js"></script>
|
||||||
|
<script src="./assets/js/map-image-resolver.js"></script>
|
||||||
<script src="./assets/js/historico-partida.js"></script>
|
<script src="./assets/js/historico-partida.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="./assets/js/config.js"></script>
|
<script src="./assets/js/config.js"></script>
|
||||||
|
<script src="./assets/js/map-image-resolver.js"></script>
|
||||||
<script src="./assets/js/current-match-weapon-icons.js"></script>
|
<script src="./assets/js/current-match-weapon-icons.js"></script>
|
||||||
<script src="./assets/js/partida-actual.js"></script>
|
<script src="./assets/js/partida-actual.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user