Skip to content

Commit fa1395b

Browse files
author
DavidQ
committed
Implemented. Games now mirrors the samples-style tool roundtrip flow.
### What changed - Added **Tool filter** to Games hub UI: - [games/index.html](C:\Users\davidq\Documents\GitHub\HTML-JavaScript-Gaming\games\index.html) - [games/index.render.js](C:\Users\davidq\Documents\GitHub\HTML-JavaScript-Gaming\games\index.render.js) - Added **per-game tool roundtrip links** (metadata-driven) that pass: - `gameId`, `gameTitle`, `gameHref` - `workspaceHref` (to Workspace Manager with `?game=...`) - `returnTo` (to Games hub with preselected tool filter) - Added styling for game tool roundtrip section: - [games/index.css](C:\Users\davidq\Documents\GitHub\HTML-JavaScript-Gaming\games\index.css) - Added **return line in tools header** when launched from a game: - Shows `Game Source` - `Return to Games` - `Open Game` - `Open in Workspace Manager` - [tools/shared/platformShell.js](C:\Users\davidq\Documents\GitHub\HTML-JavaScript-Gaming\tools\shared\platformShell.js) - [tools/shared/platformShell.css](C:\Users\davidq\Documents\GitHub\HTML-JavaScript-Gaming\tools\shared\platformShell.css) - Added explicit game `toolHints` for all launchable games (11/11) so filter + links are metadata-driven: - [games/metadata/games.index.metadata.json](C:\Users\davidq\Documents\GitHub\HTML-JavaScript-Gaming\games\metadata\games.index.metadata.json) ### Validation run - `node --check games/index.render.js` passed - `node --check tools/shared/platformShell.js` passed - JSON parse check for games metadata passed Commit comment: `Games hub now has metadata-driven Tool filtering and game-to-tool roundtrip links with workspace + return context, and tools header renders a game return line when launched from games.`
1 parent eb167e0 commit fa1395b

6 files changed

Lines changed: 276 additions & 20 deletions

File tree

games/index.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,30 @@
116116
margin: 6px 0 8px;
117117
}
118118

119+
.game-tool-roundtrip {
120+
margin: 10px 0 0;
121+
}
122+
123+
.game-tool-roundtrip h4,
124+
.game-tool-roundtrip p,
125+
.game-tool-roundtrip li,
126+
.game-tool-roundtrip a {
127+
color: #ffffff;
128+
}
129+
130+
.game-tool-roundtrip h4 {
131+
margin: 0 0 6px;
132+
}
133+
134+
.game-tool-roundtrip p {
135+
margin: 0 0 6px;
136+
}
137+
138+
.game-tool-roundtrip ul {
139+
margin: 0;
140+
padding-left: 18px;
141+
}
142+
119143
.hub-page-games .pin-label {
120144
margin-right: 6px;
121145
}

games/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ <h2>Pinned Games</h2>
3333

3434
<section class="content-section">
3535
<h2>Filter Games</h2>
36-
<p>Filter by level, class, tag, or free-text search.</p>
36+
<p>Filter by level, class, tool, tag, or free-text search.</p>
3737
<div class="games-filter-grid">
3838
<div class="games-filter-field">
3939
<label for="games-filter-level">Level</label>
@@ -53,6 +53,12 @@ <h2>Filter Games</h2>
5353
<option value="">All tags</option>
5454
</select>
5555
</div>
56+
<div class="games-filter-field">
57+
<label for="games-filter-tool">Tool</label>
58+
<select id="games-filter-tool">
59+
<option value="">All tools</option>
60+
</select>
61+
</div>
5662
<div class="games-filter-field">
5763
<label for="games-filter-search">Search</label>
5864
<input id="games-filter-search" type="text" placeholder="arcade, physics, planned..." autocomplete="off" />

games/index.render.js

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getToolRegistry } from "../tools/toolRegistry.js";
2+
13
const METADATA_PATH = "./metadata/games.index.metadata.json";
24
const GAMES_PINNED_KEY = "games-index-pinned";
35

@@ -50,6 +52,79 @@ function escapeHtml(value) {
5052
.replaceAll("'", "&#39;");
5153
}
5254

55+
function toStandaloneToolHref(entryPoint) {
56+
const normalized = String(entryPoint || "").replace(/^\.?\/*/, "");
57+
return normalized ? `/tools/${encodeURI(normalized)}` : "";
58+
}
59+
60+
function normalizeGameHref(value) {
61+
const href = normalize(value).replace(/\\/g, "/");
62+
if (!href || href.includes("..") || !href.startsWith("/games/")) {
63+
return "";
64+
}
65+
return href;
66+
}
67+
68+
function buildWorkspaceManagerHref(gameId) {
69+
const normalizedGameId = normalize(gameId);
70+
return normalizedGameId
71+
? `/tools/Workspace%20Manager/index.html?game=${encodeURIComponent(normalizedGameId)}`
72+
: "/tools/Workspace%20Manager/index.html";
73+
}
74+
75+
function buildReturnHref(toolId) {
76+
const normalizedToolId = normalizeToken(toolId);
77+
return normalizedToolId
78+
? `/games/index.html?tool=${encodeURIComponent(normalizedToolId)}`
79+
: "/games/index.html";
80+
}
81+
82+
function buildToolTokens(toolHints, toolLabelMap) {
83+
const deduped = [...new Set(asArray(toolHints).map((entry) => normalizeToken(entry)).filter(Boolean))];
84+
return deduped
85+
.filter((toolId) => toolId !== "workspace-manager")
86+
.map((toolId) => ({
87+
value: toolId,
88+
label: toolLabelMap.get(toolId) || toolId
89+
}))
90+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
91+
}
92+
93+
function buildRoundtripLinks(game, toolRegistryMap) {
94+
const orderedToolHints = asArray(game?.toolHints)
95+
.map((entry) => normalizeToken(entry))
96+
.filter(Boolean)
97+
.filter((toolId) => toolId !== "workspace-manager");
98+
const dedupedToolHints = [...new Set(orderedToolHints)];
99+
const links = [];
100+
101+
dedupedToolHints.forEach((toolId) => {
102+
const tool = toolRegistryMap.get(toolId);
103+
if (!tool) {
104+
return;
105+
}
106+
const baseHref = toStandaloneToolHref(tool.entryPoint);
107+
if (!baseHref) {
108+
return;
109+
}
110+
const query = new URLSearchParams();
111+
query.set("gameId", game.id);
112+
if (game.title) {
113+
query.set("gameTitle", game.title);
114+
}
115+
if (game.href) {
116+
query.set("gameHref", game.href);
117+
}
118+
query.set("workspaceHref", buildWorkspaceManagerHref(game.id));
119+
query.set("returnTo", buildReturnHref(tool.id));
120+
const href = `${baseHref}?${query.toString()}`;
121+
const label = `Open ${normalize(tool.displayName) || normalize(tool.name) || toolId}`;
122+
links.push({ toolId, href, label });
123+
});
124+
125+
return links;
126+
}
127+
53128
function statusLabel(status) {
54129
switch (status) {
55130
case "playable":
@@ -83,7 +158,7 @@ function writePinnedSet(pinnedSet) {
83158
window.localStorage.setItem(GAMES_PINNED_KEY, JSON.stringify([...pinnedSet].sort()));
84159
}
85160

86-
function buildRows(metadata, pinnedSet) {
161+
function buildRows(metadata, pinnedSet, toolLabelMap, toolRegistryMap) {
87162
const rows = asArray(metadata?.games)
88163
.map((game) => {
89164
const id = normalize(game?.id);
@@ -96,17 +171,27 @@ function buildRows(metadata, pinnedSet) {
96171
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
97172
const tags = [...new Set(asArray(game?.tags).map((value) => normalizeTag(value)).filter(Boolean))]
98173
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
174+
const href = normalizeGameHref(game?.href);
175+
const toolTokens = buildToolTokens(game?.toolHints, toolLabelMap);
176+
const roundtripLinks = buildRoundtripLinks({
177+
id,
178+
title,
179+
href,
180+
toolHints: game?.toolHints
181+
}, toolRegistryMap);
99182
return {
100183
id,
101184
title,
102185
description: normalize(game?.description) || "No description available.",
103186
level,
104187
status: normalize(game?.status) || "planned",
105188
classValues,
189+
toolTokens,
190+
roundtripLinks,
106191
tags,
107192
preview: normalize(game?.preview),
108-
href: normalize(game?.href),
109-
workspaceHref: normalize(game?.href) ? `/tools/Workspace%20Manager/index.html?game=${encodeURIComponent(id)}` : "",
193+
href,
194+
workspaceHref: href ? buildWorkspaceManagerHref(id) : "",
110195
sampleTrack: game?.sampleTrack === true,
111196
debugShowcase: game?.debugShowcase === true,
112197
requiresService: game?.requiresService === true
@@ -118,8 +203,13 @@ function buildRows(metadata, pinnedSet) {
118203

119204
const levels = [...new Set(rows.map((row) => row.level))].sort(sortLevels);
120205
const classes = [...new Set(rows.flatMap((row) => row.classValues))].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
206+
const tools = [...new Map(
207+
rows.flatMap((row) => row.toolTokens).map((token) => [token.value, token.label])
208+
).entries()]
209+
.map(([value, label]) => ({ value, label }))
210+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
121211
const tags = [...new Set(rows.flatMap((row) => row.tags))].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
122-
return { rows, levels, classes, tags };
212+
return { rows, levels, classes, tools, tags };
123213
}
124214

125215
function filterRows(rows, state) {
@@ -131,13 +221,17 @@ function filterRows(rows, state) {
131221
if (state.classValue && !row.classValues.includes(state.classValue)) {
132222
return false;
133223
}
224+
if (state.toolId && !row.toolTokens.some((token) => token.value === state.toolId)) {
225+
return false;
226+
}
134227
if (state.tag && !row.tags.includes(state.tag)) {
135228
return false;
136229
}
137230
if (!query) {
138231
return true;
139232
}
140-
const haystack = `${row.level} ${row.title} ${row.description} ${row.classValues.join(" ")} ${row.tags.join(" ")}`.toLowerCase();
233+
const toolText = row.toolTokens.map((token) => `${token.label} ${token.value}`).join(" ");
234+
const haystack = `${row.level} ${row.title} ${row.description} ${row.classValues.join(" ")} ${toolText} ${row.tags.join(" ")}`.toLowerCase();
141235
return haystack.includes(query);
142236
});
143237
}
@@ -186,13 +280,25 @@ function renderCard(row, instanceKey = "main") {
186280
const launchActions = row.workspaceHref
187281
? `<p class="game-launch-actions"><a class="game-title-link" href="${escapeHtml(row.workspaceHref)}">Open In Workspace Manager</a></p>`
188282
: "";
283+
const roundtripSection = Array.isArray(row.roundtripLinks) && row.roundtripLinks.length > 0
284+
? `
285+
<section class="game-tool-roundtrip">
286+
<h4>Tool Roundtrip Links</h4>
287+
<p>Open game-related tools with workspace context and a return path back to Games.</p>
288+
<ul>
289+
${row.roundtripLinks.map((entry) => `<li><a href="${escapeHtml(entry.href)}">${escapeHtml(entry.label)}</a></li>`).join("")}
290+
</ul>
291+
</section>
292+
`
293+
: "";
189294

190295
article.innerHTML = `
191296
${titleHtml}
192297
<div class="game-badges">${badges}</div>
193298
${previewHtml}
194299
<p>${escapeHtml(row.description)}</p>
195300
${launchActions}
301+
${roundtripSection}
196302
<p>Classes: ${escapeHtml(classText)}</p>
197303
<p>Tags: ${escapeHtml(tagText)}</p>
198304
${row.requiresService ? '<p class="game-service-note">Requires background service.</p>' : ""}
@@ -262,9 +368,10 @@ export async function initGamesIndex() {
262368
const statusNode = document.getElementById("games-filter-status");
263369
const levelSelect = document.getElementById("games-filter-level");
264370
const classSelect = document.getElementById("games-filter-class");
371+
const toolSelect = document.getElementById("games-filter-tool");
265372
const tagSelect = document.getElementById("games-filter-tag");
266373
const searchInput = document.getElementById("games-filter-search");
267-
if (!container || !pinnedContainer || !statusNode || !levelSelect || !classSelect || !tagSelect || !searchInput) {
374+
if (!container || !pinnedContainer || !statusNode || !levelSelect || !classSelect || !toolSelect || !tagSelect || !searchInput) {
268375
return;
269376
}
270377

@@ -274,17 +381,39 @@ export async function initGamesIndex() {
274381
return;
275382
}
276383
const metadata = await response.json();
384+
const toolRegistry = getToolRegistry();
385+
const toolLabelMap = new Map(
386+
toolRegistry
387+
.filter((tool) => tool.id !== "workspace-manager")
388+
.map((tool) => [normalizeToken(tool.id), normalize(tool.displayName) || normalize(tool.name) || normalize(tool.id)])
389+
.filter((entry) => entry[0] && entry[1])
390+
);
391+
const toolRegistryMap = new Map(
392+
toolRegistry
393+
.filter((tool) => tool.id !== "workspace-manager")
394+
.map((tool) => [normalizeToken(tool.id), tool])
395+
.filter((entry) => entry[0] && entry[1])
396+
);
277397
let pinnedSet = readPinnedSet();
278-
let model = buildRows(metadata, pinnedSet);
398+
let model = buildRows(metadata, pinnedSet, toolLabelMap, toolRegistryMap);
279399
setSelect(levelSelect, model.levels, (value) => value);
280400
setSelect(classSelect, model.classes, (value) => value.split("/").at(-1) || value);
401+
setSelect(toolSelect, model.tools.map((entry) => entry.value), (value) => {
402+
const found = model.tools.find((entry) => entry.value === value);
403+
return found?.label || value;
404+
});
281405
setSelect(tagSelect, model.tags, (value) => value);
406+
const toolQuery = normalizeToken(new URLSearchParams(window.location.search).get("tool"));
407+
if (toolQuery && model.tools.some((entry) => entry.value === toolQuery)) {
408+
toolSelect.value = toolQuery;
409+
}
282410

283411
const apply = () => {
284-
model = buildRows(metadata, pinnedSet);
412+
model = buildRows(metadata, pinnedSet, toolLabelMap, toolRegistryMap);
285413
const state = {
286414
level: normalize(levelSelect.value),
287415
classValue: normalize(classSelect.value),
416+
toolId: normalizeToken(toolSelect.value),
288417
tag: normalize(tagSelect.value),
289418
query: normalize(searchInput.value)
290419
};
@@ -312,6 +441,7 @@ export async function initGamesIndex() {
312441

313442
levelSelect.addEventListener("change", apply);
314443
classSelect.addEventListener("change", apply);
444+
toolSelect.addEventListener("change", apply);
315445
tagSelect.addEventListener("change", apply);
316446
searchInput.addEventListener("input", apply);
317447
container.addEventListener("change", handlePin);

0 commit comments

Comments
 (0)