@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diablo Experience — wraps the existing Diablo-like game as an ExperienceDefinition.
|
|
3
|
+
*
|
|
4
|
+
* Each area (overworld, dungeon) gets its own game loop with its own system set.
|
|
5
|
+
* This module provides:
|
|
6
|
+
* - DiabloExperience: the ExperienceDefinition for hub registration
|
|
7
|
+
* - createDiabloAreaSystems(): creates the system array for a specific area
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExperienceDefinition, ExperienceSystem, PhysicsConfig, JumpConfig } from '../../../packages/core/src/index.js';
|
|
11
|
+
import { DEFAULT_PHYSICS_CONFIG } from '../../../packages/core/src/index.js';
|
|
12
|
+
import type { AreaManager, AreaSession } from '../../server/src/areaManager.js';
|
|
13
|
+
import { MovementSystem } from './systems/movementSystem.js';
|
|
14
|
+
import { EnemyAISystem } from './systems/enemyAISystem.js';
|
|
15
|
+
import { ItemPickupSystem } from './systems/itemPickupSystem.js';
|
|
16
|
+
import { PhysicsSystem, type PhysicsSystemConfig } from './systems/physicsSystem.js';
|
|
17
|
+
import { TransitionSystem, type TransitionSystemConfig } from './systems/transitionSystem.js';
|
|
18
|
+
import { DungeonClearSystem } from './systems/dungeonClearSystem.js';
|
|
19
|
+
import { EntitySyncSystem } from './systems/entitySyncSystem.js';
|
|
20
|
+
|
|
21
|
+
interface TransitionDef {
|
|
22
|
+
cellType?: string;
|
|
23
|
+
tileType?: string;
|
|
24
|
+
targetArea: string;
|
|
25
|
+
spawnX: number;
|
|
26
|
+
spawnY: number;
|
|
27
|
+
minX?: number;
|
|
28
|
+
maxX?: number;
|
|
29
|
+
minY?: number;
|
|
30
|
+
maxY?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiabloAreaSystemsConfig {
|
|
34
|
+
areaId: string;
|
|
35
|
+
areaSession: AreaSession;
|
|
36
|
+
areaManager: AreaManager;
|
|
37
|
+
physicsConfig: PhysicsConfig;
|
|
38
|
+
jumpConfig: JumpConfig;
|
|
39
|
+
transitions: TransitionDef[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create the ordered system array for a Diablo area.
|
|
44
|
+
* Each area gets its own set of systems — the order matches the original game loop.
|
|
45
|
+
*/
|
|
46
|
+
export function createDiabloAreaSystems(config: DiabloAreaSystemsConfig): ExperienceSystem[] {
|
|
47
|
+
const { areaId, areaSession, areaManager, physicsConfig, jumpConfig, transitions } = config;
|
|
48
|
+
|
|
49
|
+
const physicsSystem = new PhysicsSystem({
|
|
50
|
+
areaType: areaSession.areaType as 'voxel' | 'dungeon' | 'room',
|
|
51
|
+
areaId,
|
|
52
|
+
physicsConfig,
|
|
53
|
+
jumpConfig,
|
|
54
|
+
chunkCache: areaSession.chunkCache,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
new MovementSystem(),
|
|
59
|
+
new EnemyAISystem(),
|
|
60
|
+
new ItemPickupSystem(),
|
|
61
|
+
physicsSystem,
|
|
62
|
+
new TransitionSystem({
|
|
63
|
+
areaId,
|
|
64
|
+
areaType: areaSession.areaType as 'voxel' | 'dungeon' | 'room',
|
|
65
|
+
transitions,
|
|
66
|
+
areaManager,
|
|
67
|
+
}),
|
|
68
|
+
new DungeonClearSystem({ areaId, areaManager }),
|
|
69
|
+
new EntitySyncSystem({
|
|
70
|
+
areaType: areaSession.areaType as 'voxel' | 'dungeon' | 'room',
|
|
71
|
+
chunkCache: areaSession.chunkCache,
|
|
72
|
+
physicsSystem,
|
|
73
|
+
}),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* DiabloExperience definition — used for hub registration.
|
|
79
|
+
* The actual systems are created per-area via createDiabloAreaSystems().
|
|
80
|
+
* createSystems() returns an empty list because the Diablo experience
|
|
81
|
+
* uses multiple areas, each with its own game loop and system set.
|
|
82
|
+
*/
|
|
83
|
+
export class DiabloExperience implements ExperienceDefinition {
|
|
84
|
+
readonly id = 'diablo';
|
|
85
|
+
readonly name = 'Diablo — Overworld & Dungeon';
|
|
86
|
+
readonly description = 'Explore the voxel overworld, enter dungeons, fight enemies, collect loot.';
|
|
87
|
+
readonly defaultCameraMode = 'orbit' as const;
|
|
88
|
+
readonly physicsConfig = DEFAULT_PHYSICS_CONFIG;
|
|
89
|
+
|
|
90
|
+
createSystems(): ExperienceSystem[] {
|
|
91
|
+
// Systems are created per-area via createDiabloAreaSystems()
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/index.js';
|
|
2
|
+
import { isDungeonCleared } from '../../../../packages/core/src/index.js';
|
|
3
|
+
import type { GameSession } from '../../../../packages/server/src/index.js';
|
|
4
|
+
import type { Server } from 'socket.io';
|
|
5
|
+
import type { AreaManager } from '../../../server/src/areaManager.js';
|
|
6
|
+
|
|
7
|
+
export interface DungeonClearSystemConfig {
|
|
8
|
+
areaId: string;
|
|
9
|
+
areaManager: AreaManager;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks dungeon clearance and triggers countdown + teleport to overworld.
|
|
14
|
+
* Only active for the dungeon area.
|
|
15
|
+
*/
|
|
16
|
+
export class DungeonClearSystem implements ExperienceSystem {
|
|
17
|
+
readonly id = 'diablo-dungeon-clear';
|
|
18
|
+
private config: DungeonClearSystemConfig;
|
|
19
|
+
|
|
20
|
+
constructor(config: DungeonClearSystemConfig) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
update(session: unknown, _dt: number, context: SystemContext): void {
|
|
25
|
+
const sess = session as GameSession;
|
|
26
|
+
const io = context.io as Server;
|
|
27
|
+
const { areaId, areaManager } = this.config;
|
|
28
|
+
|
|
29
|
+
if (areaId !== 'dungeon') return;
|
|
30
|
+
if (sess.state.enemies.length === 0 || sess.clearedAt) return;
|
|
31
|
+
|
|
32
|
+
if (!isDungeonCleared(sess.state)) return;
|
|
33
|
+
|
|
34
|
+
sess.clearedAt = Date.now();
|
|
35
|
+
for (const player of sess.state.players.values()) {
|
|
36
|
+
io.sockets.sockets.get(player.socketId)?.emit('message', {
|
|
37
|
+
type: 'DUNGEON_CLEAR',
|
|
38
|
+
countdownSeconds: 10,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let remaining = 10;
|
|
43
|
+
const countdown = setInterval(() => {
|
|
44
|
+
remaining--;
|
|
45
|
+
for (const player of sess.state.players.values()) {
|
|
46
|
+
io.sockets.sockets.get(player.socketId)?.emit('message', {
|
|
47
|
+
type: 'COUNTDOWN_TICK',
|
|
48
|
+
secondsRemaining: remaining,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (remaining <= 0) {
|
|
52
|
+
clearInterval(countdown);
|
|
53
|
+
const playerIds = [...sess.state.players.keys()];
|
|
54
|
+
for (const socketId of playerIds) {
|
|
55
|
+
areaManager.movePlayer(io, socketId, 'overworld', -1, -1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, 1000);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/index.js';
|
|
2
|
+
import type { GameSession } from '../../../../packages/server/src/index.js';
|
|
3
|
+
import { updateEnemyAI } from '../../../server/src/systems/enemyAI.js';
|
|
4
|
+
|
|
5
|
+
export class EnemyAISystem implements ExperienceSystem {
|
|
6
|
+
readonly id = 'diablo-enemy-ai';
|
|
7
|
+
|
|
8
|
+
update(session: unknown, dt: number, _context: SystemContext): void {
|
|
9
|
+
updateEnemyAI(session as GameSession, dt);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/index.js';
|
|
2
|
+
import { getTerrainHeight } from '../../../../packages/core/src/index.js';
|
|
3
|
+
import type { GameSession } from '../../../../packages/server/src/index.js';
|
|
4
|
+
import type { Server } from 'socket.io';
|
|
5
|
+
import type { VoxelPhysicsMap } from '../../../server/src/voxelPlayerState.js';
|
|
6
|
+
import type { VoxelChunkCache } from '../../../server/src/voxelChunkCache.js';
|
|
7
|
+
import type { PhysicsSystem } from './physicsSystem.js';
|
|
8
|
+
|
|
9
|
+
export interface EntitySyncSystemConfig {
|
|
10
|
+
areaType: 'voxel' | 'dungeon' | 'room';
|
|
11
|
+
chunkCache?: VoxelChunkCache;
|
|
12
|
+
/** Reference to the physics system to get the physics map. */
|
|
13
|
+
physicsSystem?: PhysicsSystem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Broadcasts entity snapshots to all players in the session.
|
|
18
|
+
* Overrides Y positions with physics body data for areas with physics.
|
|
19
|
+
*/
|
|
20
|
+
export class EntitySyncSystem implements ExperienceSystem {
|
|
21
|
+
readonly id = 'diablo-entity-sync';
|
|
22
|
+
private config: EntitySyncSystemConfig;
|
|
23
|
+
|
|
24
|
+
constructor(config: EntitySyncSystemConfig) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update(session: unknown, _dt: number, context: SystemContext): void {
|
|
29
|
+
const sess = session as GameSession;
|
|
30
|
+
const io = context.io as Server;
|
|
31
|
+
const { areaType, chunkCache, physicsSystem } = this.config;
|
|
32
|
+
const tick = context.tick;
|
|
33
|
+
|
|
34
|
+
const runPhysics = areaType === 'voxel' || areaType === 'dungeon';
|
|
35
|
+
const physicsMap = physicsSystem?.getPhysicsMap();
|
|
36
|
+
let entities = sess.getEntitySnapshots();
|
|
37
|
+
|
|
38
|
+
// For areas with physics, override positions with PhysicsBody data
|
|
39
|
+
if (runPhysics && physicsMap) {
|
|
40
|
+
const isVoxelArea = areaType === 'voxel';
|
|
41
|
+
const getGroundYForSync = isVoxelArea && chunkCache
|
|
42
|
+
? (x: number, z: number) => getTerrainHeight(chunkCache.getChunkFn, x, z) ?? 0
|
|
43
|
+
: null;
|
|
44
|
+
|
|
45
|
+
entities = entities.map(e => {
|
|
46
|
+
// Players: use physics body Y
|
|
47
|
+
for (const [socketId, physics] of physicsMap) {
|
|
48
|
+
const p = sess.state.players.get(socketId);
|
|
49
|
+
if (p && p.entityId === e.entityId) {
|
|
50
|
+
return { ...e, x: physics.body.x, y: physics.body.z, z: physics.body.z, worldY: physics.body.y };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Non-player entities in voxel areas: compute terrain height
|
|
54
|
+
if (getGroundYForSync && e.type !== 'player') {
|
|
55
|
+
return { ...e, worldY: getGroundYForSync(e.x, e.y) };
|
|
56
|
+
}
|
|
57
|
+
return e;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Emit to players in this session
|
|
62
|
+
for (const player of sess.state.players.values()) {
|
|
63
|
+
const sock = io.sockets.sockets.get(player.socketId);
|
|
64
|
+
sock?.emit('message', { type: 'ENTITY_SYNC', entities, tick });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Per-socket INPUT_ACK so each WASD client can reconcile predictions
|
|
68
|
+
for (const player of sess.state.players.values()) {
|
|
69
|
+
if (player.lastProcessedInputId >= 0) {
|
|
70
|
+
const sock = io.sockets.sockets.get(player.socketId);
|
|
71
|
+
sock?.emit('message', {
|
|
72
|
+
type: 'INPUT_ACK',
|
|
73
|
+
lastProcessedInputId: player.lastProcessedInputId,
|
|
74
|
+
x: player.x,
|
|
75
|
+
y: player.y,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/index.js';
|
|
2
|
+
import type { GameSession } from '../../../../packages/server/src/index.js';
|
|
3
|
+
import { updateItemPickup } from '../../../server/src/systems/itemPickup.js';
|
|
4
|
+
|
|
5
|
+
export class ItemPickupSystem implements ExperienceSystem {
|
|
6
|
+
readonly id = 'diablo-item-pickup';
|
|
7
|
+
|
|
8
|
+
update(session: unknown, _dt: number, _context: SystemContext): void {
|
|
9
|
+
updateItemPickup(session as GameSession);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/index.js';
|
|
2
|
+
import type { GameSession } from '../../../../packages/server/src/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps session.updateMovement() — moves players along their path each tick.
|
|
6
|
+
*/
|
|
7
|
+
export class MovementSystem implements ExperienceSystem {
|
|
8
|
+
readonly id = 'diablo-movement';
|
|
9
|
+
|
|
10
|
+
update(session: unknown, dt: number, _context: SystemContext): void {
|
|
11
|
+
(session as GameSession).updateMovement(dt);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ExperienceSystem, SystemContext, PhysicsConfig, JumpConfig } from '../../../../packages/core/src/index.js';
|
|
2
|
+
import { getTerrainHeight, updatePhysicsBody, updateJumpState, snapToGround, consumeJumpBuffer } from '../../../../packages/core/src/index.js';
|
|
3
|
+
import type { GameSession } from '../../../../packages/server/src/index.js';
|
|
4
|
+
import { createVoxelPlayerPhysics, type VoxelPhysicsMap, voxelPhysicsRegistry } from '../../../server/src/voxelPlayerState.js';
|
|
5
|
+
import type { VoxelChunkCache } from '../../../server/src/voxelChunkCache.js';
|
|
6
|
+
import { getDungeonGroundY } from '../../../server/src/systems/dungeonPhysics.js';
|
|
7
|
+
|
|
8
|
+
export interface PhysicsSystemConfig {
|
|
9
|
+
areaType: 'voxel' | 'dungeon' | 'room';
|
|
10
|
+
areaId: string;
|
|
11
|
+
physicsConfig: PhysicsConfig;
|
|
12
|
+
jumpConfig: JumpConfig;
|
|
13
|
+
chunkCache?: VoxelChunkCache;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps the physics tick from the monolithic game loop.
|
|
18
|
+
* Handles both voxel and dungeon areas depending on config.
|
|
19
|
+
*/
|
|
20
|
+
export class PhysicsSystem implements ExperienceSystem {
|
|
21
|
+
readonly id = 'diablo-physics';
|
|
22
|
+
private physicsMap: VoxelPhysicsMap = new Map();
|
|
23
|
+
private config: PhysicsSystemConfig;
|
|
24
|
+
|
|
25
|
+
constructor(config: PhysicsSystemConfig) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
init(_session: unknown, _context: SystemContext): void {
|
|
30
|
+
const runPhysics = this.config.areaType === 'voxel' || this.config.areaType === 'dungeon';
|
|
31
|
+
if (runPhysics) {
|
|
32
|
+
voxelPhysicsRegistry.set(this.config.areaId, this.physicsMap);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
update(session: unknown, dt: number, _context: SystemContext): void {
|
|
37
|
+
const sess = session as GameSession;
|
|
38
|
+
const { areaType, physicsConfig, jumpConfig, chunkCache } = this.config;
|
|
39
|
+
|
|
40
|
+
if (areaType !== 'voxel' && areaType !== 'dungeon') return;
|
|
41
|
+
|
|
42
|
+
const isVoxelArea = areaType === 'voxel';
|
|
43
|
+
const getGroundY = isVoxelArea && chunkCache
|
|
44
|
+
? (x: number, z: number) => getTerrainHeight(chunkCache.getChunkFn, x, z)
|
|
45
|
+
: (x: number, z: number) => getDungeonGroundY(sess, x, z);
|
|
46
|
+
|
|
47
|
+
for (const player of sess.state.players.values()) {
|
|
48
|
+
// Ensure physics state exists
|
|
49
|
+
if (!this.physicsMap.has(player.socketId)) {
|
|
50
|
+
const spawnHeight = getGroundY(player.x, player.y) ?? 32;
|
|
51
|
+
this.physicsMap.set(player.socketId, createVoxelPlayerPhysics(player.x, spawnHeight, player.y));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const physics = this.physicsMap.get(player.socketId)!;
|
|
55
|
+
|
|
56
|
+
// Sync horizontal position from PLAYER_MOVE_DIRECT
|
|
57
|
+
physics.body.x = player.x;
|
|
58
|
+
physics.body.z = player.y;
|
|
59
|
+
|
|
60
|
+
// Update jump timers
|
|
61
|
+
updateJumpState(physics.jumpState, physics.body, dt);
|
|
62
|
+
|
|
63
|
+
// Apply gravity + ground check
|
|
64
|
+
updatePhysicsBody(physics.body, dt, getGroundY, physicsConfig);
|
|
65
|
+
|
|
66
|
+
// Auto-jump if buffered and just landed
|
|
67
|
+
consumeJumpBuffer(physics.body, physics.jumpState, jumpConfig);
|
|
68
|
+
|
|
69
|
+
// Snap grounded entities to terrain after horizontal movement
|
|
70
|
+
if (physics.body.isGrounded) {
|
|
71
|
+
snapToGround(physics.body, getGroundY, physicsConfig);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Clean up physics for disconnected players
|
|
76
|
+
for (const socketId of this.physicsMap.keys()) {
|
|
77
|
+
if (!sess.state.players.has(socketId)) {
|
|
78
|
+
this.physicsMap.delete(socketId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cleanup(_session: unknown): void {
|
|
84
|
+
voxelPhysicsRegistry.delete(this.config.areaId);
|
|
85
|
+
this.physicsMap.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Access the physics map (needed by entity sync and other systems). */
|
|
89
|
+
getPhysicsMap(): VoxelPhysicsMap {
|
|
90
|
+
return this.physicsMap;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ExperienceSystem, SystemContext } from '../../../../packages/core/src/index.js';
|
|
2
|
+
import type { GameSession } from '../../../../packages/server/src/index.js';
|
|
3
|
+
import type { Server } from 'socket.io';
|
|
4
|
+
import type { AreaManager } from '../../../server/src/areaManager.js';
|
|
5
|
+
import { getOverworldTileType } from '../../../server/src/areaManager.js';
|
|
6
|
+
import { TILE_SIZE } from '../../../game-data/src/world/overworld-layout.js';
|
|
7
|
+
|
|
8
|
+
interface TransitionDef {
|
|
9
|
+
cellType?: string;
|
|
10
|
+
tileType?: string;
|
|
11
|
+
targetArea: string;
|
|
12
|
+
spawnX: number;
|
|
13
|
+
spawnY: number;
|
|
14
|
+
minX?: number;
|
|
15
|
+
maxX?: number;
|
|
16
|
+
minY?: number;
|
|
17
|
+
maxY?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TransitionSystemConfig {
|
|
21
|
+
areaId: string;
|
|
22
|
+
areaType: 'voxel' | 'dungeon' | 'room';
|
|
23
|
+
transitions: TransitionDef[];
|
|
24
|
+
areaManager: AreaManager;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detects area transitions (overworld tile-based and dungeon cell-based).
|
|
29
|
+
* Extracted from the monolithic game loop.
|
|
30
|
+
*/
|
|
31
|
+
export class TransitionSystem implements ExperienceSystem {
|
|
32
|
+
readonly id = 'diablo-transition';
|
|
33
|
+
private config: TransitionSystemConfig;
|
|
34
|
+
private transitionCooldown = new Map<string, number>();
|
|
35
|
+
|
|
36
|
+
constructor(config: TransitionSystemConfig) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
update(session: unknown, _dt: number, context: SystemContext): void {
|
|
41
|
+
const sess = session as GameSession;
|
|
42
|
+
const io = context.io as Server;
|
|
43
|
+
const { areaId, areaType, transitions, areaManager } = this.config;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const isVoxelArea = areaType === 'voxel';
|
|
46
|
+
|
|
47
|
+
if (transitions.length === 0) return;
|
|
48
|
+
|
|
49
|
+
for (const player of sess.state.players.values()) {
|
|
50
|
+
const lastTransition = this.transitionCooldown.get(player.socketId) ?? 0;
|
|
51
|
+
if (now - lastTransition < 1000) continue;
|
|
52
|
+
|
|
53
|
+
let matched = false;
|
|
54
|
+
|
|
55
|
+
// Tile-based transition (for voxel overworld)
|
|
56
|
+
if (isVoxelArea) {
|
|
57
|
+
const tile = getOverworldTileType(areaManager.layout, player.x, player.y);
|
|
58
|
+
if (tile) {
|
|
59
|
+
const tr = transitions.find(t => t.tileType && t.tileType === tile.def.type);
|
|
60
|
+
if (tr) {
|
|
61
|
+
const tileCenterX = (tile.col + 0.5) * TILE_SIZE;
|
|
62
|
+
const tileCenterZ = (tile.row + 0.5) * TILE_SIZE;
|
|
63
|
+
const dx = player.x - tileCenterX;
|
|
64
|
+
const dz = player.y - tileCenterZ;
|
|
65
|
+
const distToCenter = Math.sqrt(dx * dx + dz * dz);
|
|
66
|
+
if (distToCenter <= 4) {
|
|
67
|
+
this.transitionCooldown.set(player.socketId, now);
|
|
68
|
+
areaManager.movePlayer(io, player.socketId, tr.targetArea, tr.spawnX, tr.spawnY);
|
|
69
|
+
matched = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (matched) break;
|
|
76
|
+
|
|
77
|
+
// Cell-based transition (for dungeons with stamped cells)
|
|
78
|
+
const cx = Math.round(player.x);
|
|
79
|
+
const cy = Math.round(player.y);
|
|
80
|
+
const cell = sess.getCell(cx, cy);
|
|
81
|
+
if (cell?.type === 'transition') {
|
|
82
|
+
const tr = transitions.find(t => {
|
|
83
|
+
if (t.cellType !== 'transition' && t.cellType !== undefined) return false;
|
|
84
|
+
if (t.minX !== undefined && player.x < t.minX) return false;
|
|
85
|
+
if (t.maxX !== undefined && player.x > t.maxX) return false;
|
|
86
|
+
if (t.minY !== undefined && player.y < t.minY) return false;
|
|
87
|
+
if (t.maxY !== undefined && player.y > t.maxY) return false;
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
if (tr) {
|
|
91
|
+
this.transitionCooldown.set(player.socketId, now);
|
|
92
|
+
let sx = tr.spawnX;
|
|
93
|
+
let sy = tr.spawnY;
|
|
94
|
+
if (tr.targetArea === 'overworld' && sx < 0) {
|
|
95
|
+
const entrance = areaManager.getDungeonEntrancePos();
|
|
96
|
+
sx = entrance.x - TILE_SIZE * 0.5;
|
|
97
|
+
sy = entrance.z;
|
|
98
|
+
}
|
|
99
|
+
areaManager.movePlayer(io, player.socketId, tr.targetArea, sx, sy);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner experience configuration — speed, physics, lane settings.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CharacterConfig } from '../../../../packages/core/src/physics/characterController.js';
|
|
6
|
+
import type { PhysicsConfig } from '../../../../packages/core/src/physics/gravity.js';
|
|
7
|
+
import type { JumpConfig } from '../../../../packages/core/src/physics/jump.js';
|
|
8
|
+
|
|
9
|
+
/** Character controller config tuned for runner feel */
|
|
10
|
+
export const RUNNER_CHARACTER_CONFIG: CharacterConfig = {
|
|
11
|
+
walkSpeed: 10, // Base auto-run speed (overridden by speed curve)
|
|
12
|
+
sprintMultiplier: 1, // No manual sprint in runner
|
|
13
|
+
maxSpeed: 30, // Allows speed curve to go high
|
|
14
|
+
friction: 30, // Snappy deceleration
|
|
15
|
+
airFriction: 3, // Low air friction for glide momentum
|
|
16
|
+
airControl: 0.5, // Moderate air control for lane adjustment mid-jump
|
|
17
|
+
glideGravityMultiplier: 0.25, // Reduced gravity while gliding
|
|
18
|
+
glideFriction: 1, // Very low friction while gliding
|
|
19
|
+
acceleration: 50, // Fast acceleration (auto-run, not manual)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Physics config for runner — slightly reduced gravity for more floaty jumps */
|
|
23
|
+
export const RUNNER_PHYSICS_CONFIG: PhysicsConfig = {
|
|
24
|
+
gravity: -18,
|
|
25
|
+
terminalVelocity: -40,
|
|
26
|
+
groundSnapThreshold: 0.5,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Jump config for runner — generous coyote time, no double jump (glide instead) */
|
|
30
|
+
export const RUNNER_JUMP_CONFIG: JumpConfig = {
|
|
31
|
+
jumpVelocity: 10,
|
|
32
|
+
coyoteTime: 0.15,
|
|
33
|
+
jumpBuffer: 0.15,
|
|
34
|
+
allowDoubleJump: false,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Base speed at distance 0 */
|
|
38
|
+
export const BASE_SPEED = 10;
|
|
39
|
+
|
|
40
|
+
/** Maximum speed cap */
|
|
41
|
+
export const MAX_SPEED = 25;
|
|
42
|
+
|
|
43
|
+
/** How fast speed increases with distance */
|
|
44
|
+
export const SPEED_RAMP_RATE = 0.002;
|
|
45
|
+
|
|
46
|
+
/** Y threshold below which player is considered dead (fell off track) */
|
|
47
|
+
export const DEATH_Y_THRESHOLD = -10;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Speed increases linearly with distance, capped at MAX_SPEED.
|
|
51
|
+
*/
|
|
52
|
+
export function getSpeedForDistance(distance: number): number {
|
|
53
|
+
return Math.min(BASE_SPEED + distance * SPEED_RAMP_RATE, MAX_SPEED);
|
|
54
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner Experience — Infinity Runner with procedural track generation.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - RunnerExperience: ExperienceDefinition for hub registration
|
|
6
|
+
* - RunnerSession: custom session state for runner gameplay
|
|
7
|
+
* - RunnerPlayer: per-player state including physics body, lane, track
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExperienceDefinition, ExperienceSystem, PhysicsBody } from '../../../packages/core/src/index.js';
|
|
11
|
+
import { createPhysicsBody } from '../../../packages/core/src/index.js';
|
|
12
|
+
import type { CharacterModifier } from '../../../packages/core/src/physics/modifiers.js';
|
|
13
|
+
import type { TrackGeneratorState } from './track/trackGenerator.js';
|
|
14
|
+
import { createTrackGenerator } from './track/trackGenerator.js';
|
|
15
|
+
import type { LaneState } from './track/laneSystem.js';
|
|
16
|
+
import { createLaneState } from './track/laneSystem.js';
|
|
17
|
+
import type { JumpState } from '../../../packages/core/src/physics/jump.js';
|
|
18
|
+
import { createJumpState } from '../../../packages/core/src/physics/jump.js';
|
|
19
|
+
import { RUNNER_PHYSICS_CONFIG } from './data/runner-config.js';
|
|
20
|
+
import { RunnerPhysicsSystem } from './systems/runnerPhysicsSystem.js';
|
|
21
|
+
import { TrackStreamSystem } from './systems/trackStreamSystem.js';
|
|
22
|
+
import { DeathSystem } from './systems/deathSystem.js';
|
|
23
|
+
import { CollectibleSystem } from './systems/collectibleSystem.js';
|
|
24
|
+
import { ObstacleSystem } from './systems/obstacleSystem.js';
|
|
25
|
+
import { RunnerEntitySyncSystem } from './systems/entitySyncSystem.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Runner-specific session & player state
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export interface ComboState {
|
|
32
|
+
count: number;
|
|
33
|
+
/** Time remaining before combo resets (seconds) */
|
|
34
|
+
timer: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RunnerPlayer {
|
|
38
|
+
socketId: string;
|
|
39
|
+
playerId: string;
|
|
40
|
+
entityId: number;
|
|
41
|
+
body: PhysicsBody;
|
|
42
|
+
jumpState: JumpState;
|
|
43
|
+
lane: LaneState;
|
|
44
|
+
track: TrackGeneratorState;
|
|
45
|
+
distanceRan: number;
|
|
46
|
+
isGliding: boolean;
|
|
47
|
+
dead: boolean;
|
|
48
|
+
/** Input flags from client */
|
|
49
|
+
input: {
|
|
50
|
+
left: boolean;
|
|
51
|
+
right: boolean;
|
|
52
|
+
jump: boolean;
|
|
53
|
+
glide: boolean;
|
|
54
|
+
};
|
|
55
|
+
/** Score state */
|
|
56
|
+
score: number;
|
|
57
|
+
coins: number;
|
|
58
|
+
gems: number;
|
|
59
|
+
bestCombo: number;
|
|
60
|
+
combo: ComboState;
|
|
61
|
+
/** Active character modifiers from powerups */
|
|
62
|
+
modifiers: CharacterModifier[];
|
|
63
|
+
/** Magnet powerup state */
|
|
64
|
+
magnetActive: boolean;
|
|
65
|
+
magnetTimer: number;
|
|
66
|
+
/** Gravity modifier from low-gravity powerup */
|
|
67
|
+
gravityModifier: { multiplier: number; remainingTime: number } | null;
|
|
68
|
+
/** Session high score */
|
|
69
|
+
highScore: number;
|
|
70
|
+
/** Next unique ID for collectibles/obstacles */
|
|
71
|
+
nextItemId: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RunnerSession {
|
|
75
|
+
seed: number;
|
|
76
|
+
players: Map<string, RunnerPlayer>;
|
|
77
|
+
nextEntityId: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createRunnerSession(seed: number): RunnerSession {
|
|
81
|
+
return {
|
|
82
|
+
seed,
|
|
83
|
+
players: new Map(),
|
|
84
|
+
nextEntityId: 1,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function addRunnerPlayer(session: RunnerSession, socketId: string, playerId: string): RunnerPlayer {
|
|
89
|
+
const entityId = session.nextEntityId++;
|
|
90
|
+
const player: RunnerPlayer = {
|
|
91
|
+
socketId,
|
|
92
|
+
playerId,
|
|
93
|
+
entityId,
|
|
94
|
+
body: createPhysicsBody(0, 0, 0), // Start at origin, center lane
|
|
95
|
+
jumpState: createJumpState(),
|
|
96
|
+
lane: createLaneState(),
|
|
97
|
+
track: createTrackGenerator(session.seed),
|
|
98
|
+
distanceRan: 0,
|
|
99
|
+
isGliding: false,
|
|
100
|
+
dead: false,
|
|
101
|
+
input: { left: false, right: false, jump: false, glide: false },
|
|
102
|
+
score: 0,
|
|
103
|
+
coins: 0,
|
|
104
|
+
gems: 0,
|
|
105
|
+
bestCombo: 0,
|
|
106
|
+
combo: { count: 0, timer: 0 },
|
|
107
|
+
modifiers: [],
|
|
108
|
+
magnetActive: false,
|
|
109
|
+
magnetTimer: 0,
|
|
110
|
+
gravityModifier: null,
|
|
111
|
+
highScore: 0,
|
|
112
|
+
nextItemId: 1,
|
|
113
|
+
};
|
|
114
|
+
session.players.set(socketId, player);
|
|
115
|
+
return player;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function removeRunnerPlayer(session: RunnerSession, socketId: string): void {
|
|
119
|
+
session.players.delete(socketId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Experience Definition
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
export class RunnerExperience implements ExperienceDefinition {
|
|
127
|
+
readonly id = 'runner';
|
|
128
|
+
readonly name = 'Infinity Runner';
|
|
129
|
+
readonly description = 'Run endlessly on a procedural track. Jump, glide, and dodge obstacles.';
|
|
130
|
+
readonly defaultCameraMode = 'follow' as const;
|
|
131
|
+
readonly physicsConfig = RUNNER_PHYSICS_CONFIG;
|
|
132
|
+
|
|
133
|
+
createSystems(): ExperienceSystem[] {
|
|
134
|
+
return [
|
|
135
|
+
new TrackStreamSystem(),
|
|
136
|
+
new RunnerPhysicsSystem(),
|
|
137
|
+
new CollectibleSystem(),
|
|
138
|
+
new ObstacleSystem(),
|
|
139
|
+
new DeathSystem(),
|
|
140
|
+
new RunnerEntitySyncSystem(),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
}
|