csterm-server 1.0.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/dist/GameRunner.d.ts +54 -0
- package/dist/GameRunner.js +998 -0
- package/dist/LockstepRunner.d.ts +39 -0
- package/dist/LockstepRunner.js +276 -0
- package/dist/Room.d.ts +43 -0
- package/dist/Room.js +498 -0
- package/dist/RoomManager.d.ts +29 -0
- package/dist/RoomManager.js +277 -0
- package/dist/VoiceRelay.d.ts +62 -0
- package/dist/VoiceRelay.js +167 -0
- package/dist/hub/HubServer.d.ts +37 -0
- package/dist/hub/HubServer.js +266 -0
- package/dist/hub/PoolRegistry.d.ts +35 -0
- package/dist/hub/PoolRegistry.js +185 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +175 -0
- package/dist/pool/GameServer.d.ts +29 -0
- package/dist/pool/GameServer.js +185 -0
- package/dist/pool/PoolClient.d.ts +48 -0
- package/dist/pool/PoolClient.js +204 -0
- package/dist/protocol.d.ts +433 -0
- package/dist/protocol.js +59 -0
- package/dist/test/HeadlessClient.d.ts +44 -0
- package/dist/test/HeadlessClient.js +196 -0
- package/dist/test/testTeamSync.d.ts +2 -0
- package/dist/test/testTeamSync.js +82 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +139 -0
- package/package.json +50 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Headless WebSocket client for testing multiplayer server
|
|
2
|
+
// Can simulate multiple clients connecting, joining rooms, and performing actions
|
|
3
|
+
import WebSocket from 'ws';
|
|
4
|
+
export class HeadlessClient {
|
|
5
|
+
socket = null;
|
|
6
|
+
callbacks = {};
|
|
7
|
+
playerId = null;
|
|
8
|
+
playerName;
|
|
9
|
+
roomId = null;
|
|
10
|
+
team = null;
|
|
11
|
+
messageLog = [];
|
|
12
|
+
constructor(playerName = 'TestPlayer') {
|
|
13
|
+
this.playerName = playerName;
|
|
14
|
+
}
|
|
15
|
+
async connect(serverUrl = 'ws://localhost:8080') {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
this.socket = new WebSocket(serverUrl);
|
|
18
|
+
this.socket.onopen = () => {
|
|
19
|
+
console.log(`[${this.playerName}] Connected to server`);
|
|
20
|
+
this.callbacks.onConnect?.();
|
|
21
|
+
resolve();
|
|
22
|
+
};
|
|
23
|
+
this.socket.onclose = (event) => {
|
|
24
|
+
const reason = event.reason || 'Connection closed';
|
|
25
|
+
console.log(`[${this.playerName}] Disconnected: ${reason}`);
|
|
26
|
+
this.callbacks.onDisconnect?.(reason);
|
|
27
|
+
};
|
|
28
|
+
this.socket.onerror = (error) => {
|
|
29
|
+
console.error(`[${this.playerName}] WebSocket error`);
|
|
30
|
+
reject(new Error('WebSocket error'));
|
|
31
|
+
};
|
|
32
|
+
this.socket.onmessage = (event) => {
|
|
33
|
+
this.handleMessage(event.data.toString());
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
disconnect() {
|
|
38
|
+
if (this.socket) {
|
|
39
|
+
this.socket.close();
|
|
40
|
+
this.socket = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
setCallbacks(callbacks) {
|
|
44
|
+
this.callbacks = { ...this.callbacks, ...callbacks };
|
|
45
|
+
}
|
|
46
|
+
// Lobby operations
|
|
47
|
+
listRooms() {
|
|
48
|
+
this.send({ type: 'list_rooms' });
|
|
49
|
+
}
|
|
50
|
+
createRoom(config) {
|
|
51
|
+
const fullConfig = {
|
|
52
|
+
name: config.name || `${this.playerName}'s Room`,
|
|
53
|
+
map: config.map || 'dm_arena',
|
|
54
|
+
mode: config.mode || 'deathmatch',
|
|
55
|
+
maxPlayers: config.maxPlayers || 10,
|
|
56
|
+
botCount: config.botCount || 0,
|
|
57
|
+
botDifficulty: config.botDifficulty || 'medium',
|
|
58
|
+
isPrivate: config.isPrivate || false,
|
|
59
|
+
password: config.password,
|
|
60
|
+
};
|
|
61
|
+
this.send({ type: 'create_room', config: fullConfig });
|
|
62
|
+
}
|
|
63
|
+
joinRoom(roomId, password) {
|
|
64
|
+
this.send({
|
|
65
|
+
type: 'join_room',
|
|
66
|
+
roomId,
|
|
67
|
+
playerName: this.playerName,
|
|
68
|
+
password,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
leaveRoom() {
|
|
72
|
+
this.send({ type: 'leave_room' });
|
|
73
|
+
this.roomId = null;
|
|
74
|
+
}
|
|
75
|
+
setReady() {
|
|
76
|
+
this.send({ type: 'ready' });
|
|
77
|
+
}
|
|
78
|
+
startGame() {
|
|
79
|
+
this.send({ type: 'start_game' });
|
|
80
|
+
}
|
|
81
|
+
changeTeam(team) {
|
|
82
|
+
this.send({ type: 'change_team', team });
|
|
83
|
+
}
|
|
84
|
+
// Game operations
|
|
85
|
+
sendInput(forward, strafe, yaw, pitch, jump = false) {
|
|
86
|
+
this.send({
|
|
87
|
+
type: 'input',
|
|
88
|
+
input: { forward, strafe, yaw, pitch, jump, crouch: false },
|
|
89
|
+
sequence: Date.now(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
sendFire() {
|
|
93
|
+
this.send({ type: 'fire' });
|
|
94
|
+
}
|
|
95
|
+
// Getters
|
|
96
|
+
getPlayerId() {
|
|
97
|
+
return this.playerId;
|
|
98
|
+
}
|
|
99
|
+
getRoomId() {
|
|
100
|
+
return this.roomId;
|
|
101
|
+
}
|
|
102
|
+
getTeam() {
|
|
103
|
+
return this.team;
|
|
104
|
+
}
|
|
105
|
+
getMessageLog() {
|
|
106
|
+
return this.messageLog;
|
|
107
|
+
}
|
|
108
|
+
clearMessageLog() {
|
|
109
|
+
this.messageLog = [];
|
|
110
|
+
}
|
|
111
|
+
// Wait for a specific message type
|
|
112
|
+
async waitForMessage(type, timeout = 5000) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const startTime = Date.now();
|
|
115
|
+
const checkInterval = setInterval(() => {
|
|
116
|
+
const message = this.messageLog.find(m => m.type === type);
|
|
117
|
+
if (message) {
|
|
118
|
+
clearInterval(checkInterval);
|
|
119
|
+
resolve(message);
|
|
120
|
+
}
|
|
121
|
+
else if (Date.now() - startTime > timeout) {
|
|
122
|
+
clearInterval(checkInterval);
|
|
123
|
+
reject(new Error(`Timeout waiting for message type: ${type}`));
|
|
124
|
+
}
|
|
125
|
+
}, 50);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
send(message) {
|
|
129
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
130
|
+
this.socket.send(JSON.stringify(message));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
handleMessage(data) {
|
|
134
|
+
try {
|
|
135
|
+
const message = JSON.parse(data);
|
|
136
|
+
this.messageLog.push(message);
|
|
137
|
+
this.callbacks.onMessage?.(message);
|
|
138
|
+
this.dispatchMessage(message);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error(`[${this.playerName}] Failed to parse message:`, error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
dispatchMessage(message) {
|
|
145
|
+
switch (message.type) {
|
|
146
|
+
case 'room_list':
|
|
147
|
+
console.log(`[${this.playerName}] Room list: ${message.rooms.length} rooms`);
|
|
148
|
+
this.callbacks.onRoomList?.(message.rooms);
|
|
149
|
+
break;
|
|
150
|
+
case 'room_joined':
|
|
151
|
+
this.playerId = message.playerId;
|
|
152
|
+
this.roomId = message.roomId;
|
|
153
|
+
console.log(`[${this.playerName}] Joined room ${message.roomId} as ${message.playerId}`);
|
|
154
|
+
this.callbacks.onRoomJoined?.(message.roomId, message.playerId, message.room);
|
|
155
|
+
break;
|
|
156
|
+
case 'room_error':
|
|
157
|
+
console.error(`[${this.playerName}] Room error: ${message.error}`);
|
|
158
|
+
break;
|
|
159
|
+
case 'player_joined':
|
|
160
|
+
console.log(`[${this.playerName}] Player joined: ${message.playerName} (${message.playerId})`);
|
|
161
|
+
this.callbacks.onPlayerJoined?.(message.playerId, message.playerName);
|
|
162
|
+
break;
|
|
163
|
+
case 'player_team_changed':
|
|
164
|
+
console.log(`[${this.playerName}] Player ${message.playerId} changed to team ${message.team}`);
|
|
165
|
+
this.callbacks.onPlayerTeamChanged?.(message.playerId, message.team);
|
|
166
|
+
break;
|
|
167
|
+
case 'assigned_team':
|
|
168
|
+
this.team = message.team;
|
|
169
|
+
console.log(`[${this.playerName}] Assigned to team: ${message.team}`);
|
|
170
|
+
break;
|
|
171
|
+
case 'player_ready':
|
|
172
|
+
console.log(`[${this.playerName}] Player ${message.playerId} ready: ${message.ready}`);
|
|
173
|
+
break;
|
|
174
|
+
case 'game_starting':
|
|
175
|
+
console.log(`[${this.playerName}] Game starting in ${message.countdown}...`);
|
|
176
|
+
break;
|
|
177
|
+
case 'phase_change':
|
|
178
|
+
console.log(`[${this.playerName}] Phase: ${message.phase}, Round ${message.roundNumber}`);
|
|
179
|
+
this.callbacks.onPhaseChange?.(message.phase, message.roundNumber, message.tScore, message.ctScore);
|
|
180
|
+
break;
|
|
181
|
+
case 'game_state':
|
|
182
|
+
// Frequent message, don't log
|
|
183
|
+
break;
|
|
184
|
+
case 'kill_event':
|
|
185
|
+
console.log(`[${this.playerName}] Kill: ${message.event.killerName} killed ${message.event.victimName}`);
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
// Ignore other messages
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Helper function to run tests
|
|
194
|
+
export async function delay(ms) {
|
|
195
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
// Test script to verify team synchronization between players
|
|
3
|
+
// Run with: npx ts-node server/src/test/testTeamSync.ts
|
|
4
|
+
import { HeadlessClient, delay } from './HeadlessClient.js';
|
|
5
|
+
async function testTeamSync() {
|
|
6
|
+
console.log('=== Team Sync Test ===\n');
|
|
7
|
+
// Create two clients
|
|
8
|
+
const host = new HeadlessClient('Host');
|
|
9
|
+
const joiner = new HeadlessClient('Joiner');
|
|
10
|
+
try {
|
|
11
|
+
// Connect host
|
|
12
|
+
console.log('1. Connecting host...');
|
|
13
|
+
await host.connect();
|
|
14
|
+
await delay(100);
|
|
15
|
+
// Host creates room
|
|
16
|
+
console.log('2. Host creating room...');
|
|
17
|
+
host.createRoom({ name: 'Test Room', botCount: 0 });
|
|
18
|
+
await delay(500);
|
|
19
|
+
// Get room ID
|
|
20
|
+
const roomId = host.getRoomId();
|
|
21
|
+
if (!roomId) {
|
|
22
|
+
throw new Error('Host failed to create room');
|
|
23
|
+
}
|
|
24
|
+
console.log(` Room created: ${roomId}`);
|
|
25
|
+
console.log(` Host team: ${host.getTeam()}`);
|
|
26
|
+
// Host changes team to T
|
|
27
|
+
console.log('3. Host changing team to T...');
|
|
28
|
+
host.changeTeam('T');
|
|
29
|
+
await delay(200);
|
|
30
|
+
console.log(` Host team after change: ${host.getTeam()}`);
|
|
31
|
+
// Connect joiner
|
|
32
|
+
console.log('4. Connecting joiner...');
|
|
33
|
+
await joiner.connect();
|
|
34
|
+
await delay(100);
|
|
35
|
+
// Track team changes received by joiner
|
|
36
|
+
let receivedTeamChanges = [];
|
|
37
|
+
joiner.setCallbacks({
|
|
38
|
+
onPlayerTeamChanged: (playerId, team) => {
|
|
39
|
+
receivedTeamChanges.push({ playerId, team });
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
// Joiner joins the room
|
|
43
|
+
console.log('5. Joiner joining room...');
|
|
44
|
+
joiner.joinRoom(roomId);
|
|
45
|
+
await delay(500);
|
|
46
|
+
// Check what joiner received
|
|
47
|
+
console.log('\n=== Results ===');
|
|
48
|
+
console.log(`Joiner received ${receivedTeamChanges.length} team change messages:`);
|
|
49
|
+
for (const change of receivedTeamChanges) {
|
|
50
|
+
console.log(` - Player ${change.playerId} -> ${change.team}`);
|
|
51
|
+
}
|
|
52
|
+
// Check if host's team was received
|
|
53
|
+
const hostId = host.getPlayerId();
|
|
54
|
+
const hostTeamReceived = receivedTeamChanges.find(c => c.playerId === hostId);
|
|
55
|
+
if (hostTeamReceived) {
|
|
56
|
+
console.log(`\n✓ SUCCESS: Joiner received host's team (${hostTeamReceived.team})`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(`\n✗ FAILURE: Joiner did NOT receive host's team`);
|
|
60
|
+
console.log(` Host ID: ${hostId}`);
|
|
61
|
+
console.log(` Messages in joiner log: ${joiner.getMessageLog().map(m => m.type).join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
// Print full message log for debugging
|
|
64
|
+
console.log('\n=== Joiner Message Log ===');
|
|
65
|
+
for (const msg of joiner.getMessageLog()) {
|
|
66
|
+
if (msg.type !== 'game_state') {
|
|
67
|
+
console.log(` ${msg.type}: ${JSON.stringify(msg).substring(0, 100)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Test failed:', error);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
// Cleanup
|
|
76
|
+
host.disconnect();
|
|
77
|
+
joiner.disconnect();
|
|
78
|
+
console.log('\n=== Test Complete ===');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Run the test
|
|
82
|
+
testTeamSync().catch(console.error);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { RoomConfig, TeamId, Vec3, PlayerInput, GamePhase, WeaponType, BotDifficulty } from './protocol.js';
|
|
3
|
+
export interface ServerConfig {
|
|
4
|
+
port: number;
|
|
5
|
+
tickRate: number;
|
|
6
|
+
broadcastRate: number;
|
|
7
|
+
maxRooms: number;
|
|
8
|
+
maxPlayersPerRoom: number;
|
|
9
|
+
roomIdleTimeout: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_SERVER_CONFIG: ServerConfig;
|
|
12
|
+
export interface ConnectedClient {
|
|
13
|
+
id: string;
|
|
14
|
+
socket: WebSocket;
|
|
15
|
+
name: string | null;
|
|
16
|
+
roomId: string | null;
|
|
17
|
+
isReady: boolean;
|
|
18
|
+
lastActivity: number;
|
|
19
|
+
pendingInputs: Array<{
|
|
20
|
+
sequence: number;
|
|
21
|
+
input: PlayerInput;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface ServerPlayerState {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
team: TeamId;
|
|
28
|
+
position: Vec3;
|
|
29
|
+
velocity: Vec3;
|
|
30
|
+
yaw: number;
|
|
31
|
+
pitch: number;
|
|
32
|
+
health: number;
|
|
33
|
+
armor: number;
|
|
34
|
+
isAlive: boolean;
|
|
35
|
+
currentWeapon: WeaponType;
|
|
36
|
+
weapons: Map<number, ServerWeaponState>;
|
|
37
|
+
money: number;
|
|
38
|
+
kills: number;
|
|
39
|
+
deaths: number;
|
|
40
|
+
lastInputSequence: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ServerWeaponState {
|
|
43
|
+
type: WeaponType;
|
|
44
|
+
currentAmmo: number;
|
|
45
|
+
reserveAmmo: number;
|
|
46
|
+
isReloading: boolean;
|
|
47
|
+
reloadStartTime: number;
|
|
48
|
+
lastFireTime: number;
|
|
49
|
+
}
|
|
50
|
+
export interface ServerBotState {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
team: TeamId;
|
|
54
|
+
difficulty: BotDifficulty;
|
|
55
|
+
position: Vec3;
|
|
56
|
+
velocity: Vec3;
|
|
57
|
+
yaw: number;
|
|
58
|
+
pitch: number;
|
|
59
|
+
health: number;
|
|
60
|
+
armor: number;
|
|
61
|
+
isAlive: boolean;
|
|
62
|
+
currentWeapon: WeaponType;
|
|
63
|
+
kills: number;
|
|
64
|
+
deaths: number;
|
|
65
|
+
targetId: string | null;
|
|
66
|
+
lastTargetSeen: number;
|
|
67
|
+
wanderAngle: number;
|
|
68
|
+
nextFireTime: number;
|
|
69
|
+
}
|
|
70
|
+
export interface ServerDroppedWeapon {
|
|
71
|
+
id: string;
|
|
72
|
+
weaponType: WeaponType;
|
|
73
|
+
position: Vec3;
|
|
74
|
+
ammo: number;
|
|
75
|
+
reserveAmmo: number;
|
|
76
|
+
dropTime: number;
|
|
77
|
+
}
|
|
78
|
+
export interface SpawnPoint {
|
|
79
|
+
position: Vec3;
|
|
80
|
+
angle: number;
|
|
81
|
+
team: TeamId | 'DM';
|
|
82
|
+
}
|
|
83
|
+
export interface MapCollider {
|
|
84
|
+
min: Vec3;
|
|
85
|
+
max: Vec3;
|
|
86
|
+
}
|
|
87
|
+
export interface MapData {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
spawnPoints: SpawnPoint[];
|
|
91
|
+
colliders: MapCollider[];
|
|
92
|
+
bounds: {
|
|
93
|
+
min: Vec3;
|
|
94
|
+
max: Vec3;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export interface ServerGameState {
|
|
98
|
+
phase: GamePhase;
|
|
99
|
+
phaseStartTime: number;
|
|
100
|
+
roundNumber: number;
|
|
101
|
+
tScore: number;
|
|
102
|
+
ctScore: number;
|
|
103
|
+
roundWinner: TeamId | null;
|
|
104
|
+
players: Map<string, ServerPlayerState>;
|
|
105
|
+
bots: Map<string, ServerBotState>;
|
|
106
|
+
droppedWeapons: Map<string, ServerDroppedWeapon>;
|
|
107
|
+
tick: number;
|
|
108
|
+
lastBroadcastTick: number;
|
|
109
|
+
}
|
|
110
|
+
export interface RoomState {
|
|
111
|
+
id: string;
|
|
112
|
+
config: RoomConfig;
|
|
113
|
+
hostId: string;
|
|
114
|
+
createdAt: number;
|
|
115
|
+
lastActivity: number;
|
|
116
|
+
clients: Map<string, ConnectedClient>;
|
|
117
|
+
gameState: ServerGameState | null;
|
|
118
|
+
gameLoopInterval: ReturnType<typeof setInterval> | null;
|
|
119
|
+
broadcastInterval: ReturnType<typeof setInterval> | null;
|
|
120
|
+
mapData: MapData;
|
|
121
|
+
}
|
|
122
|
+
export interface RaycastHit {
|
|
123
|
+
entityId: string;
|
|
124
|
+
entityType: 'player' | 'bot';
|
|
125
|
+
distance: number;
|
|
126
|
+
hitPoint: Vec3;
|
|
127
|
+
isHeadshot: boolean;
|
|
128
|
+
}
|
|
129
|
+
export interface ServerEconomyConfig {
|
|
130
|
+
startMoney: number;
|
|
131
|
+
maxMoney: number;
|
|
132
|
+
roundWinBonus: number;
|
|
133
|
+
roundLoseBonus: number;
|
|
134
|
+
roundLoseStreakBonus: number;
|
|
135
|
+
maxLoseStreak: number;
|
|
136
|
+
killReward: Record<WeaponType, number>;
|
|
137
|
+
}
|
|
138
|
+
export declare const DEFAULT_ECONOMY_CONFIG: ServerEconomyConfig;
|
|
139
|
+
export interface ServerWeaponDef {
|
|
140
|
+
type: WeaponType;
|
|
141
|
+
name: string;
|
|
142
|
+
slot: number;
|
|
143
|
+
damage: number;
|
|
144
|
+
fireRate: number;
|
|
145
|
+
reloadTime: number;
|
|
146
|
+
magazineSize: number;
|
|
147
|
+
reserveAmmo: number;
|
|
148
|
+
spread: number;
|
|
149
|
+
range: number;
|
|
150
|
+
moveSpeed: number;
|
|
151
|
+
pellets: number;
|
|
152
|
+
isAutomatic: boolean;
|
|
153
|
+
headshotMultiplier: number;
|
|
154
|
+
cost: number;
|
|
155
|
+
}
|
|
156
|
+
export declare const WEAPON_DEFS: Record<WeaponType, ServerWeaponDef>;
|
|
157
|
+
export declare function createVec3(x?: number, y?: number, z?: number): Vec3;
|
|
158
|
+
export declare function vec3Add(a: Vec3, b: Vec3): Vec3;
|
|
159
|
+
export declare function vec3Sub(a: Vec3, b: Vec3): Vec3;
|
|
160
|
+
export declare function vec3Scale(v: Vec3, s: number): Vec3;
|
|
161
|
+
export declare function vec3Length(v: Vec3): number;
|
|
162
|
+
export declare function vec3Normalize(v: Vec3): Vec3;
|
|
163
|
+
export declare function vec3Distance(a: Vec3, b: Vec3): number;
|
|
164
|
+
export declare function vec3Dot(a: Vec3, b: Vec3): number;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Server-specific type definitions for CS-CLI multiplayer
|
|
2
|
+
export const DEFAULT_SERVER_CONFIG = {
|
|
3
|
+
port: 8080,
|
|
4
|
+
tickRate: 60,
|
|
5
|
+
broadcastRate: 20,
|
|
6
|
+
maxRooms: 100,
|
|
7
|
+
maxPlayersPerRoom: 10,
|
|
8
|
+
roomIdleTimeout: 300000, // 5 minutes
|
|
9
|
+
};
|
|
10
|
+
export const DEFAULT_ECONOMY_CONFIG = {
|
|
11
|
+
startMoney: 800,
|
|
12
|
+
maxMoney: 16000,
|
|
13
|
+
roundWinBonus: 3250,
|
|
14
|
+
roundLoseBonus: 1400,
|
|
15
|
+
roundLoseStreakBonus: 500,
|
|
16
|
+
maxLoseStreak: 4,
|
|
17
|
+
killReward: {
|
|
18
|
+
knife: 1500,
|
|
19
|
+
pistol: 300,
|
|
20
|
+
rifle: 300,
|
|
21
|
+
shotgun: 900,
|
|
22
|
+
sniper: 100,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export const WEAPON_DEFS = {
|
|
26
|
+
knife: {
|
|
27
|
+
type: 'knife',
|
|
28
|
+
name: 'Knife',
|
|
29
|
+
slot: 3,
|
|
30
|
+
damage: 40,
|
|
31
|
+
fireRate: 60,
|
|
32
|
+
reloadTime: 0,
|
|
33
|
+
magazineSize: Infinity,
|
|
34
|
+
reserveAmmo: Infinity,
|
|
35
|
+
spread: 0,
|
|
36
|
+
range: 2,
|
|
37
|
+
moveSpeed: 1.0,
|
|
38
|
+
pellets: 1,
|
|
39
|
+
isAutomatic: false,
|
|
40
|
+
headshotMultiplier: 1.0,
|
|
41
|
+
cost: 0,
|
|
42
|
+
},
|
|
43
|
+
pistol: {
|
|
44
|
+
type: 'pistol',
|
|
45
|
+
name: 'Pistol',
|
|
46
|
+
slot: 2,
|
|
47
|
+
damage: 25,
|
|
48
|
+
fireRate: 400,
|
|
49
|
+
reloadTime: 2.2,
|
|
50
|
+
magazineSize: 12,
|
|
51
|
+
reserveAmmo: 36,
|
|
52
|
+
spread: 2,
|
|
53
|
+
range: 50,
|
|
54
|
+
moveSpeed: 1.0,
|
|
55
|
+
pellets: 1,
|
|
56
|
+
isAutomatic: false,
|
|
57
|
+
headshotMultiplier: 2.0,
|
|
58
|
+
cost: 200,
|
|
59
|
+
},
|
|
60
|
+
rifle: {
|
|
61
|
+
type: 'rifle',
|
|
62
|
+
name: 'Rifle',
|
|
63
|
+
slot: 1,
|
|
64
|
+
damage: 30,
|
|
65
|
+
fireRate: 600,
|
|
66
|
+
reloadTime: 2.5,
|
|
67
|
+
magazineSize: 30,
|
|
68
|
+
reserveAmmo: 90,
|
|
69
|
+
spread: 3,
|
|
70
|
+
range: 80,
|
|
71
|
+
moveSpeed: 0.9,
|
|
72
|
+
pellets: 1,
|
|
73
|
+
isAutomatic: true,
|
|
74
|
+
headshotMultiplier: 2.5,
|
|
75
|
+
cost: 2700,
|
|
76
|
+
},
|
|
77
|
+
shotgun: {
|
|
78
|
+
type: 'shotgun',
|
|
79
|
+
name: 'Shotgun',
|
|
80
|
+
slot: 1,
|
|
81
|
+
damage: 20,
|
|
82
|
+
fireRate: 70,
|
|
83
|
+
reloadTime: 0.5,
|
|
84
|
+
magazineSize: 8,
|
|
85
|
+
reserveAmmo: 32,
|
|
86
|
+
spread: 8,
|
|
87
|
+
range: 20,
|
|
88
|
+
moveSpeed: 0.9,
|
|
89
|
+
pellets: 8,
|
|
90
|
+
isAutomatic: false,
|
|
91
|
+
headshotMultiplier: 1.5,
|
|
92
|
+
cost: 1200,
|
|
93
|
+
},
|
|
94
|
+
sniper: {
|
|
95
|
+
type: 'sniper',
|
|
96
|
+
name: 'Sniper',
|
|
97
|
+
slot: 1,
|
|
98
|
+
damage: 100,
|
|
99
|
+
fireRate: 40,
|
|
100
|
+
reloadTime: 3.5,
|
|
101
|
+
magazineSize: 5,
|
|
102
|
+
reserveAmmo: 20,
|
|
103
|
+
spread: 0.5,
|
|
104
|
+
range: 150,
|
|
105
|
+
moveSpeed: 0.8,
|
|
106
|
+
pellets: 1,
|
|
107
|
+
isAutomatic: false,
|
|
108
|
+
headshotMultiplier: 4.0,
|
|
109
|
+
cost: 4750,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
// ============ Utility Functions ============
|
|
113
|
+
export function createVec3(x = 0, y = 0, z = 0) {
|
|
114
|
+
return { x, y, z };
|
|
115
|
+
}
|
|
116
|
+
export function vec3Add(a, b) {
|
|
117
|
+
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
|
118
|
+
}
|
|
119
|
+
export function vec3Sub(a, b) {
|
|
120
|
+
return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
|
|
121
|
+
}
|
|
122
|
+
export function vec3Scale(v, s) {
|
|
123
|
+
return { x: v.x * s, y: v.y * s, z: v.z * s };
|
|
124
|
+
}
|
|
125
|
+
export function vec3Length(v) {
|
|
126
|
+
return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
|
127
|
+
}
|
|
128
|
+
export function vec3Normalize(v) {
|
|
129
|
+
const len = vec3Length(v);
|
|
130
|
+
if (len === 0)
|
|
131
|
+
return { x: 0, y: 0, z: 0 };
|
|
132
|
+
return { x: v.x / len, y: v.y / len, z: v.z / len };
|
|
133
|
+
}
|
|
134
|
+
export function vec3Distance(a, b) {
|
|
135
|
+
return vec3Length(vec3Sub(a, b));
|
|
136
|
+
}
|
|
137
|
+
export function vec3Dot(a, b) {
|
|
138
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "csterm-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multiplayer game server for CSterm - Counter-Strike in your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"csterm-server": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"start:hub": "node dist/index.js --hub-only",
|
|
17
|
+
"start:pool": "node dist/index.js --pool",
|
|
18
|
+
"dev": "tsc --watch & node --watch dist/index.js",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"game",
|
|
23
|
+
"server",
|
|
24
|
+
"multiplayer",
|
|
25
|
+
"websocket",
|
|
26
|
+
"fps",
|
|
27
|
+
"counter-strike",
|
|
28
|
+
"terminal",
|
|
29
|
+
"csterm"
|
|
30
|
+
],
|
|
31
|
+
"author": "idanbeck",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": ""
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"ws": "^8.14.2",
|
|
42
|
+
"uuid": "^9.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.10.0",
|
|
46
|
+
"@types/ws": "^8.5.10",
|
|
47
|
+
"@types/uuid": "^9.0.7",
|
|
48
|
+
"typescript": "^5.3.2"
|
|
49
|
+
}
|
|
50
|
+
}
|