@auraindustry/aurajs 0.0.5 → 0.0.7
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 +5 -3
- package/benchmarks/perf-thresholds.json +27 -0
- package/package.json +2 -1
- package/src/build-contract.mjs +1644 -56
- package/src/cli.mjs +1048 -321
- package/src/conformance.mjs +356 -11
- package/src/cutscene.mjs +205 -0
- package/src/game-state-runtime.mjs +260 -0
- package/src/headless-test.mjs +92 -9
- package/src/perf-benchmark.mjs +103 -0
- package/src/scaffold.mjs +413 -13
- package/src/state-artifacts.mjs +321 -0
- package/src/state-dev-reload.mjs +120 -0
- package/templates/create/2d-survivor/aura.config.json +28 -0
- package/templates/create/2d-survivor/src/main.js +344 -0
- package/templates/create/3d-collectathon/aura.config.json +28 -0
- package/templates/create/3d-collectathon/src/main.js +367 -0
- package/templates/skills/aurajs/api-contract-3d.md +1 -1
- package/templates/skills/aurajs/api-contract.md +1 -1
- package/src/.gitkeep +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// {{PROJECT_TITLE}} - AuraJS 2D survivor starter
|
|
2
|
+
|
|
3
|
+
import { axisFromKeys, clamp, consumeCooldown, createCooldown, createWaveDirector, pickEnemyArchetypeForWave2D, removeWhere, spawnEnemy2D, stepWaveDirector, tickCooldown } from './starter-utils/index.js';
|
|
4
|
+
|
|
5
|
+
const PLAYER_SIZE = 28;
|
|
6
|
+
const MOVE_SPEED = 320;
|
|
7
|
+
const DASH_SPEED = 560;
|
|
8
|
+
const DASH_DURATION = 0.16;
|
|
9
|
+
const DASH_COOLDOWN = 1.2;
|
|
10
|
+
const BULLET_SIZE = 6;
|
|
11
|
+
const BULLET_SPEED = 560;
|
|
12
|
+
const BULLET_LIFETIME = 1.1;
|
|
13
|
+
const AUTO_FIRE_COOLDOWN = 0.16;
|
|
14
|
+
const PLAYER_TOUCH_RADIUS = 24;
|
|
15
|
+
const ENEMY_HIT_INVULN = 0.65;
|
|
16
|
+
const STARTING_HEALTH = 5;
|
|
17
|
+
|
|
18
|
+
const SPAWN_WAVES = [
|
|
19
|
+
{ maxSpawns: 16, spawnEvery: 0.52, archetype: 'scout', hpScale: 1.0, speedScale: 1.0 },
|
|
20
|
+
{ maxSpawns: 14, spawnEvery: 0.48, archetype: 'striker', hpScale: 1.3, speedScale: 1.05 },
|
|
21
|
+
{ maxSpawns: 12, spawnEvery: 0.44, archetype: 'tank', hpScale: 1.9, speedScale: 1.02 },
|
|
22
|
+
{ maxSpawns: 22, spawnEvery: 0.36, archetype: null, hpScale: 1.4, speedScale: 1.12 },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
let player = { x: 0, y: 0 };
|
|
26
|
+
let bullets = [];
|
|
27
|
+
let enemies = [];
|
|
28
|
+
let score = 0;
|
|
29
|
+
let elapsed = 0;
|
|
30
|
+
let health = STARTING_HEALTH;
|
|
31
|
+
let gameOver = false;
|
|
32
|
+
|
|
33
|
+
let shotCooldown = createCooldown(AUTO_FIRE_COOLDOWN);
|
|
34
|
+
let dashCooldown = createCooldown(DASH_COOLDOWN);
|
|
35
|
+
let touchInvulnerability = createCooldown(ENEMY_HIT_INVULN);
|
|
36
|
+
let dashTimer = 0;
|
|
37
|
+
let spawnDirector = createWaveDirector({ waves: SPAWN_WAVES, loop: true, betweenWaveDelay: 0.8 });
|
|
38
|
+
|
|
39
|
+
function hasMethod(obj, method) {
|
|
40
|
+
return Boolean(obj) && typeof obj[method] === 'function';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function failWithReason(reasonCode, message) {
|
|
44
|
+
throw new Error(`[2d-survivor-template] ${message} [reason:${reasonCode}]`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertRuntimeCapabilities() {
|
|
48
|
+
const missing = [];
|
|
49
|
+
if (!hasMethod(aura.window, 'getSize')) missing.push('aura.window.getSize');
|
|
50
|
+
if (!hasMethod(aura.window, 'getFPS')) missing.push('aura.window.getFPS');
|
|
51
|
+
if (!hasMethod(aura.input, 'isKeyDown')) missing.push('aura.input.isKeyDown');
|
|
52
|
+
if (!hasMethod(aura.input, 'isKeyPressed')) missing.push('aura.input.isKeyPressed');
|
|
53
|
+
if (!hasMethod(aura.draw2d, 'clear')) missing.push('aura.draw2d.clear');
|
|
54
|
+
if (!hasMethod(aura.draw2d, 'rect')) missing.push('aura.draw2d.rect');
|
|
55
|
+
if (!hasMethod(aura.draw2d, 'text')) missing.push('aura.draw2d.text');
|
|
56
|
+
if (!hasMethod(aura.draw2d, 'measureText')) missing.push('aura.draw2d.measureText');
|
|
57
|
+
if (typeof aura.rgb !== 'function') missing.push('aura.rgb');
|
|
58
|
+
if (typeof aura.rgba !== 'function') missing.push('aura.rgba');
|
|
59
|
+
if (!aura.Color || !aura.Color.WHITE) missing.push('aura.Color.WHITE');
|
|
60
|
+
|
|
61
|
+
if (missing.length > 0) {
|
|
62
|
+
failWithReason('missing_runtime_api', `runtime missing required APIs: ${missing.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const small = aura.draw2d.measureText('Probe', { size: 8 });
|
|
66
|
+
const large = aura.draw2d.measureText('Probe', { size: 24 });
|
|
67
|
+
if (!Number.isFinite(Number(small?.width)) || !Number.isFinite(Number(large?.width)) || Number(large.width) <= Number(small.width)) {
|
|
68
|
+
failWithReason(
|
|
69
|
+
'placeholder_runtime_behavior',
|
|
70
|
+
'draw2d.measureText appears to be placeholder behavior (size does not affect width).',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function distanceSquared2D(ax, ay, bx, by) {
|
|
76
|
+
const dx = ax - bx;
|
|
77
|
+
const dy = ay - by;
|
|
78
|
+
return (dx * dx) + (dy * dy);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resetRun() {
|
|
82
|
+
const size = aura.window.getSize();
|
|
83
|
+
player = {
|
|
84
|
+
x: size.width * 0.5,
|
|
85
|
+
y: size.height * 0.5,
|
|
86
|
+
};
|
|
87
|
+
bullets = [];
|
|
88
|
+
enemies = [];
|
|
89
|
+
score = 0;
|
|
90
|
+
elapsed = 0;
|
|
91
|
+
health = STARTING_HEALTH;
|
|
92
|
+
gameOver = false;
|
|
93
|
+
dashTimer = 0;
|
|
94
|
+
shotCooldown = createCooldown(AUTO_FIRE_COOLDOWN);
|
|
95
|
+
dashCooldown = createCooldown(DASH_COOLDOWN);
|
|
96
|
+
touchInvulnerability = createCooldown(ENEMY_HIT_INVULN);
|
|
97
|
+
spawnDirector = createWaveDirector({ waves: SPAWN_WAVES, loop: true, betweenWaveDelay: 0.8 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function spawnEnemyOnEdge(width, height, waveState) {
|
|
101
|
+
const side = Math.floor(Math.random() * 4);
|
|
102
|
+
const padding = 28;
|
|
103
|
+
let x = width * 0.5;
|
|
104
|
+
let y = height * 0.5;
|
|
105
|
+
|
|
106
|
+
if (side === 0) {
|
|
107
|
+
x = Math.random() * width;
|
|
108
|
+
y = -padding;
|
|
109
|
+
} else if (side === 1) {
|
|
110
|
+
x = width + padding;
|
|
111
|
+
y = Math.random() * height;
|
|
112
|
+
} else if (side === 2) {
|
|
113
|
+
x = Math.random() * width;
|
|
114
|
+
y = height + padding;
|
|
115
|
+
} else {
|
|
116
|
+
x = -padding;
|
|
117
|
+
y = Math.random() * height;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const wave = waveState?.wave || null;
|
|
121
|
+
const archetypeId = wave?.archetype || pickEnemyArchetypeForWave2D(waveState?.waveIndex || 0).id;
|
|
122
|
+
const enemy = spawnEnemy2D({
|
|
123
|
+
x,
|
|
124
|
+
y,
|
|
125
|
+
waveIndex: waveState?.waveIndex || 0,
|
|
126
|
+
archetypeId,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const hpScale = Number.isFinite(Number(wave?.hpScale)) ? Number(wave.hpScale) : 1.0;
|
|
130
|
+
const speedScale = Number.isFinite(Number(wave?.speedScale)) ? Number(wave.speedScale) : 1.0;
|
|
131
|
+
enemy.hp = Math.max(1, Math.round((enemy.size / 14) * hpScale));
|
|
132
|
+
enemy.speed = Math.max(60, enemy.speed * speedScale);
|
|
133
|
+
enemies.push(enemy);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function nearestEnemyToPlayer() {
|
|
137
|
+
if (enemies.length === 0) return null;
|
|
138
|
+
|
|
139
|
+
let best = enemies[0];
|
|
140
|
+
let bestDistance = distanceSquared2D(player.x, player.y, best.x, best.y);
|
|
141
|
+
|
|
142
|
+
for (let i = 1; i < enemies.length; i += 1) {
|
|
143
|
+
const candidate = enemies[i];
|
|
144
|
+
const scoreDistance = distanceSquared2D(player.x, player.y, candidate.x, candidate.y);
|
|
145
|
+
if (scoreDistance < bestDistance) {
|
|
146
|
+
best = candidate;
|
|
147
|
+
bestDistance = scoreDistance;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return best;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fireAutoBullet(target) {
|
|
155
|
+
if (!target) return;
|
|
156
|
+
const dx = target.x - player.x;
|
|
157
|
+
const dy = target.y - player.y;
|
|
158
|
+
const magnitude = Math.hypot(dx, dy) || 1;
|
|
159
|
+
|
|
160
|
+
bullets.push({
|
|
161
|
+
x: player.x,
|
|
162
|
+
y: player.y,
|
|
163
|
+
vx: (dx / magnitude) * BULLET_SPEED,
|
|
164
|
+
vy: (dy / magnitude) * BULLET_SPEED,
|
|
165
|
+
ttl: BULLET_LIFETIME,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function updateEnemyMovement(dt) {
|
|
170
|
+
for (const enemy of enemies) {
|
|
171
|
+
const dx = player.x - enemy.x;
|
|
172
|
+
const dy = player.y - enemy.y;
|
|
173
|
+
const magnitude = Math.hypot(dx, dy) || 1;
|
|
174
|
+
enemy.x += (dx / magnitude) * enemy.speed * dt;
|
|
175
|
+
enemy.y += (dy / magnitude) * enemy.speed * dt;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveBulletHits() {
|
|
180
|
+
for (let bulletIndex = bullets.length - 1; bulletIndex >= 0; bulletIndex -= 1) {
|
|
181
|
+
const bullet = bullets[bulletIndex];
|
|
182
|
+
let consumed = false;
|
|
183
|
+
|
|
184
|
+
for (let enemyIndex = enemies.length - 1; enemyIndex >= 0; enemyIndex -= 1) {
|
|
185
|
+
const enemy = enemies[enemyIndex];
|
|
186
|
+
const enemyRadius = enemy.size * 0.46;
|
|
187
|
+
const hitRadius = enemyRadius + (BULLET_SIZE * 0.5);
|
|
188
|
+
const hitDistanceSq = distanceSquared2D(bullet.x, bullet.y, enemy.x, enemy.y);
|
|
189
|
+
if (hitDistanceSq > (hitRadius * hitRadius)) continue;
|
|
190
|
+
|
|
191
|
+
enemy.hp -= 1;
|
|
192
|
+
bullets.splice(bulletIndex, 1);
|
|
193
|
+
consumed = true;
|
|
194
|
+
if (enemy.hp <= 0) {
|
|
195
|
+
score += enemy.scoreValue;
|
|
196
|
+
enemies.splice(enemyIndex, 1);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (consumed) continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolvePlayerContacts() {
|
|
206
|
+
if (touchInvulnerability.remaining > 0) return;
|
|
207
|
+
|
|
208
|
+
for (let i = enemies.length - 1; i >= 0; i -= 1) {
|
|
209
|
+
const enemy = enemies[i];
|
|
210
|
+
const enemyRadius = enemy.size * 0.44;
|
|
211
|
+
const touchRadius = enemyRadius + PLAYER_TOUCH_RADIUS;
|
|
212
|
+
const distanceSq = distanceSquared2D(player.x, player.y, enemy.x, enemy.y);
|
|
213
|
+
if (distanceSq > (touchRadius * touchRadius)) continue;
|
|
214
|
+
|
|
215
|
+
enemies.splice(i, 1);
|
|
216
|
+
health -= 1;
|
|
217
|
+
touchInvulnerability.remaining = ENEMY_HIT_INVULN;
|
|
218
|
+
if (health <= 0) {
|
|
219
|
+
gameOver = true;
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
aura.setup = function () {
|
|
226
|
+
assertRuntimeCapabilities();
|
|
227
|
+
resetRun();
|
|
228
|
+
console.log('{{PROJECT_TITLE}} started (2D survivor template)');
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
aura.update = function (dt) {
|
|
232
|
+
const size = aura.window.getSize();
|
|
233
|
+
elapsed += dt;
|
|
234
|
+
|
|
235
|
+
if (gameOver) {
|
|
236
|
+
if (aura.input.isKeyPressed('enter')) resetRun();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
tickCooldown(shotCooldown, dt);
|
|
241
|
+
tickCooldown(dashCooldown, dt);
|
|
242
|
+
tickCooldown(touchInvulnerability, dt);
|
|
243
|
+
|
|
244
|
+
if (aura.input.isKeyPressed('shift') && consumeCooldown(dashCooldown)) {
|
|
245
|
+
dashTimer = DASH_DURATION;
|
|
246
|
+
}
|
|
247
|
+
dashTimer = Math.max(0, dashTimer - dt);
|
|
248
|
+
|
|
249
|
+
const moveX = axisFromKeys(aura.input, ['arrowleft', 'a'], ['arrowright', 'd']);
|
|
250
|
+
const moveY = axisFromKeys(aura.input, ['arrowup', 'w'], ['arrowdown', 's']);
|
|
251
|
+
const moveSpeed = dashTimer > 0 ? DASH_SPEED : MOVE_SPEED;
|
|
252
|
+
|
|
253
|
+
player.x += moveX * moveSpeed * dt;
|
|
254
|
+
player.y += moveY * moveSpeed * dt;
|
|
255
|
+
player.x = clamp(player.x, PLAYER_SIZE * 0.5, size.width - (PLAYER_SIZE * 0.5));
|
|
256
|
+
player.y = clamp(player.y, PLAYER_SIZE * 0.5, size.height - (PLAYER_SIZE * 0.5));
|
|
257
|
+
|
|
258
|
+
if (consumeCooldown(shotCooldown)) {
|
|
259
|
+
fireAutoBullet(nearestEnemyToPlayer());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
stepWaveDirector(spawnDirector, dt, (waveState) => {
|
|
263
|
+
spawnEnemyOnEdge(size.width, size.height, waveState);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
for (const bullet of bullets) {
|
|
267
|
+
bullet.x += bullet.vx * dt;
|
|
268
|
+
bullet.y += bullet.vy * dt;
|
|
269
|
+
bullet.ttl -= dt;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
removeWhere(bullets, (bullet) => {
|
|
273
|
+
if (bullet.ttl <= 0) return true;
|
|
274
|
+
if (bullet.x < -24 || bullet.y < -24) return true;
|
|
275
|
+
if (bullet.x > size.width + 24 || bullet.y > size.height + 24) return true;
|
|
276
|
+
return false;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
updateEnemyMovement(dt);
|
|
280
|
+
resolveBulletHits();
|
|
281
|
+
resolvePlayerContacts();
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
aura.draw = function () {
|
|
285
|
+
const fps = aura.window.getFPS().toFixed(0);
|
|
286
|
+
aura.draw2d.clear(aura.rgba(0.05, 0.05, 0.08, 1.0));
|
|
287
|
+
|
|
288
|
+
for (const enemy of enemies) {
|
|
289
|
+
const half = enemy.size * 0.5;
|
|
290
|
+
aura.draw2d.rect(enemy.x - half, enemy.y - half, enemy.size, enemy.size, aura.rgb(enemy.color.r, enemy.color.g, enemy.color.b));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const bullet of bullets) {
|
|
294
|
+
aura.draw2d.rect(
|
|
295
|
+
bullet.x - (BULLET_SIZE * 0.5),
|
|
296
|
+
bullet.y - (BULLET_SIZE * 0.5),
|
|
297
|
+
BULLET_SIZE,
|
|
298
|
+
BULLET_SIZE,
|
|
299
|
+
aura.rgb(1.0, 0.94, 0.72),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const playerColor = touchInvulnerability.remaining > 0
|
|
304
|
+
? aura.rgb(1.0, 0.72, 0.72)
|
|
305
|
+
: (dashTimer > 0 ? aura.rgb(0.6, 1.0, 0.82) : aura.rgb(0.4, 0.84, 1.0));
|
|
306
|
+
|
|
307
|
+
aura.draw2d.rect(
|
|
308
|
+
player.x - (PLAYER_SIZE * 0.5),
|
|
309
|
+
player.y - (PLAYER_SIZE * 0.5),
|
|
310
|
+
PLAYER_SIZE,
|
|
311
|
+
PLAYER_SIZE,
|
|
312
|
+
playerColor,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const waveLabel = spawnDirector.completed ? 'done' : String(spawnDirector.waveIndex + 1);
|
|
316
|
+
aura.draw2d.text('Move: Arrows/WASD Dash: Shift', 12, 12, {
|
|
317
|
+
color: aura.Color.WHITE,
|
|
318
|
+
size: 14,
|
|
319
|
+
align: 'left',
|
|
320
|
+
});
|
|
321
|
+
aura.draw2d.text(`Auto-fire online HP ${health} Score ${score} Wave ${waveLabel} FPS ${fps}`, 12, 34, {
|
|
322
|
+
color: aura.Color.WHITE,
|
|
323
|
+
size: 14,
|
|
324
|
+
align: 'left',
|
|
325
|
+
});
|
|
326
|
+
aura.draw2d.text(`Time ${elapsed.toFixed(1)}s`, 12, 56, {
|
|
327
|
+
color: aura.Color.WHITE,
|
|
328
|
+
size: 13,
|
|
329
|
+
align: 'left',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (gameOver) {
|
|
333
|
+
aura.draw2d.text('RUN FAILED', 12, 88, {
|
|
334
|
+
color: aura.rgb(1.0, 0.44, 0.44),
|
|
335
|
+
size: 24,
|
|
336
|
+
align: 'left',
|
|
337
|
+
});
|
|
338
|
+
aura.draw2d.text('Press Enter to restart', 12, 116, {
|
|
339
|
+
color: aura.Color.WHITE,
|
|
340
|
+
size: 16,
|
|
341
|
+
align: 'left',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"identity": {
|
|
3
|
+
"name": "{{PROJECT_TITLE}}",
|
|
4
|
+
"version": "{{PROJECT_VERSION}}",
|
|
5
|
+
"executable": "{{PROJECT_BIN_NAME}}",
|
|
6
|
+
"icon": null
|
|
7
|
+
},
|
|
8
|
+
"window": {
|
|
9
|
+
"title": "{{PROJECT_TITLE}}",
|
|
10
|
+
"width": 1280,
|
|
11
|
+
"height": 720,
|
|
12
|
+
"resizable": true,
|
|
13
|
+
"fullscreen": false,
|
|
14
|
+
"vsync": true,
|
|
15
|
+
"hidpi": true
|
|
16
|
+
},
|
|
17
|
+
"build": {
|
|
18
|
+
"entry": "src/main.js",
|
|
19
|
+
"outDir": "build",
|
|
20
|
+
"assetDir": "assets",
|
|
21
|
+
"assetMode": "embed"
|
|
22
|
+
},
|
|
23
|
+
"modules": {
|
|
24
|
+
"physics": false,
|
|
25
|
+
"network": true,
|
|
26
|
+
"steam": false
|
|
27
|
+
}
|
|
28
|
+
}
|