@auraindustry/aurajs 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +7 -0
  2. package/benchmarks/perf-thresholds.json +27 -0
  3. package/package.json +6 -1
  4. package/src/ai-guidance.mjs +302 -0
  5. package/src/authored-project.mjs +498 -2
  6. package/src/build-contract/capabilities.mjs +87 -1
  7. package/src/build-contract/constants.mjs +1 -0
  8. package/src/build-contract.mjs +2 -0
  9. package/src/bundler.mjs +143 -13
  10. package/src/cli.mjs +681 -13
  11. package/src/commands/packs.mjs +741 -0
  12. package/src/commands/project-authoring.mjs +128 -1
  13. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
  14. package/src/conformance/cases/core-runtime-cases.mjs +6 -2
  15. package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
  16. package/src/conformance/cases/systems-and-gameplay-cases.mjs +265 -4
  17. package/src/conformance-mobile.mjs +166 -0
  18. package/src/conformance.mjs +89 -30
  19. package/src/evidence-bundle.mjs +242 -0
  20. package/src/headless-test/runtime-coordinator.mjs +186 -33
  21. package/src/headless-test.mjs +2 -0
  22. package/src/helpers/2d/index.mjs +183 -0
  23. package/src/helpers/index.mjs +26 -0
  24. package/src/helpers/starter-utils/adventure-objectives.js +102 -0
  25. package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
  26. package/src/helpers/starter-utils/animation-2d.js +337 -0
  27. package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
  28. package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
  29. package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
  30. package/src/helpers/starter-utils/avatar-3d.js +404 -0
  31. package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
  32. package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
  33. package/src/helpers/starter-utils/core.js +150 -0
  34. package/src/helpers/starter-utils/dialogue-2d.js +351 -0
  35. package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
  36. package/src/helpers/starter-utils/index.js +26 -0
  37. package/src/helpers/starter-utils/inventory-2d.js +268 -0
  38. package/src/helpers/starter-utils/journal-2d.js +267 -0
  39. package/src/helpers/starter-utils/platformer-3d.js +132 -0
  40. package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
  41. package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
  42. package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
  43. package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
  44. package/src/helpers/starter-utils/triggers.js +662 -0
  45. package/src/helpers/starter-utils/tween-2d.js +615 -0
  46. package/src/helpers/starter-utils/wave-director.js +101 -0
  47. package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
  48. package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
  49. package/src/mobile/android/build.mjs +606 -0
  50. package/src/mobile/android/host-artifact.mjs +280 -0
  51. package/src/mobile/ios/build.mjs +1323 -0
  52. package/src/mobile/ios/host-artifact.mjs +819 -0
  53. package/src/mobile/shared/capabilities.mjs +174 -0
  54. package/src/packs/catalog.mjs +259 -0
  55. package/src/perf-benchmark-runner.mjs +17 -12
  56. package/src/perf-benchmark.mjs +408 -4
  57. package/src/publish-command.mjs +303 -6
  58. package/src/replay-runtime.mjs +257 -0
  59. package/src/scaffold/config.mjs +2 -0
  60. package/src/scaffold/fs.mjs +8 -1
  61. package/src/scaffold/project-docs.mjs +43 -1
  62. package/src/scaffold.mjs +4 -0
  63. package/src/session-runtime.mjs +4 -3
  64. package/src/web-conformance.mjs +0 -36
  65. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
  66. package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
  67. package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
  68. package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
  69. package/templates/create/2d-adventure/docs/design/loop.md +4 -3
  70. package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
  71. package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
  72. package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
  73. package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
  74. package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
  75. package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
  76. package/templates/create/3d/scenes/gameplay.scene.js +30 -3
  77. package/templates/create/3d/src/runtime/capabilities.js +5 -0
  78. package/templates/create/3d/src/runtime/materials.js +10 -0
  79. package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
  80. package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
  81. package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
  82. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
  83. package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
  84. package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
  85. package/templates/create/shared/src/runtime/ui-forms.js +552 -0
  86. package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
  87. package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
  88. package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
  89. package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
  90. package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
  91. package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
  92. package/templates/create/shared/src/starter-utils/index.js +15 -1
  93. package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
  94. package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
  95. package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
  96. package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
  97. package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
  98. package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
  99. package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
  100. package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
  101. package/templates/create-bin/play.js +36 -7
  102. package/templates/skills/auramaxx/SKILL.md +46 -0
  103. package/templates/skills/auramaxx/project-requirements.md +68 -0
  104. package/templates/skills/auramaxx/starter-recipes.md +104 -0
  105. package/templates/skills/auramaxx/validation-checklist.md +49 -0
  106. package/templates/skills/aurajs/SKILL.md +0 -96
  107. package/templates/skills/aurajs/api-contract-3d.md +0 -7
  108. package/templates/skills/aurajs/api-contract.md +0 -7
@@ -1,31 +1,75 @@
1
1
  import {
2
2
  axisFromKeys,
3
3
  centeredRect,
4
- collectAdventureObjectives2D,
5
- countCompletedObjectives,
6
- createAdventureObjectives,
7
- createInteractionPromptState,
8
- createRectTrigger2D,
9
- createTriggerTracker,
10
- objectivesComplete,
11
- resetAdventureObjectives,
12
- stepRectTriggers2D,
13
- updateInteractionPromptState,
4
+ addInventoryItem2D,
5
+ advanceDialogueConversation2D,
6
+ chooseDialogueChoice2D,
7
+ createAdventureInteractablesFromTilemapObjects2D,
8
+ createAdventureInteractionState2D,
9
+ createDialogueState2D,
10
+ createInventory2D,
11
+ createJournal2D,
12
+ getDialogueView2D,
13
+ getInventoryView2D,
14
+ getJournalView2D,
15
+ hasInventoryItem2D,
16
+ listInventoryItems2D,
17
+ listJournalEntries2D,
18
+ selectInventoryCategory2D,
19
+ selectInventoryItem2D,
20
+ selectJournalCategory2D,
21
+ selectJournalEntry2D,
22
+ setJournalEntryStatus2D,
23
+ startDialogueConversation2D,
24
+ stepAdventureInteractables2D,
25
+ upsertJournalEntry2D,
14
26
  } from '../src/starter-utils/index.js';
15
27
  import { ensureSessionState, ensureUiState } from '../src/runtime/app-state.js';
16
28
  import { ADVENTURE_CONFIG, ADVENTURE_TEXT } from '../config/gameplay/adventure.config.js';
17
29
  import { drawAdventurePlayer, createPlayerState, resetPlayerPosition } from '../prefabs/player.prefab.js';
18
30
  import { drawAdventureRelics } from '../prefabs/relic.prefab.js';
19
- import { drawAdventureWorld, moveActorThroughAdventureWorld } from '../prefabs/world.prefab.js';
20
- import { BEACON_SITES, EXIT_GATE, PLAYER_SPAWN, RELIC_SPAWNS } from '../content/gameplay/world.js';
31
+ import {
32
+ createAdventureWorldState,
33
+ drawAdventureWorld,
34
+ ensureAdventureWorldLoaded,
35
+ listAdventureWorldObjects,
36
+ moveActorThroughAdventureWorld,
37
+ resolveAdventurePlayerSpawn,
38
+ } from '../prefabs/world.prefab.js';
39
+ import {
40
+ INVENTORY_CATEGORIES,
41
+ STARTING_INVENTORY_ITEMS,
42
+ WORLD_OBJECT_LAYERS,
43
+ WORLD_TEXT,
44
+ } from '../content/gameplay/world.js';
45
+ import {
46
+ ARCHIVE_CONSOLE_DIALOGUE_ID,
47
+ ADVENTURE_DIALOGUES,
48
+ JOURNAL_CATEGORIES,
49
+ JOURNAL_ENTRY_IDS,
50
+ STARTING_JOURNAL_ENTRIES,
51
+ } from '../content/gameplay/dialogue.js';
21
52
 
22
53
  const INTERACT_KEYS = ['e', 'enter'];
54
+ const DIALOGUE_ADVANCE_KEYS = ['space'];
55
+ const DIALOGUE_CHOICE_PREV_KEYS = ['w', 'arrowup'];
56
+ const DIALOGUE_CHOICE_NEXT_KEYS = ['s', 'arrowdown'];
57
+ const INVENTORY_TOGGLE_KEYS = ['i', 'tab'];
58
+ const INVENTORY_CATEGORY_PREV_KEYS = ['a', 'arrowleft'];
59
+ const INVENTORY_CATEGORY_NEXT_KEYS = ['d', 'arrowright'];
60
+ const INVENTORY_ITEM_PREV_KEYS = ['w', 'arrowup'];
61
+ const INVENTORY_ITEM_NEXT_KEYS = ['s', 'arrowdown'];
62
+ const JOURNAL_TOGGLE_KEYS = ['j'];
63
+ const JOURNAL_CATEGORY_PREV_KEYS = ['a', 'arrowleft'];
64
+ const JOURNAL_CATEGORY_NEXT_KEYS = ['d', 'arrowright'];
65
+ const JOURNAL_ENTRY_PREV_KEYS = ['w', 'arrowup'];
66
+ const JOURNAL_ENTRY_NEXT_KEYS = ['s', 'arrowdown'];
23
67
 
24
68
  function createAdventureSessionSeed() {
25
69
  return {
26
70
  runsStarted: 0,
27
71
  bestRelicCount: 0,
28
- bestBeaconCount: 0,
72
+ bestCheckpointCount: 0,
29
73
  bestTimeSeconds: 0,
30
74
  lastOutcome: 'idle',
31
75
  lastSceneId: 'gameplay',
@@ -35,150 +79,537 @@ function createAdventureSessionSeed() {
35
79
  function createAdventureUiStateSeed() {
36
80
  return {
37
81
  showControlsHint: true,
82
+ showInventory: false,
83
+ showJournal: false,
84
+ inventorySelectedCategoryId: null,
85
+ inventorySelectedItemId: null,
86
+ journalSelectedCategoryId: null,
87
+ journalSelectedEntryId: null,
88
+ dialogueSelectedChoiceId: null,
38
89
  };
39
90
  }
40
91
 
41
- function createBeaconState() {
42
- return BEACON_SITES.map((site) => ({
43
- id: site.id,
44
- label: site.label,
45
- x: site.x,
46
- y: site.y,
47
- size: site.size,
48
- radius: site.radius,
49
- requiredRelics: site.requiredRelics,
50
- lit: false,
51
- }));
92
+ function pressedAny(keys = []) {
93
+ return keys.some((key) => aura.input.isKeyPressed(key));
52
94
  }
53
95
 
54
96
  function pressedInteract() {
55
- return INTERACT_KEYS.some((key) => aura.input.isKeyPressed(key));
97
+ return pressedAny(INTERACT_KEYS);
98
+ }
99
+
100
+ function objectProperty(object, name, fallback = null) {
101
+ if (!object || typeof object !== 'object') return fallback;
102
+ const properties = object.properties;
103
+ if (Array.isArray(properties)) {
104
+ const entry = properties.find((candidate) => candidate && candidate.name === name) || null;
105
+ return entry && Object.prototype.hasOwnProperty.call(entry, 'value') ? entry.value : fallback;
106
+ }
107
+ if (properties && typeof properties === 'object' && Object.prototype.hasOwnProperty.call(properties, name)) {
108
+ return properties[name];
109
+ }
110
+ return fallback;
111
+ }
112
+
113
+ function normalizeInteractables(world, layer) {
114
+ const objects = listAdventureWorldObjects(world, { layer, includeHidden: true });
115
+ const base = createAdventureInteractablesFromTilemapObjects2D(objects) || [];
116
+ const sourceById = new Map(objects.map((entry) => [String(entry.id), entry]));
117
+
118
+ return base.map((entry) => {
119
+ const source = sourceById.get(String(entry.id)) || null;
120
+ if (entry.kind === 'pickup') {
121
+ return {
122
+ ...entry,
123
+ priority: 6,
124
+ grantItem: {
125
+ id: String(objectProperty(source, 'grantItemId', entry.id)),
126
+ label: String(entry.label || source?.name || entry.id),
127
+ categoryId: String(objectProperty(source, 'grantCategoryId', 'relics')),
128
+ categoryLabel: String(objectProperty(source, 'grantCategoryLabel', 'Relics')),
129
+ description: String(objectProperty(source, 'grantDescription', `${entry.label} recovered from the archive.`)),
130
+ detail: String(objectProperty(source, 'grantDetail', 'Recovered during the route.')),
131
+ badge: String(objectProperty(source, 'grantBadge', 'ITEM')),
132
+ quantity: 1,
133
+ },
134
+ };
135
+ }
136
+ if (entry.kind === 'lock') {
137
+ return {
138
+ ...entry,
139
+ priority: 8,
140
+ openedPromptText: String(objectProperty(source, 'openedPromptText', `${entry.label} is open`)),
141
+ consumeRequiredItem: objectProperty(source, 'consumeRequiredItem', false) === true,
142
+ };
143
+ }
144
+ if (entry.kind === 'checkpoint') {
145
+ return {
146
+ ...entry,
147
+ priority: 7,
148
+ };
149
+ }
150
+ return {
151
+ ...entry,
152
+ priority: 3,
153
+ };
154
+ });
155
+ }
156
+
157
+ function createRunInventory() {
158
+ return createInventory2D({
159
+ categories: INVENTORY_CATEGORIES,
160
+ items: STARTING_INVENTORY_ITEMS,
161
+ });
162
+ }
163
+
164
+ function createRunJournal() {
165
+ return createJournal2D({
166
+ categories: JOURNAL_CATEGORIES,
167
+ entries: STARTING_JOURNAL_ENTRIES,
168
+ });
169
+ }
170
+
171
+ function createRunDialogue() {
172
+ return createDialogueState2D({
173
+ conversations: ADVENTURE_DIALOGUES,
174
+ });
175
+ }
176
+
177
+ function cycleValue(values, current, direction) {
178
+ if (!Array.isArray(values) || values.length <= 0) return current;
179
+ const index = values.indexOf(current);
180
+ const start = index >= 0 ? index : 0;
181
+ const nextIndex = (start + direction + values.length) % values.length;
182
+ return values[nextIndex];
183
+ }
184
+
185
+ function syncInventoryUiState(inventory, uiState) {
186
+ const view = getInventoryView2D(inventory);
187
+ uiState.inventorySelectedCategoryId = view.selectedCategoryId;
188
+ uiState.inventorySelectedItemId = view.selectedItemId;
189
+ }
190
+
191
+ function applyInventoryUiSelection(inventory, uiState) {
192
+ if (uiState.inventorySelectedCategoryId) {
193
+ selectInventoryCategory2D(inventory, uiState.inventorySelectedCategoryId);
194
+ }
195
+ const visibleItems = getInventoryView2D(inventory).items;
196
+ if (uiState.inventorySelectedItemId && visibleItems.some((entry) => entry.id === uiState.inventorySelectedItemId)) {
197
+ selectInventoryItem2D(inventory, uiState.inventorySelectedItemId);
198
+ }
199
+ syncInventoryUiState(inventory, uiState);
200
+ }
201
+
202
+ function handleInventoryNavigation(inventory, uiState) {
203
+ const categoryIds = getInventoryView2D(inventory).categories.map((entry) => entry.id);
204
+ if (pressedAny(INVENTORY_CATEGORY_PREV_KEYS)) {
205
+ selectInventoryCategory2D(
206
+ inventory,
207
+ cycleValue(categoryIds, getInventoryView2D(inventory).selectedCategoryId, -1),
208
+ );
209
+ } else if (pressedAny(INVENTORY_CATEGORY_NEXT_KEYS)) {
210
+ selectInventoryCategory2D(
211
+ inventory,
212
+ cycleValue(categoryIds, getInventoryView2D(inventory).selectedCategoryId, 1),
213
+ );
214
+ }
215
+
216
+ const visibleItems = getInventoryView2D(inventory).items.map((entry) => entry.id);
217
+ if (pressedAny(INVENTORY_ITEM_PREV_KEYS)) {
218
+ selectInventoryItem2D(
219
+ inventory,
220
+ cycleValue(visibleItems, getInventoryView2D(inventory).selectedItemId, -1),
221
+ );
222
+ } else if (pressedAny(INVENTORY_ITEM_NEXT_KEYS)) {
223
+ selectInventoryItem2D(
224
+ inventory,
225
+ cycleValue(visibleItems, getInventoryView2D(inventory).selectedItemId, 1),
226
+ );
227
+ }
228
+
229
+ syncInventoryUiState(inventory, uiState);
230
+ }
231
+
232
+ function syncJournalUiState(journal, uiState) {
233
+ const view = getJournalView2D(journal);
234
+ uiState.journalSelectedCategoryId = view.selectedCategoryId;
235
+ uiState.journalSelectedEntryId = view.selectedEntryId;
236
+ }
237
+
238
+ function applyJournalUiSelection(journal, uiState) {
239
+ if (uiState.journalSelectedCategoryId) {
240
+ selectJournalCategory2D(journal, uiState.journalSelectedCategoryId);
241
+ }
242
+ const visibleEntries = getJournalView2D(journal).entries;
243
+ if (uiState.journalSelectedEntryId && visibleEntries.some((entry) => entry.id === uiState.journalSelectedEntryId)) {
244
+ selectJournalEntry2D(journal, uiState.journalSelectedEntryId);
245
+ }
246
+ syncJournalUiState(journal, uiState);
247
+ }
248
+
249
+ function handleJournalNavigation(journal, uiState) {
250
+ const categoryIds = getJournalView2D(journal).categories.map((entry) => entry.id);
251
+ if (pressedAny(JOURNAL_CATEGORY_PREV_KEYS)) {
252
+ selectJournalCategory2D(
253
+ journal,
254
+ cycleValue(categoryIds, getJournalView2D(journal).selectedCategoryId, -1),
255
+ );
256
+ } else if (pressedAny(JOURNAL_CATEGORY_NEXT_KEYS)) {
257
+ selectJournalCategory2D(
258
+ journal,
259
+ cycleValue(categoryIds, getJournalView2D(journal).selectedCategoryId, 1),
260
+ );
261
+ }
262
+
263
+ const entryIds = getJournalView2D(journal).entries.map((entry) => entry.id);
264
+ if (pressedAny(JOURNAL_ENTRY_PREV_KEYS)) {
265
+ selectJournalEntry2D(
266
+ journal,
267
+ cycleValue(entryIds, getJournalView2D(journal).selectedEntryId, -1),
268
+ );
269
+ } else if (pressedAny(JOURNAL_ENTRY_NEXT_KEYS)) {
270
+ selectJournalEntry2D(
271
+ journal,
272
+ cycleValue(entryIds, getJournalView2D(journal).selectedEntryId, 1),
273
+ );
274
+ }
275
+
276
+ syncJournalUiState(journal, uiState);
277
+ }
278
+
279
+ function syncDialogueUiState(dialogue, uiState) {
280
+ const view = getDialogueView2D(dialogue);
281
+ if (!view.awaitingChoice) {
282
+ uiState.dialogueSelectedChoiceId = null;
283
+ return;
284
+ }
285
+ const choiceIds = view.choices
286
+ .filter((entry) => entry.disabled !== true)
287
+ .map((entry) => entry.id);
288
+ if (choiceIds.length <= 0) {
289
+ uiState.dialogueSelectedChoiceId = null;
290
+ return;
291
+ }
292
+ if (!choiceIds.includes(uiState.dialogueSelectedChoiceId)) {
293
+ uiState.dialogueSelectedChoiceId = choiceIds[0];
294
+ }
295
+ }
296
+
297
+ function applyInteractionResult(interactables, result) {
298
+ for (const entry of interactables) {
299
+ if (!entry) continue;
300
+ if (result.collectedIds.includes(entry.id)) entry.collected = true;
301
+ if (result.openedIds.includes(entry.id)) entry.opened = true;
302
+ if (result.activatedIds.includes(entry.id)) entry.activated = true;
303
+ }
56
304
  }
57
305
 
58
306
  export function createGameplayScene(context = {}) {
59
307
  const appState = context.appState && typeof context.appState === 'object' ? context.appState : (context.appState = {});
60
308
  const adventureSession = ensureSessionState(appState, 'adventure2d', createAdventureSessionSeed());
61
309
  const adventureUiState = ensureUiState(appState, 'adventureHud', createAdventureUiStateSeed());
62
- const promptState = createInteractionPromptState({ prompt: 'Press E to interact' });
63
- const triggerTracker = createTriggerTracker({ subjectId: 'player', dimension: '2d' });
310
+ const world = createAdventureWorldState();
64
311
  const sceneState = {
65
- player: createPlayerState(PLAYER_SPAWN),
66
- relics: createAdventureObjectives(RELIC_SPAWNS, {
67
- dimension: '2d',
68
- defaultRadius: ADVENTURE_CONFIG.relicRadius,
312
+ world,
313
+ player: createPlayerState(),
314
+ inventory: createRunInventory(),
315
+ journal: createRunJournal(),
316
+ dialogue: createRunDialogue(),
317
+ interaction: createAdventureInteractionState2D({
318
+ subjectId: 'player',
319
+ prompt: 'Press E to interact',
69
320
  }),
70
- beacons: createBeaconState(),
321
+ interactables: [],
322
+ activeCheckpointId: null,
71
323
  elapsed: 0,
72
324
  runComplete: false,
325
+ lastDialogueCompletionRevision: 0,
73
326
  };
74
327
 
75
328
  function relicCount() {
76
- return countCompletedObjectives(sceneState.relics);
329
+ return listInventoryItems2D(sceneState.inventory, 'relics')
330
+ .reduce((total, entry) => total + Number(entry.quantity || 0), 0);
331
+ }
332
+
333
+ function checkpointCount() {
334
+ return sceneState.interactables
335
+ .filter((entry) => entry.kind === 'checkpoint' && entry.activated === true)
336
+ .length;
337
+ }
338
+
339
+ function inventoryView() {
340
+ return getInventoryView2D(sceneState.inventory);
341
+ }
342
+
343
+ function journalView() {
344
+ return getJournalView2D(sceneState.journal);
345
+ }
346
+
347
+ function dialogueView() {
348
+ return getDialogueView2D(sceneState.dialogue);
349
+ }
350
+
351
+ function objectiveText() {
352
+ if (sceneState.runComplete) {
353
+ return 'Route secured. Restart when ready.';
354
+ }
355
+ if (!hasInventoryItem2D(sceneState.inventory, 'route-console-log')) {
356
+ return 'Inspect the archive console to recover the route note.';
357
+ }
358
+ if (!hasInventoryItem2D(sceneState.inventory, 'dawn-key')) {
359
+ return 'Recover the Dawn Key from the south gallery.';
360
+ }
361
+ return 'Use the Dawn Key on the Moon Gate to clear the route.';
362
+ }
363
+
364
+ function activeCheckpointLabel() {
365
+ const checkpoint = sceneState.interactables.find((entry) => entry.kind === 'checkpoint' && entry.checkpointId === sceneState.activeCheckpointId) || null;
366
+ return checkpoint?.label || 'Route Entry';
367
+ }
368
+
369
+ function rebuildInteractables() {
370
+ sceneState.interactables = [
371
+ ...normalizeInteractables(sceneState.world, WORLD_OBJECT_LAYERS.pickups),
372
+ ...normalizeInteractables(sceneState.world, WORLD_OBJECT_LAYERS.checkpoints),
373
+ ...normalizeInteractables(sceneState.world, WORLD_OBJECT_LAYERS.props),
374
+ ...normalizeInteractables(sceneState.world, WORLD_OBJECT_LAYERS.locks),
375
+ ];
77
376
  }
78
377
 
79
- function beaconCount() {
80
- return sceneState.beacons.reduce((total, beacon) => total + (beacon.lit === true ? 1 : 0), 0);
378
+ function focusJournalEntry(categoryId, entryId) {
379
+ if (categoryId) {
380
+ selectJournalCategory2D(sceneState.journal, categoryId);
381
+ }
382
+ if (entryId) {
383
+ selectJournalEntry2D(sceneState.journal, entryId);
384
+ }
385
+ syncJournalUiState(sceneState.journal, adventureUiState);
81
386
  }
82
387
 
83
- function gateReady() {
84
- return objectivesComplete(sceneState.relics) && beaconCount() >= sceneState.beacons.length;
388
+ function registerConsoleRouteUnlock() {
389
+ if (!hasInventoryItem2D(sceneState.inventory, 'route-console-log')) {
390
+ addInventoryItem2D(sceneState.inventory, {
391
+ id: 'route-console-log',
392
+ label: WORLD_TEXT.consoleNoteTitle,
393
+ categoryId: 'field',
394
+ categoryLabel: 'Field Notes',
395
+ quantity: 1,
396
+ badge: 'NOTE',
397
+ description: 'Recovered from the archive console.',
398
+ detail: WORLD_TEXT.consoleNoteDetail,
399
+ });
400
+ }
401
+
402
+ setJournalEntryStatus2D(sceneState.journal, JOURNAL_ENTRY_IDS.routeBrief, 'completed', {
403
+ badge: 'Completed',
404
+ meta: 'Archive console route restored',
405
+ });
406
+
407
+ upsertJournalEntry2D(sceneState.journal, {
408
+ id: JOURNAL_ENTRY_IDS.consoleLog,
409
+ label: WORLD_TEXT.consoleNoteTitle,
410
+ categoryId: 'intel',
411
+ categoryLabel: 'Archive Intel',
412
+ description: 'Recovered from the archive console.',
413
+ detail: WORLD_TEXT.consoleNoteDetail,
414
+ status: 'completed',
415
+ badge: 'Recovered',
416
+ updatedAt: 'Recovered at the archive console',
417
+ });
418
+
419
+ upsertJournalEntry2D(sceneState.journal, {
420
+ id: JOURNAL_ENTRY_IDS.moonGate,
421
+ label: 'Moon Gate Route',
422
+ categoryId: 'route',
423
+ categoryLabel: 'Route',
424
+ description: 'Carry the Dawn Key to the Moon Gate and restore the exit route.',
425
+ detail: hasInventoryItem2D(sceneState.inventory, 'dawn-key')
426
+ ? 'The Dawn Key is secured. Return to the lower gallery and open the gate.'
427
+ : 'Recover the Dawn Key from the south gallery, then return to the gate.',
428
+ status: 'active',
429
+ badge: hasInventoryItem2D(sceneState.inventory, 'dawn-key') ? 'Key Ready' : 'Active',
430
+ meta: 'Route goal updated',
431
+ });
432
+
433
+ adventureUiState.showJournal = true;
434
+ focusJournalEntry('route', JOURNAL_ENTRY_IDS.moonGate);
435
+ }
436
+
437
+ function handleDialogueCompletion() {
438
+ const completion = dialogueView().lastCompletion;
439
+ if (!completion || completion.revision === sceneState.lastDialogueCompletionRevision) {
440
+ return;
441
+ }
442
+ sceneState.lastDialogueCompletionRevision = completion.revision;
443
+ if (completion.conversationId === ARCHIVE_CONSOLE_DIALOGUE_ID && completion.outcomeId === 'route-briefed') {
444
+ registerConsoleRouteUnlock();
445
+ }
85
446
  }
86
447
 
87
448
  function syncSessionState() {
88
449
  adventureSession.lastSceneId = 'gameplay';
89
450
  adventureSession.bestRelicCount = Math.max(Number(adventureSession.bestRelicCount || 0), relicCount());
90
- adventureSession.bestBeaconCount = Math.max(Number(adventureSession.bestBeaconCount || 0), beaconCount());
451
+ adventureSession.bestCheckpointCount = Math.max(Number(adventureSession.bestCheckpointCount || 0), checkpointCount());
91
452
  adventureSession.bestTimeSeconds = Math.max(Number(adventureSession.bestTimeSeconds || 0), sceneState.elapsed);
92
453
  adventureSession.lastElapsedSeconds = Number(sceneState.elapsed.toFixed(2));
93
- adventureSession.lastOutcome = sceneState.runComplete ? 'route-cleared' : 'surveying';
454
+ adventureSession.lastOutcome = sceneState.runComplete
455
+ ? 'route-cleared'
456
+ : dialogueView().active === true
457
+ ? 'dialogue-open'
458
+ : adventureUiState.showJournal === true
459
+ ? 'journal-open'
460
+ : adventureUiState.showInventory === true
461
+ ? 'inventory-open'
462
+ : 'surveying';
94
463
  }
95
464
 
96
465
  function syncHud() {
97
466
  context.setHudScreen?.('hud', {
98
467
  relicCount: relicCount(),
99
- relicTarget: sceneState.relics.length,
100
- beaconCount: beaconCount(),
101
- beaconTarget: sceneState.beacons.length,
102
- promptText: promptState.prompt || '',
468
+ relicTarget: 3,
469
+ checkpointCount: checkpointCount(),
470
+ checkpointLabel: activeCheckpointLabel(),
471
+ promptText: sceneState.interaction.prompt.prompt || '',
472
+ objectiveText: objectiveText(),
473
+ zoneLabel: WORLD_TEXT.zoneLabel,
103
474
  runComplete: sceneState.runComplete,
104
475
  showControlsHint: adventureUiState.showControlsHint !== false,
476
+ showInventory: adventureUiState.showInventory === true,
477
+ showJournal: adventureUiState.showJournal === true,
478
+ inventory: inventoryView(),
479
+ journal: journalView(),
480
+ inventoryHint: WORLD_TEXT.inventoryHint,
481
+ journalHint: WORLD_TEXT.journalHint,
482
+ dialogueActive: dialogueView().active === true,
105
483
  fps: Number(aura.window.getFPS?.() || 0).toFixed(0),
106
484
  });
107
485
  }
108
486
 
487
+ function syncOverlay() {
488
+ const activeDialogue = dialogueView();
489
+ if (activeDialogue.active === true) {
490
+ syncDialogueUiState(sceneState.dialogue, adventureUiState);
491
+ context.showOverlayScreen?.('journal', {
492
+ mode: 'dialogue',
493
+ dialogue: {
494
+ ...activeDialogue,
495
+ selectedChoiceId: adventureUiState.dialogueSelectedChoiceId,
496
+ },
497
+ journal: journalView(),
498
+ zoneLabel: WORLD_TEXT.zoneLabel,
499
+ title: 'Archive Console Link',
500
+ controlsText: ADVENTURE_TEXT.dialogueControls,
501
+ });
502
+ return;
503
+ }
504
+ if (adventureUiState.showJournal === true) {
505
+ const activeJournal = journalView();
506
+ context.showOverlayScreen?.('journal', {
507
+ mode: 'journal',
508
+ journal: {
509
+ ...activeJournal,
510
+ activeEntries: listJournalEntries2D(sceneState.journal, activeJournal.selectedCategoryId),
511
+ },
512
+ objectiveText: objectiveText(),
513
+ zoneLabel: WORLD_TEXT.zoneLabel,
514
+ controlsText: ADVENTURE_TEXT.journalControls,
515
+ });
516
+ return;
517
+ }
518
+ context.clearOverlayScreen?.();
519
+ }
520
+
109
521
  function resetRun() {
110
- resetPlayerPosition(sceneState.player, PLAYER_SPAWN);
111
- resetAdventureObjectives(sceneState.relics);
112
- sceneState.beacons = createBeaconState();
522
+ ensureAdventureWorldLoaded(sceneState.world);
523
+ sceneState.inventory = createRunInventory();
524
+ sceneState.journal = createRunJournal();
525
+ sceneState.dialogue = createRunDialogue();
526
+ sceneState.lastDialogueCompletionRevision = 0;
527
+ rebuildInteractables();
528
+ sceneState.activeCheckpointId = null;
529
+ resetPlayerPosition(sceneState.player, resolveAdventurePlayerSpawn(sceneState.world));
530
+ sceneState.interaction = createAdventureInteractionState2D({
531
+ subjectId: 'player',
532
+ prompt: 'Press E to interact',
533
+ });
113
534
  sceneState.elapsed = 0;
114
535
  sceneState.runComplete = false;
115
- promptState.visible = false;
116
- promptState.ready = false;
117
- promptState.prompt = null;
118
- promptState.triggerId = null;
119
- promptState.accepted = false;
120
- promptState.acceptedTriggerId = null;
536
+ adventureUiState.showInventory = false;
537
+ adventureUiState.showJournal = false;
538
+ adventureUiState.dialogueSelectedChoiceId = null;
121
539
  adventureSession.runsStarted += 1;
540
+ syncInventoryUiState(sceneState.inventory, adventureUiState);
541
+ syncJournalUiState(sceneState.journal, adventureUiState);
542
+ syncDialogueUiState(sceneState.dialogue, adventureUiState);
122
543
  syncSessionState();
123
544
  syncHud();
545
+ syncOverlay();
124
546
  }
125
547
 
126
- function buildInteractionTriggers() {
127
- const collectedRelics = relicCount();
128
- const triggers = [];
129
-
130
- for (const beacon of sceneState.beacons) {
131
- if (beacon.lit) continue;
132
- const canLight = collectedRelics >= beacon.requiredRelics;
133
- triggers.push(createRectTrigger2D(
134
- `beacon:${beacon.id}`,
135
- beacon.x - beacon.radius,
136
- beacon.y - beacon.radius,
137
- beacon.radius * 2,
138
- beacon.radius * 2,
139
- {
140
- role: 'beacon',
141
- priority: canLight ? 5 : 2,
142
- promptText: canLight
143
- ? `Press E to light ${beacon.label}`
144
- : `${beacon.label} unlocks at ${beacon.requiredRelics} relic${beacon.requiredRelics === 1 ? '' : 's'}`,
145
- interactionKey: 'E',
146
- },
147
- ));
548
+ function handleInteractionSideEffects(result) {
549
+ if (result.checkpointId) {
550
+ sceneState.activeCheckpointId = result.checkpointId;
551
+ }
552
+
553
+ if (result.activatedIds.includes('301') && !hasInventoryItem2D(sceneState.inventory, 'route-console-log')) {
554
+ startDialogueConversation2D(sceneState.dialogue, ARCHIVE_CONSOLE_DIALOGUE_ID);
555
+ adventureUiState.showInventory = false;
556
+ adventureUiState.showJournal = false;
557
+ syncDialogueUiState(sceneState.dialogue, adventureUiState);
558
+ }
559
+
560
+ if (result.collectedIds.includes('104')) {
561
+ upsertJournalEntry2D(sceneState.journal, {
562
+ id: JOURNAL_ENTRY_IDS.moonGate,
563
+ label: 'Moon Gate Route',
564
+ categoryId: 'route',
565
+ categoryLabel: 'Route',
566
+ description: 'Carry the Dawn Key to the Moon Gate and restore the exit route.',
567
+ detail: 'The Dawn Key is secured. Return to the lower gallery and open the gate.',
568
+ status: 'active',
569
+ badge: 'Key Ready',
570
+ meta: 'Dawn Key secured',
571
+ });
148
572
  }
149
573
 
150
- triggers.push(createRectTrigger2D(
151
- 'exit-gate',
152
- EXIT_GATE.x - ADVENTURE_CONFIG.exitPadding,
153
- EXIT_GATE.y - ADVENTURE_CONFIG.exitPadding,
154
- EXIT_GATE.w + (ADVENTURE_CONFIG.exitPadding * 2),
155
- EXIT_GATE.h + (ADVENTURE_CONFIG.exitPadding * 2),
156
- {
157
- role: 'goal',
158
- priority: gateReady() ? 8 : 1,
159
- promptText: gateReady()
160
- ? 'Press E to leave through the Moon Gate'
161
- : 'Collect every relic and light both beacons to open the gate',
162
- interactionKey: 'E',
163
- },
164
- ));
165
-
166
- return triggers;
574
+ if (result.openedIds.includes('401')) {
575
+ sceneState.runComplete = true;
576
+ setJournalEntryStatus2D(sceneState.journal, JOURNAL_ENTRY_IDS.moonGate, 'completed', {
577
+ badge: 'Completed',
578
+ meta: 'Moon Gate opened',
579
+ });
580
+ }
167
581
  }
168
582
 
169
- function handleAcceptedPrompt() {
170
- const acceptedTriggerId = promptState.acceptedTriggerId;
171
- if (!acceptedTriggerId) return;
583
+ function handleDialogueInput() {
584
+ const view = dialogueView();
585
+ if (view.active !== true) {
586
+ return false;
587
+ }
172
588
 
173
- if (acceptedTriggerId.startsWith('beacon:')) {
174
- const beaconId = acceptedTriggerId.slice('beacon:'.length);
175
- const beacon = sceneState.beacons.find((entry) => entry.id === beaconId) || null;
176
- if (beacon && relicCount() >= beacon.requiredRelics) {
177
- beacon.lit = true;
589
+ syncDialogueUiState(sceneState.dialogue, adventureUiState);
590
+ if (view.awaitingChoice === true) {
591
+ const choiceIds = view.choices
592
+ .filter((entry) => entry.disabled !== true)
593
+ .map((entry) => entry.id);
594
+ if (pressedAny(DIALOGUE_CHOICE_PREV_KEYS)) {
595
+ adventureUiState.dialogueSelectedChoiceId = cycleValue(choiceIds, adventureUiState.dialogueSelectedChoiceId, -1);
596
+ } else if (pressedAny(DIALOGUE_CHOICE_NEXT_KEYS)) {
597
+ adventureUiState.dialogueSelectedChoiceId = cycleValue(choiceIds, adventureUiState.dialogueSelectedChoiceId, 1);
178
598
  }
179
- } else if (acceptedTriggerId === 'exit-gate' && gateReady()) {
180
- sceneState.runComplete = true;
599
+ if (pressedInteract() || pressedAny(DIALOGUE_ADVANCE_KEYS)) {
600
+ chooseDialogueChoice2D(
601
+ sceneState.dialogue,
602
+ adventureUiState.dialogueSelectedChoiceId || choiceIds[0] || null,
603
+ );
604
+ }
605
+ } else if (pressedInteract() || pressedAny(DIALOGUE_ADVANCE_KEYS)) {
606
+ advanceDialogueConversation2D(sceneState.dialogue);
181
607
  }
608
+
609
+ syncDialogueUiState(sceneState.dialogue, adventureUiState);
610
+ adventureUiState.showControlsHint = false;
611
+ handleDialogueCompletion();
612
+ return true;
182
613
  }
183
614
 
184
615
  return {
@@ -188,19 +619,65 @@ export function createGameplayScene(context = {}) {
188
619
  },
189
620
 
190
621
  update(dt) {
622
+ applyInventoryUiSelection(sceneState.inventory, adventureUiState);
623
+ applyJournalUiSelection(sceneState.journal, adventureUiState);
624
+ syncDialogueUiState(sceneState.dialogue, adventureUiState);
191
625
  sceneState.elapsed += dt;
192
626
 
193
627
  if (sceneState.runComplete) {
628
+ if (pressedAny(JOURNAL_TOGGLE_KEYS)) {
629
+ adventureUiState.showJournal = adventureUiState.showJournal !== true;
630
+ }
194
631
  if (aura.input.isKeyPressed('enter')) {
195
632
  resetRun();
633
+ return;
196
634
  }
635
+ syncSessionState();
197
636
  syncHud();
637
+ syncOverlay();
638
+ return;
639
+ }
640
+
641
+ if (handleDialogueInput()) {
642
+ syncSessionState();
643
+ syncHud();
644
+ syncOverlay();
198
645
  return;
199
646
  }
200
647
 
201
648
  if (aura.input.isKeyPressed('h')) {
202
649
  adventureUiState.showControlsHint = adventureUiState.showControlsHint !== true;
203
650
  }
651
+ if (pressedAny(INVENTORY_TOGGLE_KEYS)) {
652
+ adventureUiState.showInventory = adventureUiState.showInventory !== true;
653
+ if (adventureUiState.showInventory === true) {
654
+ adventureUiState.showJournal = false;
655
+ }
656
+ adventureUiState.showControlsHint = false;
657
+ }
658
+ if (pressedAny(JOURNAL_TOGGLE_KEYS)) {
659
+ adventureUiState.showJournal = adventureUiState.showJournal !== true;
660
+ if (adventureUiState.showJournal === true) {
661
+ adventureUiState.showInventory = false;
662
+ }
663
+ adventureUiState.showControlsHint = false;
664
+ }
665
+
666
+ if (adventureUiState.showInventory === true) {
667
+ handleInventoryNavigation(sceneState.inventory, adventureUiState);
668
+ syncSessionState();
669
+ syncHud();
670
+ syncOverlay();
671
+ return;
672
+ }
673
+
674
+ if (adventureUiState.showJournal === true) {
675
+ handleJournalNavigation(sceneState.journal, adventureUiState);
676
+ syncSessionState();
677
+ syncHud();
678
+ syncOverlay();
679
+ return;
680
+ }
204
681
 
205
682
  const moveX = axisFromKeys(aura.input, ['arrowleft', 'a'], ['arrowright', 'd']);
206
683
  const moveY = axisFromKeys(aura.input, ['arrowup', 'w'], ['arrowdown', 's']);
@@ -213,41 +690,55 @@ export function createGameplayScene(context = {}) {
213
690
  }
214
691
 
215
692
  moveActorThroughAdventureWorld(
693
+ sceneState.world,
216
694
  sceneState.player,
217
695
  moveX * ADVENTURE_CONFIG.playerSpeed * dt,
218
696
  moveY * ADVENTURE_CONFIG.playerSpeed * dt,
219
697
  ADVENTURE_CONFIG.playerSize,
220
698
  );
221
699
 
222
- collectAdventureObjectives2D(sceneState.player, sceneState.relics);
223
-
224
700
  const playerRect = centeredRect(
225
701
  sceneState.player.x,
226
702
  sceneState.player.y,
227
703
  ADVENTURE_CONFIG.playerSize * 0.84,
228
704
  ADVENTURE_CONFIG.playerSize * 0.84,
229
705
  );
230
- const triggerStep = stepRectTriggers2D(triggerTracker, playerRect, buildInteractionTriggers());
231
- updateInteractionPromptState(promptState, triggerStep, {
232
- interactPressed: pressedInteract(),
233
- });
234
- handleAcceptedPrompt();
706
+ const interactionResult = stepAdventureInteractables2D(
707
+ sceneState.interaction,
708
+ playerRect,
709
+ sceneState.interactables,
710
+ {
711
+ inventory: sceneState.inventory,
712
+ interactPressed: pressedInteract(),
713
+ },
714
+ );
715
+ applyInteractionResult(sceneState.interactables, interactionResult);
716
+ handleInteractionSideEffects(interactionResult);
717
+ if (interactionResult.acceptedId) {
718
+ syncInventoryUiState(sceneState.inventory, adventureUiState);
719
+ syncJournalUiState(sceneState.journal, adventureUiState);
720
+ }
721
+
235
722
  syncSessionState();
236
723
  syncHud();
724
+ syncOverlay();
237
725
  },
238
726
 
239
727
  onExit() {
240
728
  context.clearHudScreen?.();
729
+ context.clearOverlayScreen?.();
241
730
  },
242
731
 
243
732
  draw() {
244
- aura.draw2d.clear(aura.rgba(0.03, 0.04, 0.06, 1.0));
733
+ aura.draw2d.clear(aura.rgba(0.02, 0.03, 0.05, 1.0));
245
734
  drawAdventureWorld({
246
- beacons: sceneState.beacons,
247
- gateReady: gateReady(),
735
+ world: sceneState.world,
736
+ interactables: sceneState.interactables,
248
737
  elapsed: sceneState.elapsed,
738
+ activeCheckpointId: sceneState.activeCheckpointId,
739
+ runComplete: sceneState.runComplete,
249
740
  });
250
- drawAdventureRelics(sceneState.relics, sceneState.elapsed);
741
+ drawAdventureRelics(sceneState.interactables, sceneState.elapsed);
251
742
  drawAdventurePlayer(sceneState.player, ADVENTURE_CONFIG, {
252
743
  runComplete: sceneState.runComplete,
253
744
  });