Home Reference Source

src/client/js/socket.js

import { GLOBAL, generateID } from './global'
import { cookieInputs, quitGame, updateLobby, updateScores, hideElement, displayWinner, updateAtomList } from './app'
import ChatClient from './lib/chat-client'
import { loadTextures, app, createPlayer, isSetup, startGame, setIngame, spritesheet } from './pixigame'
import { createRenderAtom, createRenderCompound } from './obj/create'
import { DamageIndicator } from './obj/damageindicator'

/**
 * Socket.js contains all of the clientside networking interface.
 * It contains all variables which are synced between client and server.
 */

// Socket.io instance
export var socket

/* Object containing all synced objects. Contains nested objects, which correspond to different types
 * (for example, objects[atoms], objects[players], objects[compounds])
 */
export var objects = {
	players: {},
	atoms: {},
	compounds: {},
	tiles: {}
}

/**
 * Team colors object. Number corresponds to index at GLOBAL.TEAM_COLORS.
 * Format: {
 * 	teamname1: 0,
 * 	teamname: colorid,
 * 	...
 * }
 */
export var teamColors = {}

/**
 * Attempts to connect to the server. Run on 'start game' press.
 *  - Manages connecting to main server vs. devserver
 *  - Sets up socket listeners
 *  - Loads textures
 *  - Loads pixi
 */
export function beginConnection() {
	// Joins debug server if conditions are met
	let room = (cookieInputs[7].value === 'private' ? cookieInputs[1].value : GLOBAL.NO_ROOM_IDENTIFIER)
	let teamInput = (document.querySelector('input[name="queue-type"]:checked').id === 'team-option') ? cookieInputs[2].value : GLOBAL.NO_TEAM_IDENTIFIER

	// if (cookieInputs[1].value === 'test') {
	// 	console.info('Connecting to: ' + GLOBAL.TEST_IP)
	// 	// DEVELOPMENT server - auto deploy from pixi branch
	// 	socket = io.connect(GLOBAL.TEST_IP, {
	// 		query: `room=${room}&name=${cookieInputs[0].value}&team=${teamInput}&roomType=${cookieInputs[7].value}`,
	// 		reconnectionAttempts: 3
	// 	})
	// }
	// else if (cookieInputs[1].value === 'jurassicexp') {
	// 	console.log('Dev Backdoor Initiated! Connecting to devserver')
	// 	// Local server
	// 	socket = io.connect(GLOBAL.LOCAL_HOST, {
	// 		query: `room=${room}&name=${cookieInputs[0].value}&team=${teamInput}&roomType=${cookieInputs[7].value}`,
	// 		reconnectionAttempts: 3
	// 	})
	// }
	// else {
	// 	// Production server
	// 	console.log('connecting to main server')
	// 	socket = io.connect(GLOBAL.SERVER_IP, {
	// 		query: `room=${room}&name=${cookieInputs[0].value}&team=${teamInput}&roomType=${cookieInputs[7].value}`,
	// 		reconnectionAttempts: 3
	// 	})
	// }

	console.log('Connecting to localhost')
	// Local server
	socket = io.connect(GLOBAL.LOCAL_HOST, {
		query: `room=${room}&name=${cookieInputs[0].value}&team=${teamInput}&roomType=${cookieInputs[7].value}`,
		reconnectionAttempts: 3
	})

	socket.on('connect', () => {
		setupSocket()
		// Init pixi
		loadTextures()
		if (typeof app !== 'undefined') {
			app.start()
		}
	})
}

/**
 * Run on disconnect to reset all server-based variables and connections
 */
export function disconnect() {
	app.stop()
	socket.disconnect()

	// Wipe objects list
	for (let objType in objects) {
		objects[objType] = {}
	}
}

/**
 * First time setup when connection starts. Run on connect event to ensure that the socket is connected first.
 */
function setupSocket() {
	// Debug
	console.log('Socket:', socket)

	// Instantiate Chat System
	let chat = new ChatClient({ player: cookieInputs[0].value, room: cookieInputs[1].value, team: cookieInputs[2].value })
	chat.addLoginMessage(cookieInputs[0].value, true)
	chat.registerFunctions()

	// Setup listeners
	setupSocketConnection()
	setupSocketInfo(chat)
	setupSocketObjectRetrieval()

	// Emit join message,
	socket.emit('playerJoin', { sender: chat.player, team: chat.team })
}

/**
 * Sets up socket object syncing.
 * Run in setupSocket().
 */
function setupSocketObjectRetrieval() {
	// Syncs all objects from server once a frame
	socket.on('objectSync', (data) => {
		for (let objType in data) {
			if (objType !== 'tiles') {
				for (let obj in data[objType]) {
					if (data[objType][obj] !== null) {
						let objRef = data[objType][obj]
						let clientObj = objects[objType][obj]
						// Already exists in database
						if (clientObj !== undefined && clientObj !== null) {
							if (objRef.id !== socket.id) {
								objects[objType][obj].setData(objRef.posX, objRef.posY, objRef.vx, objRef.vy)
							}
							if (objType === 'players') {
								objects[objType][obj].health = objRef.health
								objects[objType][obj].damagedBy = objRef.damagedBy
								objects[objType][obj].atomList = objRef.atomList
								objects[objType][obj].speedMult = objRef.speedMult

								if (objRef.spectating && !objects[objType][obj].spectating) {
									objects[objType][obj].spectating = true
									objects[objType][obj].beginSpectate()
								}

								if (objects[objType][obj].shield !== objRef.shield || objects[objType][obj].stronghold !== objRef.stronghold) {
									objects[objType][obj].changeSprite(objRef.shield, objRef.stronghold)
									console.log('change tex')
								}

								objects[objType][obj].shield = objRef.shield
								objects[objType][obj].stronghold = objRef.stronghold

								if (objRef.id === socket.id) {
									for (let atom in objRef.atomList) {
										updateAtomList(atom)
									}
								}
							}
							if (objType === 'compounds' && objRef.ignited) {
								objects[objType][obj].ignited = objRef.ignited
							}
						}
						// Does not exist - need to clone to clientside
						else if (isSetup) {
							switch (objType) {
								case 'players':
									objects[objType][obj] = createPlayer(objRef)
									break
								case 'atoms':
									objects[objType][obj] = createRenderAtom(objRef)
									break
								case 'compounds':
									objects[objType][obj] = createRenderCompound(objRef)
									break
							}
						}
					}
				}
			}
			// else { //Tile drawing
			//     for (let tile of data.tiles) {

			//         let tileName = 'tile_' + tile.col + '_' + tile.row;
			//         if (objects.tiles[tileName] === undefined) {
			//             // console.log(tileName);
			//             objects.tiles[tileName] = new MapTile(MAP_LAYOUT[tile.row][tile.col], tile.col, tile.row);
			//         }

			//     }
			// }
		}
	})

	// Sync objects when they are deleted or move out of view. ONLY call after objectSync to avoid issue
	socket.on('serverSendObjectRemoval', (data) => {
		if (GLOBAL.VERBOSE_SOCKET) {
			console.info('serverSendObjectRemoval() called on: ')
			console.info(data)
			console.info(objects[data.type][data.id])
			console.info(objects)
		}
		if (objects[data.type][data.id] === undefined || objects[data.type][data.id] === null) {
			if (GLOBAL.VERBOSE_SOCKET) {
				console.warn('serverSendObjectRemoval() called on invalid object. Retry.', data)
			}
			setTimeout(() => {
				try {
					if (removeObject(data)) {
						if (GLOBAL.VERBOSE_SOCKET) {
							console.info('Retry successfully removed object. While this worked, it should not happen. Please fix root cause of issue. ')
						}
						return 0
					}
				}
				catch (err) {
					if (GLOBAL.VERBOSE_SOCKET) {
						console.error('Retry failed. Object removal failed. Abandoning request. ')
					}
					return 1
				}
				// removeObject(data);
			}, 1000 / 60)
			// return 1
		}
		else {
			// console.log(objects[data.type][data.id].destroyed);
			// An object was removed
			if (!objects[data.type][data.id].destroyed) { // Only remove if not already
				removeObject(data)
			}
			else {
				console.warn('serverSendObjectRemoval() called despite object has already been destroyed.') // Sanity check
				return 1
			}
		}

		// Must keep checking if the object was not created at time of destruction.
		// One example of this needing to be run is when a player instantly collects an atom on spawn.
		// if (objects[data.type][data.id] === undefined) {
		// 	let thisInterval = setTimeout(() => {
		// 		if (objects[data.type][data.id].destroyed) {
		// 			clearInterval(thisInterval)
		// 		}
		// 		else {
		// 			removeObject(data)
		// 		}
		// 	}, 200)
		// }
	})
}

/**
 * Sets up socket connection listeners.
 * Run in setupSocket().
 */
function setupSocketConnection() {
	// On Connection Failure
	socket.on('reconnect_failed', () => {
		alert('You have lost connection to the server!')
	})

	socket.on('reconnecting', (attempt) => {
		console.log('Lost connection. Reconnecting on attempt: ' + attempt)
		quitGame('Lost connection to server')
	})

	socket.on('reconnect_error', (err) => {
		console.log('CRITICAL: Reconnect failed! ' + err)
	})

	socket.on('pong', (ping) => {
		console.log('Your Ping Is: ' + ping)
	})

	socket.on('disconnectedPlayer', (data) => {
		console.log('Player ' + data.id + ' has disconnected')
		chat.addSystemLine('Player ' + objects.players[data.id].name + ' has disconnected')
		if (objects.players[data.id] !== undefined) {
			objects.players[data.id].hide()
			delete objects.players[data.id]
		}
	})

	socket.on('serverSendDisconnect', () => {
		quitGame('The game has ended.', false)
		hideElement('winner-panel')
	})

	// Errors on join
	socket.on('connectionError', (data) => {
		socket.disconnect()
		quitGame(data.msg, true)
	})
}

/**
 * Sets up socket information transfer listeners.
 * Run in setupSocket().
 * @param {*} chat The chat client instance to be used for notifications
 */
function setupSocketInfo(chat) {
	// Chat system receiver
	socket.on('serverMSG', data => {
		chat.addSystemLine(data)
	})

	socket.on('serverAnnouncement', data => {
		chat.addChatAnnouncement(data.message, data.sendingTeam)
	})

	socket.on('serverSendPlayerChat', data => {
		chat.addChatLine(data.sender, data.message, false, data.sendingTeam)
	})

	socket.on('serverSendLoginMessage', data => {
		chat.addLoginMessage(data.sender, false)
	})

	// Receive information about room players
	socket.on('roomInfo', (data) => {
		// Update lobby info. Pass to app.js
		updateLobby(data)

		// if(GLOBAL.DEBUG) {
		//     console.log("rcvd: ",data);
		// }
	})

	socket.on('serverSendStartGame', (data) => {
		console.log('game has started')
		startGame(false, data.teams)
	})

	socket.on('levelUp', (data) => {
		console.log('You LEVELED UP! Level: ' + data.newLevel)
	})

	// Respawn
	socket.on('serverSendPlayerDeath', (data) => {
		console.log('You Died!')
		objects.players[socket.id].setData(data.posX, data.posY, data.vx, data.vy)
		socket.emit('verifyPlayerDeath', { id: socket.id })
		console.log(objects.players[socket.id])
		updateAtomList()
	})

	// Another player died
	socket.on('serverSendNotifyPlayerDeath', (data) => {
		// Append to chat TODO
	})

	// Update timer
	socket.on('time', (data) => {
		document.getElementById('timer').innerHTML = '<p>' + data.time + '</p>'
	})

	// Update scores
	socket.on('serverSendScoreUpdate', (data) => {
		updateScores(data.teamSlot, data.increment)
	})

	// A player has won
	socket.on('serverSendWinner', (data) => {
		setIngame(false) // Disable keyboard controls and rendering
		displayWinner(data)
	})

	// Sync team colors
	socket.on('serverSendTeamColors', (data) => {
		teamColors = data
		console.log(teamColors)
	})

	// Change texture when a tile has been captured
	socket.on('serverSendTileCapture', (data) => {
		objects.tiles['tile_' + data.tileX + '_' + data.tileY].texture = (spritesheet.textures[data.teamNumber + objects.tiles['tile_' + data.tileX + '_' + data.tileY].tile.texture])
		// console.log(objects.tiles['tile_' + data.tileY + '_' + data.tileX].texture)
	})

	// Tile health changed
	socket.on('serverSendTileHealth', (data) => {
		objects.tiles['tile_' + data.tileX + '_' + data.tileY].updateHealth(data.newHealth)
	})

	// Damage Indicators
	objects.di = {}
	socket.on('serverSendDamageIndicator', (data) => {
		let randID = generateID()
		objects.di[randID] = (new DamageIndicator(data.damage, data.posX, data.posY))
		setTimeout(() => {
			app.stage.removeChild(objects.di[randID])
			delete objects.di[randID]
		}, 1000)
	})
}

/*
 ********************
 * Helper Functions *
 ********************
*/

// Helper function for serverSendObjectRemoval
function removeObject(data) {
	if (objects[data.type][data.id] !== undefined && objects[data.type][data.id] !== null) {
		objects[data.type][data.id].hide()
		objects[data.type][data.id].destroy()
		// delete objects[data.type][data.id];
		return true
	}
	else {
		return false
	}
}