@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
@@ -0,0 +1,320 @@
1
+ function finite(value, fallback = 0) {
2
+ const numeric = Number(value);
3
+ return Number.isFinite(numeric) ? numeric : fallback;
4
+ }
5
+
6
+ function positiveNumber(value, fallback, label) {
7
+ const numeric = finite(value, fallback);
8
+ if (!(numeric > 0)) {
9
+ throw new Error(`${label} must be a positive number.`);
10
+ }
11
+ return numeric;
12
+ }
13
+
14
+ function positiveInteger(value, fallback, label) {
15
+ const numeric = Math.floor(finite(value, fallback));
16
+ if (!(numeric >= 1)) {
17
+ throw new Error(`${label} must be a positive integer.`);
18
+ }
19
+ return numeric;
20
+ }
21
+
22
+ function clamp01(value) {
23
+ return Math.max(0, Math.min(1, finite(value, 0)));
24
+ }
25
+
26
+ const EPSILON = 1e-9;
27
+
28
+ function easeOutQuad(value) {
29
+ const t = clamp01(value);
30
+ return t * (2 - t);
31
+ }
32
+
33
+ function defaultTextAlign(value, fallback = 'center') {
34
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : fallback;
35
+ }
36
+
37
+ export function createFloatingTextLayer2D(options = {}) {
38
+ return {
39
+ nextId: 1,
40
+ entries: [],
41
+ maxEntries: positiveInteger(options.maxEntries, 32, 'floating text maxEntries'),
42
+ defaultRise: positiveNumber(options.rise, 28, 'floating text rise'),
43
+ defaultLifetime: positiveNumber(options.lifetime, 0.6, 'floating text lifetime'),
44
+ defaultSize: positiveNumber(options.size, 14, 'floating text size'),
45
+ defaultDriftX: finite(options.driftX, 0),
46
+ defaultAlign: defaultTextAlign(options.align, 'center'),
47
+ defaultColor: Object.prototype.hasOwnProperty.call(options, 'color') ? options.color : null,
48
+ };
49
+ }
50
+
51
+ function updateFloatingTextEntry(entry) {
52
+ const progress = clamp01(entry.elapsed / entry.lifetime);
53
+ entry.x = entry.baseX + (entry.driftX * progress);
54
+ entry.y = entry.baseY - (entry.rise * easeOutQuad(progress));
55
+ entry.alpha = 1 - progress;
56
+ }
57
+
58
+ export function spawnFloatingText2D(layer, options = {}) {
59
+ if (!layer?.entries || !Array.isArray(layer.entries)) {
60
+ throw new Error('floating text layer is required.');
61
+ }
62
+
63
+ const entry = {
64
+ id: layer.nextId++,
65
+ text: String(
66
+ Object.prototype.hasOwnProperty.call(options, 'text')
67
+ ? options.text
68
+ : (Object.prototype.hasOwnProperty.call(options, 'value') ? options.value : ''),
69
+ ),
70
+ baseX: finite(options.x, 0),
71
+ baseY: finite(options.y, 0),
72
+ x: finite(options.x, 0),
73
+ y: finite(options.y, 0),
74
+ driftX: finite(options.driftX, layer.defaultDriftX),
75
+ rise: positiveNumber(options.rise, layer.defaultRise, 'floating text rise'),
76
+ lifetime: positiveNumber(options.lifetime, layer.defaultLifetime, 'floating text lifetime'),
77
+ elapsed: 0,
78
+ size: positiveNumber(options.size, layer.defaultSize, 'floating text size'),
79
+ align: defaultTextAlign(options.align, layer.defaultAlign),
80
+ color: Object.prototype.hasOwnProperty.call(options, 'color') ? options.color : layer.defaultColor,
81
+ alpha: 1,
82
+ };
83
+
84
+ if (layer.entries.length >= layer.maxEntries) {
85
+ layer.entries.shift();
86
+ }
87
+ layer.entries.push(entry);
88
+ return entry;
89
+ }
90
+
91
+ export function stepFloatingTextLayer2D(layer, dt) {
92
+ if (!layer?.entries || !Array.isArray(layer.entries)) {
93
+ throw new Error('floating text layer is required.');
94
+ }
95
+
96
+ const stepDt = Math.max(0, finite(dt, 0));
97
+ for (let index = layer.entries.length - 1; index >= 0; index -= 1) {
98
+ const entry = layer.entries[index];
99
+ entry.elapsed = Math.min(entry.lifetime, entry.elapsed + stepDt);
100
+ updateFloatingTextEntry(entry);
101
+ if (entry.elapsed >= entry.lifetime) {
102
+ layer.entries.splice(index, 1);
103
+ }
104
+ }
105
+ return layer.entries.length;
106
+ }
107
+
108
+ export function getFloatingTextRenderState2D(entry) {
109
+ if (!entry) return null;
110
+ return {
111
+ id: entry.id,
112
+ text: entry.text,
113
+ x: entry.x,
114
+ y: entry.y,
115
+ size: entry.size,
116
+ align: entry.align,
117
+ color: entry.color,
118
+ alpha: entry.alpha,
119
+ };
120
+ }
121
+
122
+ export function drawFloatingTextLayer2D(aura, layer, options = {}) {
123
+ if (!aura?.draw2d || typeof aura.draw2d.text !== 'function') {
124
+ throw new Error('floating text drawing requires aura.draw2d.text support.');
125
+ }
126
+ if (!layer?.entries || !Array.isArray(layer.entries)) {
127
+ throw new Error('floating text layer is required.');
128
+ }
129
+
130
+ const resolveColor = typeof options.resolveColor === 'function' ? options.resolveColor : null;
131
+ let drawCount = 0;
132
+ for (const entry of layer.entries) {
133
+ const renderState = getFloatingTextRenderState2D(entry);
134
+ const color = resolveColor ? resolveColor(renderState, entry) : renderState.color;
135
+ aura.draw2d.text(renderState.text, renderState.x, renderState.y, {
136
+ size: renderState.size,
137
+ align: renderState.align,
138
+ color,
139
+ });
140
+ drawCount += 1;
141
+ }
142
+ return drawCount;
143
+ }
144
+
145
+ export function createHitFlash2D(options = {}) {
146
+ return {
147
+ duration: positiveNumber(options.duration, 0.4, 'hit flash duration'),
148
+ flashesPerSecond: positiveNumber(options.flashesPerSecond, 12, 'hit flash flashesPerSecond'),
149
+ timeRemaining: 0,
150
+ active: false,
151
+ };
152
+ }
153
+
154
+ export function triggerHitFlash2D(flash, options = {}) {
155
+ if (!flash || typeof flash !== 'object') {
156
+ throw new Error('hit flash state is required.');
157
+ }
158
+ flash.duration = positiveNumber(options.duration, flash.duration, 'hit flash duration');
159
+ flash.flashesPerSecond = positiveNumber(
160
+ options.flashesPerSecond,
161
+ flash.flashesPerSecond,
162
+ 'hit flash flashesPerSecond',
163
+ );
164
+ flash.timeRemaining = flash.duration;
165
+ flash.active = true;
166
+ return flash.timeRemaining;
167
+ }
168
+
169
+ export function stepHitFlash2D(flash, dt) {
170
+ if (!flash || typeof flash !== 'object') {
171
+ throw new Error('hit flash state is required.');
172
+ }
173
+ const stepDt = Math.max(0, finite(dt, 0));
174
+ const remaining = flash.timeRemaining - stepDt;
175
+ flash.timeRemaining = remaining > EPSILON ? remaining : 0;
176
+ flash.active = flash.timeRemaining > 0;
177
+ return flash.timeRemaining;
178
+ }
179
+
180
+ export function isHitFlashVisible2D(flash) {
181
+ if (!flash?.active || !(flash.timeRemaining > 0)) return false;
182
+ return Math.floor(flash.timeRemaining * flash.flashesPerSecond) % 2 === 0;
183
+ }
184
+
185
+ export function createHitSparkLayer2D(options = {}) {
186
+ return {
187
+ nextId: 1,
188
+ bursts: [],
189
+ maxEntries: positiveInteger(options.maxEntries, 24, 'hit spark maxEntries'),
190
+ defaultCount: positiveInteger(options.count, 6, 'hit spark count'),
191
+ defaultRadius: positiveNumber(options.radius, 18, 'hit spark radius'),
192
+ defaultInnerRadius: positiveNumber(options.innerRadius, 2.5, 'hit spark innerRadius'),
193
+ defaultLifetime: positiveNumber(options.lifetime, 0.16, 'hit spark lifetime'),
194
+ defaultLineWidth: positiveNumber(options.lineWidth, 2, 'hit spark lineWidth'),
195
+ defaultColor: Object.prototype.hasOwnProperty.call(options, 'color') ? options.color : null,
196
+ };
197
+ }
198
+
199
+ export function spawnHitSpark2D(layer, options = {}) {
200
+ if (!layer?.bursts || !Array.isArray(layer.bursts)) {
201
+ throw new Error('hit spark layer is required.');
202
+ }
203
+
204
+ const count = positiveInteger(options.count, layer.defaultCount, 'hit spark count');
205
+ const rotation = finite(options.rotation, 0);
206
+ const burst = {
207
+ id: layer.nextId++,
208
+ x: finite(options.x, 0),
209
+ y: finite(options.y, 0),
210
+ radius: positiveNumber(options.radius, layer.defaultRadius, 'hit spark radius'),
211
+ innerRadius: positiveNumber(options.innerRadius, layer.defaultInnerRadius, 'hit spark innerRadius'),
212
+ lifetime: positiveNumber(options.lifetime, layer.defaultLifetime, 'hit spark lifetime'),
213
+ lineWidth: positiveNumber(options.lineWidth, layer.defaultLineWidth, 'hit spark lineWidth'),
214
+ elapsed: 0,
215
+ color: Object.prototype.hasOwnProperty.call(options, 'color') ? options.color : layer.defaultColor,
216
+ angles: Array.from({ length: count }, (_, index) => rotation + ((Math.PI * 2 * index) / count)),
217
+ };
218
+
219
+ if (layer.bursts.length >= layer.maxEntries) {
220
+ layer.bursts.shift();
221
+ }
222
+ layer.bursts.push(burst);
223
+ return burst;
224
+ }
225
+
226
+ export function stepHitSparkLayer2D(layer, dt) {
227
+ if (!layer?.bursts || !Array.isArray(layer.bursts)) {
228
+ throw new Error('hit spark layer is required.');
229
+ }
230
+
231
+ const stepDt = Math.max(0, finite(dt, 0));
232
+ for (let index = layer.bursts.length - 1; index >= 0; index -= 1) {
233
+ const burst = layer.bursts[index];
234
+ burst.elapsed = Math.min(burst.lifetime, burst.elapsed + stepDt);
235
+ if (burst.elapsed >= burst.lifetime) {
236
+ layer.bursts.splice(index, 1);
237
+ }
238
+ }
239
+ return layer.bursts.length;
240
+ }
241
+
242
+ function resolveHitSparkGeometry(burst) {
243
+ const progress = clamp01(burst.elapsed / burst.lifetime);
244
+ const eased = easeOutQuad(progress);
245
+ return {
246
+ progress,
247
+ alpha: 1 - progress,
248
+ innerRadius: burst.innerRadius * (1 - (progress * 0.35)),
249
+ reach: burst.radius * (0.45 + (eased * 0.55)),
250
+ };
251
+ }
252
+
253
+ export function drawHitSparkLayer2D(aura, layer, options = {}) {
254
+ if (!aura?.draw2d || typeof aura.draw2d.line !== 'function') {
255
+ throw new Error('hit spark drawing requires aura.draw2d.line support.');
256
+ }
257
+ if (!layer?.bursts || !Array.isArray(layer.bursts)) {
258
+ throw new Error('hit spark layer is required.');
259
+ }
260
+
261
+ const resolveColor = typeof options.resolveColor === 'function' ? options.resolveColor : null;
262
+ let drawCount = 0;
263
+ for (const burst of layer.bursts) {
264
+ const geometry = resolveHitSparkGeometry(burst);
265
+ const color = resolveColor ? resolveColor(burst, geometry) : burst.color;
266
+ if (typeof aura.draw2d.circleFill === 'function' && geometry.innerRadius > 0.25) {
267
+ aura.draw2d.circleFill(burst.x, burst.y, geometry.innerRadius, color);
268
+ drawCount += 1;
269
+ }
270
+ for (const angle of burst.angles) {
271
+ const startX = burst.x + (Math.cos(angle) * geometry.innerRadius);
272
+ const startY = burst.y + (Math.sin(angle) * geometry.innerRadius);
273
+ const endX = burst.x + (Math.cos(angle) * geometry.reach);
274
+ const endY = burst.y + (Math.sin(angle) * geometry.reach);
275
+ aura.draw2d.line(startX, startY, endX, endY, color, burst.lineWidth);
276
+ drawCount += 1;
277
+ }
278
+ }
279
+ return drawCount;
280
+ }
281
+
282
+ export function createHoverLift2D(options = {}) {
283
+ return {
284
+ active: false,
285
+ lift: positiveNumber(options.lift, 12, 'hover lift amount'),
286
+ bob: finite(options.bob, 1.5),
287
+ phase: finite(options.phase, 0),
288
+ travel: clamp01(options.travel),
289
+ travelSpeed: positiveNumber(options.travelSpeed, 12, 'hover lift travelSpeed'),
290
+ bobSpeed: positiveNumber(options.bobSpeed, 7, 'hover lift bobSpeed'),
291
+ };
292
+ }
293
+
294
+ export function setHoverLiftActive2D(state, active) {
295
+ if (!state || typeof state !== 'object') {
296
+ throw new Error('hover lift state is required.');
297
+ }
298
+ state.active = active === true;
299
+ return state.active;
300
+ }
301
+
302
+ export function stepHoverLift2D(state, dt) {
303
+ if (!state || typeof state !== 'object') {
304
+ throw new Error('hover lift state is required.');
305
+ }
306
+ const stepDt = Math.max(0, finite(dt, 0));
307
+ const targetTravel = state.active ? 1 : 0;
308
+ const mix = Math.min(1, stepDt * state.travelSpeed);
309
+ state.travel = state.travel + ((targetTravel - state.travel) * mix);
310
+ state.phase += stepDt * state.bobSpeed;
311
+ return getHoverLiftOffset2D(state);
312
+ }
313
+
314
+ export function getHoverLiftOffset2D(state) {
315
+ if (!state || typeof state !== 'object') {
316
+ throw new Error('hover lift state is required.');
317
+ }
318
+ const bobOffset = state.travel > 0 ? (Math.sin(state.phase) * state.bob * state.travel) : 0;
319
+ return -((state.lift * state.travel) + bobOffset);
320
+ }
@@ -0,0 +1,290 @@
1
+ function finite(value, fallback = 0) {
2
+ const numeric = Number(value);
3
+ return Number.isFinite(numeric) ? numeric : fallback;
4
+ }
5
+
6
+ function positiveNumber(value, fallback, label) {
7
+ const numeric = finite(value, fallback);
8
+ if (!(numeric > 0)) {
9
+ throw new Error(`${label} must be a positive number.`);
10
+ }
11
+ return numeric;
12
+ }
13
+
14
+ function positiveInteger(value, fallback, label) {
15
+ const numeric = Math.floor(finite(value, fallback));
16
+ if (!(numeric >= 1)) {
17
+ throw new Error(`${label} must be a positive integer.`);
18
+ }
19
+ return numeric;
20
+ }
21
+
22
+ function ratio01(value) {
23
+ return Math.max(0, Math.min(1, finite(value, 0)));
24
+ }
25
+
26
+ function keyName(value, fallback) {
27
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : fallback;
28
+ }
29
+
30
+ export function applyShieldedDamage2D(target, amount, options = {}) {
31
+ if (!target || typeof target !== 'object') {
32
+ throw new Error('shielded damage target is required.');
33
+ }
34
+
35
+ const hpKey = keyName(options.hpKey, 'hp');
36
+ const shieldKey = keyName(options.shieldKey, 'shield');
37
+ const invulnKey = keyName(options.invulnKey, 'invuln');
38
+ const rechargeDelayKey = keyName(options.rechargeDelayKey, 'shieldRechargeDelay');
39
+ const rechargeTimerKey = keyName(options.rechargeTimerKey, 'shieldRechargeTimer');
40
+ const damageAmount = Math.max(0, finite(amount, 0));
41
+ const invulnDuration = Math.max(0, finite(options.invulnDuration, 0));
42
+
43
+ if (damageAmount <= 0) {
44
+ return {
45
+ ok: true,
46
+ applied: false,
47
+ reasonCode: 'no_damage',
48
+ defeated: finite(target[hpKey], 0) <= 0,
49
+ absorbed: 0,
50
+ hpLost: 0,
51
+ hp: finite(target[hpKey], 0),
52
+ shield: finite(target[shieldKey], 0),
53
+ };
54
+ }
55
+
56
+ if (finite(target[invulnKey], 0) > 0 && options.ignoreInvuln !== true) {
57
+ return {
58
+ ok: true,
59
+ applied: false,
60
+ reasonCode: 'invulnerable',
61
+ defeated: finite(target[hpKey], 0) <= 0,
62
+ absorbed: 0,
63
+ hpLost: 0,
64
+ hp: finite(target[hpKey], 0),
65
+ shield: finite(target[shieldKey], 0),
66
+ };
67
+ }
68
+
69
+ target[rechargeTimerKey] = finite(target[rechargeDelayKey], 0);
70
+
71
+ let remaining = damageAmount;
72
+ const shield = Math.max(0, finite(target[shieldKey], 0));
73
+ const absorbed = Math.min(shield, remaining);
74
+ if (absorbed > 0) {
75
+ target[shieldKey] = shield - absorbed;
76
+ remaining -= absorbed;
77
+ }
78
+
79
+ const hpBefore = Math.max(0, finite(target[hpKey], 0));
80
+ const hpLost = Math.min(hpBefore, remaining);
81
+ if (hpLost > 0) {
82
+ target[hpKey] = hpBefore - hpLost;
83
+ }
84
+
85
+ if (invulnDuration > 0) {
86
+ target[invulnKey] = Math.max(finite(target[invulnKey], 0), invulnDuration);
87
+ }
88
+
89
+ return {
90
+ ok: true,
91
+ applied: absorbed > 0 || hpLost > 0,
92
+ reasonCode: null,
93
+ defeated: finite(target[hpKey], 0) <= 0,
94
+ absorbed,
95
+ hpLost,
96
+ hp: Math.max(0, finite(target[hpKey], 0)),
97
+ shield: Math.max(0, finite(target[shieldKey], 0)),
98
+ };
99
+ }
100
+
101
+ export function stepShieldRecharge2D(target, dt, options = {}) {
102
+ if (!target || typeof target !== 'object') {
103
+ throw new Error('shield recharge target is required.');
104
+ }
105
+
106
+ const shieldKey = keyName(options.shieldKey, 'shield');
107
+ const maxShieldKey = keyName(options.maxShieldKey, 'maxShield');
108
+ const rechargeTimerKey = keyName(options.rechargeTimerKey, 'shieldRechargeTimer');
109
+ const rate = Math.max(0, finite(options.rate, 0));
110
+ const stepDt = Math.max(0, finite(dt, 0));
111
+
112
+ target[rechargeTimerKey] = Math.max(0, finite(target[rechargeTimerKey], 0) - stepDt);
113
+ if (target[rechargeTimerKey] > 0 || rate <= 0) {
114
+ return {
115
+ ok: true,
116
+ recharged: 0,
117
+ shield: Math.max(0, finite(target[shieldKey], 0)),
118
+ timer: target[rechargeTimerKey],
119
+ };
120
+ }
121
+
122
+ const maxShield = Math.max(0, finite(target[maxShieldKey], 0));
123
+ const shield = Math.max(0, finite(target[shieldKey], 0));
124
+ const nextShield = Math.min(maxShield, shield + (rate * stepDt));
125
+ const recharged = nextShield - shield;
126
+ target[shieldKey] = nextShield;
127
+
128
+ return {
129
+ ok: true,
130
+ recharged,
131
+ shield: nextShield,
132
+ timer: target[rechargeTimerKey],
133
+ };
134
+ }
135
+
136
+ export function restoreShieldedHealth2D(target, options = {}) {
137
+ if (!target || typeof target !== 'object') {
138
+ throw new Error('shield restore target is required.');
139
+ }
140
+
141
+ const hpKey = keyName(options.hpKey, 'hp');
142
+ const maxHpKey = keyName(options.maxHpKey, 'maxHp');
143
+ const shieldKey = keyName(options.shieldKey, 'shield');
144
+ const maxShieldKey = keyName(options.maxShieldKey, 'maxShield');
145
+ const hpAmount = Math.max(0, finite(options.hp, 0));
146
+ const shieldAmount = Math.max(0, finite(options.shield, 0));
147
+
148
+ if (hpAmount > 0) {
149
+ target[hpKey] = Math.min(Math.max(0, finite(target[maxHpKey], 0)), Math.max(0, finite(target[hpKey], 0)) + hpAmount);
150
+ }
151
+ if (shieldAmount > 0) {
152
+ target[shieldKey] = Math.min(Math.max(0, finite(target[maxShieldKey], 0)), Math.max(0, finite(target[shieldKey], 0)) + shieldAmount);
153
+ }
154
+
155
+ return {
156
+ ok: true,
157
+ hp: Math.max(0, finite(target[hpKey], 0)),
158
+ shield: Math.max(0, finite(target[shieldKey], 0)),
159
+ };
160
+ }
161
+
162
+ function normalizeDropEntries(entries = []) {
163
+ let runningWeight = 0;
164
+ return (Array.isArray(entries) ? entries : [])
165
+ .map((entry, index) => {
166
+ const raw = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
167
+ if (!raw) return null;
168
+ const id = keyName(raw.id, `drop-${index + 1}`);
169
+ const weight = positiveNumber(raw.weight, 1, `drop "${id}" weight`);
170
+ runningWeight += weight;
171
+ return {
172
+ ...raw,
173
+ id,
174
+ weight,
175
+ cumulativeWeight: runningWeight,
176
+ };
177
+ })
178
+ .filter(Boolean);
179
+ }
180
+
181
+ export function createWeightedDropTable2D(entries = []) {
182
+ const normalized = normalizeDropEntries(entries);
183
+ return {
184
+ entries: normalized,
185
+ totalWeight: normalized.length > 0 ? normalized[normalized.length - 1].cumulativeWeight : 0,
186
+ };
187
+ }
188
+
189
+ export function rollWeightedDrop2D(table, sample = Math.random()) {
190
+ if (!table || !Array.isArray(table.entries) || table.entries.length === 0 || !(table.totalWeight > 0)) {
191
+ return null;
192
+ }
193
+
194
+ const threshold = ratio01(sample) * table.totalWeight;
195
+ for (const entry of table.entries) {
196
+ if (threshold <= entry.cumulativeWeight) {
197
+ return entry;
198
+ }
199
+ }
200
+ return table.entries[table.entries.length - 1] || null;
201
+ }
202
+
203
+ export function createEncounterPressure2D(options = {}) {
204
+ return {
205
+ kills: Math.max(0, Math.floor(finite(options.kills, 0))),
206
+ wave: Math.max(1, Math.floor(finite(options.wave, 1))),
207
+ combo: Math.max(0, Math.floor(finite(options.combo, 0))),
208
+ comboTimer: Math.max(0, finite(options.comboTimer, 0)),
209
+ comboWindow: positiveNumber(options.comboWindow, 2.1, 'combat pressure comboWindow'),
210
+ waveEveryKills: positiveInteger(options.waveEveryKills, 10, 'combat pressure waveEveryKills'),
211
+ };
212
+ }
213
+
214
+ export function recordEncounterKill2D(pressure, options = {}) {
215
+ if (!pressure || typeof pressure !== 'object') {
216
+ throw new Error('encounter pressure state is required.');
217
+ }
218
+
219
+ const comboWindow = positiveNumber(options.comboWindow, pressure.comboWindow, 'combat pressure comboWindow');
220
+ const waveEveryKills = positiveInteger(options.waveEveryKills, pressure.waveEveryKills, 'combat pressure waveEveryKills');
221
+
222
+ pressure.comboWindow = comboWindow;
223
+ pressure.waveEveryKills = waveEveryKills;
224
+ pressure.kills = Math.max(0, Math.floor(finite(pressure.kills, 0))) + 1;
225
+ pressure.combo = Math.max(0, Math.floor(finite(pressure.combo, 0))) + 1;
226
+ pressure.comboTimer = comboWindow;
227
+
228
+ const waveAdvanced = pressure.kills % waveEveryKills === 0;
229
+ if (waveAdvanced) {
230
+ pressure.wave = Math.max(1, Math.floor(finite(pressure.wave, 1))) + 1;
231
+ }
232
+
233
+ return {
234
+ ok: true,
235
+ waveAdvanced,
236
+ kills: pressure.kills,
237
+ combo: pressure.combo,
238
+ comboTimer: pressure.comboTimer,
239
+ wave: pressure.wave,
240
+ };
241
+ }
242
+
243
+ export function stepEncounterPressure2D(pressure, dt) {
244
+ if (!pressure || typeof pressure !== 'object') {
245
+ throw new Error('encounter pressure state is required.');
246
+ }
247
+
248
+ pressure.comboTimer = Math.max(0, finite(pressure.comboTimer, 0) - Math.max(0, finite(dt, 0)));
249
+ if (pressure.comboTimer <= 0) {
250
+ pressure.combo = 0;
251
+ }
252
+
253
+ return {
254
+ ok: true,
255
+ combo: pressure.combo,
256
+ comboTimer: pressure.comboTimer,
257
+ wave: Math.max(1, Math.floor(finite(pressure.wave, 1))),
258
+ kills: Math.max(0, Math.floor(finite(pressure.kills, 0))),
259
+ };
260
+ }
261
+
262
+ export function advanceBossPhase2D(entity, healthRatio, thresholds = [], options = {}) {
263
+ if (!entity || typeof entity !== 'object') {
264
+ throw new Error('boss phase entity is required.');
265
+ }
266
+
267
+ const phaseKey = keyName(options.phaseKey, 'phaseStage');
268
+ const normalizedThresholds = [...(Array.isArray(thresholds) ? thresholds : [])]
269
+ .map((entry) => ratio01(entry))
270
+ .sort((left, right) => right - left);
271
+ const currentPhase = Math.max(0, Math.floor(finite(entity[phaseKey], 0)));
272
+ let nextPhase = currentPhase;
273
+
274
+ for (let index = 0; index < normalizedThresholds.length; index += 1) {
275
+ if (ratio01(healthRatio) <= normalizedThresholds[index]) {
276
+ nextPhase = Math.max(nextPhase, index + 1);
277
+ }
278
+ }
279
+
280
+ if (options.mutate === true) {
281
+ entity[phaseKey] = nextPhase;
282
+ }
283
+
284
+ return {
285
+ ok: true,
286
+ shifted: nextPhase > currentPhase,
287
+ phaseStage: nextPhase,
288
+ previousPhaseStage: currentPhase,
289
+ };
290
+ }