Skip to content

Commit 54b4c4a

Browse files
committed
Merge PR_26179_ALFA_014 Objects Journey Readiness
2 parents 22464cc + 2050352 commit 54b4c4a

15 files changed

Lines changed: 1539 additions & 697 deletions

assets/toolbox/game-journey/js/index.js

Lines changed: 278 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1516
const repository = createGameJourneyApiRepository();
17+
const objectsRepository = createServerRepositoryClient("objects");
1618
const registryDiagnostic = getToolRegistryApiDiagnostic();
1719
const registryTools = getActiveToolRegistry();
1820
const params = new URLSearchParams(window.location.search);
@@ -51,6 +53,9 @@ const diagnostics = document.querySelector("[data-journey-diagnostics]");
5153
const searchInput = document.querySelector("[data-journey-search-input]");
5254
const searchStatus = document.querySelector("[data-journey-search-status]");
5355
const 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]");
5459
const recommendedTargets = document.querySelector("[data-journey-recommended-targets]");
5560
const recommendedTargetStatus = document.querySelector("[data-journey-target-status]");
5661
const SUMMARY_TABLE_COLUMN_COUNT = 13;
@@ -88,6 +93,54 @@ let routeForcesNoActiveGame = false;
8893
const 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

92145
function 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+
138195
function 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+
149320
function 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+
9551172
function 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+
10851344
function 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+
17912061
addNoteButton?.addEventListener("click", () => {
17922062
if (redirectGuestWriteAction(noteStatus)) {
17932063
return;
@@ -1821,4 +2091,5 @@ addTypeButton?.addEventListener("click", () => {
18212091
renderStatusOptions();
18222092
refreshCompletionMetricsSnapshot();
18232093
applyInitialGameRoute();
2094+
refreshObjectsReadinessSnapshot();
18242095
render();

0 commit comments

Comments
 (0)