@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collectible & powerup pickup system for Runner experience.
|
|
3
|
+
* Handles coin/gem collection, powerup application, combo tracking, and score.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/experience/types.js';
|
|
7
|
+
import type { CharacterModifier } from '../../../../packages/core/src/physics/modifiers.js';
|
|
8
|
+
import { updateModifiers } from '../../../../packages/core/src/physics/modifiers.js';
|
|
9
|
+
import type { RunnerSession, RunnerPlayer } from '../index.js';
|
|
10
|
+
import type { PlacedCollectible, PlacedPowerup } from '../track/segmentTypes.js';
|
|
11
|
+
import { LANE_WIDTH } from '../track/laneSystem.js';
|
|
12
|
+
import { getSpeedForDistance } from '../data/runner-config.js';
|
|
13
|
+
|
|
14
|
+
/** Pickup radius in world units */
|
|
15
|
+
const PICKUP_RADIUS = 1.5;
|
|
16
|
+
/** Magnet pickup radius */
|
|
17
|
+
const MAGNET_RADIUS = 6;
|
|
18
|
+
/** Combo timeout in seconds */
|
|
19
|
+
const COMBO_TIMEOUT = 3;
|
|
20
|
+
|
|
21
|
+
/** Get combo multiplier based on consecutive collects */
|
|
22
|
+
export function getComboMultiplier(count: number): number {
|
|
23
|
+
if (count >= 20) return 5;
|
|
24
|
+
if (count >= 10) return 3;
|
|
25
|
+
if (count >= 5) return 2;
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Check if a collectible/powerup is in pickup range */
|
|
30
|
+
export function isInPickupRange(
|
|
31
|
+
playerX: number,
|
|
32
|
+
playerZ: number,
|
|
33
|
+
itemX: number,
|
|
34
|
+
itemZ: number,
|
|
35
|
+
magnetActive: boolean,
|
|
36
|
+
): boolean {
|
|
37
|
+
const dx = playerX - itemX;
|
|
38
|
+
const dz = playerZ - itemZ;
|
|
39
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
40
|
+
return dist < (magnetActive ? MAGNET_RADIUS : PICKUP_RADIUS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Apply a powerup effect to a player */
|
|
44
|
+
export function applyPowerup(player: RunnerPlayer, type: string): void {
|
|
45
|
+
switch (type) {
|
|
46
|
+
case 'speed-boost':
|
|
47
|
+
player.modifiers.push({
|
|
48
|
+
id: 'speed-boost',
|
|
49
|
+
multipliers: { walkSpeed: 1.5, maxSpeed: 1.5 },
|
|
50
|
+
remainingTime: 5,
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
case 'low-gravity':
|
|
54
|
+
player.gravityModifier = { multiplier: 0.3, remainingTime: 5 };
|
|
55
|
+
break;
|
|
56
|
+
case 'magnet':
|
|
57
|
+
player.magnetActive = true;
|
|
58
|
+
player.magnetTimer = 8;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class CollectibleSystem implements ExperienceSystem {
|
|
64
|
+
readonly id = 'runner-collectibles';
|
|
65
|
+
|
|
66
|
+
update(session: unknown, dt: number, context: SystemContext): void {
|
|
67
|
+
const s = session as RunnerSession;
|
|
68
|
+
const io = context.io as import('socket.io').Server;
|
|
69
|
+
|
|
70
|
+
for (const player of s.players.values()) {
|
|
71
|
+
if (player.dead) continue;
|
|
72
|
+
|
|
73
|
+
const playerX = player.body.x;
|
|
74
|
+
const playerZ = player.body.z;
|
|
75
|
+
|
|
76
|
+
// Check collectible pickups across all active segments
|
|
77
|
+
for (const segment of player.track.segments) {
|
|
78
|
+
for (const collectible of segment.collectibles) {
|
|
79
|
+
if (collectible.collected) continue;
|
|
80
|
+
if (isInPickupRange(playerX, playerZ, collectible.x, collectible.z, player.magnetActive)) {
|
|
81
|
+
collectible.collected = true;
|
|
82
|
+
const value = collectible.type === 'gem' ? 100 : 10;
|
|
83
|
+
const multiplier = getComboMultiplier(player.combo.count);
|
|
84
|
+
player.score += value * multiplier;
|
|
85
|
+
if (collectible.type === 'gem') player.gems++;
|
|
86
|
+
else player.coins++;
|
|
87
|
+
player.combo.count++;
|
|
88
|
+
player.combo.timer = COMBO_TIMEOUT;
|
|
89
|
+
if (player.combo.count > player.bestCombo) {
|
|
90
|
+
player.bestCombo = player.combo.count;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Notify client
|
|
94
|
+
const sock = io.sockets.sockets.get(player.socketId);
|
|
95
|
+
if (sock) {
|
|
96
|
+
sock.emit('message', {
|
|
97
|
+
type: 'RUNNER_COLLECT',
|
|
98
|
+
collectibleType: collectible.type,
|
|
99
|
+
value,
|
|
100
|
+
multiplier,
|
|
101
|
+
combo: player.combo.count,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check powerup pickup
|
|
108
|
+
if (segment.powerup && !segment.powerup.collected) {
|
|
109
|
+
if (isInPickupRange(playerX, playerZ, segment.powerup.x, segment.powerup.z, false)) {
|
|
110
|
+
segment.powerup.collected = true;
|
|
111
|
+
applyPowerup(player, segment.powerup.type);
|
|
112
|
+
|
|
113
|
+
const sock = io.sockets.sockets.get(player.socketId);
|
|
114
|
+
if (sock) {
|
|
115
|
+
sock.emit('message', {
|
|
116
|
+
type: 'RUNNER_POWERUP',
|
|
117
|
+
powerupType: segment.powerup.type,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Update combo timer
|
|
125
|
+
if (player.combo.count > 0) {
|
|
126
|
+
player.combo.timer -= dt;
|
|
127
|
+
if (player.combo.timer <= 0) {
|
|
128
|
+
player.combo.count = 0;
|
|
129
|
+
player.combo.timer = 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Distance score: 1 point per unit
|
|
134
|
+
player.score += getSpeedForDistance(player.distanceRan) * dt;
|
|
135
|
+
|
|
136
|
+
// Update modifiers (expire speed boost etc.)
|
|
137
|
+
player.modifiers = updateModifiers(player.modifiers, dt);
|
|
138
|
+
|
|
139
|
+
// Update magnet timer
|
|
140
|
+
if (player.magnetActive) {
|
|
141
|
+
player.magnetTimer -= dt;
|
|
142
|
+
if (player.magnetTimer <= 0) {
|
|
143
|
+
player.magnetActive = false;
|
|
144
|
+
player.magnetTimer = 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Update gravity modifier timer
|
|
149
|
+
if (player.gravityModifier) {
|
|
150
|
+
player.gravityModifier.remainingTime -= dt;
|
|
151
|
+
if (player.gravityModifier.remainingTime <= 0) {
|
|
152
|
+
player.gravityModifier = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Death detection system — player falls off track = death.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/experience/types.js';
|
|
6
|
+
import type { RunnerSession } from '../index.js';
|
|
7
|
+
import { DEATH_Y_THRESHOLD } from '../data/runner-config.js';
|
|
8
|
+
|
|
9
|
+
export class DeathSystem implements ExperienceSystem {
|
|
10
|
+
readonly id = 'runner-death';
|
|
11
|
+
|
|
12
|
+
update(session: unknown, dt: number, context: SystemContext): void {
|
|
13
|
+
const s = session as RunnerSession;
|
|
14
|
+
const io = context.io as import('socket.io').Server;
|
|
15
|
+
|
|
16
|
+
for (const player of s.players.values()) {
|
|
17
|
+
if (player.dead) continue;
|
|
18
|
+
|
|
19
|
+
// Death: fell below threshold
|
|
20
|
+
if (player.body.y < DEATH_Y_THRESHOLD) {
|
|
21
|
+
player.dead = true;
|
|
22
|
+
const finalScore = Math.floor(player.score);
|
|
23
|
+
if (finalScore > player.highScore) {
|
|
24
|
+
player.highScore = finalScore;
|
|
25
|
+
}
|
|
26
|
+
const sock = io.sockets.sockets.get(player.socketId);
|
|
27
|
+
if (sock) {
|
|
28
|
+
sock.emit('message', {
|
|
29
|
+
type: 'RUNNER_DEAD',
|
|
30
|
+
distance: Math.floor(player.distanceRan),
|
|
31
|
+
score: finalScore,
|
|
32
|
+
coins: player.coins,
|
|
33
|
+
gems: player.gems,
|
|
34
|
+
bestCombo: player.bestCombo,
|
|
35
|
+
highScore: player.highScore,
|
|
36
|
+
cause: 'fall',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity sync system for Runner — broadcasts player positions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/experience/types.js';
|
|
6
|
+
import type { RunnerSession } from '../index.js';
|
|
7
|
+
|
|
8
|
+
export class RunnerEntitySyncSystem implements ExperienceSystem {
|
|
9
|
+
readonly id = 'runner-entity-sync';
|
|
10
|
+
|
|
11
|
+
update(session: unknown, dt: number, context: SystemContext): void {
|
|
12
|
+
const s = session as RunnerSession;
|
|
13
|
+
const io = context.io as import('socket.io').Server;
|
|
14
|
+
|
|
15
|
+
const entities: Array<{
|
|
16
|
+
entityId: number;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
worldY: number;
|
|
20
|
+
type: string;
|
|
21
|
+
}> = [];
|
|
22
|
+
|
|
23
|
+
for (const player of s.players.values()) {
|
|
24
|
+
entities.push({
|
|
25
|
+
entityId: player.entityId,
|
|
26
|
+
x: player.body.x,
|
|
27
|
+
y: player.body.z, // y in 2D = z in 3D (forward)
|
|
28
|
+
worldY: player.body.y, // vertical height
|
|
29
|
+
type: 'player',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const player of s.players.values()) {
|
|
34
|
+
const sock = io.sockets.sockets.get(player.socketId);
|
|
35
|
+
if (sock) {
|
|
36
|
+
sock.emit('message', {
|
|
37
|
+
type: 'ENTITY_SYNC',
|
|
38
|
+
tick: context.tick,
|
|
39
|
+
entities,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Send runner state update
|
|
43
|
+
sock.emit('message', {
|
|
44
|
+
type: 'RUNNER_STATE',
|
|
45
|
+
distance: Math.floor(player.distanceRan),
|
|
46
|
+
speed: player.body.velocityZ,
|
|
47
|
+
lane: player.lane.currentLane,
|
|
48
|
+
isGliding: player.isGliding,
|
|
49
|
+
score: Math.floor(player.score),
|
|
50
|
+
coins: player.coins,
|
|
51
|
+
combo: player.combo.count,
|
|
52
|
+
magnetActive: player.magnetActive,
|
|
53
|
+
hasSpeedBoost: player.modifiers.some(m => m.id === 'speed-boost'),
|
|
54
|
+
hasLowGravity: player.gravityModifier !== null,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Obstacle collision system for Runner experience.
|
|
3
|
+
* Checks if the player collides with obstacles on the track.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/experience/types.js';
|
|
7
|
+
import type { RunnerSession } from '../index.js';
|
|
8
|
+
import type { PlacedObstacle } from '../track/segmentTypes.js';
|
|
9
|
+
import { LANE_WIDTH } from '../track/laneSystem.js';
|
|
10
|
+
|
|
11
|
+
/** How close the player must be in Z to trigger obstacle collision */
|
|
12
|
+
const OBSTACLE_Z_TOLERANCE = 0.8;
|
|
13
|
+
/** How close the player must be in X (lane) to trigger collision */
|
|
14
|
+
const OBSTACLE_X_TOLERANCE = 0.8;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if a player collides with an obstacle.
|
|
18
|
+
* Returns true if the player should die.
|
|
19
|
+
*/
|
|
20
|
+
export function checkObstacleCollision(
|
|
21
|
+
playerX: number,
|
|
22
|
+
playerY: number,
|
|
23
|
+
playerZ: number,
|
|
24
|
+
obstacle: PlacedObstacle,
|
|
25
|
+
): boolean {
|
|
26
|
+
const dx = Math.abs(playerX - obstacle.x);
|
|
27
|
+
const dz = Math.abs(playerZ - obstacle.z);
|
|
28
|
+
|
|
29
|
+
if (dx > OBSTACLE_X_TOLERANCE || dz > OBSTACLE_Z_TOLERANCE) return false;
|
|
30
|
+
|
|
31
|
+
switch (obstacle.type) {
|
|
32
|
+
case 'wall':
|
|
33
|
+
// Wall blocks entire height — always fatal
|
|
34
|
+
return true;
|
|
35
|
+
case 'low-barrier':
|
|
36
|
+
// Low barrier — can jump over (player Y must be above barrier height)
|
|
37
|
+
return playerY < obstacle.height;
|
|
38
|
+
case 'high-barrier':
|
|
39
|
+
// High barrier — blocks upper space, must stay low (not jumping)
|
|
40
|
+
// Fatal if player is in the air above ground level
|
|
41
|
+
return playerY > 0.5;
|
|
42
|
+
default:
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ObstacleSystem implements ExperienceSystem {
|
|
48
|
+
readonly id = 'runner-obstacles';
|
|
49
|
+
|
|
50
|
+
update(session: unknown, dt: number, context: SystemContext): void {
|
|
51
|
+
const s = session as RunnerSession;
|
|
52
|
+
const io = context.io as import('socket.io').Server;
|
|
53
|
+
|
|
54
|
+
for (const player of s.players.values()) {
|
|
55
|
+
if (player.dead) continue;
|
|
56
|
+
|
|
57
|
+
const playerX = player.body.x;
|
|
58
|
+
const playerY = player.body.y;
|
|
59
|
+
const playerZ = player.body.z;
|
|
60
|
+
|
|
61
|
+
for (const segment of player.track.segments) {
|
|
62
|
+
for (const obstacle of segment.obstacles) {
|
|
63
|
+
if (checkObstacleCollision(playerX, playerY, playerZ, obstacle)) {
|
|
64
|
+
player.dead = true;
|
|
65
|
+
// Update high score
|
|
66
|
+
const finalScore = Math.floor(player.score);
|
|
67
|
+
if (finalScore > player.highScore) {
|
|
68
|
+
player.highScore = finalScore;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sock = io.sockets.sockets.get(player.socketId);
|
|
72
|
+
if (sock) {
|
|
73
|
+
sock.emit('message', {
|
|
74
|
+
type: 'RUNNER_DEAD',
|
|
75
|
+
distance: Math.floor(player.distanceRan),
|
|
76
|
+
score: finalScore,
|
|
77
|
+
coins: player.coins,
|
|
78
|
+
gems: player.gems,
|
|
79
|
+
bestCombo: player.bestCombo,
|
|
80
|
+
highScore: player.highScore,
|
|
81
|
+
cause: obstacle.type,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (player.dead) break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner physics system — auto-run forward, lane movement, jump, glide, gravity.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/experience/types.js';
|
|
6
|
+
import type { RunnerSession } from '../index.js';
|
|
7
|
+
import { updatePhysicsBody } from '../../../../packages/core/src/physics/gravity.js';
|
|
8
|
+
import { updateJumpState, tryJump } from '../../../../packages/core/src/physics/jump.js';
|
|
9
|
+
import { getGlideGravityMultiplier } from '../../../../packages/core/src/physics/characterController.js';
|
|
10
|
+
import { applyModifiers } from '../../../../packages/core/src/physics/modifiers.js';
|
|
11
|
+
import { requestLaneSwitch, updateLane } from '../track/laneSystem.js';
|
|
12
|
+
import { getTrackGroundHeight } from '../track/trackGenerator.js';
|
|
13
|
+
import { RUNNER_PHYSICS_CONFIG, RUNNER_CHARACTER_CONFIG, RUNNER_JUMP_CONFIG, getSpeedForDistance } from '../data/runner-config.js';
|
|
14
|
+
|
|
15
|
+
export class RunnerPhysicsSystem implements ExperienceSystem {
|
|
16
|
+
readonly id = 'runner-physics';
|
|
17
|
+
|
|
18
|
+
update(session: unknown, dt: number, context: SystemContext): void {
|
|
19
|
+
const s = session as RunnerSession;
|
|
20
|
+
for (const player of s.players.values()) {
|
|
21
|
+
if (player.dead) continue;
|
|
22
|
+
|
|
23
|
+
// Process lane input (inverted: camera looks in +Z, so screen-left = +X)
|
|
24
|
+
if (player.input.left) {
|
|
25
|
+
requestLaneSwitch(player.lane, 1);
|
|
26
|
+
player.input.left = false; // consume input
|
|
27
|
+
}
|
|
28
|
+
if (player.input.right) {
|
|
29
|
+
requestLaneSwitch(player.lane, -1);
|
|
30
|
+
player.input.right = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Process jump input
|
|
34
|
+
if (player.input.jump) {
|
|
35
|
+
tryJump(player.body, player.jumpState, RUNNER_JUMP_CONFIG);
|
|
36
|
+
player.input.jump = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Glide state
|
|
40
|
+
player.isGliding = player.input.glide && !player.body.isGrounded;
|
|
41
|
+
|
|
42
|
+
// Update lane interpolation
|
|
43
|
+
updateLane(player.lane, dt);
|
|
44
|
+
|
|
45
|
+
// Apply character modifiers (speed boost etc.)
|
|
46
|
+
const effectiveConfig = player.modifiers.length > 0
|
|
47
|
+
? applyModifiers(RUNNER_CHARACTER_CONFIG, player.modifiers)
|
|
48
|
+
: RUNNER_CHARACTER_CONFIG;
|
|
49
|
+
|
|
50
|
+
// Auto-run: constant forward velocity (Z axis), modified by powerups
|
|
51
|
+
const baseSpeed = getSpeedForDistance(player.distanceRan);
|
|
52
|
+
const speedMultiplier = effectiveConfig.walkSpeed / RUNNER_CHARACTER_CONFIG.walkSpeed;
|
|
53
|
+
const speed = baseSpeed * speedMultiplier;
|
|
54
|
+
player.body.velocityZ = speed;
|
|
55
|
+
|
|
56
|
+
// Lane movement (X axis) — directly set X from lane offset
|
|
57
|
+
player.body.x = player.lane.laneOffset;
|
|
58
|
+
|
|
59
|
+
// Apply forward movement
|
|
60
|
+
player.body.z += player.body.velocityZ * dt;
|
|
61
|
+
|
|
62
|
+
// Apply glide and low-gravity powerup gravity modifiers
|
|
63
|
+
const glideMultiplier = getGlideGravityMultiplier(player.body, player.isGliding, effectiveConfig);
|
|
64
|
+
const gravityPowerupMultiplier = player.gravityModifier ? player.gravityModifier.multiplier : 1;
|
|
65
|
+
const totalGravityMultiplier = glideMultiplier * gravityPowerupMultiplier;
|
|
66
|
+
const physicsConfig = totalGravityMultiplier < 1
|
|
67
|
+
? { ...RUNNER_PHYSICS_CONFIG, gravity: RUNNER_PHYSICS_CONFIG.gravity * totalGravityMultiplier }
|
|
68
|
+
: RUNNER_PHYSICS_CONFIG;
|
|
69
|
+
|
|
70
|
+
// Ground height based on track
|
|
71
|
+
const groundHeightFn = (x: number, z: number) =>
|
|
72
|
+
getTrackGroundHeight(player.track, x, z);
|
|
73
|
+
|
|
74
|
+
// Update vertical physics (gravity, ground snap)
|
|
75
|
+
updateJumpState(player.jumpState, player.body, dt);
|
|
76
|
+
updatePhysicsBody(player.body, dt, groundHeightFn, physicsConfig);
|
|
77
|
+
|
|
78
|
+
// Update distance ran
|
|
79
|
+
player.distanceRan += speed * dt;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track streaming system — generates segments ahead, disposes behind.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/experience/types.js';
|
|
6
|
+
import type { RunnerSession } from '../index.js';
|
|
7
|
+
import { updateTrack } from '../track/trackGenerator.js';
|
|
8
|
+
|
|
9
|
+
export class TrackStreamSystem implements ExperienceSystem {
|
|
10
|
+
readonly id = 'runner-track-stream';
|
|
11
|
+
|
|
12
|
+
update(session: unknown, dt: number, context: SystemContext): void {
|
|
13
|
+
const s = session as RunnerSession;
|
|
14
|
+
for (const player of s.players.values()) {
|
|
15
|
+
if (player.dead) continue;
|
|
16
|
+
updateTrack(player.track, player.body.z, 200, 50);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3-lane switching system for the Runner experience.
|
|
3
|
+
* Smooth interpolation between lanes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Lane = -1 | 0 | 1; // left, center, right
|
|
7
|
+
|
|
8
|
+
export interface LaneState {
|
|
9
|
+
currentLane: Lane;
|
|
10
|
+
targetLane: Lane;
|
|
11
|
+
/** Current X offset (smooth interpolation) */
|
|
12
|
+
laneOffset: number;
|
|
13
|
+
/** Time remaining in lane switch animation */
|
|
14
|
+
switchTimer: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Distance between lane centers in world units */
|
|
18
|
+
export const LANE_WIDTH = 2;
|
|
19
|
+
|
|
20
|
+
/** Duration of a lane switch in seconds */
|
|
21
|
+
export const LANE_SWITCH_TIME = 0.15;
|
|
22
|
+
|
|
23
|
+
export function createLaneState(): LaneState {
|
|
24
|
+
return { currentLane: 0, targetLane: 0, laneOffset: 0, switchTimer: 0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Request a lane switch. Direction: -1 = left, 1 = right.
|
|
29
|
+
* Clamped to valid lane bounds (-1 to 1).
|
|
30
|
+
*/
|
|
31
|
+
export function requestLaneSwitch(state: LaneState, direction: -1 | 1): void {
|
|
32
|
+
const newLane = Math.max(-1, Math.min(1, state.currentLane + direction)) as Lane;
|
|
33
|
+
if (newLane !== state.currentLane) {
|
|
34
|
+
state.targetLane = newLane;
|
|
35
|
+
state.switchTimer = LANE_SWITCH_TIME;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update lane interpolation. Call every tick.
|
|
41
|
+
*/
|
|
42
|
+
export function updateLane(state: LaneState, dt: number): void {
|
|
43
|
+
if (state.switchTimer > 0) {
|
|
44
|
+
state.switchTimer -= dt;
|
|
45
|
+
if (state.switchTimer <= 0) {
|
|
46
|
+
state.currentLane = state.targetLane;
|
|
47
|
+
state.switchTimer = 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Smooth interpolation toward target lane position
|
|
51
|
+
const targetX = state.targetLane * LANE_WIDTH;
|
|
52
|
+
state.laneOffset += (targetX - state.laneOffset) * Math.min(1, dt * 15);
|
|
53
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment type definitions for the Runner track generator.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type SegmentType = 'straight' | 'gap' | 'ramp' | 'narrow-left' | 'narrow-right' | 'narrow-center';
|
|
6
|
+
|
|
7
|
+
export type CollectibleType = 'coin' | 'gem';
|
|
8
|
+
export type PowerupType = 'speed-boost' | 'low-gravity' | 'magnet';
|
|
9
|
+
export type ObstacleType = 'wall' | 'low-barrier' | 'high-barrier';
|
|
10
|
+
|
|
11
|
+
export type Lane = -1 | 0 | 1;
|
|
12
|
+
|
|
13
|
+
export interface SegmentCollectible {
|
|
14
|
+
lane: Lane;
|
|
15
|
+
/** Offset from segment start along Z (0-1 fraction of length) */
|
|
16
|
+
offset: number;
|
|
17
|
+
type: CollectibleType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SegmentPowerup {
|
|
21
|
+
lane: Lane;
|
|
22
|
+
offset: number;
|
|
23
|
+
type: PowerupType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SegmentObstacle {
|
|
27
|
+
lane: Lane;
|
|
28
|
+
offset: number;
|
|
29
|
+
type: ObstacleType;
|
|
30
|
+
/** Height of obstacle (for low-barrier/high-barrier checks) */
|
|
31
|
+
height: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SegmentDef {
|
|
35
|
+
type: SegmentType;
|
|
36
|
+
/** Segment length in units along Z axis */
|
|
37
|
+
length: number;
|
|
38
|
+
/** Which lanes exist: [left, center, right] */
|
|
39
|
+
lanes: [boolean, boolean, boolean];
|
|
40
|
+
/** Ramp upward angle in radians (only for ramp type) */
|
|
41
|
+
rampAngle?: number;
|
|
42
|
+
/** Gap length within the segment (only for gap type) */
|
|
43
|
+
gapLength?: number;
|
|
44
|
+
/** Height of the ramp peak (only for ramp type) */
|
|
45
|
+
rampHeight?: number;
|
|
46
|
+
/** Collectibles placed on this segment */
|
|
47
|
+
collectibles?: SegmentCollectible[];
|
|
48
|
+
/** Powerup on this segment (at most one) */
|
|
49
|
+
powerup?: SegmentPowerup;
|
|
50
|
+
/** Obstacles on this segment */
|
|
51
|
+
obstacles?: SegmentObstacle[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PlacedCollectible {
|
|
55
|
+
id: number;
|
|
56
|
+
type: CollectibleType;
|
|
57
|
+
lane: Lane;
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
z: number;
|
|
61
|
+
collected: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PlacedPowerup {
|
|
65
|
+
id: number;
|
|
66
|
+
type: PowerupType;
|
|
67
|
+
lane: Lane;
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
z: number;
|
|
71
|
+
collected: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PlacedObstacle {
|
|
75
|
+
id: number;
|
|
76
|
+
type: ObstacleType;
|
|
77
|
+
lane: Lane;
|
|
78
|
+
x: number;
|
|
79
|
+
z: number;
|
|
80
|
+
height: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PlacedSegment {
|
|
84
|
+
def: SegmentDef;
|
|
85
|
+
/** World Z position where segment starts */
|
|
86
|
+
startZ: number;
|
|
87
|
+
/** startZ + length */
|
|
88
|
+
endZ: number;
|
|
89
|
+
/** Segment number (for difficulty calculation) */
|
|
90
|
+
index: number;
|
|
91
|
+
/** Runtime: placed collectibles with world positions */
|
|
92
|
+
collectibles: PlacedCollectible[];
|
|
93
|
+
/** Runtime: placed powerup with world position */
|
|
94
|
+
powerup: PlacedPowerup | null;
|
|
95
|
+
/** Runtime: placed obstacles with world positions */
|
|
96
|
+
obstacles: PlacedObstacle[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** All 3 lanes exist */
|
|
100
|
+
export const STRAIGHT_SEGMENT: SegmentDef = {
|
|
101
|
+
type: 'straight',
|
|
102
|
+
length: 20,
|
|
103
|
+
lanes: [true, true, true],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Gap in the middle — must jump */
|
|
107
|
+
export const GAP_SEGMENT: SegmentDef = {
|
|
108
|
+
type: 'gap',
|
|
109
|
+
length: 20,
|
|
110
|
+
lanes: [true, true, true],
|
|
111
|
+
gapLength: 6,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Ramp launches player upward */
|
|
115
|
+
export const RAMP_SEGMENT: SegmentDef = {
|
|
116
|
+
type: 'ramp',
|
|
117
|
+
length: 20,
|
|
118
|
+
lanes: [true, true, true],
|
|
119
|
+
rampHeight: 4,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** Only center and right lanes */
|
|
123
|
+
export const NARROW_LEFT_SEGMENT: SegmentDef = {
|
|
124
|
+
type: 'narrow-left',
|
|
125
|
+
length: 15,
|
|
126
|
+
lanes: [false, true, true],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Only left and center lanes */
|
|
130
|
+
export const NARROW_RIGHT_SEGMENT: SegmentDef = {
|
|
131
|
+
type: 'narrow-right',
|
|
132
|
+
length: 15,
|
|
133
|
+
lanes: [true, true, false],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** Only center lane */
|
|
137
|
+
export const NARROW_CENTER_SEGMENT: SegmentDef = {
|
|
138
|
+
type: 'narrow-center',
|
|
139
|
+
length: 12,
|
|
140
|
+
lanes: [false, true, false],
|
|
141
|
+
};
|