@@ -11,8 +11,10 @@ import {
1111 getActiveToolRegistry ,
1212 getToolRegistryApiDiagnostic ,
1313} from "../../../../toolbox/tool-registry-api-client.js" ;
14+ import { createServerRepositoryClient } from "../../../../src/api/server-api-client.js" ;
1415
1516const repository = createGameJourneyApiRepository ( ) ;
17+ const objectsRepository = createServerRepositoryClient ( "objects" ) ;
1618const registryDiagnostic = getToolRegistryApiDiagnostic ( ) ;
1719const registryTools = getActiveToolRegistry ( ) ;
1820const params = new URLSearchParams ( window . location . search ) ;
@@ -51,6 +53,9 @@ const diagnostics = document.querySelector("[data-journey-diagnostics]");
5153const searchInput = document . querySelector ( "[data-journey-search-input]" ) ;
5254const searchStatus = document . querySelector ( "[data-journey-search-status]" ) ;
5355const completionMetrics = document . querySelector ( "[data-journey-completion-metrics]" ) ;
56+ const objectsReadinessTable = document . querySelector ( "[data-journey-objects-readiness]" ) ;
57+ const objectsReadinessStatus = document . querySelector ( "[data-journey-objects-readiness-status]" ) ;
58+ const objectsReviewChecklist = document . querySelector ( "[data-journey-objects-review-checklist]" ) ;
5459const recommendedTargets = document . querySelector ( "[data-journey-recommended-targets]" ) ;
5560const recommendedTargetStatus = document . querySelector ( "[data-journey-target-status]" ) ;
5661const SUMMARY_TABLE_COLUMN_COUNT = 13 ;
@@ -88,6 +93,54 @@ let routeForcesNoActiveGame = false;
8893const recommendedTargetValues = new Map (
8994 GAME_JOURNEY_RECOMMENDED_TARGETS . map ( ( target ) => [ target . key , target . suggestedCount ] ) ,
9095) ;
96+ const OBJECTS_COMPLETION_BUCKET_KEY = "006-objects" ;
97+ const OBJECTS_STAGE_CRITERIA = Object . freeze ( [
98+ Object . freeze ( {
99+ key : "saved-objects" ,
100+ label : "At least one object is saved for the current game." ,
101+ } ) ,
102+ Object . freeze ( {
103+ key : "named-typed" ,
104+ label : "Every saved object has a name, type, and starting state." ,
105+ } ) ,
106+ Object . freeze ( {
107+ key : "interactive-object" ,
108+ label : "At least one object represents something the player can use, collect, avoid, or reach." ,
109+ } ) ,
110+ Object . freeze ( {
111+ key : "render-links" ,
112+ label : "Sprite-rendered objects have a sprite asset reference." ,
113+ } ) ,
114+ Object . freeze ( {
115+ key : "owner-review-details" ,
116+ label : "Object Details include Product Owner review context such as description, tags, or defaults." ,
117+ } ) ,
118+ ] ) ;
119+ const OBJECTS_REVIEW_CHECKLIST = Object . freeze ( [
120+ "Confirm the object list matches the current Game Hub game." ,
121+ "Confirm each object name and type is understandable to a Creator." ,
122+ "Confirm sprite, audio, and message references are resolved or intentionally empty." ,
123+ "Confirm at least one meaningful player-facing object exists." ,
124+ "Confirm Objects validation is clean before expanding later gameplay work." ,
125+ ] ) ;
126+ const INTERACTIVE_OBJECT_ROLES = Object . freeze ( [
127+ "collectible" ,
128+ "enemy" ,
129+ "goal" ,
130+ "hazard" ,
131+ "hero" ,
132+ "projectile" ,
133+ ] ) ;
134+ const INTERACTIVE_OBJECT_TRAITS = Object . freeze ( [
135+ "collectible" ,
136+ "goal" ,
137+ "hazard" ,
138+ "movable" ,
139+ "playerControlled" ,
140+ "scores" ,
141+ ] ) ;
142+ let objectsReadinessSnapshot = null ;
143+ let objectsReadinessDiagnostic = "" ;
91144
92145function currentRecommendedTargets ( ) {
93146 const result = repository . listRecommendedTargets ( ) ;
@@ -135,6 +188,10 @@ function routedNotes(filterId) {
135188 return routeForcesNoActiveGame ? [ ] : repository . listNotes ( filterId ) ;
136189}
137190
191+ function normalizeText ( value ) {
192+ return String ( value || "" ) . trim ( ) ;
193+ }
194+
138195function createElement ( tagName , options = { } ) {
139196 const element = document . createElement ( tagName ) ;
140197 if ( options . className ) {
@@ -146,6 +203,120 @@ function createElement(tagName, options = {}) {
146203 return element ;
147204}
148205
206+ function objectDetails ( object = { } ) {
207+ return object . details && typeof object . details === "object" ? object . details : { } ;
208+ }
209+
210+ function objectHasNameTypeAndState ( object = { } ) {
211+ return Boolean ( normalizeText ( object . name ) && normalizeText ( object . role || object . type ) && normalizeText ( object . state ) ) ;
212+ }
213+
214+ function normalizedObjectRole ( object = { } ) {
215+ return normalizeText ( object . role || object . type ) . toLowerCase ( ) ;
216+ }
217+
218+ function normalizedObjectTraits ( object = { } ) {
219+ return Array . isArray ( object . traits )
220+ ? object . traits . map ( normalizeText ) . filter ( Boolean )
221+ : [ ] ;
222+ }
223+
224+ function objectIsInteractive ( object = { } ) {
225+ const role = normalizedObjectRole ( object ) ;
226+ if ( INTERACTIVE_OBJECT_ROLES . includes ( role ) ) {
227+ return true ;
228+ }
229+ const traits = normalizedObjectTraits ( object ) ;
230+ return traits . some ( ( trait ) => INTERACTIVE_OBJECT_TRAITS . includes ( trait ) ) ;
231+ }
232+
233+ function objectSpriteReference ( object = { } ) {
234+ const details = objectDetails ( object ) ;
235+ return normalizeText ( object . render ?. assetKey || details . spriteReference ) ;
236+ }
237+
238+ function objectHasRenderReference ( object = { } ) {
239+ return normalizeText ( object . render ?. type ) !== "Sprite" || Boolean ( objectSpriteReference ( object ) ) ;
240+ }
241+
242+ function objectHasOwnerReviewDetails ( object = { } ) {
243+ const details = objectDetails ( object ) ;
244+ return Boolean (
245+ normalizeText ( details . description ) ||
246+ normalizeText ( details . defaultValues ) ||
247+ normalizeText ( details . spriteReference ) ||
248+ normalizeText ( details . audioReference ) ||
249+ normalizeText ( details . messageReference ) ||
250+ ( Array . isArray ( details . tags ) && details . tags . length > 0 )
251+ ) ;
252+ }
253+
254+ function evaluateObjectsStageReadiness ( objects = [ ] ) {
255+ const savedObjects = Array . isArray ( objects ) ? objects : [ ] ;
256+ const hasObjects = savedObjects . length > 0 ;
257+ const criteriaByKey = {
258+ "saved-objects" : hasObjects ,
259+ "named-typed" : hasObjects && savedObjects . every ( objectHasNameTypeAndState ) ,
260+ "interactive-object" : savedObjects . some ( objectIsInteractive ) ,
261+ "render-links" : hasObjects && savedObjects . every ( objectHasRenderReference ) ,
262+ "owner-review-details" : savedObjects . some ( objectHasOwnerReviewDetails ) ,
263+ } ;
264+ const criteria = OBJECTS_STAGE_CRITERIA . map ( ( criterion ) => ( {
265+ ...criterion ,
266+ complete : Boolean ( criteriaByKey [ criterion . key ] ) ,
267+ } ) ) ;
268+ const completedCriteria = criteria . filter ( ( criterion ) => criterion . complete ) . length ;
269+ return {
270+ available : true ,
271+ completedCriteria,
272+ criteria,
273+ meaningful : completedCriteria >= 3 ,
274+ objectCount : savedObjects . length ,
275+ ready : completedCriteria === criteria . length ,
276+ totalCriteria : criteria . length ,
277+ } ;
278+ }
279+
280+ function unavailableObjectsStageReadiness ( message ) {
281+ return {
282+ available : false ,
283+ completedCriteria : 0 ,
284+ criteria : OBJECTS_STAGE_CRITERIA . map ( ( criterion ) => ( {
285+ ...criterion ,
286+ complete : false ,
287+ } ) ) ,
288+ meaningful : false ,
289+ objectCount : 0 ,
290+ ready : false ,
291+ totalCriteria : OBJECTS_STAGE_CRITERIA . length ,
292+ unavailableMessage : message ,
293+ } ;
294+ }
295+
296+ function objectRepositoryErrorMessage ( result ) {
297+ if ( result ?. error || objectsRepository . __apiDiagnostic ?. ( ) ) {
298+ return "Objects readiness is temporarily unavailable. Continue building while the API refreshes." ;
299+ }
300+ return "Objects readiness is temporarily unavailable. Continue building while the API refreshes." ;
301+ }
302+
303+ function refreshObjectsReadinessSnapshot ( ) {
304+ const activeGame = routedActiveGame ( ) ;
305+ if ( ! activeGame ) {
306+ objectsReadinessDiagnostic = "Open a game in Game Hub before checking Objects readiness." ;
307+ objectsReadinessSnapshot = unavailableObjectsStageReadiness ( objectsReadinessDiagnostic ) ;
308+ return ;
309+ }
310+ const result = objectsRepository . listObjects ( activeGame . id || activeGame . key ) ;
311+ if ( Array . isArray ( result ) ) {
312+ objectsReadinessDiagnostic = "" ;
313+ objectsReadinessSnapshot = evaluateObjectsStageReadiness ( result ) ;
314+ return ;
315+ }
316+ objectsReadinessDiagnostic = objectRepositoryErrorMessage ( result ) ;
317+ objectsReadinessSnapshot = unavailableObjectsStageReadiness ( objectsReadinessDiagnostic ) ;
318+ }
319+
149320function formatDate ( value ) {
150321 const date = new Date ( value ) ;
151322 if ( Number . isNaN ( date . getTime ( ) ) ) {
@@ -952,6 +1123,52 @@ function formatCompletionFocusStatus(metric) {
9521123 return metric . active ? "Active focus" : "Planning context" ;
9531124}
9541125
1126+ function metricPercentComplete ( completedCount , plannedCount ) {
1127+ if ( ! plannedCount ) {
1128+ return 0 ;
1129+ }
1130+ return Math . round ( ( completedCount / plannedCount ) * 100 ) ;
1131+ }
1132+
1133+ function recalculateCompletionSnapshot ( snapshot , records ) {
1134+ const plannedCount = records . reduce ( ( total , metric ) => total + metric . plannedCount , 0 ) ;
1135+ const completedCount = records . reduce ( ( total , metric ) => total + metric . completedCount , 0 ) ;
1136+ const activeCount = records . filter ( ( metric ) => metric . active ) . length ;
1137+ return {
1138+ ...snapshot ,
1139+ activeCount,
1140+ completedCount,
1141+ inactiveCount : records . length - activeCount ,
1142+ percentComplete : metricPercentComplete ( completedCount , plannedCount ) ,
1143+ plannedCount,
1144+ records,
1145+ } ;
1146+ }
1147+
1148+ function completionSnapshotWithObjectsReadiness ( snapshot ) {
1149+ if ( ! snapshot ?. records || ! objectsReadinessSnapshot ?. available || objectsReadinessSnapshot . completedCriteria <= 0 ) {
1150+ return snapshot ;
1151+ }
1152+ const records = snapshot . records . map ( ( metric ) => {
1153+ if ( metric . bucketKey !== OBJECTS_COMPLETION_BUCKET_KEY ) {
1154+ return metric ;
1155+ }
1156+ const plannedCount = metric . plannedCount || objectsReadinessSnapshot . totalCriteria ;
1157+ const completedCount = Math . min (
1158+ plannedCount ,
1159+ Math . max ( metric . completedCount , objectsReadinessSnapshot . completedCriteria ) ,
1160+ ) ;
1161+ return {
1162+ ...metric ,
1163+ completedCount,
1164+ percentComplete : metricPercentComplete ( completedCount , plannedCount ) ,
1165+ plannedCount,
1166+ status : metric . active ? "active" : metric . status ,
1167+ } ;
1168+ } ) ;
1169+ return recalculateCompletionSnapshot ( snapshot , records ) ;
1170+ }
1171+
9551172function renderCompletionMetrics ( ) {
9561173 if ( ! completionMetrics ) {
9571174 return ;
@@ -967,7 +1184,8 @@ function renderCompletionMetrics() {
9671184 return ;
9681185 }
9691186
970- const records = completionMetricsSnapshot ?. records || [ ] ;
1187+ const snapshot = completionSnapshotWithObjectsReadiness ( completionMetricsSnapshot ) ;
1188+ const records = snapshot ?. records || [ ] ;
9711189 if ( ! records . length ) {
9721190 completionMetrics . append ( createElement ( "p" , { text : "No Journey progress targets are set yet. Add suggested targets to start tracking progress." } ) ) ;
9731191 return ;
@@ -977,16 +1195,16 @@ function renderCompletionMetrics() {
9771195 dashboard . dataset . journeyProgressDashboard = "" ;
9781196 const summary = createElement ( "p" , {
9791197 className : "status" ,
980- text : `Journey progress: ${ completionMetricsSnapshot . completedCount } of ${ completionMetricsSnapshot . plannedCount } planned items done (${ completionMetricsSnapshot . percentComplete } %). ${ completionMetricsSnapshot . activeCount } sections are active focus areas; ${ completionMetricsSnapshot . inactiveCount } are planning context.` ,
1198+ text : `Journey progress: ${ snapshot . completedCount } of ${ snapshot . plannedCount } planned items done (${ snapshot . percentComplete } %). ${ snapshot . activeCount } sections are active focus areas; ${ snapshot . inactiveCount } are planning context.` ,
9811199 } ) ;
9821200 const overallHeading = createElement ( "h3" , { text : "Overall Progress" } ) ;
9831201 const overallGrid = createElement ( "div" , { className : "grid cols-2" } ) ;
9841202 overallGrid . dataset . journeyOverallProgress = "" ;
9851203 [
986- [ "Overall" , `${ completionMetricsSnapshot . percentComplete } %` ] ,
987- [ "Done" , String ( completionMetricsSnapshot . completedCount ) ] ,
988- [ "Planned" , String ( completionMetricsSnapshot . plannedCount ) ] ,
989- [ "Active Focus" , String ( completionMetricsSnapshot . activeCount ) ] ,
1204+ [ "Overall" , `${ snapshot . percentComplete } %` ] ,
1205+ [ "Done" , String ( snapshot . completedCount ) ] ,
1206+ [ "Planned" , String ( snapshot . plannedCount ) ] ,
1207+ [ "Active Focus" , String ( snapshot . activeCount ) ] ,
9901208 ] . forEach ( ( [ label , value ] ) => {
9911209 const stat = createElement ( "article" , { className : "mini-stat mini-stat--inline" } ) ;
9921210 stat . append (
@@ -1062,7 +1280,7 @@ function renderCompletionMetrics() {
10621280 const insightHeading = createElement ( "h3" , { text : "What To Do Next" } ) ;
10631281 const insightList = createElement ( "ul" ) ;
10641282 insightList . dataset . journeyCompletionInsights = "" ;
1065- completionInsightMessages ( completionMetricsSnapshot , records , mostComplete , leastComplete ) . forEach ( ( message ) => {
1283+ completionInsightMessages ( snapshot , records , mostComplete , leastComplete ) . forEach ( ( message ) => {
10661284 insightList . append ( createElement ( "li" , { text : message } ) ) ;
10671285 } ) ;
10681286
@@ -1082,6 +1300,47 @@ function renderCompletionMetrics() {
10821300 completionMetrics . append ( dashboard ) ;
10831301}
10841302
1303+ function objectsReadinessStatusText ( snapshot ) {
1304+ if ( ! snapshot ?. available ) {
1305+ return snapshot ?. unavailableMessage || "Objects readiness is temporarily unavailable." ;
1306+ }
1307+ if ( snapshot . objectCount === 0 ) {
1308+ return "Objects readiness: no saved objects for this game yet." ;
1309+ }
1310+ if ( snapshot . ready ) {
1311+ return `Objects readiness: ${ snapshot . completedCriteria } of ${ snapshot . totalCriteria } criteria complete. Product Owner review can start.` ;
1312+ }
1313+ if ( snapshot . meaningful ) {
1314+ return `Objects readiness: ${ snapshot . completedCriteria } of ${ snapshot . totalCriteria } criteria complete. Journey progress now reflects meaningful Objects work.` ;
1315+ }
1316+ return `Objects readiness: ${ snapshot . completedCriteria } of ${ snapshot . totalCriteria } criteria complete. Continue Objects before treating this stage as ready.` ;
1317+ }
1318+
1319+ function renderObjectsReadiness ( ) {
1320+ const snapshot = objectsReadinessSnapshot || unavailableObjectsStageReadiness ( "Objects readiness is waiting for API data." ) ;
1321+ if ( objectsReadinessStatus ) {
1322+ objectsReadinessStatus . textContent = objectsReadinessStatusText ( snapshot ) ;
1323+ }
1324+ if ( objectsReadinessTable ) {
1325+ objectsReadinessTable . replaceChildren ( ) ;
1326+ snapshot . criteria . forEach ( ( criterion ) => {
1327+ const row = createElement ( "tr" ) ;
1328+ row . dataset . journeyObjectsCriterion = criterion . key ;
1329+ row . append (
1330+ createElement ( "td" , { text : criterion . label } ) ,
1331+ createElement ( "td" , { text : criterion . complete ? "PASS" : "Needs work" } ) ,
1332+ ) ;
1333+ objectsReadinessTable . append ( row ) ;
1334+ } ) ;
1335+ }
1336+ if ( objectsReviewChecklist ) {
1337+ objectsReviewChecklist . replaceChildren ( ) ;
1338+ OBJECTS_REVIEW_CHECKLIST . forEach ( ( item ) => {
1339+ objectsReviewChecklist . append ( createElement ( "li" , { text : item } ) ) ;
1340+ } ) ;
1341+ }
1342+ }
1343+
10851344function normalizeTargetCount ( value ) {
10861345 const parsed = Number ( value ) ;
10871346 if ( ! Number . isFinite ( parsed ) ) {
@@ -1474,6 +1733,7 @@ function render() {
14741733 renderStatScope ( selectedStatsNote , notes ) ;
14751734 renderStats ( statCounts ) ;
14761735 renderCompletionMetrics ( ) ;
1736+ renderObjectsReadiness ( ) ;
14771737 renderRecommendedTargets ( ) ;
14781738 renderSuggestedTools ( displayNote ) ;
14791739 renderRecentActivity ( ) ;
@@ -1788,6 +2048,16 @@ recommendedTargets?.addEventListener("input", (event) => {
17882048 }
17892049} ) ;
17902050
2051+ window . addEventListener ( "focus" , ( ) => {
2052+ refreshObjectsReadinessSnapshot ( ) ;
2053+ render ( ) ;
2054+ } ) ;
2055+
2056+ window . addEventListener ( "pageshow" , ( ) => {
2057+ refreshObjectsReadinessSnapshot ( ) ;
2058+ render ( ) ;
2059+ } ) ;
2060+
17912061addNoteButton ?. addEventListener ( "click" , ( ) => {
17922062 if ( redirectGuestWriteAction ( noteStatus ) ) {
17932063 return ;
@@ -1821,4 +2091,5 @@ addTypeButton?.addEventListener("click", () => {
18212091renderStatusOptions ( ) ;
18222092refreshCompletionMetricsSnapshot ( ) ;
18232093applyInitialGameRoute ( ) ;
2094+ refreshObjectsReadinessSnapshot ( ) ;
18242095render ( ) ;
0 commit comments