forked from techninja/ninjanode
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ninjaserver.js
executable file
·477 lines (409 loc) · 13.6 KB
/
ninjaserver.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
/**
* @file ninjanode main node.js server file!
* - Install with 'npm install', requires express & socket.io
* - Run with 'node ninjaserver.js [port]' replace "[port]" with HTTP port,
* if run without argument, defaults to port 4242
* - Once running, visit localhost:[port] and you'll be up and running!
*
*/
var arguments = process.argv.splice(2);
var port = arguments[0] ? arguments[0] : 4242;
var sanitizer = require('sanitizer');
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io').listen(server, {log: false});
var ships = require('./ninjaroot/ninjaships.node.js');
var lastData = {}; // Ensures duplicate data for positions isn't sent
var lastShieldData = {}; // Ensures duplicate data for shield values isn't sent
var lastPowerUpData = {}; // Ensures duplicate data for powerup values isn't sent
var lastPowerUpOrbData = {}; // Ensures duplicate data for powerup orbs isn't sent
var users = {count:0, playerCount:0} // ID keyed object to hold on to user data for stats
// Start express hosting the site from "ninjaroot" folder on the given port
server.listen(port);
app.use("/", express.static(__dirname + '/ninjaroot'));
console.log('ninjanode server listening on localhost:' + port);
// ninjanode API!
// Return the entire users object, with players, names, kill/death stats etc.
app.get("/users", function(req, res){
res.set('Content-Type', 'application/json');
res.send(JSON.stringify(users));
});
// Return just the numbers of playing and lobby users
app.get("/users/count", function(req, res){
res.set('Content-Type', 'application/json');
res.send(JSON.stringify(
{
lobby: users.count - users.playerCount,
players: users.playerCount
}
));
});
// All websockets client connect/disconnect management
io.sockets.on('connection', function (clientSocket) {
var id = clientSocket.id;
users.count++;
users[id] = {
status: 'lobby',
name: '',
type: '',
started: new Date().getTime()
}
// User is expected to 'connect' immediately, but isn't in game until they send
// their name, ship type, etc.
console.log('New user connected: ' + id);
// Send out list of existing ships & projectiles for this new client (gets sent to everyone)
emitAllShips(id);
emitAllProjectiles(id);
emitAllShipTypes(id)
emitAllPowerUps(id);
emitAllPNBITS(id);
// This client's new ship data recieved! Create it.
clientSocket.on('shipstat', function (data) {
if (data.status == 'create'){ // New ship!
users[id].status = 'playing';
users[id].deaths = 0;
users[id].kills = 0;
users.playerCount++;
data.name = sanitizer.escape(data.name);
console.log('Creating ship for user: ' + id + ': ' + data.name);
data.style = sanitizer.sanitize(data.style);
data.id = id;
data.hit = shipHit;
data.boom = shipBoom;
ships.addShip(data);
// Only store verified and cleaned user input data
var s = ships.shipGet(id);
users[id].name = s.name;
users[id].type = s.data.name;
emitAllShips();
emitSystemMessage(id, 'join'); // Must send after create...
}
});
// Client disconnected! Let everyone else know...
clientSocket.on('disconnect', function (){
console.log('Disconnected user: ' + id);
users.count--; // Remove from total user count
// Only if the connected socket was playing...
if (users[id].status == 'playing'){
users.playerCount--; // Remove from player count
// Send ship destroy and disconnect message
var shipStat = {};
shipStat[id] = {status: 'destroy'};
emitSystemMessage(id, 'disconnect'); // Must send before delete...
io.sockets.emit('shipstat', shipStat);
ships.shipRemove(id);
}
delete users[id];
});
// Broadcast incoming chats to all clients
clientSocket.on('chat', function (data) {
io.sockets.emit('chat', {
type: 'chat',
msg: sanitizer.escape(data.msg),
id: id
});
});
// Keypresses from individual clients
clientSocket.on('key', function (data) {
switch (data.c){
case 'u': // Up (thrust forward)
ships.shipSetThrust(id, data.s ? 1 : 0); break;
case 'd': // Down (thrust back)
ships.shipSetThrust(id, data.s ? -1 : 0); break;
case 'l':
case 'r': // Turn Right/Left
ships.shipSetTurn(id, data.s ? data.c : false); break;
case 's':
case 'f': // Main/Secondary Fire
if (data.s) {
ships.shipSetFire(id, projectileCreate, projectileDestroy, data.c == 'f' ? 0 : 1);
}
break;
case 'm': // Mouse / touch control
ships.shipSetTouch(id, data.s ? data.d : false);
}
});
// Handle projectile creation emit
function projectileCreate(data){
var p = {};
p[id + '_' + data.id] = {
shipID: id,
status: 'create',
pos: data.pos,
weaponID: data.weaponID,
style: data.style,
type: data.type
};
io.sockets.emit('projstat', p);
}
// Handle projectile destruction emit
function projectileDestroy(data){
var p = {};
p[id + '_' + data.id] = {
status: 'destroy'
};
io.sockets.emit('projstat', p);
}
// Send out ship hit status
function shipHit(data){
var out = {};
var includeScore = false;
out[data.target.id] = {
status: 'hit',
type: data.type,
weapon: data.weapon ? data.weapon.type : 'none'
};
// Send a system message for death if ship to ship collision...
if (data.type == 'collision'){
users[data.source.id].deaths++;
users[data.target.id].deaths++;
includeScore = true;
emitSystemMessage(data.source.id, data.type, data.target.id);
}
// Send a system message for death if ship to PNBITS collision
if (data.type == 'pnbcollision' && data.target.shieldPowerStatus == 0){
users[data.target.id].deaths++;
includeScore = true;
emitSystemMessage(data.target.id, data.type, data.source.name);
}
// ...or if target shield power is 0
if (data.type == 'projectile' && data.target.shieldPowerStatus == 0){
users[data.source.id].kills++;
users[data.target.id].deaths++;
includeScore = true;
emitSystemMessage(data.source.id, data.type, data.target.id);
}
// Only if the score has changed... include it with the hit
if (includeScore) {
out[data.target.id].scores = {};
// If the source is a user (not an inamate object)
if (users[data.source.id]) {
out[data.target.id].scores[data.source.id] = {
kills: users[data.source.id].kills,
deaths: users[data.source.id].deaths
}
}
out[data.target.id].scores[data.target.id] = {
kills: users[data.target.id].kills,
deaths: users[data.target.id].deaths
}
}
io.sockets.emit('shipstat', out);
}
// Send out ship exploding status
function shipBoom(data){
var out = {};
out[data.id] = {
status: 'boom',
stage: data.stage
};
io.sockets.emit('shipstat', out);
}
});
// Send out shipstat for every ship to everyone (for creation)
function emitAllShips(targetID){
var listShips = ships.shipGet();
var out = {};
for (var id in listShips){
out[id] = {
status: 'create',
name: listShips[id].name,
sounds: [listShips[id].data.weapons[0].data.sound, listShips[id].data.weapons[1].data.sound],
style: listShips[id].style,
shieldStyle: listShips[id].data.shield.style,
pos: listShips[id].pos,
score: {kills: users[id].kills, deaths: users[id].deaths}
}
}
if (Object.keys(out).length){
if (targetID){
// TODO: Get targetID to send to JUST that socket.io ID!
}
io.sockets.emit('shipstat', out);
}
}
// Send out ship types to all users (really uneccesary)
function emitAllShipTypes(id){
// TODO: Tweak/Add to data?
io.sockets.emit('shiptypes', ships.shipTypesGet());
}
// Send out positions for every new ship position to everyone
function emitShipPositionUpdates(){
var positions = ships.getAllPos();
var out = {};
// Only add to the output json that has changed since last send
for (var id in positions){
if (lastData[id] != positions[id].str) {
lastData[id] = positions[id].str;
out[id] = positions[id].pos;
out[id].vel = positions[id].vel;
}
}
// Only *if* there's useful data to be sent, send only that pos data to all clients
if (Object.keys(out).length) {
io.sockets.emit('pos', out);
}
}
// Send out shield amounts for every new shield change to everyone
function emitShipShieldUpdates(){
var vessels = ships.shipGet();
var out = {};
// Only add to the output json that has changed since last send
for (var id in vessels){
var roundedPercent = (vessels[id].shieldPowerStatus * 100) / vessels[id].data.shield.max;
roundedPercent = Math.round(roundedPercent / 5) * 5;
if (lastShieldData[id] != roundedPercent) {
lastShieldData[id] = roundedPercent;
out[id] = {
status: 'shield',
amount: roundedPercent
};
}
}
// Only *if* there's useful data to be sent, send that data to all clients
if (Object.keys(out).length) {
io.sockets.emit('shipstat', out);
}
}
// Send out updates on powerup orb visibility
function emitPowerUpOrbUpdates(){
var powerUps = ships.powerUpGet();
var out = {};
// Only add to the output json that has changed since last send
for (var i in powerUps){
if (lastPowerUpOrbData[i] != powerUps[i].visible) {
lastPowerUpOrbData[i] = powerUps[i].visible;
out[i] = {
visible: powerUps[i].visible
};
}
}
// Only *if* there's useful data to be sent, send that data to all clients
if (Object.keys(out).length) {
io.sockets.emit('powerupstat', out);
}
}
// Send out updates on powerup use
function emitShipPowerUpUpdates(){
var vessels = ships.shipGet();
var out = {};
// Only add to the output json that has changed since last send
for (var id in vessels){
var list = vessels[id].powerUps.active.join(' ');
if (lastPowerUpData[id] != list) {
lastShieldData[id] = list;
out[id] = {
status: 'powerup',
addClasses: list,
removeClasses: vessels[id].powerUps.inactive.join(' ')
};
}
}
// Only *if* there's useful data to be sent, send that data to all clients
if (Object.keys(out).length) {
io.sockets.emit('shipstat', out);
}
}
// Send out positions for every projectile position to everyone
function emitProjectilePositionUpdates(){
var projectiles = ships.getActiveProjectiles();
var out = {};
// Filter out non-moving projectiles, and simplify output to just positions
for (var i in projectiles) {
var proj = projectiles[i];
if (proj.data.speed){
out[i] = {
x: Math.round(proj.pos.x * 100)/100,
y: Math.round(proj.pos.y * 100)/100,
d: proj.pos.d
};
}
}
if (Object.keys(out).length) {
io.sockets.emit('projpos', out);
}
}
// Send out projstat for every projectile to everyone (for creation on connect)
function emitAllProjectiles(targetID){
var projectiles = ships.getActiveProjectiles();
var out = {};
for (var id in projectiles){
var proj = projectiles[id];
out[id] = {
shipID: proj.shipID,
status: 'create',
pos: proj.pos,
noSound: true, // Don't play the sound for bulk create
weaponID: proj.weaponID,
style: proj.style,
type: proj.type
}
}
if (Object.keys(out).length) {
if (targetID){
// TODO: Get targetID to send to JUST that socket.io ID!
}
io.sockets.emit('projstat', out);
}
}
// Send out pustat for every powerup to everyone (for creation on connect)
function emitAllPowerUps(targetID){
var powerUps = ships.getPowerUps();
var out = {};
for (var id in powerUps){
out[id] = {
pos: powerUps[id].pos,
cssClass: powerUps[id].type.id,
visible: powerUps[id].visible
}
}
if (Object.keys(out).length) {
if (targetID){
// TODO: Get targetID to send to JUST that socket.io ID!
}
io.sockets.emit('powerupstat', out);
}
}
// Send out pnbitsstat for every PNBITS to everyone (for creation on connect)
function emitAllPNBITS(targetID){
var pnbits = ships.getPNBITS();
var out = {};
for (var id in pnbits){
out[id] = {
pos: pnbits[id].pos,
cssClass: pnbits[id].type.cssClass,
radius: pnbits[id].radius
}
}
if (Object.keys(out).length) {
if (targetID){
// TODO: Get targetID to send to JUST that socket.io ID!
}
io.sockets.emit('pnbitsstat', out);
}
}
// Send out system messages
function emitSystemMessage(id, action, target){
var out = {
type: 'system',
action: action,
id: id,
target: target
}
io.sockets.emit('chat', out);
}
// Main loop to run processing for all ship positions, collisions, projectiles
// Also compiles changed positions and sends out to all clients
setInterval(function(){
ships.processShipFrame();
emitShipShieldUpdates();
emitShipPositionUpdates();
emitProjectilePositionUpdates();
emitShipPowerUpUpdates();
emitPowerUpOrbUpdates();
}, 60);
// Every 5 Minutes clear out lastData cache (frees memory from disconnected clients)
setInterval(function(){
lastData = {};
}, 5 * 60 * 1000);