Home Reference Source

src/server/server.js

const express = require('express')
const app = express()
const http = require('http').Server(app)
const io = require('socket.io')(http)
import colors from 'colors' // Console colors :D
import { GLOBAL } from '../client/js/global'
import { roomMatchmaker } from './utils/matchmaker'
import { initGlobal, initPlayer } from './utils/serverinit'
import { frameSync } from './utils/framesync'
import { damage } from './utils/ondamage'
import { createCompound } from './utils/compound'
import { spawnAtomAtVent } from './utils/atoms'
import { getTeamColors } from './utils/serverutils'
var config = require('./config.json')

const DEBUG = true

app.use(express.static(`${__dirname}/../client`))

/* Contains all game data, including which rooms and players are active.
 *
 * Structure of Rooms object:
 *
// rooms = {
//     roomName: {
//         joinable: true,
//         type: '4v4',
//         teams: [
//             name: 'teamname',
//             players: ['id1', 'id2'...],
//			   dead: false
//         ],
//         players: { id, name, room, team, health, posX, posY, vx, vy, dead, experience, damagedBy, stronghold, defense, spectating },
//         atoms: { typeID, id, posX, posY, vx, vy, team },
//         compounds: {	id, posX, posY, vx, vy, blueprint, sendingTeam, sender },
//		   tiles: { id, type, globalX, globalY, captured, owner, health }
//         time: {
//			   frames: 0,
//             minutes: 0,
//             seconds: 0,
//             formattedTime: '0:00'
//         }
//     }
// }
*/
let rooms = {}

/**
 * Teams object containing all the currently playing teams.
 * Structure:
 * teamName: {
 *    room: 'roomName',
 *    players: ['playerSocketId', 'player2SocketId', ...],
 *    joinable: false/true
 * }
 *
 * -> Create a Team when the first player joins any lobby. Populate room when this occurs.
 * -> Change joinable to false when a Team is either full or the game has begun.
 * -> Delete the room from the database when the last player leaves.
 * -> There cannot be two teams with the same name. Throw an error if this occurs.
 */
let teams = {}

// Initialize Server. Includes atom spawning and timer mechanics
initGlobal()

// Initialize all socket listeners when a request is established
io.on('connection', socket => {
	// Local variable declaration
	let room = socket.handshake.query.room
	let team = socket.handshake.query.team

	// Run matchmaker
	let matchData = roomMatchmaker(socket, room, team)
	room = matchData.room
	team = matchData.team

	// Init player
	initPlayer(socket, room, team)
	let thisPlayer = rooms[room].players[socket.id]
	thisPlayer.team = team
	thisPlayer.atomList = {}
	thisPlayer.speedMult = 1
	for (let atom of GLOBAL.ATOM_IDS) {
		thisPlayer.atomList[atom] = 0
	}

	// Announce colors
	socket.emit('serverSendTeamColors', getTeamColors(room))
	socket.to(room).emit('serverSendTeamColors', getTeamColors(room))

	// Receives a chat from a player, then broadcasts it to other players
	socket.to(room).on('playerChat', data => {
		// console.log('sender: ' + data.sender);
		const _sender = data.sender.replace(/(<([^>]+)>)/ig, '')
		const _message = data.message.replace(/(<([^>]+)>)/ig, '')

		console.log('[CHAT] '.bold.blue + `${(new Date()).getHours()}:${(new Date()).getMinutes()} ${_sender}: ${_message}`.magenta)

		socket.to(room).broadcast.emit('serverSendPlayerChat', { sender: _sender, message: _message.substring(0, 35), sendingTeam: data.sendingTeam })
	})

	// Other player joins the socket.to(room)
	socket.to(room).on('playerJoin', data => {
		// console.log('sender: ' + data.sender);
		const _sender = data.sender.replace(/(<([^>]+)>)/ig, '')
		socket.to(room).broadcast.emit('serverSendLoginMessage', { sender: _sender, team: data.team })
		if (DEBUG) {
			socket.to(room).broadcast.emit('serverMSG', 'You are connected to a DEBUG enabled server. ')
		}
	})

	// Broadcasts player join message

	socket.to(room).broadcast.emit('serverSendLoginMessage', {
		sender: socket.id
	})
	if (DEBUG) {
		socket.to(room).broadcast.emit('serverMSG', 'You are connected to a DEBUG enabled server. ')
	}

	// Hides the lobby screen if the game has already started
	if (rooms[room].started) {
		socket.emit('serverSendStartGame', { teams: rooms[room].teams })
	}

	/**
   * On player movement:
   * data is in format
   *  - id: index of player that moved
   *  - type: atoms, players, or compounds
   *  - posX: new x position
   *  - posY: new y position
   *  - vx: x-velocity
   *  - vy: y-velocity
   */
	socket.to(room).on('move', data => {
		// Player exists in database already because it was created serverside - no need for extra checking
		if (rooms[room][data.type][data.id] !== undefined && !rooms[room][data.type][data.id].dead) {
			rooms[room][data.type][data.id].posX = data.posX
			rooms[room][data.type][data.id].posY = data.posY
			rooms[room][data.type][data.id].vx = data.vx
			rooms[room][data.type][data.id].vy = data.vy
		}
	})

	socket.to(room).on('damage', data => {
		damage(data, room, socket)
	})

	socket.on('verifyPlayerDeath', data => {
		rooms[room].players[data.id].dead = false
	})

	// A player spawned a Compound
	socket.to(room).on('requestCreateCompound', data => {
		let newCompound = createCompound(data, room, thisPlayer, socket)
		if (newCompound) {
			rooms[room].compounds[newCompound.id] = newCompound
		}
	})

	socket.on('startGame', data => {
		console.log('Game has started in room ' + room)
		// Make the room and teams unjoinable
		for (let tm of rooms[room].teams) {
			teams[tm.name].joinable = false
		}
		rooms[room].joinable = false

		// Assign nucleus teams
		for (let tile in rooms[room].tiles) {
			if (rooms[room].tiles[tile].type === 'nucleus') {
				// console.log(rooms[room].teams)
				try {
					rooms[room].tiles[tile].owner = rooms[room].teams[parseInt(rooms[room].tiles[tile].id.substring(1))].name
				}
				catch (e) {
					// console.warn('No team exists for nucleus ' + rooms[room].tiles[tile].id.substring(1))
				}
			}
		}

		socket.broadcast.to(room).emit('serverSendStartGame', { start: data.start, teams: rooms[room].teams })
		socket.emit('serverSendStartGame', { start: data.start, teams: rooms[room].teams })
		rooms[room].started = true
	})

	// Testing purposes- give yourself 5000 of each atom
	socket.on('testCommand', (data) => {
		if (GLOBAL.DEBUG) {
			// console.log(rooms[room].players[data.player].atomList)
			for (let i in rooms[room].players[data.player].atomList) {
				rooms[room].players[data.player].atomList[i] += 5000
			}
		}
	})

	socket.on('disconnect', data => {
		console.log('[Server]'.bold.blue + ' Disconnect Received: '.red + ('' + socket.id).yellow + ('' + rooms[room].players[socket.id]).green + ': ' + data)

		socket.to(room).broadcast.emit('disconnectedPlayer', { id: socket.id }) // Broadcast to everyone in the room to delete the player

		delete rooms[room].players[socket.id] // Remove the server side player

		// Delete room if there is nobody inside
		if (Object.keys(rooms[room].players).length === 0) {
			console.log('[Server] '.bold.blue + 'Closing room '.red + (room + '').bold.red)
			delete io.sockets.adapter.rooms[socket.id]
			delete rooms[room]

			if (room !== GLOBAL.NO_ROOM_IDENTIFIER) {
				// Remove from teams array
				teams[team].players.splice(teams[team].players.indexOf(socket.id), 1)
				// rooms[room].teams[team].players.splice(rooms[room].teams[team].players.indexOf(socket.id), 1);

				// Delete team if all players have left
				if (teams[team].players.length === 0) {
					delete teams[team]
				}

				// Final sweep over teams object
				for (let team in teams) {
					if (teams[team].room === room) {
						delete teams[team]
					}
				}
			}
		}
	})
})

// Notify on console when server has started
const serverPort = process.env.PORT || config.port
http.listen(serverPort, () => {
	rooms = {}
	console.log('[Server] '.bold.blue + `started on port: ${serverPort}`.blue)
})

/**
 * Sets a new value for a protected server field.
 * Adopted from https://stackoverflow.com/questions/18936915/dynamically-set-property-of-nested-object
 * @param {*} value The value to set
 * @param {*} path Array containing all of the subobject identifiers, with the 0th index being the lowest level.
 *                 Example: rooms.myRoom.players could be accessed through a path value of ['rooms', 'myRoom', 'players']
 */
export function setField (value, path) {
	if (path === undefined || path.length === 0) {
		throw new Error('Error in setField: path cannot be empty')
	}

	let schema = (path[0] === 'rooms') ? rooms : (path[0] === 'teams') ? teams : undefined
	if (schema === undefined) {
		throw new Error('Base object ' + path[0] + ' does not exist!')
	}

	let len = path.length
	for (let i = 1; i < len - 1; i++) {
		let elem = path[i]
		if (!schema[elem]) schema[elem] = {}
		schema = schema[elem]
	}

	schema[path[len - 1]] = value
}

/**
 * Shorthand to add or concatenate an amount to a field.
 * Best used with numbers or strings.
 * @param {*} amount Amount to increment the field by.
 * @param {*} path Path to the field.
 */
export function incrementField (amount, path) {
	setField(getField(path) + amount, path)
}

/**
 * Returns the value given a path to that value.
 * Adopted from https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
 * @param {*} path Array containing all of the subobject identifiers, with the 0th index being the lowest level.
 *                 Example: rooms.myRoom.players could be accessed through a path value of ['rooms', 'myRoom', 'players']
 * @returns The value for the given field.
 */
export function getField (path) {
	if (path === undefined || path.length === 0) {
		throw new Error('Error in setField: path cannot be empty')
	}
	if (path.length === undefined) {
		throw new Error('Error in setField: path must be an array')
	}

	let obj = (path[0] === 'rooms') ? rooms : (path[0] === 'teams') ? teams : undefined
	if (obj === undefined) {
		throw new Error('Error in setField: Base object ' + path[0] + ' does not exist!')
	}

	for (let i = 1; i < path.length; i++) {
		obj = obj[path[i]]
	}
	// console.log(path, obj);
	return obj
}

/**
 * Deletes one of the three types of gameObjects synced to the server
 * @param {string} type Either players, atoms, compounds
 * @param {*} id ID of the object to delete
 * @param {string} room Room name to delete in
 * @param {*} socket socket.io instance. INDEPENDENT OF PLAYER (any valid socket connection can go here!!!!!)
 */
export function deleteObject (type, id, room, socket) {
	delete rooms[room][type][id]

	// Send clientside message
	socket.to(room).broadcast.emit('serverSendObjectRemoval', { id: id, type: type })
	socket.emit('serverSendObjectRemoval', { id: id, type: type })
}