@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,597 @@
|
|
|
1
|
+
import { createGameServer, ExperienceManager, createExperienceGameLoop, createTickLoop } from '@loonylabs/gamedev-server';
|
|
2
|
+
import { PlayerMoveSchema, PlayerMoveDirectSchema, ActionEventSchema, PlayerConnectSchema, CraftRequestSchema, EquipRequestSchema, UnequipRequestSchema, RequestNewDungeonSchema, ActionRaycastSchema, DebugSpawnSchema, UseSkillSchema, PlayerJumpSchema, ExperienceJoinSchema, ExperienceLeaveSchema, ShootIntentSchema, ReloadSchema, ShooterMoveSchema, ShooterJumpSchema, ShooterRestartSchema, RunnerInputSchema, RunnerRestartSchema } from '@loonylabs/gamedev-protocol';
|
|
3
|
+
import { AreaManager } from './areaManager.js';
|
|
4
|
+
import { loadRooms } from './rooms.js';
|
|
5
|
+
import { db } from './db/client.js';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { resolve, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { handleMeleeAction } from './handlers/actionEvent.js';
|
|
10
|
+
import { handleRaycastAction } from './handlers/raycastHandler.js';
|
|
11
|
+
import { handleUseSkill } from './handlers/skillHandler.js';
|
|
12
|
+
import { handleCraftRequest } from './handlers/craftHandler.js';
|
|
13
|
+
import { handleEquipRequest, handleUnequipRequest } from './handlers/equipHandler.js';
|
|
14
|
+
import { loadPlayerState, savePlayerState, toInventoryItems } from './persistence.js';
|
|
15
|
+
import type { Recipe } from '@loonylabs/gamedev-core';
|
|
16
|
+
import { normaliseSkillBook, tryJump, getTerrainHeight, createJumpState, createPhysicsBody } from '@loonylabs/gamedev-core';
|
|
17
|
+
import type { SkillBook } from '@loonylabs/gamedev-core';
|
|
18
|
+
import { voxelPhysicsRegistry } from './voxelPlayerState.js';
|
|
19
|
+
import { GAME_JUMP_CONFIG } from '../../game-data/src/physics/jump-config.js';
|
|
20
|
+
import { DiabloExperience, createDiabloAreaSystems } from '../../experiences/diablo/index.js';
|
|
21
|
+
import { ShooterExperience, createShooterSession, addShooterPlayer, removeShooterPlayer } from '../../experiences/shooter/index.js';
|
|
22
|
+
import { createWeaponState, DEFAULT_WEAPON_CONFIG } from '../../experiences/shooter/data/weapon-config.js';
|
|
23
|
+
import { SANDBOX_PHYSICS_CONFIG } from '../../game-data/src/voxel/sandbox-terrain-config.js';
|
|
24
|
+
import { DUNGEON_PHYSICS_CONFIG } from '../../game-data/src/physics/dungeon-physics-config.js';
|
|
25
|
+
import { RunnerExperience, createRunnerSession, addRunnerPlayer, removeRunnerPlayer } from '../../experiences/runner/index.js';
|
|
26
|
+
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const recipeBook: Recipe[] = JSON.parse(
|
|
29
|
+
readFileSync(resolve(__dirname, '../../game-data/src/recipes/recipe-book.json'), 'utf-8')
|
|
30
|
+
);
|
|
31
|
+
const skillBook: SkillBook = normaliseSkillBook(JSON.parse(
|
|
32
|
+
readFileSync(resolve(__dirname, '../../game-data/src/skills/skill-book.json'), 'utf-8')
|
|
33
|
+
));
|
|
34
|
+
|
|
35
|
+
const AREA_IDS = ['overworld', 'dungeon'] as const;
|
|
36
|
+
|
|
37
|
+
export interface GameServer {
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createServer(port: number, seed?: number): Promise<GameServer> {
|
|
42
|
+
const { io, addStopCallback, close } = await createGameServer({ port, cors: { origin: '*' } });
|
|
43
|
+
|
|
44
|
+
const rooms = loadRooms();
|
|
45
|
+
// db is initialized in ./db/client.ts as a singleton
|
|
46
|
+
const areaManager = new AreaManager(rooms, seed ?? Date.now());
|
|
47
|
+
|
|
48
|
+
// Register experiences
|
|
49
|
+
const experienceManager = new ExperienceManager();
|
|
50
|
+
experienceManager.register(new DiabloExperience());
|
|
51
|
+
experienceManager.register(new ShooterExperience());
|
|
52
|
+
experienceManager.register(new RunnerExperience());
|
|
53
|
+
|
|
54
|
+
// Shooter sessions — keyed by a session ID (one shared session for now)
|
|
55
|
+
const shooterSessions = new Map<string, ReturnType<typeof createShooterSession>>();
|
|
56
|
+
const playerShooterSession = new Map<string, string>(); // socketId -> sessionId
|
|
57
|
+
// Jump states for shooter players (not stored in ShooterPlayer to keep core pure)
|
|
58
|
+
const shooterJumpStates = new Map<string, ReturnType<typeof createJumpState>>();
|
|
59
|
+
|
|
60
|
+
// Runner sessions
|
|
61
|
+
const runnerSessions = new Map<string, ReturnType<typeof createRunnerSession>>();
|
|
62
|
+
const playerRunnerSession = new Map<string, string>(); // socketId -> sessionId
|
|
63
|
+
|
|
64
|
+
// Load area manifest for transitions
|
|
65
|
+
const areaManifest = JSON.parse(
|
|
66
|
+
readFileSync(resolve(__dirname, '../../game-data/src/areas/area-manifest.json'), 'utf-8')
|
|
67
|
+
);
|
|
68
|
+
const transitionsByArea = new Map<string, Array<{ cellType?: string; tileType?: string; targetArea: string; spawnX: number; spawnY: number; minX?: number; maxX?: number; minY?: number; maxY?: number }>>(
|
|
69
|
+
areaManifest.areas.map((a: { id: string; transitions: unknown[] }) => [a.id, a.transitions])
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Start game loops via experience system — one per area with extracted systems
|
|
73
|
+
for (const areaId of AREA_IDS) {
|
|
74
|
+
const areaSession = areaManager.getAreaSession(areaId);
|
|
75
|
+
if (!areaSession) continue;
|
|
76
|
+
|
|
77
|
+
const physicsConfig = areaSession.areaType === 'voxel' ? SANDBOX_PHYSICS_CONFIG : DUNGEON_PHYSICS_CONFIG;
|
|
78
|
+
const systems = createDiabloAreaSystems({
|
|
79
|
+
areaId,
|
|
80
|
+
areaSession,
|
|
81
|
+
areaManager,
|
|
82
|
+
physicsConfig,
|
|
83
|
+
jumpConfig: GAME_JUMP_CONFIG,
|
|
84
|
+
transitions: transitionsByArea.get(areaId) ?? [],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const stopLoop = createExperienceGameLoop(io, areaSession.session, systems, 'diablo');
|
|
88
|
+
addStopCallback(stopLoop);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Wire pickup events for all sessions
|
|
92
|
+
const pickupHandler = (socketId: string, inventory: Array<{ id: string; name: string; rarity: string; statBonus: Record<string, number> }>) => {
|
|
93
|
+
const sock = io.sockets.sockets.get(socketId);
|
|
94
|
+
if (sock) sock.emit('message', { type: 'INVENTORY_UPDATE', items: toInventoryItems(inventory) });
|
|
95
|
+
};
|
|
96
|
+
for (const areaId of [...AREA_IDS]) {
|
|
97
|
+
const s = areaManager.getSession(areaId);
|
|
98
|
+
if (s) s.onItemPickup = pickupHandler;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Convenience getter: current session for a socket
|
|
102
|
+
const getSession = (socketId: string) =>
|
|
103
|
+
areaManager.getSession(areaManager.getPlayerArea(socketId));
|
|
104
|
+
|
|
105
|
+
/** Join the Shooter experience. */
|
|
106
|
+
function joinShooterExperience(socket: import('socket.io').Socket) {
|
|
107
|
+
const sessionId = 'shooter-main';
|
|
108
|
+
let shooterSession = shooterSessions.get(sessionId);
|
|
109
|
+
if (!shooterSession) {
|
|
110
|
+
shooterSession = createShooterSession(Date.now());
|
|
111
|
+
shooterSessions.set(sessionId, shooterSession);
|
|
112
|
+
|
|
113
|
+
// Start the experience game loop with a lightweight GameSession wrapper
|
|
114
|
+
const shooterDef = experienceManager.getDefinition('shooter')!;
|
|
115
|
+
const systems = shooterDef.createSystems();
|
|
116
|
+
|
|
117
|
+
// Init all systems with the shooter session as the "session" object
|
|
118
|
+
for (const system of systems) {
|
|
119
|
+
system.init?.(shooterSession, { io, tick: 0, experienceId: 'shooter' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Start tick loop
|
|
123
|
+
let tick = 0;
|
|
124
|
+
const context = { io, tick: 0, experienceId: 'shooter' };
|
|
125
|
+
const stopLoop = createTickLoop(20, (dt: number) => {
|
|
126
|
+
tick++;
|
|
127
|
+
context.tick = tick;
|
|
128
|
+
for (const system of systems) {
|
|
129
|
+
system.update(shooterSession!, dt, context);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
addStopCallback(stopLoop);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const player = addShooterPlayer(shooterSession, socket.id, socket.id);
|
|
136
|
+
playerShooterSession.set(socket.id, sessionId);
|
|
137
|
+
shooterJumpStates.set(socket.id, createJumpState());
|
|
138
|
+
|
|
139
|
+
socket.emit('message', {
|
|
140
|
+
type: 'EXPERIENCE_CHANGE',
|
|
141
|
+
experienceId: 'shooter',
|
|
142
|
+
experienceName: 'Shooter Arena',
|
|
143
|
+
cameraMode: 'third-person',
|
|
144
|
+
spawnX: player.body.x,
|
|
145
|
+
spawnY: player.body.z,
|
|
146
|
+
spawnZ: player.body.y,
|
|
147
|
+
localEntityId: player.entityId,
|
|
148
|
+
seed: shooterSession.arena.seed,
|
|
149
|
+
payload: {
|
|
150
|
+
arena: {
|
|
151
|
+
width: shooterSession.arena.width,
|
|
152
|
+
depth: shooterSession.arena.depth,
|
|
153
|
+
walls: shooterSession.arena.walls,
|
|
154
|
+
covers: shooterSession.arena.covers,
|
|
155
|
+
spawnPoints: shooterSession.arena.spawnPoints,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Send initial weapon state so HUD has ammo info
|
|
161
|
+
socket.emit('message', {
|
|
162
|
+
type: 'WEAPON_STATE',
|
|
163
|
+
currentAmmo: player.weapon.currentAmmo,
|
|
164
|
+
maxAmmo: DEFAULT_WEAPON_CONFIG.maxAmmo,
|
|
165
|
+
isReloading: false,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Join the Runner experience. */
|
|
170
|
+
function joinRunnerExperience(socket: import('socket.io').Socket) {
|
|
171
|
+
const sessionId = 'runner-main';
|
|
172
|
+
let runnerSession = runnerSessions.get(sessionId);
|
|
173
|
+
if (!runnerSession) {
|
|
174
|
+
runnerSession = createRunnerSession(Date.now());
|
|
175
|
+
runnerSessions.set(sessionId, runnerSession);
|
|
176
|
+
|
|
177
|
+
// Start the experience game loop
|
|
178
|
+
const runnerDef = experienceManager.getDefinition('runner')!;
|
|
179
|
+
const systems = runnerDef.createSystems();
|
|
180
|
+
|
|
181
|
+
for (const system of systems) {
|
|
182
|
+
system.init?.(runnerSession, { io, tick: 0, experienceId: 'runner' });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let tick = 0;
|
|
186
|
+
const context = { io, tick: 0, experienceId: 'runner' };
|
|
187
|
+
const stopLoop = createTickLoop(20, (dt: number) => {
|
|
188
|
+
tick++;
|
|
189
|
+
context.tick = tick;
|
|
190
|
+
for (const system of systems) {
|
|
191
|
+
system.update(runnerSession!, dt, context);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
addStopCallback(stopLoop);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const player = addRunnerPlayer(runnerSession, socket.id, socket.id);
|
|
198
|
+
playerRunnerSession.set(socket.id, sessionId);
|
|
199
|
+
|
|
200
|
+
// Send track data in payload for client rendering
|
|
201
|
+
socket.emit('message', {
|
|
202
|
+
type: 'EXPERIENCE_CHANGE',
|
|
203
|
+
experienceId: 'runner',
|
|
204
|
+
experienceName: 'Infinity Runner',
|
|
205
|
+
cameraMode: 'follow',
|
|
206
|
+
spawnX: 0,
|
|
207
|
+
spawnY: 0,
|
|
208
|
+
spawnZ: 0,
|
|
209
|
+
localEntityId: player.entityId,
|
|
210
|
+
seed: runnerSession.seed,
|
|
211
|
+
payload: {
|
|
212
|
+
laneWidth: 2,
|
|
213
|
+
trackWidth: 6, // 3 lanes * 2 units
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Join the Diablo experience (legacy path — used by EXPERIENCE_JOIN and kept for now). */
|
|
219
|
+
function joinDiabloExperience(socket: import('socket.io').Socket) {
|
|
220
|
+
const player = areaManager.addPlayerToArea(socket.id, socket.id, 'overworld');
|
|
221
|
+
const session = areaManager.getSession('overworld')!;
|
|
222
|
+
|
|
223
|
+
socket.emit('message', {
|
|
224
|
+
type: 'EXPERIENCE_CHANGE',
|
|
225
|
+
experienceId: 'diablo',
|
|
226
|
+
experienceName: 'Diablo — Overworld & Dungeon',
|
|
227
|
+
cameraMode: 'orbit',
|
|
228
|
+
spawnX: session.state.dungeon.spawnPosition.x,
|
|
229
|
+
spawnY: session.state.dungeon.spawnPosition.y,
|
|
230
|
+
localEntityId: player.entityId,
|
|
231
|
+
seed: session.state.dungeon.seed,
|
|
232
|
+
payload: { voxelConfig: { terrainOptions: null, tileLayout: true }, targetArea: 'overworld' },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Also send legacy AREA_CHANGE for backward compatibility
|
|
236
|
+
socket.emit('message', {
|
|
237
|
+
type: 'AREA_CHANGE',
|
|
238
|
+
targetArea: 'overworld',
|
|
239
|
+
spawnX: session.state.dungeon.spawnPosition.x,
|
|
240
|
+
spawnY: session.state.dungeon.spawnPosition.y,
|
|
241
|
+
localEntityId: player.entityId,
|
|
242
|
+
seed: session.state.dungeon.seed,
|
|
243
|
+
voxelConfig: { terrainOptions: null, tileLayout: true },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
io.on('connection', (socket) => {
|
|
248
|
+
console.log(`[server] connect: ${socket.id}`);
|
|
249
|
+
|
|
250
|
+
// Send experience list — player starts in hub
|
|
251
|
+
socket.emit('message', {
|
|
252
|
+
type: 'EXPERIENCE_LIST',
|
|
253
|
+
experiences: experienceManager.getAvailableExperiences(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
let persistedPlayerId: string | null = null;
|
|
257
|
+
const getCurrentPlayer = () => getSession(socket.id)?.state.players.get(socket.id);
|
|
258
|
+
|
|
259
|
+
socket.on('message', (data: unknown) => {
|
|
260
|
+
// EXPERIENCE_JOIN — join an experience from hub
|
|
261
|
+
const joinResult = ExperienceJoinSchema.safeParse(data);
|
|
262
|
+
if (joinResult.success) {
|
|
263
|
+
const { experienceId } = joinResult.data;
|
|
264
|
+
if (experienceId === 'diablo') {
|
|
265
|
+
joinDiabloExperience(socket);
|
|
266
|
+
// Load persisted state if we have a playerId
|
|
267
|
+
if (persistedPlayerId) {
|
|
268
|
+
const p = getCurrentPlayer();
|
|
269
|
+
const sess = getSession(socket.id);
|
|
270
|
+
if (p && sess) {
|
|
271
|
+
loadPlayerState(db, sess, p, persistedPlayerId);
|
|
272
|
+
socket.emit('message', {
|
|
273
|
+
type: 'INVENTORY_UPDATE',
|
|
274
|
+
items: toInventoryItems(p.inventory),
|
|
275
|
+
});
|
|
276
|
+
socket.emit('message', {
|
|
277
|
+
type: 'EQUIPMENT_UPDATE',
|
|
278
|
+
equipped: Object.fromEntries(
|
|
279
|
+
Object.entries(p.equipped).map(([slotId, item]) => [slotId, {
|
|
280
|
+
id: item!.id, name: item!.name, rarity: item!.rarity, gridW: 1, gridH: 1, slot: 0, statBonus: item!.statBonus,
|
|
281
|
+
}])
|
|
282
|
+
),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} else if (experienceId === 'shooter') {
|
|
287
|
+
joinShooterExperience(socket);
|
|
288
|
+
} else if (experienceId === 'runner') {
|
|
289
|
+
joinRunnerExperience(socket);
|
|
290
|
+
} else {
|
|
291
|
+
console.warn(`[server] Unknown experience: ${experienceId}`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// EXPERIENCE_LEAVE — return to hub
|
|
297
|
+
const leaveResult = ExperienceLeaveSchema.safeParse(data);
|
|
298
|
+
if (leaveResult.success) {
|
|
299
|
+
const p = getCurrentPlayer();
|
|
300
|
+
if (p) savePlayerState(db, p);
|
|
301
|
+
areaManager.removePlayerFromArea(socket.id);
|
|
302
|
+
// Also clean up shooter session if player was in shooter
|
|
303
|
+
const shooterSessId = playerShooterSession.get(socket.id);
|
|
304
|
+
if (shooterSessId) {
|
|
305
|
+
const ss = shooterSessions.get(shooterSessId);
|
|
306
|
+
if (ss) removeShooterPlayer(ss, socket.id);
|
|
307
|
+
playerShooterSession.delete(socket.id);
|
|
308
|
+
shooterJumpStates.delete(socket.id);
|
|
309
|
+
}
|
|
310
|
+
// Clean up runner session if player was in runner
|
|
311
|
+
const runnerSessId = playerRunnerSession.get(socket.id);
|
|
312
|
+
if (runnerSessId) {
|
|
313
|
+
const rs = runnerSessions.get(runnerSessId);
|
|
314
|
+
if (rs) removeRunnerPlayer(rs, socket.id);
|
|
315
|
+
playerRunnerSession.delete(socket.id);
|
|
316
|
+
}
|
|
317
|
+
// Re-send experience list so hub is populated
|
|
318
|
+
socket.emit('message', {
|
|
319
|
+
type: 'EXPERIENCE_LIST',
|
|
320
|
+
experiences: experienceManager.getAvailableExperiences(),
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Shooter-specific messages ---
|
|
326
|
+
const shooterSessId = playerShooterSession.get(socket.id);
|
|
327
|
+
if (shooterSessId) {
|
|
328
|
+
const ss = shooterSessions.get(shooterSessId);
|
|
329
|
+
if (ss) {
|
|
330
|
+
const sp = ss.players.get(socket.id);
|
|
331
|
+
if (sp) {
|
|
332
|
+
const shootResult = ShootIntentSchema.safeParse(data);
|
|
333
|
+
if (shootResult.success) {
|
|
334
|
+
sp.shootQueue.push({
|
|
335
|
+
origin: { x: shootResult.data.originX, y: shootResult.data.originY, z: shootResult.data.originZ },
|
|
336
|
+
direction: { x: shootResult.data.dirX, y: shootResult.data.dirY, z: shootResult.data.dirZ },
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const reloadResult = ReloadSchema.safeParse(data);
|
|
342
|
+
if (reloadResult.success) {
|
|
343
|
+
sp.wantsReload = true;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const moveResult = ShooterMoveSchema.safeParse(data);
|
|
348
|
+
if (moveResult.success) {
|
|
349
|
+
sp.input.x = moveResult.data.x;
|
|
350
|
+
sp.input.z = moveResult.data.z;
|
|
351
|
+
sp.input.sprint = moveResult.data.sprint;
|
|
352
|
+
sp.facingAngle = moveResult.data.facingAngle;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const jumpResult = ShooterJumpSchema.safeParse(data);
|
|
357
|
+
if (jumpResult.success) {
|
|
358
|
+
const jumpState = shooterJumpStates.get(socket.id);
|
|
359
|
+
if (jumpState) {
|
|
360
|
+
tryJump(sp.body, jumpState, { jumpVelocity: 8, coyoteTime: 0.1, maxJumps: 2, jumpBufferTime: 0.1 });
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const restartResult = ShooterRestartSchema.safeParse(data);
|
|
366
|
+
if (restartResult.success) {
|
|
367
|
+
// Reset player state
|
|
368
|
+
const spawnIdx = 0;
|
|
369
|
+
const spawn = ss.arena.spawnPoints[spawnIdx];
|
|
370
|
+
sp.hp = sp.maxHp;
|
|
371
|
+
sp.body = createPhysicsBody(spawn.x, spawn.y, spawn.z);
|
|
372
|
+
sp.weapon = createWeaponState(DEFAULT_WEAPON_CONFIG);
|
|
373
|
+
sp.shootQueue = [];
|
|
374
|
+
sp.wantsReload = false;
|
|
375
|
+
sp.input = { x: 0, z: 0, sprint: false, glide: false };
|
|
376
|
+
|
|
377
|
+
// Reset wave state
|
|
378
|
+
ss.enemies = [];
|
|
379
|
+
ss.waveState = { currentWave: 0, waveActive: false, delayTimer: 2 };
|
|
380
|
+
|
|
381
|
+
// Reset jump state
|
|
382
|
+
shooterJumpStates.set(socket.id, createJumpState());
|
|
383
|
+
|
|
384
|
+
socket.emit('message', {
|
|
385
|
+
type: 'SHOOTER_RESTARTED',
|
|
386
|
+
hp: sp.hp,
|
|
387
|
+
maxHp: sp.maxHp,
|
|
388
|
+
maxAmmo: DEFAULT_WEAPON_CONFIG.maxAmmo,
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// --- Runner-specific messages ---
|
|
397
|
+
const runnerSessId = playerRunnerSession.get(socket.id);
|
|
398
|
+
if (runnerSessId) {
|
|
399
|
+
const rs = runnerSessions.get(runnerSessId);
|
|
400
|
+
if (rs) {
|
|
401
|
+
const rp = rs.players.get(socket.id);
|
|
402
|
+
if (rp) {
|
|
403
|
+
const inputResult = RunnerInputSchema.safeParse(data);
|
|
404
|
+
if (inputResult.success) {
|
|
405
|
+
if (inputResult.data.left) rp.input.left = true;
|
|
406
|
+
if (inputResult.data.right) rp.input.right = true;
|
|
407
|
+
if (inputResult.data.jump) rp.input.jump = true;
|
|
408
|
+
rp.input.glide = inputResult.data.glide;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const restartResult = RunnerRestartSchema.safeParse(data);
|
|
413
|
+
if (restartResult.success) {
|
|
414
|
+
// Reset player state
|
|
415
|
+
rp.body = createPhysicsBody(0, 0, 0);
|
|
416
|
+
rp.jumpState = createJumpState();
|
|
417
|
+
rp.lane = { currentLane: 0, targetLane: 0, laneOffset: 0, switchTimer: 0 };
|
|
418
|
+
rp.track = { seed: rs.seed, nextZ: 0, nextIndex: 0, segments: [], nextItemId: 1 };
|
|
419
|
+
rp.distanceRan = 0;
|
|
420
|
+
rp.isGliding = false;
|
|
421
|
+
rp.dead = false;
|
|
422
|
+
rp.input = { left: false, right: false, jump: false, glide: false };
|
|
423
|
+
rp.score = 0;
|
|
424
|
+
rp.coins = 0;
|
|
425
|
+
rp.gems = 0;
|
|
426
|
+
rp.bestCombo = 0;
|
|
427
|
+
rp.combo = { count: 0, timer: 0 };
|
|
428
|
+
rp.modifiers = [];
|
|
429
|
+
rp.magnetActive = false;
|
|
430
|
+
rp.magnetTimer = 0;
|
|
431
|
+
rp.gravityModifier = null;
|
|
432
|
+
rp.nextItemId = 1;
|
|
433
|
+
|
|
434
|
+
socket.emit('message', { type: 'RUNNER_RESTARTED' });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const sess = getSession(socket.id);
|
|
442
|
+
|
|
443
|
+
// PLAYER_CONNECT — store playerId for later use (state loaded on experience join)
|
|
444
|
+
const connectResult = PlayerConnectSchema.safeParse(data);
|
|
445
|
+
if (connectResult.success) {
|
|
446
|
+
persistedPlayerId = connectResult.data.playerId;
|
|
447
|
+
// If player is already in an experience, load state now
|
|
448
|
+
const p = getCurrentPlayer();
|
|
449
|
+
if (p && sess) {
|
|
450
|
+
loadPlayerState(db, sess, p, persistedPlayerId);
|
|
451
|
+
socket.emit('message', {
|
|
452
|
+
type: 'INVENTORY_UPDATE',
|
|
453
|
+
items: toInventoryItems(p.inventory),
|
|
454
|
+
});
|
|
455
|
+
socket.emit('message', {
|
|
456
|
+
type: 'EQUIPMENT_UPDATE',
|
|
457
|
+
equipped: Object.fromEntries(
|
|
458
|
+
Object.entries(p.equipped).map(([slotId, item]) => [slotId, {
|
|
459
|
+
id: item!.id, name: item!.name, rarity: item!.rarity, gridW: 1, gridH: 1, slot: 0, statBonus: item!.statBonus,
|
|
460
|
+
}])
|
|
461
|
+
),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!sess) return;
|
|
468
|
+
|
|
469
|
+
const regenResult = RequestNewDungeonSchema.safeParse(data);
|
|
470
|
+
if (regenResult.success) {
|
|
471
|
+
const dungeonSess = areaManager.getSession('dungeon');
|
|
472
|
+
if (dungeonSess) {
|
|
473
|
+
console.log(`[server] regenerating dungeon (seed: ${regenResult.data.seed ?? 'random'})`);
|
|
474
|
+
dungeonSess.regenerateDungeon(rooms, regenResult.data.seed);
|
|
475
|
+
areaManager.spawnInitialDungeonEnemies(dungeonSess);
|
|
476
|
+
io.emit('message', {
|
|
477
|
+
type: 'WORLD_STATE',
|
|
478
|
+
dungeon: dungeonSess.state.dungeon,
|
|
479
|
+
spawnPosition: dungeonSess.state.dungeon.spawnPosition,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const moveResult = PlayerMoveSchema.safeParse(data);
|
|
486
|
+
if (moveResult.success) { sess.setPlayerTarget(socket.id, moveResult.data.targetX, moveResult.data.targetY); return; }
|
|
487
|
+
|
|
488
|
+
const moveDirectResult = PlayerMoveDirectSchema.safeParse(data);
|
|
489
|
+
if (moveDirectResult.success) { sess.setPlayerPositionDirect(socket.id, moveDirectResult.data.x, moveDirectResult.data.y, moveDirectResult.data.inputId); return; }
|
|
490
|
+
|
|
491
|
+
const actionResult = ActionEventSchema.safeParse(data);
|
|
492
|
+
if (actionResult.success && actionResult.data.action === 'melee') { handleMeleeAction(sess, socket.id); return; }
|
|
493
|
+
|
|
494
|
+
const raycastResult = ActionRaycastSchema.safeParse(data);
|
|
495
|
+
if (raycastResult.success) {
|
|
496
|
+
const playerArea = areaManager.getPlayerArea(socket.id);
|
|
497
|
+
const areaSess = areaManager.getAreaSession(playerArea);
|
|
498
|
+
const groundYFn = areaSess?.chunkCache
|
|
499
|
+
? (x: number, z: number) => getTerrainHeight(areaSess.chunkCache!.getChunkFn, x, z)
|
|
500
|
+
: undefined;
|
|
501
|
+
handleRaycastAction(sess, io, raycastResult.data.entityId, raycastResult.data.origin, raycastResult.data.direction, raycastResult.data.range, undefined, groundYFn);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const useSkillResult = UseSkillSchema.safeParse(data);
|
|
506
|
+
if (useSkillResult.success) {
|
|
507
|
+
const playerArea2 = areaManager.getPlayerArea(socket.id);
|
|
508
|
+
const areaSess2 = areaManager.getAreaSession(playerArea2);
|
|
509
|
+
const groundYFn2 = areaSess2?.chunkCache
|
|
510
|
+
? (x: number, z: number) => getTerrainHeight(areaSess2.chunkCache!.getChunkFn, x, z)
|
|
511
|
+
: undefined;
|
|
512
|
+
handleUseSkill(sess, io, socket.id, useSkillResult.data.skillId, skillBook, useSkillResult.data.origin, useSkillResult.data.direction, groundYFn2);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const craftResult = CraftRequestSchema.safeParse(data);
|
|
517
|
+
if (craftResult.success) { handleCraftRequest(sess, socket, craftResult.data.grid, recipeBook); return; }
|
|
518
|
+
|
|
519
|
+
const equipResult = EquipRequestSchema.safeParse(data);
|
|
520
|
+
if (equipResult.success) { handleEquipRequest(sess, socket, equipResult.data.slotId, equipResult.data.itemId); return; }
|
|
521
|
+
|
|
522
|
+
const unequipResult = UnequipRequestSchema.safeParse(data);
|
|
523
|
+
if (unequipResult.success) { handleUnequipRequest(sess, socket, unequipResult.data.slotId); return; }
|
|
524
|
+
|
|
525
|
+
const jumpResult = PlayerJumpSchema.safeParse(data);
|
|
526
|
+
if (jumpResult.success) {
|
|
527
|
+
const playerArea = areaManager.getPlayerArea(socket.id);
|
|
528
|
+
const physicsMap = voxelPhysicsRegistry.get(playerArea);
|
|
529
|
+
const physics = physicsMap?.get(socket.id);
|
|
530
|
+
if (physics) {
|
|
531
|
+
tryJump(physics.body, physics.jumpState, GAME_JUMP_CONFIG);
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
537
|
+
const debugTeleportResult = (data as any)?.type === 'DEBUG_TELEPORT' && typeof (data as any)?.targetArea === 'string';
|
|
538
|
+
if (debugTeleportResult) {
|
|
539
|
+
const { targetArea } = data as { type: string; targetArea: string };
|
|
540
|
+
// Overworld: spawn at basecamp, dungeon: use natural spawn
|
|
541
|
+
const spawnLookup: Record<string, { x: number; y: number }> = {
|
|
542
|
+
overworld: { x: -1, y: -1 },
|
|
543
|
+
dungeon: { x: -1, y: -1 },
|
|
544
|
+
};
|
|
545
|
+
areaManager.movePlayer(io, socket.id, targetArea, spawnLookup[targetArea]?.x ?? -1, spawnLookup[targetArea]?.y ?? -1);
|
|
546
|
+
console.log(`[server] DEBUG_TELEPORT ${socket.id} → ${targetArea}`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const debugSpawnResult = DebugSpawnSchema.safeParse(data);
|
|
551
|
+
if (debugSpawnResult.success) {
|
|
552
|
+
const { x, y } = debugSpawnResult.data;
|
|
553
|
+
const grunt = sess.spawnEnemy({ enemyDefId: 'grunt', x, y, hp: 50, maxHp: 50, alive: true, aiState: 'idle', waypoints: [{ x, y }, { x, y }], waypointIndex: 0, path: [], attackCooldown: 0 });
|
|
554
|
+
console.log(`[server] DEBUG_SPAWN grunt #${grunt.entityId} at (${x}, ${y})`);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log(`[server] Unhandled message type: ${(data as any)?.type}, action: ${(data as any)?.action}`);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
socket.on('disconnect', () => {
|
|
563
|
+
const p = getCurrentPlayer();
|
|
564
|
+
if (p) {
|
|
565
|
+
console.log(`[server] disconnect: ${socket.id} (entity ${p.entityId})`);
|
|
566
|
+
savePlayerState(db, p);
|
|
567
|
+
}
|
|
568
|
+
areaManager.removePlayerFromArea(socket.id);
|
|
569
|
+
// Clean up shooter session
|
|
570
|
+
const shooterSessId2 = playerShooterSession.get(socket.id);
|
|
571
|
+
if (shooterSessId2) {
|
|
572
|
+
const ss2 = shooterSessions.get(shooterSessId2);
|
|
573
|
+
if (ss2) removeShooterPlayer(ss2, socket.id);
|
|
574
|
+
playerShooterSession.delete(socket.id);
|
|
575
|
+
shooterJumpStates.delete(socket.id);
|
|
576
|
+
}
|
|
577
|
+
// Clean up runner session
|
|
578
|
+
const runnerSessId2 = playerRunnerSession.get(socket.id);
|
|
579
|
+
if (runnerSessId2) {
|
|
580
|
+
const rs2 = runnerSessions.get(runnerSessId2);
|
|
581
|
+
if (rs2) removeRunnerPlayer(rs2, socket.id);
|
|
582
|
+
playerRunnerSession.delete(socket.id);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return { close };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Run as standalone if invoked directly
|
|
591
|
+
const isMain = process.argv[1]?.endsWith('index.ts') || process.argv[1]?.endsWith('index.js');
|
|
592
|
+
if (isMain) {
|
|
593
|
+
const PORT = Number(process.env.PORT ?? 3000);
|
|
594
|
+
createServer(PORT).then(() => {
|
|
595
|
+
console.log(`[server] running on port ${PORT}`);
|
|
596
|
+
});
|
|
597
|
+
}
|