@aura3d/scripting 1.4.0 → 1.4.1

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.
@@ -1,793 +0,0 @@
1
- export type WeaponKind = "laser" | "missile" | "plasma";
2
- export type FpsWeaponType = "pistol" | "rifle" | "shotgun";
3
- export type FpsFiringMode = "single" | "auto" | "burst";
4
- export type FpsEnemyTacticalState = "idle" | "patrol" | "chase" | "attack" | "flee" | "take_cover" | "investigate" | "dead";
5
- export type FpsPickupType = "health" | "ammo";
6
- export type SpaceShooterEnemyType = "fighter" | "bomber" | "turret" | "carrier" | "boss";
7
- export type SpaceShooterFormation = "line" | "v-formation" | "surround" | "random" | "sides";
8
- export type SpaceShooterPowerUpType = "health" | "shield" | "weapon" | "speed" | "life" | "multiplier";
9
-
10
- export interface WeaponBurstInput {
11
- readonly kind: WeaponKind;
12
- readonly level?: number;
13
- readonly x?: number;
14
- readonly y?: number;
15
- readonly angle?: number;
16
- readonly secondary?: boolean;
17
- readonly ammo?: number;
18
- }
19
-
20
- export interface WeaponProjectile {
21
- readonly x: number;
22
- readonly y: number;
23
- readonly vx: number;
24
- readonly vy: number;
25
- readonly damage: number;
26
- readonly lifetime: number;
27
- }
28
-
29
- export interface WeaponBurst {
30
- readonly kind: WeaponKind;
31
- readonly level: number;
32
- readonly secondary: boolean;
33
- readonly projectileCount: number;
34
- readonly ammoSpent: number;
35
- readonly totalDamage: number;
36
- readonly spreadRadians: number;
37
- readonly projectiles: readonly WeaponProjectile[];
38
- readonly source: "origin-master-space-shooter-weapons-adapted";
39
- readonly claimBoundary: string;
40
- }
41
-
42
- export interface FpsWeaponCycleInput {
43
- readonly weapon: FpsWeaponType;
44
- readonly elapsedSeconds?: number;
45
- readonly triggerHeld?: boolean;
46
- readonly reloadRequested?: boolean;
47
- readonly currentAmmo?: number;
48
- readonly reserveAmmo?: number;
49
- readonly fireTimer?: number;
50
- readonly seed?: number;
51
- }
52
-
53
- export interface FpsWeaponCycleSample {
54
- readonly weapon: FpsWeaponType;
55
- readonly name: string;
56
- readonly firingMode: FpsFiringMode;
57
- readonly fired: boolean;
58
- readonly emptyClick: boolean;
59
- readonly reloading: boolean;
60
- readonly reloadComplete: boolean;
61
- readonly currentAmmo: number;
62
- readonly reserveAmmo: number;
63
- readonly ammoSpent: number;
64
- readonly bulletsPerShot: number;
65
- readonly totalDamage: number;
66
- readonly range: number;
67
- readonly spreadDegrees: number;
68
- readonly recoilX: number;
69
- readonly recoilY: number;
70
- readonly fireCooldown: number;
71
- readonly muzzleFlashSeconds: number;
72
- readonly shellEjectionSeconds: number;
73
- readonly source: "origin-master-fps-weapon-adapted";
74
- readonly claimBoundary: string;
75
- }
76
-
77
- export interface FpsEnemyTacticsInput {
78
- readonly currentState?: FpsEnemyTacticalState;
79
- readonly playerAlive?: boolean;
80
- readonly distanceToPlayer: number;
81
- readonly angleToPlayerDegrees?: number;
82
- readonly health?: number;
83
- readonly maxHealth?: number;
84
- readonly attackTimer?: number;
85
- readonly stateTimer?: number;
86
- readonly hasCover?: boolean;
87
- readonly distanceToCover?: number;
88
- readonly hasLastKnownPlayerPosition?: boolean;
89
- readonly distanceToLastKnownPlayerPosition?: number;
90
- }
91
-
92
- export interface FpsEnemyTacticsSample {
93
- readonly state: FpsEnemyTacticalState;
94
- readonly canSeePlayer: boolean;
95
- readonly canHearPlayer: boolean;
96
- readonly targetAcquired: boolean;
97
- readonly movementSpeed: number;
98
- readonly attackReady: boolean;
99
- readonly pathRefreshRequested: boolean;
100
- readonly coverRequested: boolean;
101
- readonly inCover: boolean;
102
- readonly nextAction: "wait" | "patrol" | "chase" | "attack" | "flee" | "move_to_cover" | "investigate" | "die";
103
- readonly source: "origin-master-fps-enemy-ai-adapted";
104
- readonly claimBoundary: string;
105
- }
106
-
107
- export interface FpsLevelLayoutInput {
108
- readonly seed?: number;
109
- readonly roomCount?: number;
110
- readonly gridSize?: number;
111
- }
112
-
113
- export interface FpsLevelRoom {
114
- readonly id: string;
115
- readonly x: number;
116
- readonly z: number;
117
- readonly width: number;
118
- readonly depth: number;
119
- }
120
-
121
- export interface FpsLevelCorridor {
122
- readonly from: string;
123
- readonly to: string;
124
- readonly length: number;
125
- readonly width: number;
126
- }
127
-
128
- export interface FpsLevelPoint {
129
- readonly id: string;
130
- readonly x: number;
131
- readonly z: number;
132
- }
133
-
134
- export interface FpsLevelPickup extends FpsLevelPoint {
135
- readonly type: FpsPickupType;
136
- }
137
-
138
- export interface FpsLevelLayoutSample {
139
- readonly rooms: readonly FpsLevelRoom[];
140
- readonly corridors: readonly FpsLevelCorridor[];
141
- readonly coverPoints: readonly FpsLevelPoint[];
142
- readonly patrolPoints: readonly FpsLevelPoint[];
143
- readonly enemySpawnPoints: readonly FpsLevelPoint[];
144
- readonly pickupSpawnPoints: readonly FpsLevelPickup[];
145
- readonly playerSpawnPoint: FpsLevelPoint;
146
- readonly navMeshPointCount: number;
147
- readonly averageRoomArea: number;
148
- readonly totalCorridorLength: number;
149
- readonly source: "origin-master-fps-level-adapted";
150
- readonly claimBoundary: string;
151
- }
152
-
153
- export interface FpsHudOverlayInput {
154
- readonly health: number;
155
- readonly maxHealth?: number;
156
- readonly ammo: number;
157
- readonly reserveAmmo: number;
158
- readonly score?: number;
159
- readonly wave?: number;
160
- readonly enemiesRemaining?: number;
161
- readonly recentDamage?: number;
162
- readonly hitMarker?: boolean;
163
- readonly damageAngleDegrees?: number;
164
- readonly minimapFriendlyCount?: number;
165
- readonly minimapEnemyCount?: number;
166
- readonly killFeedCount?: number;
167
- }
168
-
169
- export interface FpsHudOverlaySample {
170
- readonly healthPercent: number;
171
- readonly healthBarPixels: number;
172
- readonly lowHealth: boolean;
173
- readonly damageFlashAlpha: number;
174
- readonly ammoText: string;
175
- readonly ammoWarning: boolean;
176
- readonly crosshairSpreadPixels: number;
177
- readonly hitMarkerVisible: boolean;
178
- readonly damageIndicatorAngleDegrees: number;
179
- readonly minimapBlips: number;
180
- readonly killFeedVisible: boolean;
181
- readonly waveText: string;
182
- readonly source: "origin-master-fps-hud-adapted";
183
- readonly claimBoundary: string;
184
- }
185
-
186
- export interface SpaceShooterWaveInput {
187
- readonly wave: number;
188
- readonly width?: number;
189
- readonly height?: number;
190
- readonly seed?: number;
191
- }
192
-
193
- export interface SpaceShooterSpawn {
194
- readonly enemyType: SpaceShooterEnemyType;
195
- readonly x: number;
196
- readonly y: number;
197
- readonly health: number;
198
- readonly radius: number;
199
- readonly scoreValue: number;
200
- }
201
-
202
- export interface SpaceShooterWaveSample {
203
- readonly wave: number;
204
- readonly enemyType: SpaceShooterEnemyType;
205
- readonly formation: SpaceShooterFormation;
206
- readonly count: number;
207
- readonly delay: number;
208
- readonly bossWave: boolean;
209
- readonly difficultyScale: number;
210
- readonly totalScoreValue: number;
211
- readonly spawns: readonly SpaceShooterSpawn[];
212
- readonly powerUpType: SpaceShooterPowerUpType;
213
- readonly powerUpWeight: number;
214
- readonly source: "origin-master-space-shooter-wave-powerup-adapted";
215
- readonly claimBoundary: string;
216
- }
217
-
218
- export interface PowerUpEffectInput {
219
- readonly type: SpaceShooterPowerUpType;
220
- readonly health?: number;
221
- readonly maxHealth?: number;
222
- readonly shield?: number;
223
- readonly maxShield?: number;
224
- readonly weaponLevel?: number;
225
- readonly speed?: number;
226
- readonly lives?: number;
227
- readonly multiplier?: number;
228
- }
229
-
230
- export interface PowerUpEffectSample {
231
- readonly type: SpaceShooterPowerUpType;
232
- readonly health: number;
233
- readonly shield: number;
234
- readonly weaponLevel: number;
235
- readonly speed: number;
236
- readonly lives: number;
237
- readonly multiplier: number;
238
- readonly changedFields: readonly string[];
239
- readonly source: "origin-master-space-shooter-powerup-effects-adapted";
240
- }
241
-
242
- export function sampleWeaponBurst(input: WeaponBurstInput): WeaponBurst {
243
- const level = Math.max(1, Math.min(5, Math.floor(input.level ?? 1)));
244
- const x = finite(input.x ?? 0, "x");
245
- const y = finite(input.y ?? 0, "y");
246
- const angle = finite(input.angle ?? 0, "angle");
247
- const secondary = input.secondary === true;
248
- const ammo = input.ammo ?? Number.POSITIVE_INFINITY;
249
- if (ammo < 0) throw new RangeError("Weapon burst ammo must be non-negative.");
250
- const projectiles = input.kind === "laser"
251
- ? laserBurst(x, y, angle, level, secondary)
252
- : input.kind === "missile"
253
- ? missileBurst(x, y, angle, level, secondary, ammo)
254
- : plasmaBurst(x, y, angle, level, secondary);
255
- const totalDamage = projectiles.reduce((sum, projectile) => sum + projectile.damage, 0);
256
- return {
257
- kind: input.kind,
258
- level,
259
- secondary,
260
- projectileCount: projectiles.length,
261
- ammoSpent: input.kind === "missile" ? (secondary ? Math.min(5, ammo) : Math.min(1, ammo)) : 0,
262
- totalDamage: Number(totalDamage.toFixed(3)),
263
- spreadRadians: Number(spreadFor(projectiles).toFixed(5)),
264
- projectiles,
265
- source: "origin-master-space-shooter-weapons-adapted",
266
- claimBoundary: "Deterministic weapon burst sampling adapted from the old space-shooter weapon concepts; this is gameplay telemetry, not a full projectile-pooling or collision system claim."
267
- };
268
- }
269
-
270
- export function sampleFpsWeaponCycle(input: FpsWeaponCycleInput): FpsWeaponCycleSample {
271
- const config = fpsWeaponConfig(input.weapon);
272
- const elapsedSeconds = Math.max(0, finite(input.elapsedSeconds ?? 0, "elapsedSeconds"));
273
- const currentAmmo = Math.max(0, Math.floor(input.currentAmmo ?? config.magazineSize));
274
- const reserveAmmo = Math.max(0, Math.floor(input.reserveAmmo ?? config.reserveAmmo));
275
- const fireTimer = Math.max(0, finite(input.fireTimer ?? 0, "fireTimer") - elapsedSeconds);
276
- const canFire = input.triggerHeld === true && input.reloadRequested !== true && fireTimer <= 0;
277
- const reloadRequested = input.reloadRequested === true || (currentAmmo === 0 && reserveAmmo > 0 && canFire);
278
- if (reloadRequested) {
279
- const reloadComplete = elapsedSeconds >= config.reloadTime && reserveAmmo > 0 && currentAmmo < config.magazineSize;
280
- const loaded = reloadComplete ? Math.min(config.magazineSize - currentAmmo, reserveAmmo) : 0;
281
- return {
282
- weapon: input.weapon,
283
- name: config.name,
284
- firingMode: config.firingMode,
285
- fired: false,
286
- emptyClick: currentAmmo === 0 && reserveAmmo === 0,
287
- reloading: !reloadComplete,
288
- reloadComplete,
289
- currentAmmo: currentAmmo + loaded,
290
- reserveAmmo: reserveAmmo - loaded,
291
- ammoSpent: 0,
292
- bulletsPerShot: 0,
293
- totalDamage: 0,
294
- range: config.range,
295
- spreadDegrees: config.spread,
296
- recoilX: 0,
297
- recoilY: 0,
298
- fireCooldown: reloadComplete ? 0 : Math.max(0, config.reloadTime - elapsedSeconds),
299
- muzzleFlashSeconds: 0,
300
- shellEjectionSeconds: 0,
301
- source: "origin-master-fps-weapon-adapted",
302
- claimBoundary: fpsWeaponClaimBoundary
303
- };
304
- }
305
- if (!canFire || currentAmmo <= 0) {
306
- return {
307
- weapon: input.weapon,
308
- name: config.name,
309
- firingMode: config.firingMode,
310
- fired: false,
311
- emptyClick: input.triggerHeld === true && currentAmmo <= 0,
312
- reloading: false,
313
- reloadComplete: false,
314
- currentAmmo,
315
- reserveAmmo,
316
- ammoSpent: 0,
317
- bulletsPerShot: 0,
318
- totalDamage: 0,
319
- range: config.range,
320
- spreadDegrees: config.spread,
321
- recoilX: 0,
322
- recoilY: 0,
323
- fireCooldown: fireTimer,
324
- muzzleFlashSeconds: 0,
325
- shellEjectionSeconds: 0,
326
- source: "origin-master-fps-weapon-adapted",
327
- claimBoundary: fpsWeaponClaimBoundary
328
- };
329
- }
330
- const seed = Math.floor(input.seed ?? currentAmmo * 31 + reserveAmmo * 17 + config.damage * 13);
331
- const spent = Math.min(1, currentAmmo);
332
- const recoil = fpsRecoil(config.recoilAmount, seed);
333
- return {
334
- weapon: input.weapon,
335
- name: config.name,
336
- firingMode: config.firingMode,
337
- fired: true,
338
- emptyClick: false,
339
- reloading: false,
340
- reloadComplete: false,
341
- currentAmmo: currentAmmo - spent,
342
- reserveAmmo,
343
- ammoSpent: spent,
344
- bulletsPerShot: config.bulletsPerShot,
345
- totalDamage: Number((config.damage * config.bulletsPerShot).toFixed(3)),
346
- range: config.range,
347
- spreadDegrees: config.spread,
348
- recoilX: recoil.x,
349
- recoilY: recoil.y,
350
- fireCooldown: Number((60 / config.fireRate).toFixed(4)),
351
- muzzleFlashSeconds: 0.05,
352
- shellEjectionSeconds: 0.2,
353
- source: "origin-master-fps-weapon-adapted",
354
- claimBoundary: fpsWeaponClaimBoundary
355
- };
356
- }
357
-
358
- export function sampleFpsEnemyTactics(input: FpsEnemyTacticsInput): FpsEnemyTacticsSample {
359
- const currentState = input.currentState ?? "patrol";
360
- const playerAlive = input.playerAlive !== false;
361
- const distanceToPlayer = finiteNonNegative(input.distanceToPlayer, "distanceToPlayer");
362
- const angleToPlayerDegrees = Math.abs(finite(input.angleToPlayerDegrees ?? 0, "angleToPlayerDegrees"));
363
- const maxHealth = positive(input.maxHealth ?? 100, "maxHealth");
364
- const health = clamp(input.health ?? maxHealth, 0, maxHealth);
365
- const attackTimer = finite(input.attackTimer ?? 0, "attackTimer");
366
- const stateTimer = Math.max(0, finite(input.stateTimer ?? 0, "stateTimer"));
367
- const hasCover = input.hasCover === true;
368
- const distanceToCover = finiteNonNegative(input.distanceToCover ?? 1_000_000, "distanceToCover");
369
- const hasLastKnown = input.hasLastKnownPlayerPosition === true;
370
- const distanceToLastKnown = finiteNonNegative(input.distanceToLastKnownPlayerPosition ?? 1_000_000, "distanceToLastKnownPlayerPosition");
371
- const canSeePlayer = playerAlive && distanceToPlayer <= 20 && angleToPlayerDegrees <= 60;
372
- const canHearPlayer = playerAlive && distanceToPlayer <= 15;
373
- const targetAcquired = canSeePlayer || (currentState !== "patrol" && currentState !== "idle" && currentState !== "investigate" && playerAlive);
374
- const lowHealth = health < maxHealth * 0.3;
375
- let state: FpsEnemyTacticalState;
376
- let nextAction: FpsEnemyTacticsSample["nextAction"];
377
- if (health <= 0 || currentState === "dead") {
378
- state = "dead";
379
- nextAction = "die";
380
- } else if (!playerAlive) {
381
- state = "patrol";
382
- nextAction = "patrol";
383
- } else if (canSeePlayer && lowHealth && hasCover) {
384
- state = "take_cover";
385
- nextAction = distanceToCover > 2 ? "move_to_cover" : "wait";
386
- } else if (canSeePlayer && lowHealth) {
387
- state = "flee";
388
- nextAction = "flee";
389
- } else if (canSeePlayer && distanceToPlayer <= 2) {
390
- state = "attack";
391
- nextAction = attackTimer <= 0 ? "attack" : "wait";
392
- } else if (canSeePlayer || targetAcquired) {
393
- state = "chase";
394
- nextAction = "chase";
395
- } else if (canHearPlayer || hasLastKnown) {
396
- state = "investigate";
397
- nextAction = distanceToLastKnown <= 2 && stateTimer > 3 ? "patrol" : "investigate";
398
- } else {
399
- state = stateTimer > 2 || currentState === "patrol" ? "patrol" : "idle";
400
- nextAction = state === "patrol" ? "patrol" : "wait";
401
- }
402
- const movementSpeed = state === "chase" || state === "flee" || state === "take_cover"
403
- ? 5
404
- : state === "patrol" || state === "investigate"
405
- ? 2.5
406
- : 0;
407
- return {
408
- state,
409
- canSeePlayer,
410
- canHearPlayer,
411
- targetAcquired,
412
- movementSpeed,
413
- attackReady: state === "attack" && attackTimer <= 0,
414
- pathRefreshRequested: state === "chase" && targetAcquired,
415
- coverRequested: state === "take_cover",
416
- inCover: state === "take_cover" && distanceToCover <= 2,
417
- nextAction,
418
- source: "origin-master-fps-enemy-ai-adapted",
419
- claimBoundary: "Deterministic patrol/chase/attack/flee/cover/investigate state sampling adapted from the old FPS enemy example; this is tactical runtime telemetry, not a full navmesh, ragdoll, audio, or networked FPS AI claim."
420
- };
421
- }
422
-
423
- export function sampleFpsLevelLayout(input: FpsLevelLayoutInput = {}): FpsLevelLayoutSample {
424
- const roomCount = Math.max(3, Math.min(12, Math.floor(input.roomCount ?? 8)));
425
- const gridSize = positive(input.gridSize ?? 25, "gridSize");
426
- const seed = Math.floor(input.seed ?? 0x1e7e1);
427
- const rooms = Array.from({ length: roomCount }, (_, index): FpsLevelRoom => {
428
- const angle = (index / roomCount) * Math.PI * 2 + seeded01(seed + index * 13) * 0.34;
429
- const radius = gridSize * (0.24 + seeded01(seed + index * 19) * 0.5);
430
- const width = 6 + Math.floor(seeded01(seed + index * 29) * 7);
431
- const depth = 6 + Math.floor(seeded01(seed + index * 31) * 7);
432
- return {
433
- id: `room-${index}`,
434
- x: Number((Math.cos(angle) * radius).toFixed(3)),
435
- z: Number((Math.sin(angle) * radius).toFixed(3)),
436
- width,
437
- depth
438
- };
439
- });
440
- const corridors = rooms.slice(1).map((room, index): FpsLevelCorridor => {
441
- const previous = rooms[index] ?? rooms[0]!;
442
- return {
443
- from: previous.id,
444
- to: room.id,
445
- length: Number(Math.hypot(room.x - previous.x, room.z - previous.z).toFixed(3)),
446
- width: 3
447
- };
448
- });
449
- const coverPoints = rooms.flatMap((room, roomIndex) => [0, 1].map((slot): FpsLevelPoint => ({
450
- id: `cover-${roomIndex}-${slot}`,
451
- x: Number((room.x + (slot === 0 ? -0.28 : 0.28) * room.width).toFixed(3)),
452
- z: Number((room.z + (slot === 0 ? 0.2 : -0.22) * room.depth).toFixed(3))
453
- })));
454
- const patrolPoints = rooms.map((room, index): FpsLevelPoint => ({
455
- id: `patrol-${index}`,
456
- x: Number(room.x.toFixed(3)),
457
- z: Number(room.z.toFixed(3))
458
- }));
459
- const enemySpawnPoints = rooms.slice(1, Math.min(rooms.length, 6)).map((room, index): FpsLevelPoint => ({
460
- id: `enemy-spawn-${index}`,
461
- x: Number((room.x + room.width * 0.18).toFixed(3)),
462
- z: Number((room.z - room.depth * 0.18).toFixed(3))
463
- }));
464
- const pickupSpawnPoints = rooms.slice(0, Math.min(rooms.length, 6)).map((room, index): FpsLevelPickup => ({
465
- id: `pickup-${index}`,
466
- type: index % 2 === 0 ? "health" : "ammo",
467
- x: Number((room.x - room.width * 0.18).toFixed(3)),
468
- z: Number((room.z + room.depth * 0.18).toFixed(3))
469
- }));
470
- const totalRoomArea = rooms.reduce((sum, room) => sum + room.width * room.depth, 0);
471
- const totalCorridorLength = corridors.reduce((sum, corridor) => sum + corridor.length, 0);
472
- return {
473
- rooms,
474
- corridors,
475
- coverPoints,
476
- patrolPoints,
477
- enemySpawnPoints,
478
- pickupSpawnPoints,
479
- playerSpawnPoint: { id: "player-spawn", x: rooms[0]!.x, z: rooms[0]!.z },
480
- navMeshPointCount: rooms.length + coverPoints.length + patrolPoints.length + enemySpawnPoints.length + pickupSpawnPoints.length,
481
- averageRoomArea: Number((totalRoomArea / rooms.length).toFixed(3)),
482
- totalCorridorLength: Number(totalCorridorLength.toFixed(3)),
483
- source: "origin-master-fps-level-adapted",
484
- claimBoundary: "Deterministic room, corridor, spawn, pickup, patrol, cover, and nav-point telemetry adapted from the old FPS level example; this is layout/runtime evidence, not a full procedural level renderer, collision mesh, or navmesh bake claim."
485
- };
486
- }
487
-
488
- export function sampleFpsHudOverlay(input: FpsHudOverlayInput): FpsHudOverlaySample {
489
- const maxHealth = positive(input.maxHealth ?? 100, "maxHealth");
490
- const health = clamp(input.health, 0, maxHealth);
491
- const ammo = Math.max(0, Math.floor(finite(input.ammo, "ammo")));
492
- const reserveAmmo = Math.max(0, Math.floor(finite(input.reserveAmmo, "reserveAmmo")));
493
- const healthPercent = health / maxHealth;
494
- const recentDamage = Math.max(0, finite(input.recentDamage ?? 0, "recentDamage"));
495
- const enemiesRemaining = Math.max(0, Math.floor(input.enemiesRemaining ?? 0));
496
- const minimapBlips = Math.max(0, Math.floor(input.minimapFriendlyCount ?? 1)) + Math.max(0, Math.floor(input.minimapEnemyCount ?? enemiesRemaining));
497
- return {
498
- healthPercent: Number(healthPercent.toFixed(4)),
499
- healthBarPixels: Math.round(200 * healthPercent),
500
- lowHealth: healthPercent <= 0.3,
501
- damageFlashAlpha: Number(Math.min(0.55, recentDamage / maxHealth).toFixed(4)),
502
- ammoText: `${ammo} / ${reserveAmmo}`,
503
- ammoWarning: ammo <= 5,
504
- crosshairSpreadPixels: Number((10 + (input.hitMarker === true ? 2 : 0) + (ammo <= 5 ? 3 : 0)).toFixed(3)),
505
- hitMarkerVisible: input.hitMarker === true,
506
- damageIndicatorAngleDegrees: Number(finite(input.damageAngleDegrees ?? 0, "damageAngleDegrees").toFixed(3)),
507
- minimapBlips,
508
- killFeedVisible: Math.max(0, Math.floor(input.killFeedCount ?? 0)) > 0,
509
- waveText: `WAVE: ${Math.max(1, Math.floor(input.wave ?? 1))} | ENEMIES: ${enemiesRemaining}`,
510
- source: "origin-master-fps-hud-adapted",
511
- claimBoundary: "Deterministic health, ammo, crosshair, damage, kill-feed, wave, and minimap HUD telemetry adapted from the old FPS HUD example; this is runtime UI evidence, not a complete first-person UI framework claim."
512
- };
513
- }
514
-
515
- export function sampleSpaceShooterWave(input: SpaceShooterWaveInput): SpaceShooterWaveSample {
516
- const wave = Math.max(1, Math.floor(positive(input.wave, "wave")));
517
- const width = positive(input.width ?? 1280, "width");
518
- const height = positive(input.height ?? 720, "height");
519
- const seed = Math.floor(input.seed ?? 0x5150 + wave * 97);
520
- const base = wave % 10 === 0 ? { enemyType: "boss" as const, count: 1, formation: "random" as const, delay: 0 } : waveDefinition(wave);
521
- const difficultyScale = 1 + Math.floor((wave - 1) / 10) * 0.5;
522
- const count = base.enemyType === "boss" ? 1 : Math.max(1, Math.floor(base.count * difficultyScale));
523
- const spawns = Array.from({ length: count }, (_, index) => {
524
- const position = formationPosition(base.formation, index, count, width, height, seed);
525
- const stats = enemyStats(base.enemyType);
526
- return {
527
- enemyType: base.enemyType,
528
- x: Number(position.x.toFixed(3)),
529
- y: Number(position.y.toFixed(3)),
530
- ...stats
531
- };
532
- });
533
- const powerUp = weightedPowerUp(seed ^ wave);
534
- return {
535
- wave,
536
- enemyType: base.enemyType,
537
- formation: base.formation,
538
- count,
539
- delay: base.delay,
540
- bossWave: base.enemyType === "boss",
541
- difficultyScale,
542
- totalScoreValue: spawns.reduce((sum, spawn) => sum + spawn.scoreValue, 0),
543
- spawns,
544
- powerUpType: powerUp.type,
545
- powerUpWeight: powerUp.weight,
546
- source: "origin-master-space-shooter-wave-powerup-adapted",
547
- claimBoundary: "Deterministic wave, formation, enemy-stat, and weighted power-up sampling adapted from the old space-shooter systems; this is runtime evidence, not a full shooter game loop claim."
548
- };
549
- }
550
-
551
- export function samplePowerUpEffect(input: PowerUpEffectInput): PowerUpEffectSample {
552
- const maxHealth = positive(input.maxHealth ?? 100, "maxHealth");
553
- const maxShield = positive(input.maxShield ?? 100, "maxShield");
554
- const before = {
555
- health: clamp(input.health ?? 50, 0, maxHealth),
556
- shield: clamp(input.shield ?? 40, 0, maxShield),
557
- weaponLevel: Math.max(1, Math.min(5, Math.floor(input.weaponLevel ?? 1))),
558
- speed: positive(input.speed ?? 320, "speed"),
559
- lives: Math.max(0, Math.floor(input.lives ?? 3)),
560
- multiplier: Math.max(1, input.multiplier ?? 1)
561
- };
562
- const after = { ...before };
563
- switch (input.type) {
564
- case "health":
565
- after.health = Math.min(maxHealth, after.health + 50);
566
- break;
567
- case "shield":
568
- after.shield = Math.min(maxShield, after.shield + 50);
569
- break;
570
- case "weapon":
571
- after.weaponLevel = Math.min(5, after.weaponLevel + 1);
572
- break;
573
- case "speed":
574
- after.speed += 100;
575
- break;
576
- case "life":
577
- after.lives += 1;
578
- break;
579
- case "multiplier":
580
- after.multiplier *= 2;
581
- break;
582
- }
583
- const changedFields = (Object.keys(after) as (keyof typeof after)[]).filter((key) => after[key] !== before[key]);
584
- return {
585
- type: input.type,
586
- ...after,
587
- changedFields,
588
- source: "origin-master-space-shooter-powerup-effects-adapted"
589
- };
590
- }
591
-
592
- function laserBurst(x: number, y: number, angle: number, level: number, secondary: boolean): readonly WeaponProjectile[] {
593
- if (secondary) {
594
- return radialBurst(x, y, 8, 1000, 7.5, 0.5);
595
- }
596
- const spread = Math.min(level - 1, 2);
597
- const projectiles: WeaponProjectile[] = [];
598
- for (let index = -spread; index <= spread; index += 1) {
599
- projectiles.push(projectile(x, y, angle + index * 0.1, 800, 15 * Math.pow(1.2, level - 1), 2));
600
- }
601
- return projectiles;
602
- }
603
-
604
- const fpsWeaponClaimBoundary = "Deterministic magazine, reload, recoil, spread, muzzle-flash, and shell-ejection sampling adapted from the old FPS weapon example; this is runtime telemetry, not a full hitscan, projectile-pooling, audio, or networked FPS weapon claim.";
605
-
606
- function fpsWeaponConfig(weapon: FpsWeaponType): {
607
- readonly name: string;
608
- readonly damage: number;
609
- readonly fireRate: number;
610
- readonly magazineSize: number;
611
- readonly reserveAmmo: number;
612
- readonly reloadTime: number;
613
- readonly range: number;
614
- readonly spread: number;
615
- readonly recoilAmount: number;
616
- readonly firingMode: FpsFiringMode;
617
- readonly bulletsPerShot: number;
618
- } {
619
- switch (weapon) {
620
- case "pistol":
621
- return { name: "M1911 Pistol", damage: 25, fireRate: 300, magazineSize: 12, reserveAmmo: 60, reloadTime: 1.5, range: 50, spread: 2, recoilAmount: 0.3, firingMode: "single", bulletsPerShot: 1 };
622
- case "rifle":
623
- return { name: "M4A1 Rifle", damage: 30, fireRate: 700, magazineSize: 30, reserveAmmo: 120, reloadTime: 2, range: 100, spread: 1, recoilAmount: 0.2, firingMode: "auto", bulletsPerShot: 1 };
624
- case "shotgun":
625
- return { name: "Pump Shotgun", damage: 15, fireRate: 80, magazineSize: 8, reserveAmmo: 32, reloadTime: 3, range: 30, spread: 10, recoilAmount: 1, firingMode: "single", bulletsPerShot: 8 };
626
- }
627
- }
628
-
629
- function fpsRecoil(amount: number, seed: number): { readonly x: number; readonly y: number } {
630
- const horizontal = seededUnit(seed) * 2 - 1;
631
- const vertical = 0.72 + seededUnit(seed ^ 0x9e3779b9) * 0.28;
632
- return {
633
- x: Number((horizontal * amount * 0.35).toFixed(4)),
634
- y: Number((vertical * amount).toFixed(4))
635
- };
636
- }
637
-
638
- function seededUnit(seed: number): number {
639
- let value = seed >>> 0;
640
- value ^= value << 13;
641
- value ^= value >>> 17;
642
- value ^= value << 5;
643
- return ((value >>> 0) % 10_000) / 10_000;
644
- }
645
-
646
- function missileBurst(x: number, y: number, angle: number, level: number, secondary: boolean, ammo: number): readonly WeaponProjectile[] {
647
- if (ammo < (secondary ? 5 : 1)) return [];
648
- const projectiles: WeaponProjectile[] = [];
649
- if (secondary) {
650
- for (let index = 0; index < 12; index += 1) {
651
- projectiles.push(projectile(x, y, angle + (index - 6) * 0.15, 500, 75 * Math.pow(1.2, level - 1), 4));
652
- }
653
- return projectiles;
654
- }
655
- const count = Math.min(Math.floor(level / 2) + 1, 3);
656
- for (let index = 0; index < count; index += 1) {
657
- const offset = index - (count - 1) / 2;
658
- const px = x - Math.sin(angle) * offset * 20;
659
- const py = y + Math.cos(angle) * offset * 20;
660
- projectiles.push(projectile(px, py, angle, 400, 50 * Math.pow(1.2, level - 1), 3));
661
- }
662
- return projectiles;
663
- }
664
-
665
- function plasmaBurst(x: number, y: number, angle: number, level: number, secondary: boolean): readonly WeaponProjectile[] {
666
- if (secondary) {
667
- return [0, 1, 2].flatMap((wave) => radialBurst(x, y, 16, 700 + wave * 100, 20 * Math.pow(1.2, level - 1), 1));
668
- }
669
- const shots = 3 + level;
670
- const spreadAngle = 0.3 + level * 0.1;
671
- const projectiles: WeaponProjectile[] = [];
672
- for (let index = 0; index < shots; index += 1) {
673
- projectiles.push(projectile(x, y, angle + (index - (shots - 1) / 2) * (spreadAngle / shots), 600, 25 * Math.pow(1.2, level - 1), 1.5));
674
- }
675
- return projectiles;
676
- }
677
-
678
- function radialBurst(x: number, y: number, count: number, speed: number, damage: number, lifetime: number): readonly WeaponProjectile[] {
679
- return Array.from({ length: count }, (_, index) => projectile(x, y, (index / count) * Math.PI * 2, speed, damage, lifetime));
680
- }
681
-
682
- function projectile(x: number, y: number, angle: number, speed: number, damage: number, lifetime: number): WeaponProjectile {
683
- return {
684
- x: Number(x.toFixed(3)),
685
- y: Number(y.toFixed(3)),
686
- vx: Number((Math.cos(angle) * speed).toFixed(3)),
687
- vy: Number((Math.sin(angle) * speed).toFixed(3)),
688
- damage: Number(damage.toFixed(3)),
689
- lifetime
690
- };
691
- }
692
-
693
- function waveDefinition(wave: number): { readonly enemyType: SpaceShooterEnemyType; readonly count: number; readonly formation: SpaceShooterFormation; readonly delay: number } {
694
- const waves = [
695
- { enemyType: "fighter", count: 5, formation: "line", delay: 0.5 },
696
- { enemyType: "fighter", count: 8, formation: "v-formation", delay: 0.4 },
697
- { enemyType: "bomber", count: 4, formation: "line", delay: 0.8 },
698
- { enemyType: "fighter", count: 10, formation: "surround", delay: 0.3 },
699
- { enemyType: "turret", count: 6, formation: "random", delay: 0.6 },
700
- { enemyType: "bomber", count: 6, formation: "sides", delay: 0.5 },
701
- { enemyType: "fighter", count: 12, formation: "v-formation", delay: 0.3 },
702
- { enemyType: "carrier", count: 2, formation: "line", delay: 2 },
703
- { enemyType: "turret", count: 8, formation: "surround", delay: 0.5 }
704
- ] as const;
705
- return waves[(wave - 1) % waves.length] ?? waves[0];
706
- }
707
-
708
- function formationPosition(formation: SpaceShooterFormation, index: number, total: number, width: number, height: number, seed: number): { readonly x: number; readonly y: number } {
709
- const padding = 100;
710
- const usableWidth = Math.max(1, width - padding * 2);
711
- switch (formation) {
712
- case "line":
713
- return { x: padding + (index / Math.max(1, total - 1)) * usableWidth, y: -50 };
714
- case "v-formation": {
715
- const centerIndex = (total - 1) / 2;
716
- return { x: padding + (index / Math.max(1, total - 1)) * usableWidth, y: -50 - Math.abs(index - centerIndex) * 40 };
717
- }
718
- case "surround": {
719
- const angle = (index / total) * Math.PI;
720
- return { x: width / 2 + Math.cos(angle - Math.PI / 2) * 300, y: -100 + Math.sin(angle - Math.PI / 2) * 300 };
721
- }
722
- case "sides": {
723
- const side = index % 2;
724
- const groupIndex = Math.floor(index / 2);
725
- return { x: side === 0 ? padding : width - padding, y: -50 - groupIndex * 80 };
726
- }
727
- case "random":
728
- return { x: padding + seeded01(seed + index * 17) * usableWidth, y: -50 - seeded01(seed + index * 31) * Math.max(80, height * 0.28) };
729
- }
730
- }
731
-
732
- function enemyStats(enemyType: SpaceShooterEnemyType): Omit<SpaceShooterSpawn, "enemyType" | "x" | "y"> {
733
- switch (enemyType) {
734
- case "fighter":
735
- return { health: 30, radius: 15, scoreValue: 100 };
736
- case "bomber":
737
- return { health: 80, radius: 25, scoreValue: 250 };
738
- case "turret":
739
- return { health: 100, radius: 20, scoreValue: 300 };
740
- case "carrier":
741
- return { health: 200, radius: 35, scoreValue: 600 };
742
- case "boss":
743
- return { health: 1000, radius: 80, scoreValue: 5000 };
744
- }
745
- }
746
-
747
- function weightedPowerUp(seed: number): { readonly type: SpaceShooterPowerUpType; readonly weight: number } {
748
- const entries = [
749
- ["health", 30],
750
- ["shield", 30],
751
- ["weapon", 25],
752
- ["speed", 10],
753
- ["life", 3],
754
- ["multiplier", 2]
755
- ] as const;
756
- const total = entries.reduce((sum, [, weight]) => sum + weight, 0);
757
- let cursor = seeded01(seed) * total;
758
- for (const [type, weight] of entries) {
759
- cursor -= weight;
760
- if (cursor <= 0) return { type, weight };
761
- }
762
- return { type: "health", weight: 30 };
763
- }
764
-
765
- function seeded01(seed: number): number {
766
- const value = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
767
- return value - Math.floor(value);
768
- }
769
-
770
- function spreadFor(projectiles: readonly WeaponProjectile[]): number {
771
- if (projectiles.length <= 1) return 0;
772
- const angles = projectiles.map((entry) => Math.atan2(entry.vy, entry.vx)).sort((left, right) => left - right);
773
- return (angles[angles.length - 1] ?? 0) - (angles[0] ?? 0);
774
- }
775
-
776
- function finite(value: number, label: string): number {
777
- if (!Number.isFinite(value)) throw new RangeError(`Weapon burst ${label} must be finite.`);
778
- return value;
779
- }
780
-
781
- function positive(value: number, label: string): number {
782
- if (!Number.isFinite(value) || value <= 0) throw new RangeError(`Weapon burst ${label} must be finite and positive.`);
783
- return value;
784
- }
785
-
786
- function finiteNonNegative(value: number, label: string): number {
787
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`Weapon burst ${label} must be finite and non-negative.`);
788
- return value;
789
- }
790
-
791
- function clamp(value: number, min: number, max: number): number {
792
- return Math.max(min, Math.min(max, value));
793
- }