1+ import { readGameJourneyCompletionMetrics } from "/src/api/game-journey-completion-api-client.js" ;
12import { createServerRepositoryClient } from "/src/api/server-api-client.js" ;
3+ import { getToolBySlug } from "/src/shared/toolbox/tool-metadata-inventory.js" ;
24
35const EXCLUDED_SELECTED_GAME_TOOLS = new Set ( [ "idea-board" ] ) ;
46const STATUS_BAR_SELECTOR = "[data-toolbox-status-bar]" ;
7+ const TOOL_PROGRESS_BUCKET_BY_SLUG = Object . freeze ( {
8+ "achievements" : "Progression" ,
9+ "assets" : "Graphics" ,
10+ "audio" : "Audio" ,
11+ "audio-effects" : "Audio" ,
12+ "characters" : "Objects" ,
13+ "colors" : "Graphics" ,
14+ "community" : "Share" ,
15+ "controls" : "Controls" ,
16+ "events" : "Rules" ,
17+ "game-configuration" : "Create" ,
18+ "game-design" : "Design" ,
19+ "game-hub" : "Create" ,
20+ "game-testing" : "Play Test" ,
21+ "hitboxes" : "Objects" ,
22+ "idea-board" : "Idea" ,
23+ "input-mapping-v2" : "Controls" ,
24+ "marketplace" : "Share" ,
25+ "messages" : "Interface" ,
26+ "music" : "Audio" ,
27+ "objects" : "Objects" ,
28+ "publish" : "Publish" ,
29+ "ratings" : "Share" ,
30+ "sprites" : "Graphics" ,
31+ "tags" : "Progression" ,
32+ "text-to-speech" : "Audio" ,
33+ "videos" : "Graphics" ,
34+ "voices" : "Audio" ,
35+ "worlds" : "Worlds" ,
36+ } ) ;
537
638let repository = null ;
739let messageObserver = null ;
@@ -101,7 +133,10 @@ function createStatusBar() {
101133 message . setAttribute ( "role" , "status" ) ;
102134 center . append ( message ) ;
103135
104- inner . append ( game , center ) ;
136+ const progress = createText ( "p" , "toolbox-status-bar__progress" , "toolboxStatusProgress" ) ;
137+ progress . setAttribute ( "aria-label" , "Tool and journey progress" ) ;
138+
139+ inner . append ( game , center , progress ) ;
105140 bar . append ( inner ) ;
106141 return bar ;
107142}
@@ -233,6 +268,107 @@ function classifyToolContext(messageText, state, required) {
233268 return { kind : "action" } ;
234269}
235270
271+ function normalizeTextKey ( value ) {
272+ return String ( value || "" )
273+ . toLowerCase ( )
274+ . replace ( / [ ^ a - z 0 - 9 ] + / g, "" ) ;
275+ }
276+
277+ function formatToolSlug ( slug ) {
278+ return String ( slug || "Tool" )
279+ . split ( / [ - _ \s ] + / )
280+ . filter ( Boolean )
281+ . map ( ( part ) => `${ part . charAt ( 0 ) . toUpperCase ( ) } ${ part . slice ( 1 ) } ` )
282+ . join ( " " ) || "Tool" ;
283+ }
284+
285+ function currentToolContext ( ) {
286+ const slug = toolSlugFromPath ( mountOptions . pagePath ) ;
287+ const tool = getToolBySlug ( slug ) ;
288+ return {
289+ label : tool ?. shortLabel || tool ?. displayName || tool ?. name || formatToolSlug ( slug ) ,
290+ slug,
291+ } ;
292+ }
293+
294+ function normalizeProgressRecord ( record ) {
295+ const total = Math . max ( 0 , Number ( record ?. plannedCount ) || 0 ) ;
296+ const complete = Math . max ( 0 , Math . min ( Number ( record ?. completedCount ) || 0 , total ) ) ;
297+ const percent = Number . isFinite ( Number ( record ?. percentComplete ) )
298+ ? Math . max ( 0 , Math . min ( Number ( record . percentComplete ) , 100 ) )
299+ : total > 0
300+ ? Math . round ( ( complete / total ) * 100 )
301+ : 0 ;
302+ return {
303+ complete,
304+ percent,
305+ total,
306+ } ;
307+ }
308+
309+ function formatProgressRecord ( label , record ) {
310+ const progress = normalizeProgressRecord ( record ) ;
311+ return `${ label } ${ progress . complete } /${ progress . total } (${ progress . percent } %)` ;
312+ }
313+
314+ function findMetricForCurrentTool ( snapshot , toolContext ) {
315+ if ( toolContext . slug === "game-journey" ) {
316+ return snapshot ;
317+ }
318+
319+ const records = Array . isArray ( snapshot ?. records ) ? snapshot . records : [ ] ;
320+ const explicitBucketName = TOOL_PROGRESS_BUCKET_BY_SLUG [ toolContext . slug ] ;
321+ const explicitBucket = normalizeTextKey ( explicitBucketName ) ;
322+ const toolLabel = normalizeTextKey ( toolContext . label ) ;
323+ const toolSlug = normalizeTextKey ( toolContext . slug ) ;
324+
325+ return records . find ( ( metric ) => {
326+ const bucketName = normalizeTextKey ( metric ?. bucketName ) ;
327+ return bucketName && (
328+ bucketName === explicitBucket ||
329+ bucketName === toolLabel ||
330+ bucketName === toolSlug
331+ ) ;
332+ } ) || null ;
333+ }
334+
335+ function resolveProgressContext ( ) {
336+ const toolContext = currentToolContext ( ) ;
337+ const snapshot = readGameJourneyCompletionMetrics ( ) ;
338+ const currentMetric = findMetricForCurrentTool ( snapshot , toolContext ) ;
339+ const journeyText = formatProgressRecord ( "Journey" , snapshot ) ;
340+
341+ if ( ! currentMetric ) {
342+ return {
343+ state : "unmapped" ,
344+ text : `${ toolContext . label } progress unavailable | ${ journeyText } ` ,
345+ } ;
346+ }
347+
348+ return {
349+ state : "active" ,
350+ text : `${ formatProgressRecord ( toolContext . label , currentMetric ) } | ${ journeyText } ` ,
351+ } ;
352+ }
353+
354+ function renderProgressContext ( bar ) {
355+ const progress = bar . querySelector ( "[data-toolbox-status-progress]" ) ;
356+ if ( ! progress ) {
357+ return ;
358+ }
359+
360+ try {
361+ const context = resolveProgressContext ( ) ;
362+ bar . dataset . toolboxProgressState = context . state ;
363+ progress . textContent = context . text ;
364+ progress . removeAttribute ( "title" ) ;
365+ } catch ( error ) {
366+ bar . dataset . toolboxProgressState = "error" ;
367+ progress . textContent = "Progress unavailable" ;
368+ progress . removeAttribute ( "title" ) ;
369+ }
370+ }
371+
236372function renderSelectedGame ( bar , selectedGame , state , messageText ) {
237373 const required = pageRequiresSelectedGame ( ) ;
238374 const name = bar . querySelector ( "[data-toolbox-selected-game-name]" ) ;
@@ -247,6 +383,7 @@ function renderSelectedGame(bar, selectedGame, state, messageText) {
247383 bar . dataset . selectedGameState = state ;
248384 bar . dataset . selectedGameRequired = String ( required ) ;
249385 bar . dataset . toolboxStatusContextKind = context . kind ;
386+ renderProgressContext ( bar ) ;
250387
251388 if ( selectedGame ) {
252389 name . textContent = selectedGame . name ;
0 commit comments