@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.
- package/README.md +7 -0
- package/benchmarks/perf-thresholds.json +27 -0
- package/package.json +6 -1
- package/src/ai-guidance.mjs +302 -0
- package/src/authored-project.mjs +498 -2
- package/src/build-contract/capabilities.mjs +87 -1
- package/src/build-contract/constants.mjs +1 -0
- package/src/build-contract.mjs +2 -0
- package/src/bundler.mjs +143 -13
- package/src/cli.mjs +681 -13
- package/src/commands/packs.mjs +741 -0
- package/src/commands/project-authoring.mjs +128 -1
- package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
- package/src/conformance/cases/core-runtime-cases.mjs +6 -2
- package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
- package/src/conformance/cases/systems-and-gameplay-cases.mjs +265 -4
- package/src/conformance-mobile.mjs +166 -0
- package/src/conformance.mjs +89 -30
- package/src/evidence-bundle.mjs +242 -0
- package/src/headless-test/runtime-coordinator.mjs +186 -33
- package/src/headless-test.mjs +2 -0
- package/src/helpers/2d/index.mjs +183 -0
- package/src/helpers/index.mjs +26 -0
- package/src/helpers/starter-utils/adventure-objectives.js +102 -0
- package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
- package/src/helpers/starter-utils/animation-2d.js +337 -0
- package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
- package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
- package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
- package/src/helpers/starter-utils/avatar-3d.js +404 -0
- package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
- package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
- package/src/helpers/starter-utils/core.js +150 -0
- package/src/helpers/starter-utils/dialogue-2d.js +351 -0
- package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
- package/src/helpers/starter-utils/index.js +26 -0
- package/src/helpers/starter-utils/inventory-2d.js +268 -0
- package/src/helpers/starter-utils/journal-2d.js +267 -0
- package/src/helpers/starter-utils/platformer-3d.js +132 -0
- package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
- package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
- package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
- package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
- package/src/helpers/starter-utils/triggers.js +662 -0
- package/src/helpers/starter-utils/tween-2d.js +615 -0
- package/src/helpers/starter-utils/wave-director.js +101 -0
- package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
- package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
- package/src/mobile/android/build.mjs +606 -0
- package/src/mobile/android/host-artifact.mjs +280 -0
- package/src/mobile/ios/build.mjs +1323 -0
- package/src/mobile/ios/host-artifact.mjs +819 -0
- package/src/mobile/shared/capabilities.mjs +174 -0
- package/src/packs/catalog.mjs +259 -0
- package/src/perf-benchmark-runner.mjs +17 -12
- package/src/perf-benchmark.mjs +408 -4
- package/src/publish-command.mjs +303 -6
- package/src/replay-runtime.mjs +257 -0
- package/src/scaffold/config.mjs +2 -0
- package/src/scaffold/fs.mjs +8 -1
- package/src/scaffold/project-docs.mjs +43 -1
- package/src/scaffold.mjs +4 -0
- package/src/session-runtime.mjs +4 -3
- package/src/web-conformance.mjs +0 -36
- package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
- package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
- package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
- package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
- package/templates/create/2d-adventure/docs/design/loop.md +4 -3
- package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
- package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
- package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
- package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
- package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
- package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
- package/templates/create/3d/scenes/gameplay.scene.js +30 -3
- package/templates/create/3d/src/runtime/capabilities.js +5 -0
- package/templates/create/3d/src/runtime/materials.js +10 -0
- package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
- package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
- package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
- package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
- package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
- package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
- package/templates/create/shared/src/runtime/ui-forms.js +552 -0
- package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
- package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
- package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
- package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
- package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
- package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
- package/templates/create/shared/src/starter-utils/index.js +15 -1
- package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
- package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
- package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
- package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
- package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
- package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
- package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
- package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
- package/templates/create-bin/play.js +36 -7
- package/templates/skills/auramaxx/SKILL.md +46 -0
- package/templates/skills/auramaxx/project-requirements.md +68 -0
- package/templates/skills/auramaxx/starter-recipes.md +104 -0
- package/templates/skills/auramaxx/validation-checklist.md +49 -0
- package/templates/skills/aurajs/SKILL.md +0 -96
- package/templates/skills/aurajs/api-contract-3d.md +0 -7
- package/templates/skills/aurajs/api-contract.md +0 -7
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
function normalizeId(value, fallback) {
|
|
2
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
3
|
+
return value.trim();
|
|
4
|
+
}
|
|
5
|
+
return fallback;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeLabel(value, fallback) {
|
|
9
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
10
|
+
return value.trim();
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeQuantity(value, fallback = 1) {
|
|
16
|
+
const numeric = Number(value);
|
|
17
|
+
if (!Number.isFinite(numeric)) return Math.max(0, fallback);
|
|
18
|
+
return Math.max(0, Math.round(numeric));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeCategoryEntry(entry, index) {
|
|
22
|
+
const fallbackId = `category-${index + 1}`;
|
|
23
|
+
const id = normalizeId(entry?.id, fallbackId);
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
label: normalizeLabel(entry?.label ?? entry?.name, id),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function deriveMissingCategories(entries = [], items = []) {
|
|
31
|
+
const categories = Array.isArray(entries)
|
|
32
|
+
? entries.map((entry, index) => normalizeCategoryEntry(entry, index))
|
|
33
|
+
: [];
|
|
34
|
+
const known = new Set(categories.map((entry) => entry.id));
|
|
35
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
36
|
+
const categoryId = normalizeId(item?.categoryId ?? item?.category, null);
|
|
37
|
+
if (!categoryId || known.has(categoryId)) continue;
|
|
38
|
+
categories.push({
|
|
39
|
+
id: categoryId,
|
|
40
|
+
label: normalizeLabel(item?.categoryLabel, categoryId),
|
|
41
|
+
});
|
|
42
|
+
known.add(categoryId);
|
|
43
|
+
}
|
|
44
|
+
return categories;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeItemEntry(entry, index, categories = []) {
|
|
48
|
+
const fallbackId = `item-${index + 1}`;
|
|
49
|
+
const id = normalizeId(entry?.id, fallbackId);
|
|
50
|
+
const categoryId = normalizeId(entry?.categoryId ?? entry?.category, categories[0]?.id || 'inventory');
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
label: normalizeLabel(entry?.label ?? entry?.name, id),
|
|
54
|
+
description: typeof entry?.description === 'string' ? entry.description : null,
|
|
55
|
+
detail: typeof entry?.detail === 'string' ? entry.detail : null,
|
|
56
|
+
badge: typeof entry?.badge === 'string' && entry.badge.trim().length > 0 ? entry.badge.trim() : null,
|
|
57
|
+
categoryId,
|
|
58
|
+
categoryLabel: normalizeLabel(entry?.categoryLabel, categoryId),
|
|
59
|
+
quantity: normalizeQuantity(entry?.quantity, 1),
|
|
60
|
+
equipped: entry?.equipped === true,
|
|
61
|
+
disabled: entry?.disabled === true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ensureCategory(inventory, categoryId, categoryLabel = null) {
|
|
66
|
+
if (!inventory || !Array.isArray(inventory.categories)) return null;
|
|
67
|
+
const existing = inventory.categories.find((entry) => entry.id === categoryId) || null;
|
|
68
|
+
if (existing) return existing;
|
|
69
|
+
const category = {
|
|
70
|
+
id: normalizeId(categoryId, `category-${inventory.categories.length + 1}`),
|
|
71
|
+
label: normalizeLabel(categoryLabel, categoryId),
|
|
72
|
+
};
|
|
73
|
+
inventory.categories.push(category);
|
|
74
|
+
return category;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function firstItemInCategory(inventory, categoryId = null) {
|
|
78
|
+
const items = listInventoryItems2D(inventory, categoryId);
|
|
79
|
+
return items[0] || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function activeCategoryId(inventory) {
|
|
83
|
+
if (!inventory || !Array.isArray(inventory.categories) || inventory.categories.length <= 0) return null;
|
|
84
|
+
if (inventory.selectedCategoryId && inventory.categories.some((entry) => entry.id === inventory.selectedCategoryId)) {
|
|
85
|
+
return inventory.selectedCategoryId;
|
|
86
|
+
}
|
|
87
|
+
return inventory.categories[0].id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function touchInventory(inventory) {
|
|
91
|
+
if (!inventory || typeof inventory !== 'object') return;
|
|
92
|
+
inventory.revision = Number(inventory.revision || 0) + 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createInventory2D(options = {}) {
|
|
96
|
+
const categories = deriveMissingCategories(options.categories, options.items);
|
|
97
|
+
const inventory = {
|
|
98
|
+
categories,
|
|
99
|
+
items: [],
|
|
100
|
+
selectedCategoryId: categories[0]?.id || null,
|
|
101
|
+
selectedItemId: null,
|
|
102
|
+
revision: 0,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
for (const [index, entry] of (Array.isArray(options.items) ? options.items : []).entries()) {
|
|
106
|
+
const item = normalizeItemEntry(entry, index, categories);
|
|
107
|
+
if (item.quantity <= 0) continue;
|
|
108
|
+
ensureCategory(inventory, item.categoryId, item.categoryLabel);
|
|
109
|
+
inventory.items.push(item);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof options.selectedCategoryId === 'string') {
|
|
113
|
+
selectInventoryCategory2D(inventory, options.selectedCategoryId);
|
|
114
|
+
}
|
|
115
|
+
if (typeof options.selectedItemId === 'string') {
|
|
116
|
+
selectInventoryItem2D(inventory, options.selectedItemId);
|
|
117
|
+
}
|
|
118
|
+
if (!inventory.selectedItemId) {
|
|
119
|
+
const first = firstItemInCategory(inventory, inventory.selectedCategoryId);
|
|
120
|
+
inventory.selectedItemId = first?.id || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return inventory;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function addInventoryItem2D(inventory, entry, quantity = null) {
|
|
127
|
+
if (!inventory || !Array.isArray(inventory.items)) return null;
|
|
128
|
+
const item = normalizeItemEntry(entry, inventory.items.length, inventory.categories);
|
|
129
|
+
const delta = quantity == null ? item.quantity : normalizeQuantity(quantity, item.quantity);
|
|
130
|
+
if (delta <= 0) return null;
|
|
131
|
+
|
|
132
|
+
ensureCategory(inventory, item.categoryId, item.categoryLabel);
|
|
133
|
+
const existing = inventory.items.find((candidate) => candidate.id === item.id) || null;
|
|
134
|
+
if (existing) {
|
|
135
|
+
existing.label = item.label;
|
|
136
|
+
existing.description = item.description;
|
|
137
|
+
existing.detail = item.detail;
|
|
138
|
+
existing.badge = item.badge;
|
|
139
|
+
existing.categoryId = item.categoryId;
|
|
140
|
+
existing.categoryLabel = item.categoryLabel;
|
|
141
|
+
existing.equipped = item.equipped === true ? true : existing.equipped === true;
|
|
142
|
+
existing.disabled = item.disabled === true;
|
|
143
|
+
existing.quantity += delta;
|
|
144
|
+
touchInventory(inventory);
|
|
145
|
+
if (!inventory.selectedCategoryId) inventory.selectedCategoryId = existing.categoryId;
|
|
146
|
+
if (!inventory.selectedItemId) inventory.selectedItemId = existing.id;
|
|
147
|
+
return existing;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
item.quantity = delta;
|
|
151
|
+
inventory.items.push(item);
|
|
152
|
+
if (!inventory.selectedCategoryId) inventory.selectedCategoryId = item.categoryId;
|
|
153
|
+
if (!inventory.selectedItemId) inventory.selectedItemId = item.id;
|
|
154
|
+
touchInventory(inventory);
|
|
155
|
+
return item;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function removeInventoryItem2D(inventory, itemId, quantity = 1) {
|
|
159
|
+
if (!inventory || !Array.isArray(inventory.items)) return false;
|
|
160
|
+
const targetId = normalizeId(itemId, null);
|
|
161
|
+
if (!targetId) return false;
|
|
162
|
+
const index = inventory.items.findIndex((entry) => entry.id === targetId);
|
|
163
|
+
if (index < 0) return false;
|
|
164
|
+
const item = inventory.items[index];
|
|
165
|
+
const delta = normalizeQuantity(quantity, 1);
|
|
166
|
+
if (delta <= 0) return false;
|
|
167
|
+
|
|
168
|
+
item.quantity = Math.max(0, item.quantity - delta);
|
|
169
|
+
if (item.quantity <= 0) {
|
|
170
|
+
inventory.items.splice(index, 1);
|
|
171
|
+
}
|
|
172
|
+
if (inventory.selectedItemId === targetId) {
|
|
173
|
+
inventory.selectedItemId = firstItemInCategory(inventory, inventory.selectedCategoryId)?.id || null;
|
|
174
|
+
}
|
|
175
|
+
if (!inventory.selectedItemId) {
|
|
176
|
+
inventory.selectedCategoryId = activeCategoryId(inventory);
|
|
177
|
+
inventory.selectedItemId = firstItemInCategory(inventory, inventory.selectedCategoryId)?.id || null;
|
|
178
|
+
}
|
|
179
|
+
touchInventory(inventory);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function hasInventoryItem2D(inventory, itemId, quantity = 1) {
|
|
184
|
+
if (!inventory || !Array.isArray(inventory.items)) return false;
|
|
185
|
+
const targetId = normalizeId(itemId, null);
|
|
186
|
+
if (!targetId) return false;
|
|
187
|
+
const item = inventory.items.find((entry) => entry.id === targetId) || null;
|
|
188
|
+
return Boolean(item) && item.quantity >= normalizeQuantity(quantity, 1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function listInventoryItems2D(inventory, categoryId = null) {
|
|
192
|
+
if (!inventory || !Array.isArray(inventory.items)) return [];
|
|
193
|
+
const targetCategory = normalizeId(categoryId, null) || activeCategoryId(inventory);
|
|
194
|
+
if (!targetCategory) return [...inventory.items];
|
|
195
|
+
return inventory.items.filter((entry) => entry.categoryId === targetCategory);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function selectInventoryCategory2D(inventory, categoryId) {
|
|
199
|
+
if (!inventory || !Array.isArray(inventory.categories)) return null;
|
|
200
|
+
const targetId = normalizeId(categoryId, null);
|
|
201
|
+
if (!targetId || !inventory.categories.some((entry) => entry.id === targetId)) return inventory.selectedCategoryId || null;
|
|
202
|
+
inventory.selectedCategoryId = targetId;
|
|
203
|
+
const first = firstItemInCategory(inventory, targetId);
|
|
204
|
+
if (!first) {
|
|
205
|
+
inventory.selectedItemId = null;
|
|
206
|
+
} else if (!hasInventoryItem2D(inventory, inventory.selectedItemId)) {
|
|
207
|
+
inventory.selectedItemId = first.id;
|
|
208
|
+
} else if (!listInventoryItems2D(inventory, targetId).some((entry) => entry.id === inventory.selectedItemId)) {
|
|
209
|
+
inventory.selectedItemId = first.id;
|
|
210
|
+
}
|
|
211
|
+
touchInventory(inventory);
|
|
212
|
+
return inventory.selectedCategoryId;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function selectInventoryItem2D(inventory, itemId) {
|
|
216
|
+
if (!inventory || !Array.isArray(inventory.items)) return null;
|
|
217
|
+
const targetId = normalizeId(itemId, null);
|
|
218
|
+
const item = inventory.items.find((entry) => entry.id === targetId) || null;
|
|
219
|
+
if (!item) return inventory.selectedItemId || null;
|
|
220
|
+
inventory.selectedCategoryId = item.categoryId;
|
|
221
|
+
inventory.selectedItemId = item.id;
|
|
222
|
+
touchInventory(inventory);
|
|
223
|
+
return inventory.selectedItemId;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function getSelectedInventoryItem2D(inventory) {
|
|
227
|
+
if (!inventory || !Array.isArray(inventory.items)) return null;
|
|
228
|
+
return inventory.items.find((entry) => entry.id === inventory.selectedItemId) || null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function setInventoryItemEquipped2D(inventory, itemId, equipped = true) {
|
|
232
|
+
if (!inventory || !Array.isArray(inventory.items)) return false;
|
|
233
|
+
const targetId = normalizeId(itemId, null);
|
|
234
|
+
const item = inventory.items.find((entry) => entry.id === targetId) || null;
|
|
235
|
+
if (!item) return false;
|
|
236
|
+
item.equipped = equipped === true;
|
|
237
|
+
touchInventory(inventory);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getInventoryView2D(inventory) {
|
|
242
|
+
const categories = Array.isArray(inventory?.categories) ? inventory.categories : [];
|
|
243
|
+
const items = listInventoryItems2D(inventory);
|
|
244
|
+
const selectedItem = getSelectedInventoryItem2D(inventory);
|
|
245
|
+
return {
|
|
246
|
+
selectedCategoryId: activeCategoryId(inventory),
|
|
247
|
+
selectedItemId: selectedItem?.id || null,
|
|
248
|
+
categories: categories.map((entry) => ({
|
|
249
|
+
id: entry.id,
|
|
250
|
+
label: entry.label,
|
|
251
|
+
selected: entry.id === activeCategoryId(inventory),
|
|
252
|
+
count: listInventoryItems2D(inventory, entry.id).reduce((total, item) => total + item.quantity, 0),
|
|
253
|
+
})),
|
|
254
|
+
items: items.map((entry) => ({
|
|
255
|
+
id: entry.id,
|
|
256
|
+
label: entry.label,
|
|
257
|
+
description: entry.description,
|
|
258
|
+
detail: entry.detail,
|
|
259
|
+
badge: entry.badge,
|
|
260
|
+
quantity: entry.quantity,
|
|
261
|
+
equipped: entry.equipped === true,
|
|
262
|
+
selected: entry.id === selectedItem?.id,
|
|
263
|
+
disabled: entry.disabled === true,
|
|
264
|
+
})),
|
|
265
|
+
selectedItem,
|
|
266
|
+
revision: Number(inventory?.revision || 0),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
function normalizeId(value, fallback) {
|
|
2
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
3
|
+
return value.trim();
|
|
4
|
+
}
|
|
5
|
+
return fallback;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeLabel(value, fallback) {
|
|
9
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
10
|
+
return value.trim();
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeStatus(value, fallback = 'active') {
|
|
16
|
+
switch (String(value || '').trim()) {
|
|
17
|
+
case 'active':
|
|
18
|
+
case 'completed':
|
|
19
|
+
case 'archived':
|
|
20
|
+
case 'optional':
|
|
21
|
+
return String(value).trim();
|
|
22
|
+
default:
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeCategoryEntry(entry, index) {
|
|
28
|
+
const fallbackId = `category-${index + 1}`;
|
|
29
|
+
const id = normalizeId(entry?.id, fallbackId);
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
label: normalizeLabel(entry?.label ?? entry?.name, id),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function deriveMissingCategories(entries = [], journalEntries = []) {
|
|
37
|
+
const categories = Array.isArray(entries)
|
|
38
|
+
? entries.map((entry, index) => normalizeCategoryEntry(entry, index))
|
|
39
|
+
: [];
|
|
40
|
+
const known = new Set(categories.map((entry) => entry.id));
|
|
41
|
+
for (const entry of Array.isArray(journalEntries) ? journalEntries : []) {
|
|
42
|
+
const categoryId = normalizeId(entry?.categoryId ?? entry?.category, null);
|
|
43
|
+
if (!categoryId || known.has(categoryId)) continue;
|
|
44
|
+
categories.push({
|
|
45
|
+
id: categoryId,
|
|
46
|
+
label: normalizeLabel(entry?.categoryLabel, categoryId),
|
|
47
|
+
});
|
|
48
|
+
known.add(categoryId);
|
|
49
|
+
}
|
|
50
|
+
return categories;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeJournalEntry(entry, index, categories = []) {
|
|
54
|
+
const fallbackId = `entry-${index + 1}`;
|
|
55
|
+
const id = normalizeId(entry?.id, fallbackId);
|
|
56
|
+
const categoryId = normalizeId(entry?.categoryId ?? entry?.category, categories[0]?.id || 'journal');
|
|
57
|
+
const status = normalizeStatus(entry?.status, entry?.completed === true ? 'completed' : 'active');
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
label: normalizeLabel(entry?.label ?? entry?.title ?? entry?.name, id),
|
|
61
|
+
description: typeof entry?.description === 'string'
|
|
62
|
+
? entry.description
|
|
63
|
+
: (typeof entry?.summary === 'string' ? entry.summary : null),
|
|
64
|
+
detail: typeof entry?.detail === 'string'
|
|
65
|
+
? entry.detail
|
|
66
|
+
: (typeof entry?.body === 'string' ? entry.body : null),
|
|
67
|
+
badge: typeof entry?.badge === 'string' && entry.badge.trim().length > 0
|
|
68
|
+
? entry.badge.trim()
|
|
69
|
+
: null,
|
|
70
|
+
categoryId,
|
|
71
|
+
categoryLabel: normalizeLabel(entry?.categoryLabel, categoryId),
|
|
72
|
+
status,
|
|
73
|
+
active: status === 'active',
|
|
74
|
+
completed: status === 'completed',
|
|
75
|
+
optional: status === 'optional',
|
|
76
|
+
archived: status === 'archived',
|
|
77
|
+
disabled: entry?.disabled === true,
|
|
78
|
+
meta: typeof entry?.meta === 'string' && entry.meta.trim().length > 0
|
|
79
|
+
? entry.meta.trim()
|
|
80
|
+
: (typeof entry?.updatedAt === 'string' && entry.updatedAt.trim().length > 0
|
|
81
|
+
? entry.updatedAt.trim()
|
|
82
|
+
: null),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ensureCategory(journal, categoryId, categoryLabel = null) {
|
|
87
|
+
if (!journal || !Array.isArray(journal.categories)) return null;
|
|
88
|
+
const existing = journal.categories.find((entry) => entry.id === categoryId) || null;
|
|
89
|
+
if (existing) return existing;
|
|
90
|
+
const category = {
|
|
91
|
+
id: normalizeId(categoryId, `category-${journal.categories.length + 1}`),
|
|
92
|
+
label: normalizeLabel(categoryLabel, categoryId),
|
|
93
|
+
};
|
|
94
|
+
journal.categories.push(category);
|
|
95
|
+
return category;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function activeCategoryId(journal) {
|
|
99
|
+
if (!journal || !Array.isArray(journal.categories) || journal.categories.length <= 0) return null;
|
|
100
|
+
if (journal.selectedCategoryId && journal.categories.some((entry) => entry.id === journal.selectedCategoryId)) {
|
|
101
|
+
return journal.selectedCategoryId;
|
|
102
|
+
}
|
|
103
|
+
return journal.categories[0].id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function firstEntryInCategory(journal, categoryId = null) {
|
|
107
|
+
const entries = listJournalEntries2D(journal, categoryId);
|
|
108
|
+
return entries[0] || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function touchJournal(journal) {
|
|
112
|
+
if (!journal || typeof journal !== 'object') return;
|
|
113
|
+
journal.revision = Number(journal.revision || 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createJournal2D(options = {}) {
|
|
117
|
+
const categories = deriveMissingCategories(options.categories, options.entries);
|
|
118
|
+
const journal = {
|
|
119
|
+
categories,
|
|
120
|
+
entries: [],
|
|
121
|
+
selectedCategoryId: categories[0]?.id || null,
|
|
122
|
+
selectedEntryId: null,
|
|
123
|
+
revision: 0,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (const [index, entry] of (Array.isArray(options.entries) ? options.entries : []).entries()) {
|
|
127
|
+
const normalized = normalizeJournalEntry(entry, index, categories);
|
|
128
|
+
ensureCategory(journal, normalized.categoryId, normalized.categoryLabel);
|
|
129
|
+
journal.entries.push(normalized);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof options.selectedCategoryId === 'string') {
|
|
133
|
+
selectJournalCategory2D(journal, options.selectedCategoryId);
|
|
134
|
+
}
|
|
135
|
+
if (typeof options.selectedEntryId === 'string') {
|
|
136
|
+
selectJournalEntry2D(journal, options.selectedEntryId);
|
|
137
|
+
}
|
|
138
|
+
if (!journal.selectedEntryId) {
|
|
139
|
+
const first = firstEntryInCategory(journal, journal.selectedCategoryId);
|
|
140
|
+
journal.selectedEntryId = first?.id || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return journal;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function upsertJournalEntry2D(journal, entry) {
|
|
147
|
+
if (!journal || !Array.isArray(journal.entries)) return null;
|
|
148
|
+
const normalized = normalizeJournalEntry(entry, journal.entries.length, journal.categories);
|
|
149
|
+
ensureCategory(journal, normalized.categoryId, normalized.categoryLabel);
|
|
150
|
+
|
|
151
|
+
const existing = journal.entries.find((candidate) => candidate.id === normalized.id) || null;
|
|
152
|
+
if (existing) {
|
|
153
|
+
Object.assign(existing, normalized);
|
|
154
|
+
touchJournal(journal);
|
|
155
|
+
if (!journal.selectedCategoryId) journal.selectedCategoryId = existing.categoryId;
|
|
156
|
+
if (!journal.selectedEntryId) journal.selectedEntryId = existing.id;
|
|
157
|
+
return existing;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
journal.entries.push(normalized);
|
|
161
|
+
if (!journal.selectedCategoryId) journal.selectedCategoryId = normalized.categoryId;
|
|
162
|
+
if (!journal.selectedEntryId) journal.selectedEntryId = normalized.id;
|
|
163
|
+
touchJournal(journal);
|
|
164
|
+
return normalized;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function setJournalEntryStatus2D(journal, entryId, status, updates = {}) {
|
|
168
|
+
if (!journal || !Array.isArray(journal.entries)) return false;
|
|
169
|
+
const targetId = normalizeId(entryId, null);
|
|
170
|
+
const entry = journal.entries.find((candidate) => candidate.id === targetId) || null;
|
|
171
|
+
if (!entry) return false;
|
|
172
|
+
const nextStatus = normalizeStatus(status, entry.status);
|
|
173
|
+
entry.status = nextStatus;
|
|
174
|
+
entry.active = nextStatus === 'active';
|
|
175
|
+
entry.completed = nextStatus === 'completed';
|
|
176
|
+
entry.optional = nextStatus === 'optional';
|
|
177
|
+
entry.archived = nextStatus === 'archived';
|
|
178
|
+
if (typeof updates.badge === 'string') {
|
|
179
|
+
entry.badge = updates.badge.trim().length > 0 ? updates.badge.trim() : null;
|
|
180
|
+
}
|
|
181
|
+
if (typeof updates.meta === 'string') {
|
|
182
|
+
entry.meta = updates.meta.trim().length > 0 ? updates.meta.trim() : null;
|
|
183
|
+
}
|
|
184
|
+
if (typeof updates.description === 'string') {
|
|
185
|
+
entry.description = updates.description;
|
|
186
|
+
}
|
|
187
|
+
if (typeof updates.detail === 'string') {
|
|
188
|
+
entry.detail = updates.detail;
|
|
189
|
+
}
|
|
190
|
+
touchJournal(journal);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function listJournalEntries2D(journal, categoryId = null, options = {}) {
|
|
195
|
+
if (!journal || !Array.isArray(journal.entries)) return [];
|
|
196
|
+
const targetCategory = normalizeId(categoryId, null) || activeCategoryId(journal);
|
|
197
|
+
const statusFilter = normalizeId(options.status, null);
|
|
198
|
+
return journal.entries.filter((entry) => {
|
|
199
|
+
if (targetCategory && entry.categoryId !== targetCategory) return false;
|
|
200
|
+
if (statusFilter && entry.status !== statusFilter) return false;
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function selectJournalCategory2D(journal, categoryId) {
|
|
206
|
+
if (!journal || !Array.isArray(journal.categories)) return null;
|
|
207
|
+
const targetId = normalizeId(categoryId, null);
|
|
208
|
+
if (!targetId || !journal.categories.some((entry) => entry.id === targetId)) {
|
|
209
|
+
return journal.selectedCategoryId || null;
|
|
210
|
+
}
|
|
211
|
+
journal.selectedCategoryId = targetId;
|
|
212
|
+
const first = firstEntryInCategory(journal, targetId);
|
|
213
|
+
if (!first) {
|
|
214
|
+
journal.selectedEntryId = null;
|
|
215
|
+
} else if (!journal.entries.some((entry) => entry.id === journal.selectedEntryId && entry.categoryId === targetId)) {
|
|
216
|
+
journal.selectedEntryId = first.id;
|
|
217
|
+
}
|
|
218
|
+
touchJournal(journal);
|
|
219
|
+
return journal.selectedCategoryId;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function selectJournalEntry2D(journal, entryId) {
|
|
223
|
+
if (!journal || !Array.isArray(journal.entries)) return null;
|
|
224
|
+
const targetId = normalizeId(entryId, null);
|
|
225
|
+
const entry = journal.entries.find((candidate) => candidate.id === targetId) || null;
|
|
226
|
+
if (!entry) return journal.selectedEntryId || null;
|
|
227
|
+
journal.selectedCategoryId = entry.categoryId;
|
|
228
|
+
journal.selectedEntryId = entry.id;
|
|
229
|
+
touchJournal(journal);
|
|
230
|
+
return journal.selectedEntryId;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function getSelectedJournalEntry2D(journal) {
|
|
234
|
+
if (!journal || !Array.isArray(journal.entries)) return null;
|
|
235
|
+
return journal.entries.find((entry) => entry.id === journal.selectedEntryId) || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function getJournalView2D(journal) {
|
|
239
|
+
const categories = Array.isArray(journal?.categories) ? journal.categories : [];
|
|
240
|
+
const entries = listJournalEntries2D(journal);
|
|
241
|
+
const selectedEntry = getSelectedJournalEntry2D(journal);
|
|
242
|
+
return {
|
|
243
|
+
selectedCategoryId: activeCategoryId(journal),
|
|
244
|
+
selectedEntryId: selectedEntry?.id || null,
|
|
245
|
+
categories: categories.map((entry) => ({
|
|
246
|
+
id: entry.id,
|
|
247
|
+
label: entry.label,
|
|
248
|
+
selected: entry.id === activeCategoryId(journal),
|
|
249
|
+
count: listJournalEntries2D(journal, entry.id).length,
|
|
250
|
+
activeCount: listJournalEntries2D(journal, entry.id, { status: 'active' }).length,
|
|
251
|
+
completedCount: listJournalEntries2D(journal, entry.id, { status: 'completed' }).length,
|
|
252
|
+
})),
|
|
253
|
+
entries: entries.map((entry) => ({
|
|
254
|
+
id: entry.id,
|
|
255
|
+
label: entry.label,
|
|
256
|
+
description: entry.description,
|
|
257
|
+
detail: entry.detail,
|
|
258
|
+
badge: entry.badge || (entry.completed ? 'Completed' : entry.active ? 'Active' : normalizeLabel(entry.status, 'Journal')),
|
|
259
|
+
status: entry.status,
|
|
260
|
+
selected: entry.id === selectedEntry?.id,
|
|
261
|
+
disabled: entry.disabled === true,
|
|
262
|
+
meta: entry.meta,
|
|
263
|
+
})),
|
|
264
|
+
selectedEntry,
|
|
265
|
+
revision: Number(journal?.revision || 0),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { clamp, distanceSquared3D } from './core.js';
|
|
2
|
+
|
|
3
|
+
function resolveRespawnTarget(body) {
|
|
4
|
+
if (body && typeof body === 'object' && body.position && typeof body.position === 'object') {
|
|
5
|
+
return body.position;
|
|
6
|
+
}
|
|
7
|
+
return body;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createCheckpointSystem(options = {}) {
|
|
11
|
+
const checkpoints = Array.isArray(options.checkpoints)
|
|
12
|
+
? options.checkpoints.map((entry) => ({
|
|
13
|
+
x: Number(entry?.x) || 0,
|
|
14
|
+
y: Number(entry?.y) || 0,
|
|
15
|
+
z: Number(entry?.z) || 0,
|
|
16
|
+
radius: Math.max(0.2, Number(entry?.radius) || 1.25),
|
|
17
|
+
}))
|
|
18
|
+
: [];
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
checkpoints,
|
|
22
|
+
activeIndex: checkpoints.length > 0 ? 0 : -1,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function updateCheckpointProgress(system, player) {
|
|
27
|
+
if (!system || system.activeIndex < 0) return false;
|
|
28
|
+
const nextIndex = Math.min(system.activeIndex + 1, system.checkpoints.length - 1);
|
|
29
|
+
const next = system.checkpoints[nextIndex];
|
|
30
|
+
if (!next) return false;
|
|
31
|
+
|
|
32
|
+
const reached = distanceSquared3D(player, next) <= (next.radius * next.radius);
|
|
33
|
+
if (!reached) return false;
|
|
34
|
+
|
|
35
|
+
system.activeIndex = nextIndex;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function activeCheckpoint(system) {
|
|
40
|
+
if (!system || system.activeIndex < 0) return null;
|
|
41
|
+
return system.checkpoints[system.activeIndex] || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function respawnAtCheckpoint(system, body) {
|
|
45
|
+
const checkpoint = activeCheckpoint(system);
|
|
46
|
+
if (!checkpoint) return false;
|
|
47
|
+
|
|
48
|
+
const target = resolveRespawnTarget(body);
|
|
49
|
+
if (!target || typeof target !== 'object') return false;
|
|
50
|
+
|
|
51
|
+
target.x = checkpoint.x;
|
|
52
|
+
target.y = checkpoint.y;
|
|
53
|
+
target.z = checkpoint.z;
|
|
54
|
+
|
|
55
|
+
if (body && typeof body === 'object' && body !== target) {
|
|
56
|
+
body.velocityY = 0;
|
|
57
|
+
body.grounded = false;
|
|
58
|
+
body.onSlope = false;
|
|
59
|
+
body.locomotion = 'idle';
|
|
60
|
+
body.motion = {
|
|
61
|
+
moveX: 0,
|
|
62
|
+
moveZ: 0,
|
|
63
|
+
magnitude: 0,
|
|
64
|
+
speed: 0,
|
|
65
|
+
};
|
|
66
|
+
body.input = {
|
|
67
|
+
moveX: 0,
|
|
68
|
+
moveZ: 0,
|
|
69
|
+
jump: false,
|
|
70
|
+
run: false,
|
|
71
|
+
};
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
body.vy = 0;
|
|
76
|
+
body.onGround = false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createGoalZone(options = {}) {
|
|
81
|
+
return {
|
|
82
|
+
x: Number(options.x) || 0,
|
|
83
|
+
y: Number(options.y) || 0,
|
|
84
|
+
z: Number(options.z) || 0,
|
|
85
|
+
radius: Math.max(0.2, Number(options.radius) || 1.1),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isGoalReached(goalZone, player) {
|
|
90
|
+
if (!goalZone) return false;
|
|
91
|
+
return distanceSquared3D(goalZone, player) <= (goalZone.radius * goalZone.radius);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createMovingPlatform3D(options = {}) {
|
|
95
|
+
const from = {
|
|
96
|
+
x: Number(options?.from?.x) || 0,
|
|
97
|
+
y: Number(options?.from?.y) || 0,
|
|
98
|
+
z: Number(options?.from?.z) || 0,
|
|
99
|
+
};
|
|
100
|
+
const to = {
|
|
101
|
+
x: Number(options?.to?.x) || from.x,
|
|
102
|
+
y: Number(options?.to?.y) || from.y,
|
|
103
|
+
z: Number(options?.to?.z) || from.z,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
from,
|
|
108
|
+
to,
|
|
109
|
+
size: {
|
|
110
|
+
x: Math.max(0.1, Number(options?.size?.x) || 2.4),
|
|
111
|
+
y: Math.max(0.1, Number(options?.size?.y) || 0.4),
|
|
112
|
+
z: Math.max(0.1, Number(options?.size?.z) || 2.0),
|
|
113
|
+
},
|
|
114
|
+
periodSeconds: Math.max(0.2, Number(options.periodSeconds) || 2.8),
|
|
115
|
+
t: 0,
|
|
116
|
+
position: { ...from },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function stepMovingPlatform3D(platform, dt) {
|
|
121
|
+
const frameDt = Math.max(0, Number(dt) || 0);
|
|
122
|
+
platform.t += frameDt;
|
|
123
|
+
|
|
124
|
+
const cycle = (platform.t / platform.periodSeconds) * Math.PI * 2;
|
|
125
|
+
const pingPong = (Math.sin(cycle) * 0.5) + 0.5;
|
|
126
|
+
const alpha = clamp(pingPong, 0, 1);
|
|
127
|
+
|
|
128
|
+
platform.position.x = platform.from.x + ((platform.to.x - platform.from.x) * alpha);
|
|
129
|
+
platform.position.y = platform.from.y + ((platform.to.y - platform.from.y) * alpha);
|
|
130
|
+
platform.position.z = platform.from.z + ((platform.to.z - platform.from.z) * alpha);
|
|
131
|
+
return platform.position;
|
|
132
|
+
}
|