feat: add Socket.io server with state broadcasting and player input handling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import http from 'http';
|
||||
import { GameLoop } from './game/GameLoop.js';
|
||||
import { SocketServer } from './network/SocketServer.js';
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001;
|
||||
|
||||
const gameLoop = new GameLoop();
|
||||
const httpServer = http.createServer();
|
||||
new SocketServer(httpServer, gameLoop);
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`Server listening on port ${PORT}`);
|
||||
gameLoop.start();
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Server } from 'socket.io';
|
||||
import type http from 'http';
|
||||
import type { ClientEvents, ServerEvents, PlayerInput, Position, Movement, PlayerControlled } from '@dflike/shared';
|
||||
import { Direction } from '@dflike/shared';
|
||||
import type { GameLoop } from '../game/GameLoop.js';
|
||||
import { serializeWorldState, serializeStateUpdate } from './stateSerializer.js';
|
||||
|
||||
export class SocketServer {
|
||||
private io: Server<ClientEvents, ServerEvents>;
|
||||
private gameLoop: GameLoop;
|
||||
|
||||
constructor(httpServer: http.Server, gameLoop: GameLoop) {
|
||||
this.io = new Server(httpServer, {
|
||||
cors: { origin: '*' },
|
||||
});
|
||||
this.gameLoop = gameLoop;
|
||||
|
||||
this.setupBroadcast();
|
||||
this.setupConnections();
|
||||
}
|
||||
|
||||
private setupBroadcast(): void {
|
||||
this.gameLoop.setBroadcastHandler(() => {
|
||||
const update = serializeStateUpdate(this.gameLoop.world, this.gameLoop.getTick());
|
||||
this.io.emit('state-update', update);
|
||||
});
|
||||
}
|
||||
|
||||
private setupConnections(): void {
|
||||
this.io.on('connection', (socket) => {
|
||||
const playerId = socket.id;
|
||||
console.log(`Player connected: ${playerId}`);
|
||||
|
||||
// Create player entity (starts in camera mode — no avatar)
|
||||
const world = this.gameLoop.world;
|
||||
const map = this.gameLoop.map;
|
||||
const entity = world.createEntity();
|
||||
const startPos = map.getRandomWalkable();
|
||||
world.addComponent<Position>(entity, 'position', startPos);
|
||||
world.addComponent<Movement>(entity, 'movement', {
|
||||
state: 'idle', target: null, path: [], direction: Direction.DOWN,
|
||||
});
|
||||
world.addComponent<PlayerControlled>(entity, 'playerControlled', {
|
||||
playerId, mode: 'camera',
|
||||
});
|
||||
|
||||
// Send full world state
|
||||
socket.emit('world-state', serializeWorldState(world, map));
|
||||
socket.emit('player-joined', { playerId, entityId: entity });
|
||||
|
||||
// Notify others
|
||||
socket.broadcast.emit('player-joined', { playerId, entityId: entity });
|
||||
|
||||
// Handle inputs
|
||||
socket.on('player-input', (input: PlayerInput) => {
|
||||
this.handleInput(playerId, entity, input);
|
||||
});
|
||||
|
||||
// Handle disconnect
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`Player disconnected: ${playerId}`);
|
||||
world.removeEntity(entity);
|
||||
this.io.emit('player-left', { playerId });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private handleInput(playerId: string, entityId: number, input: PlayerInput): void {
|
||||
const world = this.gameLoop.world;
|
||||
const pc = world.getComponent<PlayerControlled>(entityId, 'playerControlled');
|
||||
if (!pc) return;
|
||||
|
||||
switch (input.type) {
|
||||
case 'toggle-mode': {
|
||||
pc.mode = pc.mode === 'avatar' ? 'camera' : 'avatar';
|
||||
break;
|
||||
}
|
||||
case 'move': {
|
||||
if (pc.mode !== 'avatar' || !input.direction) break;
|
||||
const pos = world.getComponent<Position>(entityId, 'position');
|
||||
const mov = world.getComponent<Movement>(entityId, 'movement');
|
||||
if (!pos || !mov) break;
|
||||
|
||||
const newX = pos.x + input.direction.dx;
|
||||
const newY = pos.y + input.direction.dy;
|
||||
if (this.gameLoop.map.isWalkable(newX, newY)) {
|
||||
pos.x = newX;
|
||||
pos.y = newY;
|
||||
|
||||
const { dx, dy } = input.direction;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
mov.direction = dx > 0 ? Direction.RIGHT : Direction.LEFT;
|
||||
} else {
|
||||
mov.direction = dy > 0 ? Direction.DOWN : Direction.UP;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
EntityState, WorldState, StateUpdate,
|
||||
Position, Movement, Appearance, Needs, NPCBrain, PlayerControlled,
|
||||
} from '@dflike/shared';
|
||||
import { TILE_SIZE } from '@dflike/shared';
|
||||
import type { World } from '../ecs/World.js';
|
||||
import type { GameMap } from '../map/GameMap.js';
|
||||
|
||||
export function serializeEntity(world: World, entityId: number): EntityState {
|
||||
return {
|
||||
id: entityId,
|
||||
position: world.getComponent<Position>(entityId, 'position')!,
|
||||
movement: world.getComponent<Movement>(entityId, 'movement')!,
|
||||
appearance: world.getComponent<Appearance>(entityId, 'appearance')!,
|
||||
needs: world.getComponent<Needs>(entityId, 'needs'),
|
||||
npcBrain: world.getComponent<NPCBrain>(entityId, 'npcBrain'),
|
||||
playerControlled: world.getComponent<PlayerControlled>(entityId, 'playerControlled'),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeWorldState(world: World, map: GameMap): WorldState {
|
||||
const entities = world.query('position').map(id => serializeEntity(world, id));
|
||||
return {
|
||||
entities,
|
||||
worldWidth: map.width,
|
||||
worldHeight: map.height,
|
||||
tileSize: TILE_SIZE,
|
||||
obstacles: map.getObstacles(),
|
||||
pointsOfInterest: map.getPointsOfInterest(),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeStateUpdate(world: World, tick: number): StateUpdate {
|
||||
const entities = world.query('position').map(id => serializeEntity(world, id));
|
||||
return { entities, tick };
|
||||
}
|
||||
+5
-1
@@ -2,6 +2,10 @@
|
||||
"name": "@dflike/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts"
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user