1+ import { getToolRegistry } from "../tools/toolRegistry.js" ;
2+
13const METADATA_PATH = "./metadata/games.index.metadata.json" ;
24const GAMES_PINNED_KEY = "games-index-pinned" ;
35
@@ -50,6 +52,79 @@ function escapeHtml(value) {
5052 . replaceAll ( "'" , "'" ) ;
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+
53128function 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
125215function 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