@loonylabs/create-game 0.1.0
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/bin/create-game.js +213 -0
- package/package.json +18 -0
- package/template/.claude/skills/aigdtk-create-game-stories/SKILL.md +177 -0
- package/template/.claude/skills/aigdtk-create-game-stories/story-template.md +85 -0
- package/template/.claude/skills/aigdtk-implement-game-stories/SKILL.md +129 -0
- package/template/.claude/skills/aigdtk-new-game/SKILL.md +126 -0
- package/template/.claude/skills/aigdtk-shared/ascii-grammar.md +133 -0
- package/template/.claude/skills/aigdtk-shared/enemies.md +112 -0
- package/template/.claude/skills/aigdtk-shared/framework.md +93 -0
- package/template/.claude/skills/aigdtk-shared/visuals.md +125 -0
- package/template/apps/client/index.html +14 -0
- package/template/apps/client/package.json +31 -0
- package/template/apps/client/public/assets/audio/enemy_killed.wav +0 -0
- package/template/apps/client/src/components/App.svelte +290 -0
- package/template/apps/client/src/components/CraftingPanel.svelte +253 -0
- package/template/apps/client/src/components/DevPanel.svelte +180 -0
- package/template/apps/client/src/components/DungeonClearOverlay.svelte +53 -0
- package/template/apps/client/src/components/EquipmentPanel.svelte +191 -0
- package/template/apps/client/src/components/HealthBar.svelte +50 -0
- package/template/apps/client/src/components/Hub/BackToHubButton.svelte +37 -0
- package/template/apps/client/src/components/Hub/ExperienceCard.svelte +115 -0
- package/template/apps/client/src/components/Hub/Hub.svelte +88 -0
- package/template/apps/client/src/components/Inventory.svelte +174 -0
- package/template/apps/client/src/components/Runner/RunnerDeathScreen.svelte +182 -0
- package/template/apps/client/src/components/Runner/RunnerHUD.svelte +157 -0
- package/template/apps/client/src/components/Shooter/DamageNumbers.svelte +96 -0
- package/template/apps/client/src/components/Shooter/GameOverScreen.svelte +109 -0
- package/template/apps/client/src/components/Shooter/ShooterHUD.svelte +95 -0
- package/template/apps/client/src/components/SkillBar.svelte +146 -0
- package/template/apps/client/src/components/ToastSystem.svelte +158 -0
- package/template/apps/client/src/game.ts +918 -0
- package/template/apps/client/src/input.ts +92 -0
- package/template/apps/client/src/lib/audio/CombatDetector.test.ts +59 -0
- package/template/apps/client/src/lib/audio/CombatDetector.ts +53 -0
- package/template/apps/client/src/lib/audio/MusicManager.ts +137 -0
- package/template/apps/client/src/lib/audio/SoundManager.ts +59 -0
- package/template/apps/client/src/lib/audio/index.ts +9 -0
- package/template/apps/client/src/main.ts +32 -0
- package/template/apps/client/src/renderer/basecamp.ts +126 -0
- package/template/apps/client/src/renderer/dungeon.ts +250 -0
- package/template/apps/client/src/renderer/dungeonPortal.ts +73 -0
- package/template/apps/client/src/renderer/dungeonZone.ts +301 -0
- package/template/apps/client/src/renderer/entities.ts +197 -0
- package/template/apps/client/src/renderer/runnerTrack.ts +221 -0
- package/template/apps/client/src/renderer/shaders/SkyShader.ts +190 -0
- package/template/apps/client/src/renderer/shaders/TerrainShaderMaterial.ts +133 -0
- package/template/apps/client/src/renderer/shaders/floor.frag.glsl.ts +17 -0
- package/template/apps/client/src/renderer/shaders/shaderConfig.ts +18 -0
- package/template/apps/client/src/renderer/shaders/spawn.frag.glsl.ts +19 -0
- package/template/apps/client/src/renderer/shaders/terrain.frag.glsl.ts +314 -0
- package/template/apps/client/src/renderer/shaders/terrain.vert.glsl.ts +16 -0
- package/template/apps/client/src/renderer/shaders/wall.frag.glsl.ts +20 -0
- package/template/apps/client/src/renderer/shooterArena.ts +102 -0
- package/template/apps/client/src/renderer/voxelChunkStreamer.ts +79 -0
- package/template/apps/client/src/renderer/voxelMesh.ts +86 -0
- package/template/apps/client/src/renderer/voxelTerrain.ts +74 -0
- package/template/apps/client/src/socket.ts +268 -0
- package/template/apps/client/src/store.ts +74 -0
- package/template/apps/client/src/style.css +60 -0
- package/template/apps/client/tsconfig.json +11 -0
- package/template/apps/client/vite.config.ts +10 -0
- package/template/apps/client/vitest.config.ts +8 -0
- package/template/apps/experiences/diablo/index.ts +94 -0
- package/template/apps/experiences/diablo/systems/dungeonClearSystem.ts +60 -0
- package/template/apps/experiences/diablo/systems/enemyAISystem.ts +11 -0
- package/template/apps/experiences/diablo/systems/entitySyncSystem.ts +80 -0
- package/template/apps/experiences/diablo/systems/itemPickupSystem.ts +11 -0
- package/template/apps/experiences/diablo/systems/movementSystem.ts +13 -0
- package/template/apps/experiences/diablo/systems/physicsSystem.ts +92 -0
- package/template/apps/experiences/diablo/systems/transitionSystem.ts +105 -0
- package/template/apps/experiences/runner/data/runner-config.ts +54 -0
- package/template/apps/experiences/runner/index.ts +143 -0
- package/template/apps/experiences/runner/systems/collectibleSystem.ts +157 -0
- package/template/apps/experiences/runner/systems/deathSystem.ts +42 -0
- package/template/apps/experiences/runner/systems/entitySyncSystem.ts +59 -0
- package/template/apps/experiences/runner/systems/obstacleSystem.ts +91 -0
- package/template/apps/experiences/runner/systems/runnerPhysicsSystem.ts +82 -0
- package/template/apps/experiences/runner/systems/trackStreamSystem.ts +19 -0
- package/template/apps/experiences/runner/track/laneSystem.ts +53 -0
- package/template/apps/experiences/runner/track/segmentTypes.ts +141 -0
- package/template/apps/experiences/runner/track/trackGenerator.ts +292 -0
- package/template/apps/experiences/shooter/ai/aiStateMachine.ts +394 -0
- package/template/apps/experiences/shooter/ai/lineOfSight.ts +32 -0
- package/template/apps/experiences/shooter/arena/arenaGenerator.ts +101 -0
- package/template/apps/experiences/shooter/arena/arenaTypes.ts +49 -0
- package/template/apps/experiences/shooter/arena/hitscan.ts +101 -0
- package/template/apps/experiences/shooter/data/enemy-types.ts +108 -0
- package/template/apps/experiences/shooter/data/wave-definitions.ts +40 -0
- package/template/apps/experiences/shooter/data/weapon-config.ts +80 -0
- package/template/apps/experiences/shooter/index.ts +127 -0
- package/template/apps/experiences/shooter/systems/enemyAISystem.ts +113 -0
- package/template/apps/experiences/shooter/systems/entitySyncSystem.ts +68 -0
- package/template/apps/experiences/shooter/systems/shooterPhysicsSystem.ts +89 -0
- package/template/apps/experiences/shooter/systems/waveSpawnerSystem.ts +87 -0
- package/template/apps/experiences/shooter/systems/weaponSystem.ts +157 -0
- package/template/apps/game-data/src/areas/area-manifest.json +18 -0
- package/template/apps/game-data/src/assets/migration.test.ts +291 -0
- package/template/apps/game-data/src/audio/music-config.json +21 -0
- package/template/apps/game-data/src/audio/sound-config.json +11 -0
- package/template/apps/game-data/src/combat/action-types.ts +2 -0
- package/template/apps/game-data/src/combat/enemy-def.ts +12 -0
- package/template/apps/game-data/src/combat/hitboxes.ts +23 -0
- package/template/apps/game-data/src/dungeon/cell-types.ts +20 -0
- package/template/apps/game-data/src/dungeon/cell-visuals.ts +13 -0
- package/template/apps/game-data/src/dungeon/door-directions.ts +2 -0
- package/template/apps/game-data/src/enemies/enemy-defs.json +32 -0
- package/template/apps/game-data/src/equipment/slots.json +5 -0
- package/template/apps/game-data/src/events/event-defs.ts +20 -0
- package/template/apps/game-data/src/events/event-types.ts +10 -0
- package/template/apps/game-data/src/events/toast-config.json +49 -0
- package/template/apps/game-data/src/items/item-pool.json +13 -0
- package/template/apps/game-data/src/loot/item-pool.ts +14 -0
- package/template/apps/game-data/src/loot/rarities.ts +2 -0
- package/template/apps/game-data/src/physics/dungeon-physics-config.ts +12 -0
- package/template/apps/game-data/src/physics/jump-config.ts +17 -0
- package/template/apps/game-data/src/recipes/recipe-book.json +68 -0
- package/template/apps/game-data/src/rooms/room_basecamp.json +16 -0
- package/template/apps/game-data/src/rooms/room_corridor_ew.json +9 -0
- package/template/apps/game-data/src/rooms/room_corridor_ns.json +11 -0
- package/template/apps/game-data/src/rooms/room_crossroads.json +11 -0
- package/template/apps/game-data/src/rooms/room_dead_end.json +10 -0
- package/template/apps/game-data/src/rooms/room_staircase.json +12 -0
- package/template/apps/game-data/src/rooms/room_start.json +11 -0
- package/template/apps/game-data/src/skills/skill-book.json +20 -0
- package/template/apps/game-data/src/voxel/biome-terrain.ts +76 -0
- package/template/apps/game-data/src/voxel/materials.ts +45 -0
- package/template/apps/game-data/src/voxel/sandbox-terrain-config.ts +19 -0
- package/template/apps/game-data/src/world/area-config.ts +33 -0
- package/template/apps/game-data/src/world/biome-def.ts +15 -0
- package/template/apps/game-data/src/world/biomes.json +57 -0
- package/template/apps/game-data/src/world/movement.ts +2 -0
- package/template/apps/game-data/src/world/overworld-layout.test.ts +93 -0
- package/template/apps/game-data/src/world/overworld-layout.ts +127 -0
- package/template/apps/server/data/game.db +0 -0
- package/template/apps/server/package.json +30 -0
- package/template/apps/server/src/areaManager.ts +346 -0
- package/template/apps/server/src/db/client.ts +45 -0
- package/template/apps/server/src/db/schema.ts +40 -0
- package/template/apps/server/src/gameLoop.ts +267 -0
- package/template/apps/server/src/gameState.ts +3 -0
- package/template/apps/server/src/handlers/actionEvent.ts +55 -0
- package/template/apps/server/src/handlers/craftHandler.ts +59 -0
- package/template/apps/server/src/handlers/equipHandler.ts +73 -0
- package/template/apps/server/src/handlers/raycastHandler.ts +97 -0
- package/template/apps/server/src/handlers/skillHandler.ts +87 -0
- package/template/apps/server/src/handlers/terraformHandler.ts +74 -0
- package/template/apps/server/src/index.ts +597 -0
- package/template/apps/server/src/persistence.ts +135 -0
- package/template/apps/server/src/rooms.ts +20 -0
- package/template/apps/server/src/systems/dungeonPhysics.test.ts +32 -0
- package/template/apps/server/src/systems/dungeonPhysics.ts +16 -0
- package/template/apps/server/src/systems/enemyAI.ts +129 -0
- package/template/apps/server/src/systems/itemPickup.ts +31 -0
- package/template/apps/server/src/tests/areaManager.test.ts +77 -0
- package/template/apps/server/src/tests/diablo-experience.test.ts +60 -0
- package/template/apps/server/src/tests/runner-experience.test.ts +273 -0
- package/template/apps/server/src/tests/runner-powerups-scoring.test.ts +221 -0
- package/template/apps/server/src/tests/server.integration.test.ts +92 -0
- package/template/apps/server/src/tests/shooter-enemy-ai.test.ts +328 -0
- package/template/apps/server/src/tests/shooter-experience.test.ts +281 -0
- package/template/apps/server/src/tests/voxelChunkCache.test.ts +29 -0
- package/template/apps/server/src/tests/voxelSandbox.test.ts +133 -0
- package/template/apps/server/src/voxelChunkCache.ts +31 -0
- package/template/apps/server/src/voxelPlayerState.ts +23 -0
- package/template/apps/server/tsconfig.json +17 -0
- package/template/apps/server/vitest.config.ts +8 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line of Sight check — reuses hitscan raycast against arena obstacles.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AABB, Vec3 } from '../arena/arenaTypes.js';
|
|
6
|
+
import { hitscanRaycast } from '../arena/hitscan.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if there is a clear line of sight between two points.
|
|
10
|
+
* Returns true if no obstacle blocks the path.
|
|
11
|
+
*/
|
|
12
|
+
export function hasLineOfSight(from: Vec3, to: Vec3, obstacles: AABB[]): boolean {
|
|
13
|
+
const dx = to.x - from.x;
|
|
14
|
+
const dy = to.y - from.y;
|
|
15
|
+
const dz = to.z - from.z;
|
|
16
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
17
|
+
if (dist < 0.001) return true;
|
|
18
|
+
|
|
19
|
+
const dir = { x: dx / dist, y: dy / dist, z: dz / dist };
|
|
20
|
+
const hit = hitscanRaycast(from, dir, dist, obstacles, []);
|
|
21
|
+
return hit === null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the distance between two Vec3 points.
|
|
26
|
+
*/
|
|
27
|
+
export function distance3D(a: Vec3, b: Vec3): number {
|
|
28
|
+
const dx = a.x - b.x;
|
|
29
|
+
const dy = a.y - b.y;
|
|
30
|
+
const dz = a.z - b.z;
|
|
31
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
32
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedural arena generation for the Shooter experience.
|
|
3
|
+
* Seed-based — same seed produces the same arena layout.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mulberry32 } from '../../../../packages/core/src/dungeon/prng.js';
|
|
7
|
+
import type { Arena, ArenaConfig, AABB, CoverBlock, Vec3 } from './arenaTypes.js';
|
|
8
|
+
import { DEFAULT_ARENA_CONFIG } from './arenaTypes.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a shooter arena with outer walls, cover blocks, and spawn points.
|
|
12
|
+
*/
|
|
13
|
+
export function generateArena(seed: number, config: ArenaConfig = DEFAULT_ARENA_CONFIG): Arena {
|
|
14
|
+
const rng = mulberry32(seed);
|
|
15
|
+
const { width, depth, wallHeight, coverCount, coverMinSize, coverMaxSize } = config;
|
|
16
|
+
const hw = width / 2;
|
|
17
|
+
const hd = depth / 2;
|
|
18
|
+
|
|
19
|
+
// Outer walls (4 sides)
|
|
20
|
+
const wallThickness = 0.5;
|
|
21
|
+
const walls: AABB[] = [
|
|
22
|
+
// North wall (-Z)
|
|
23
|
+
{ min: { x: -hw - wallThickness, y: 0, z: -hd - wallThickness }, max: { x: hw + wallThickness, y: wallHeight, z: -hd } },
|
|
24
|
+
// South wall (+Z)
|
|
25
|
+
{ min: { x: -hw - wallThickness, y: 0, z: hd }, max: { x: hw + wallThickness, y: wallHeight, z: hd + wallThickness } },
|
|
26
|
+
// West wall (-X)
|
|
27
|
+
{ min: { x: -hw - wallThickness, y: 0, z: -hd }, max: { x: -hw, y: wallHeight, z: hd } },
|
|
28
|
+
// East wall (+X)
|
|
29
|
+
{ min: { x: hw, y: 0, z: -hd }, max: { x: hw + wallThickness, y: wallHeight, z: hd } },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Spawn points in corners (inset by 3 units)
|
|
33
|
+
const inset = 3;
|
|
34
|
+
const spawnPoints: Vec3[] = [
|
|
35
|
+
{ x: -hw + inset, y: 0, z: -hd + inset },
|
|
36
|
+
{ x: hw - inset, y: 0, z: -hd + inset },
|
|
37
|
+
{ x: -hw + inset, y: 0, z: hd - inset },
|
|
38
|
+
{ x: hw - inset, y: 0, z: hd - inset },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Generate cover blocks
|
|
42
|
+
const covers: CoverBlock[] = [];
|
|
43
|
+
const spawnClearRadius = 5; // Keep covers away from spawn points
|
|
44
|
+
let attempts = 0;
|
|
45
|
+
const maxAttempts = coverCount * 10;
|
|
46
|
+
|
|
47
|
+
while (covers.length < coverCount && attempts < maxAttempts) {
|
|
48
|
+
attempts++;
|
|
49
|
+
|
|
50
|
+
const w = coverMinSize + rng() * (coverMaxSize - coverMinSize);
|
|
51
|
+
const d = coverMinSize + rng() * (coverMaxSize - coverMinSize);
|
|
52
|
+
const h = 1.5 + rng() * 1.5; // Height 1.5-3
|
|
53
|
+
|
|
54
|
+
// Random position within arena bounds (inset by cover size)
|
|
55
|
+
const cx = -hw + w + rng() * (width - 2 * w);
|
|
56
|
+
const cz = -hd + d + rng() * (depth - 2 * d);
|
|
57
|
+
|
|
58
|
+
// Check distance from spawn points
|
|
59
|
+
const tooCloseToSpawn = spawnPoints.some(sp => {
|
|
60
|
+
const dx = sp.x - cx;
|
|
61
|
+
const dz = sp.z - cz;
|
|
62
|
+
return Math.sqrt(dx * dx + dz * dz) < spawnClearRadius;
|
|
63
|
+
});
|
|
64
|
+
if (tooCloseToSpawn) continue;
|
|
65
|
+
|
|
66
|
+
// Check overlap with existing covers (with gap)
|
|
67
|
+
const gap = 2;
|
|
68
|
+
const overlaps = covers.some(c => {
|
|
69
|
+
return Math.abs(c.position.x - cx) < (c.width + w) / 2 + gap &&
|
|
70
|
+
Math.abs(c.position.z - cz) < (c.depth + d) / 2 + gap;
|
|
71
|
+
});
|
|
72
|
+
if (overlaps) continue;
|
|
73
|
+
|
|
74
|
+
const aabb: AABB = {
|
|
75
|
+
min: { x: cx - w / 2, y: 0, z: cz - d / 2 },
|
|
76
|
+
max: { x: cx + w / 2, y: h, z: cz + d / 2 },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
covers.push({ position: { x: cx, y: 0, z: cz }, width: w, depth: d, height: h, aabb });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { width, depth, walls, covers, spawnPoints, seed };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all collidable AABBs in the arena (walls + covers).
|
|
87
|
+
*/
|
|
88
|
+
export function getArenaObstacles(arena: Arena): AABB[] {
|
|
89
|
+
return [...arena.walls, ...arena.covers.map(c => c.aabb)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the ground height at a position in the arena.
|
|
94
|
+
* Returns 0 (flat floor) if inside arena, null if outside.
|
|
95
|
+
*/
|
|
96
|
+
export function getArenaGroundHeight(arena: Arena, x: number, z: number): number | null {
|
|
97
|
+
const hw = arena.width / 2;
|
|
98
|
+
const hd = arena.depth / 2;
|
|
99
|
+
if (x < -hw || x > hw || z < -hd || z > hd) return null;
|
|
100
|
+
return 0; // Flat arena floor
|
|
101
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shooter Arena — type definitions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface Vec3 {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
z: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AABB {
|
|
12
|
+
min: Vec3;
|
|
13
|
+
max: Vec3;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CoverBlock {
|
|
17
|
+
position: Vec3;
|
|
18
|
+
width: number;
|
|
19
|
+
depth: number;
|
|
20
|
+
height: number;
|
|
21
|
+
aabb: AABB;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Arena {
|
|
25
|
+
width: number;
|
|
26
|
+
depth: number;
|
|
27
|
+
walls: AABB[];
|
|
28
|
+
covers: CoverBlock[];
|
|
29
|
+
spawnPoints: Vec3[];
|
|
30
|
+
seed: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ArenaConfig {
|
|
34
|
+
width: number;
|
|
35
|
+
depth: number;
|
|
36
|
+
wallHeight: number;
|
|
37
|
+
coverCount: number;
|
|
38
|
+
coverMinSize: number;
|
|
39
|
+
coverMaxSize: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_ARENA_CONFIG: ArenaConfig = {
|
|
43
|
+
width: 40,
|
|
44
|
+
depth: 40,
|
|
45
|
+
wallHeight: 4,
|
|
46
|
+
coverCount: 12,
|
|
47
|
+
coverMinSize: 1.5,
|
|
48
|
+
coverMaxSize: 3,
|
|
49
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hitscan raycast — ray vs AABBs + spheres for the Shooter experience.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { intersectRayAABB } from '../../../../packages/core/src/combat/raycast.js';
|
|
6
|
+
import type { AABB, Vec3 } from './arenaTypes.js';
|
|
7
|
+
|
|
8
|
+
export interface HitscanTarget {
|
|
9
|
+
entityId: number;
|
|
10
|
+
position: Vec3;
|
|
11
|
+
radius: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HitscanResult {
|
|
15
|
+
hitPoint: Vec3;
|
|
16
|
+
distance: number;
|
|
17
|
+
/** null if hit geometry, entityId if hit an entity */
|
|
18
|
+
entityId: number | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cast a ray against arena obstacles and entity spheres.
|
|
23
|
+
* Returns the closest hit, or null if nothing was hit within maxDistance.
|
|
24
|
+
*/
|
|
25
|
+
export function hitscanRaycast(
|
|
26
|
+
origin: Vec3,
|
|
27
|
+
direction: Vec3,
|
|
28
|
+
maxDistance: number,
|
|
29
|
+
obstacles: AABB[],
|
|
30
|
+
entities: HitscanTarget[],
|
|
31
|
+
): HitscanResult | null {
|
|
32
|
+
let closest: HitscanResult | null = null;
|
|
33
|
+
|
|
34
|
+
// Check obstacles (AABB)
|
|
35
|
+
for (const obs of obstacles) {
|
|
36
|
+
const dist = intersectRayAABB(origin, direction, obs.min, obs.max);
|
|
37
|
+
if (dist !== null && dist <= maxDistance) {
|
|
38
|
+
if (!closest || dist < closest.distance) {
|
|
39
|
+
closest = {
|
|
40
|
+
hitPoint: {
|
|
41
|
+
x: origin.x + direction.x * dist,
|
|
42
|
+
y: origin.y + direction.y * dist,
|
|
43
|
+
z: origin.z + direction.z * dist,
|
|
44
|
+
},
|
|
45
|
+
distance: dist,
|
|
46
|
+
entityId: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check entities (sphere intersection)
|
|
53
|
+
for (const entity of entities) {
|
|
54
|
+
const dist = intersectRaySphere(origin, direction, entity.position, entity.radius);
|
|
55
|
+
if (dist !== null && dist <= maxDistance && dist >= 0) {
|
|
56
|
+
if (!closest || dist < closest.distance) {
|
|
57
|
+
closest = {
|
|
58
|
+
hitPoint: {
|
|
59
|
+
x: origin.x + direction.x * dist,
|
|
60
|
+
y: origin.y + direction.y * dist,
|
|
61
|
+
z: origin.z + direction.z * dist,
|
|
62
|
+
},
|
|
63
|
+
distance: dist,
|
|
64
|
+
entityId: entity.entityId,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return closest;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ray-sphere intersection. Returns distance to hit or null.
|
|
75
|
+
*/
|
|
76
|
+
function intersectRaySphere(
|
|
77
|
+
origin: Vec3,
|
|
78
|
+
direction: Vec3,
|
|
79
|
+
center: Vec3,
|
|
80
|
+
radius: number,
|
|
81
|
+
): number | null {
|
|
82
|
+
const ox = origin.x - center.x;
|
|
83
|
+
const oy = origin.y - center.y;
|
|
84
|
+
const oz = origin.z - center.z;
|
|
85
|
+
|
|
86
|
+
const a = direction.x * direction.x + direction.y * direction.y + direction.z * direction.z;
|
|
87
|
+
const b = 2 * (ox * direction.x + oy * direction.y + oz * direction.z);
|
|
88
|
+
const c = ox * ox + oy * oy + oz * oz - radius * radius;
|
|
89
|
+
|
|
90
|
+
const discriminant = b * b - 4 * a * c;
|
|
91
|
+
if (discriminant < 0) return null;
|
|
92
|
+
|
|
93
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
94
|
+
const t1 = (-b - sqrtDisc) / (2 * a);
|
|
95
|
+
const t2 = (-b + sqrtDisc) / (2 * a);
|
|
96
|
+
|
|
97
|
+
// Return the nearest positive intersection
|
|
98
|
+
if (t1 >= 0) return t1;
|
|
99
|
+
if (t2 >= 0) return t2;
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enemy type definitions for the Shooter experience.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CharacterConfig } from '../../../../packages/core/src/physics/characterController.js';
|
|
6
|
+
import { DEFAULT_CHARACTER_CONFIG } from '../../../../packages/core/src/physics/characterController.js';
|
|
7
|
+
import type { WeaponConfig } from './weapon-config.js';
|
|
8
|
+
|
|
9
|
+
export type EnemyType = 'rusher' | 'patrol' | 'sniper';
|
|
10
|
+
|
|
11
|
+
export interface EnemyTypeConfig {
|
|
12
|
+
type: EnemyType;
|
|
13
|
+
health: number;
|
|
14
|
+
detectionRadius: number;
|
|
15
|
+
attackRange: number;
|
|
16
|
+
/** Radians of random aim deviation */
|
|
17
|
+
accuracySpread: number;
|
|
18
|
+
/** Seconds of reaction delay after detection */
|
|
19
|
+
reactionTime: number;
|
|
20
|
+
/** HP threshold (fraction) to trigger retreat */
|
|
21
|
+
retreatThreshold: number;
|
|
22
|
+
characterConfig: CharacterConfig;
|
|
23
|
+
weaponConfig: WeaponConfig;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const RUSHER_CONFIG: EnemyTypeConfig = {
|
|
27
|
+
type: 'rusher',
|
|
28
|
+
health: 30,
|
|
29
|
+
detectionRadius: 15,
|
|
30
|
+
attackRange: 6,
|
|
31
|
+
accuracySpread: 0.15,
|
|
32
|
+
reactionTime: 0.3,
|
|
33
|
+
retreatThreshold: 0, // Rushers never retreat
|
|
34
|
+
characterConfig: {
|
|
35
|
+
...DEFAULT_CHARACTER_CONFIG,
|
|
36
|
+
walkSpeed: 5,
|
|
37
|
+
sprintMultiplier: 1.5,
|
|
38
|
+
maxSpeed: 10,
|
|
39
|
+
friction: 20,
|
|
40
|
+
airControl: 0.2,
|
|
41
|
+
acceleration: 30,
|
|
42
|
+
},
|
|
43
|
+
weaponConfig: {
|
|
44
|
+
damage: 8,
|
|
45
|
+
fireRate: 3,
|
|
46
|
+
maxAmmo: 999, // Unlimited
|
|
47
|
+
reloadTime: 0,
|
|
48
|
+
range: 10,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const PATROL_CONFIG: EnemyTypeConfig = {
|
|
53
|
+
type: 'patrol',
|
|
54
|
+
health: 60,
|
|
55
|
+
detectionRadius: 20,
|
|
56
|
+
attackRange: 15,
|
|
57
|
+
accuracySpread: 0.08,
|
|
58
|
+
reactionTime: 0.5,
|
|
59
|
+
retreatThreshold: 0.2,
|
|
60
|
+
characterConfig: {
|
|
61
|
+
...DEFAULT_CHARACTER_CONFIG,
|
|
62
|
+
walkSpeed: 3,
|
|
63
|
+
sprintMultiplier: 1.3,
|
|
64
|
+
maxSpeed: 6,
|
|
65
|
+
friction: 25,
|
|
66
|
+
airControl: 0.2,
|
|
67
|
+
acceleration: 25,
|
|
68
|
+
},
|
|
69
|
+
weaponConfig: {
|
|
70
|
+
damage: 12,
|
|
71
|
+
fireRate: 2,
|
|
72
|
+
maxAmmo: 999,
|
|
73
|
+
reloadTime: 0,
|
|
74
|
+
range: 20,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const SNIPER_CONFIG: EnemyTypeConfig = {
|
|
79
|
+
type: 'sniper',
|
|
80
|
+
health: 40,
|
|
81
|
+
detectionRadius: 35,
|
|
82
|
+
attackRange: 30,
|
|
83
|
+
accuracySpread: 0.03,
|
|
84
|
+
reactionTime: 0.8,
|
|
85
|
+
retreatThreshold: 0.3,
|
|
86
|
+
characterConfig: {
|
|
87
|
+
...DEFAULT_CHARACTER_CONFIG,
|
|
88
|
+
walkSpeed: 1.5,
|
|
89
|
+
sprintMultiplier: 1.2,
|
|
90
|
+
maxSpeed: 3,
|
|
91
|
+
friction: 30,
|
|
92
|
+
airControl: 0.1,
|
|
93
|
+
acceleration: 15,
|
|
94
|
+
},
|
|
95
|
+
weaponConfig: {
|
|
96
|
+
damage: 30,
|
|
97
|
+
fireRate: 0.5,
|
|
98
|
+
maxAmmo: 999,
|
|
99
|
+
reloadTime: 0,
|
|
100
|
+
range: 40,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const ENEMY_CONFIGS: Record<EnemyType, EnemyTypeConfig> = {
|
|
105
|
+
rusher: RUSHER_CONFIG,
|
|
106
|
+
patrol: PATROL_CONFIG,
|
|
107
|
+
sniper: SNIPER_CONFIG,
|
|
108
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave definitions for the Shooter experience.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EnemyType } from './enemy-types.js';
|
|
6
|
+
|
|
7
|
+
export interface WaveEntry {
|
|
8
|
+
type: EnemyType;
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WaveDefinition {
|
|
13
|
+
enemies: WaveEntry[];
|
|
14
|
+
/** Seconds to wait after clearing before spawning next wave */
|
|
15
|
+
delayAfterClear: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const WAVE_DEFINITIONS: WaveDefinition[] = [
|
|
19
|
+
{ enemies: [{ type: 'rusher', count: 3 }], delayAfterClear: 3 },
|
|
20
|
+
{ enemies: [{ type: 'patrol', count: 2 }, { type: 'rusher', count: 1 }], delayAfterClear: 3 },
|
|
21
|
+
{ enemies: [{ type: 'sniper', count: 1 }, { type: 'patrol', count: 2 }, { type: 'rusher', count: 2 }], delayAfterClear: 3 },
|
|
22
|
+
{ enemies: [{ type: 'sniper', count: 2 }, { type: 'patrol', count: 3 }, { type: 'rusher', count: 3 }], delayAfterClear: 3 },
|
|
23
|
+
{ enemies: [{ type: 'sniper', count: 2 }, { type: 'patrol', count: 4 }, { type: 'rusher', count: 4 }], delayAfterClear: 3 },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get wave definition. After defined waves, scales up the last wave.
|
|
28
|
+
*/
|
|
29
|
+
export function getWaveDefinition(waveIndex: number): WaveDefinition {
|
|
30
|
+
if (waveIndex < WAVE_DEFINITIONS.length) {
|
|
31
|
+
return WAVE_DEFINITIONS[waveIndex];
|
|
32
|
+
}
|
|
33
|
+
// Scale up: add more enemies per wave beyond defined ones
|
|
34
|
+
const base = WAVE_DEFINITIONS[WAVE_DEFINITIONS.length - 1];
|
|
35
|
+
const extra = waveIndex - WAVE_DEFINITIONS.length + 1;
|
|
36
|
+
return {
|
|
37
|
+
enemies: base.enemies.map(e => ({ ...e, count: e.count + extra })),
|
|
38
|
+
delayAfterClear: 3,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weapon configuration for the Shooter experience.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface WeaponConfig {
|
|
6
|
+
damage: number;
|
|
7
|
+
/** Shots per second */
|
|
8
|
+
fireRate: number;
|
|
9
|
+
/** Magazine size */
|
|
10
|
+
maxAmmo: number;
|
|
11
|
+
/** Seconds to reload */
|
|
12
|
+
reloadTime: number;
|
|
13
|
+
/** Max hitscan distance */
|
|
14
|
+
range: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_WEAPON_CONFIG: WeaponConfig = {
|
|
18
|
+
damage: 25,
|
|
19
|
+
fireRate: 4,
|
|
20
|
+
maxAmmo: 12,
|
|
21
|
+
reloadTime: 1.5,
|
|
22
|
+
range: 100,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface WeaponState {
|
|
26
|
+
currentAmmo: number;
|
|
27
|
+
isReloading: boolean;
|
|
28
|
+
reloadTimer: number;
|
|
29
|
+
fireCooldown: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createWeaponState(config: WeaponConfig): WeaponState {
|
|
33
|
+
return {
|
|
34
|
+
currentAmmo: config.maxAmmo,
|
|
35
|
+
isReloading: false,
|
|
36
|
+
reloadTimer: 0,
|
|
37
|
+
fireCooldown: 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attempt to fire. Returns true if shot was fired.
|
|
43
|
+
*/
|
|
44
|
+
export function processShoot(state: WeaponState, config: WeaponConfig): boolean {
|
|
45
|
+
if (state.isReloading) return false;
|
|
46
|
+
if (state.fireCooldown > 0) return false;
|
|
47
|
+
if (state.currentAmmo <= 0) return false;
|
|
48
|
+
|
|
49
|
+
state.currentAmmo--;
|
|
50
|
+
state.fireCooldown = 1 / config.fireRate;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Start reloading.
|
|
56
|
+
*/
|
|
57
|
+
export function startReload(state: WeaponState, config: WeaponConfig): void {
|
|
58
|
+
if (state.isReloading) return;
|
|
59
|
+
if (state.currentAmmo >= config.maxAmmo) return;
|
|
60
|
+
state.isReloading = true;
|
|
61
|
+
state.reloadTimer = config.reloadTime;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tick the weapon state (cooldown + reload timer).
|
|
66
|
+
*/
|
|
67
|
+
export function updateWeaponState(state: WeaponState, dt: number, config: WeaponConfig): void {
|
|
68
|
+
if (state.fireCooldown > 0) {
|
|
69
|
+
state.fireCooldown = Math.max(0, state.fireCooldown - dt);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state.isReloading) {
|
|
73
|
+
state.reloadTimer -= dt;
|
|
74
|
+
if (state.reloadTimer <= 0) {
|
|
75
|
+
state.isReloading = false;
|
|
76
|
+
state.reloadTimer = 0;
|
|
77
|
+
state.currentAmmo = config.maxAmmo;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shooter Experience — FPS/TPS arena combat.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - ShooterExperience: ExperienceDefinition for hub registration
|
|
6
|
+
* - ShooterSession: custom session state for shooter gameplay
|
|
7
|
+
* - ShooterPlayer: per-player state including physics body, weapon, input
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExperienceDefinition, ExperienceSystem, PhysicsBody, MovementInput } from '../../../packages/core/src/index.js';
|
|
11
|
+
import { DEFAULT_PHYSICS_CONFIG, createPhysicsBody } from '../../../packages/core/src/index.js';
|
|
12
|
+
import type { Arena } from './arena/arenaTypes.js';
|
|
13
|
+
import type { Vec3 } from './arena/arenaTypes.js';
|
|
14
|
+
import type { WeaponState } from './data/weapon-config.js';
|
|
15
|
+
import { createWeaponState, DEFAULT_WEAPON_CONFIG } from './data/weapon-config.js';
|
|
16
|
+
import { generateArena } from './arena/arenaGenerator.js';
|
|
17
|
+
import { ShooterPhysicsSystem } from './systems/shooterPhysicsSystem.js';
|
|
18
|
+
import { WeaponSystem } from './systems/weaponSystem.js';
|
|
19
|
+
import { ShooterEntitySyncSystem } from './systems/entitySyncSystem.js';
|
|
20
|
+
import { EnemyAISystem } from './systems/enemyAISystem.js';
|
|
21
|
+
import { WaveSpawnerSystem } from './systems/waveSpawnerSystem.js';
|
|
22
|
+
import type { ShooterEnemy } from './ai/aiStateMachine.js';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Shooter-specific session & player state
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export interface ShootIntent {
|
|
29
|
+
origin: Vec3;
|
|
30
|
+
direction: Vec3;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ShooterPlayer {
|
|
34
|
+
socketId: string;
|
|
35
|
+
playerId: string;
|
|
36
|
+
entityId: number;
|
|
37
|
+
body: PhysicsBody;
|
|
38
|
+
hp: number;
|
|
39
|
+
maxHp: number;
|
|
40
|
+
weapon: WeaponState;
|
|
41
|
+
input: MovementInput;
|
|
42
|
+
facingAngle: number;
|
|
43
|
+
shootQueue: ShootIntent[];
|
|
44
|
+
wantsReload: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ShooterSession {
|
|
48
|
+
arena: Arena;
|
|
49
|
+
players: Map<string, ShooterPlayer>;
|
|
50
|
+
nextEntityId: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface WaveState {
|
|
54
|
+
currentWave: number;
|
|
55
|
+
waveActive: boolean;
|
|
56
|
+
delayTimer: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Extended session with enemy support (Story 60). */
|
|
60
|
+
export interface ShooterSessionWithEnemies extends ShooterSession {
|
|
61
|
+
enemies: ShooterEnemy[];
|
|
62
|
+
waveState: WaveState;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createShooterSession(seed: number): ShooterSessionWithEnemies {
|
|
66
|
+
const arena = generateArena(seed);
|
|
67
|
+
return {
|
|
68
|
+
arena,
|
|
69
|
+
players: new Map(),
|
|
70
|
+
nextEntityId: 1,
|
|
71
|
+
enemies: [],
|
|
72
|
+
waveState: {
|
|
73
|
+
currentWave: 0,
|
|
74
|
+
waveActive: false,
|
|
75
|
+
delayTimer: 2, // 2 second delay before first wave
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function addShooterPlayer(session: ShooterSession, socketId: string, playerId: string): ShooterPlayer {
|
|
81
|
+
const entityId = session.nextEntityId++;
|
|
82
|
+
const spawnIdx = session.players.size % session.arena.spawnPoints.length;
|
|
83
|
+
const spawn = session.arena.spawnPoints[spawnIdx];
|
|
84
|
+
|
|
85
|
+
const player: ShooterPlayer = {
|
|
86
|
+
socketId,
|
|
87
|
+
playerId,
|
|
88
|
+
entityId,
|
|
89
|
+
body: createPhysicsBody(spawn.x, spawn.y, spawn.z),
|
|
90
|
+
hp: 100,
|
|
91
|
+
maxHp: 100,
|
|
92
|
+
weapon: createWeaponState(DEFAULT_WEAPON_CONFIG),
|
|
93
|
+
input: { x: 0, z: 0, sprint: false, glide: false },
|
|
94
|
+
facingAngle: 0,
|
|
95
|
+
shootQueue: [],
|
|
96
|
+
wantsReload: false,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
session.players.set(socketId, player);
|
|
100
|
+
return player;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function removeShooterPlayer(session: ShooterSession, socketId: string): void {
|
|
104
|
+
session.players.delete(socketId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Experience Definition
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export class ShooterExperience implements ExperienceDefinition {
|
|
112
|
+
readonly id = 'shooter';
|
|
113
|
+
readonly name = 'Shooter Arena';
|
|
114
|
+
readonly description = 'Fast-paced arena combat with hitscan weapons. Run, jump, sprint, and shoot.';
|
|
115
|
+
readonly defaultCameraMode = 'third-person' as const;
|
|
116
|
+
readonly physicsConfig = DEFAULT_PHYSICS_CONFIG;
|
|
117
|
+
|
|
118
|
+
createSystems(): ExperienceSystem[] {
|
|
119
|
+
return [
|
|
120
|
+
new WaveSpawnerSystem(),
|
|
121
|
+
new EnemyAISystem(),
|
|
122
|
+
new ShooterPhysicsSystem(),
|
|
123
|
+
new WeaponSystem(),
|
|
124
|
+
new ShooterEntitySyncSystem(),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
}
|