Home Reference Source

src/client/js/app.js

/**
 * App.js is responsible for connecting the UI with the game functionality.
 * Most of the functionality is used for the main menu and connecting/disconnecting behavior.
 */
'use strict'
import { GLOBAL } from './global.js'
import * as cookies from './lib/cookies.js'
import { BLUEPRINTS, TOOLTIPS } from './obj/blueprints.js'
import { beginConnection, disconnect, teamColors } from './socket.js'
import { player, setIngame, startGame, mouseUpHandler, mouseDownHandler } from './pixigame.js'
import swal from 'sweetalert'
import VirtualJoystick from './lib/mobilejoystick.js'

// Array containing all inputs which require cookies, and their values
export const cookieInputs = GLOBAL.COOKIES.map(val => document.getElementById(val))

// Array containing the four chosen blueprints
export var selectedBlueprints = new Array(GLOBAL.BP_MAX)

const nickErrorText = document.getElementById('nickErrorText')

// Mouse position - used for tooltips
export let mouseX, mouseY

// Currently selected blueprint slot
export let selectedCompound = 0

let selectedSlot

export let music

export let sfx

let errorSound = new Sound('assets/sfx/error.mp3')

export let joystick = new VirtualJoystick()

// Starts the game if the name is valid.
function joinGame() {
	if (!allBlueprintsSelected()) {
		swal('Blueprint(s) not selected', 'Make sure all your blueprint slots are filled before joining a game!', 'error')
	}
	// check if the nick is valid
	else if (validNick()) {
		// Set cookies for inputs
		for (let i = 0; i < GLOBAL.INPUT_COUNT; i++) {
			cookies.setCookie(GLOBAL.COOKIES[i], cookieInputs[i].value, GLOBAL.COOKIE_DAYS)
		}

		// Use cookies to set the ingame blueprint slot values
		for (let i = 1; i <= GLOBAL.BP_MAX; i++) {
			selectedBlueprints[i - 1] = BLUEPRINTS[cookies.getCookie(GLOBAL.COOKIES[i - 1 + GLOBAL.INPUT_COUNT])]

			// Check whether blueprint is selected!
			document.getElementById('bp-ingame-' + i).innerHTML = selectedBlueprints[i - 1].name + ' (' + getCompoundFormula(selectedBlueprints[i - 1]) + ')'
		}

		// Show game window
		showElement('gameAreaWrapper')
		hideElement('startMenuWrapper')

		// Show loading screen
		showElement('loading')

		// Cookie Inputs: 0=player, 1=room, 2=team

		// Connect to server
		beginConnection()
	}
	else {
		nickErrorText.style.display = 'inline'
	}
}

/** check if nick is valid alphanumeric characters (and underscores)
 * @returns true if the nickname is valid, false otherwise
 */
function validNick() {
	const regex = /^(\w|_|-| |!|\.|\?){2,16}$/
	for (let i = 0; i < GLOBAL.INPUT_COUNT; i++) {
		if (regex.exec(cookieInputs[i].value) === null && !(i === 1 && cookieInputs[7].value !== 'private')) {
			return false
		}
	}

	return true
}

/**
 * Returns true if all four blueprint slots are filled.
 */
function allBlueprintsSelected() {
	for (let i = GLOBAL.INPUT_COUNT - 1; i < GLOBAL.INPUT_COUNT + GLOBAL.BP_MAX; i++) {
		if (cookieInputs[i].innerHTML.substring(0, 1) === '-') {
			return false
		}
	}
	return true
}

/**
 * Onload function. Initializes the menu screen, creates click events, and loads cookies.
 */
window.onload = () => {
	// Patch logo for firefox
	if (typeof InstallTrigger !== 'undefined') { document.getElementById('logo').innerHTML = `<img src="assets/logos/logo.svg" id="logo-firefox">`; } // eslint-disable-line

	// Cookie loading - create array of all cookie values
	let cookieValues = GLOBAL.COOKIES.map(val => cookies.getCookie(val))

	// Continue loading cookies only if it exists
	let i = 0
	for (let cookie of cookieValues) {
		if (cookie !== null && cookie.length > 0) {
			if (cookieInputs[i].tagName === 'INPUT' || cookieInputs[i].tagName === 'SELECT') {
				cookieInputs[i].value = cookie
			}
			else if (cookieInputs[i].tagName === 'BUTTON' && BLUEPRINTS[cookie] !== undefined) {
				cookieInputs[i].innerHTML = BLUEPRINTS[cookie].name
			}
		}
		i++
	}

	// Add listeners to start game to enter key and button click

	document.addEventListener('pointerdown', mouseUpHandler)
	document.addEventListener('pointerup', mouseDownHandler)

	bindHandler('startButton', function () {
		joinGame()
	})

	bindHandler('quitButton', function () {
		quitGame('You have left the game.', false)
	})

	bindHandler('exitButton', function () {
		quitGame('The game has ended.', false)
		hideElement('winner-panel')
	})

	bindHandler('resumeButton', function () {
		hideElement('menubox')
	})

	bindHandler('optionsButton', function () {
		errorSound.play()
		swal('', 'This feature is not implemented.', 'info')
	})

	bindHandler('controlsButton', function () {
		errorSound.play()
		swal('', 'This feature is not implemented.', 'info')
	})

	bindHandler('creditsButton', function () {
		swal('', 'Created by BananiumLabs.com', 'info')
	})

	bindHandler('btn-start-game', function () {
		console.log('starting game')
		startGame(true)
	})

	bindHandler('newsBox', function () {
		swal('', 'hello world', 'info')
	})

	// document.getElementById('gameView', onClick, false);

	for (let i = 0; i < selectedBlueprints.length; i++) {
		bindHandler('bp-ingame-' + (i + 1), function () {
			selectedCompound = i
			updateCompoundButtons()
		})
	}

	// Set up the blueprint slot buttons
	for (let i = 1; i <= GLOBAL.BP_MAX; i++) {
		document.getElementById('bp-slot-' + i).onclick = () => {
			showElement('bp-select')
			document.getElementById('bp-select-header').innerHTML = GLOBAL.BP_SELECT + i
			selectedSlot = i
		}
	}

	document.getElementById('btn-close').onclick = () => {
		hideElement('bp-select')
	}

	// Set up blueprint selection buttons
	for (let blueprint in BLUEPRINTS) {
		if (BLUEPRINTS[blueprint].unlocked) {
			let bp = BLUEPRINTS[blueprint]

			document.getElementById('blueprint-wrapper').innerHTML +=
				`
				<button onmouseenter="tooltipFollow(this)" class="button width-override col-6 col-12-sm btn-blueprint blueprint-${bp.type}" id="btn-blueprint-${blueprint}">
					<p>${bp.name}</p>
					<h6>-${getCompoundFormula(bp)} (${bp.type.charAt(0).toUpperCase() + bp.type.slice(1)})-</h6>
					<img src="${GLOBAL.COMPOUND_DIR + bp.texture}">
					<span class="tooltip">${bp.tooltip}</span>
				</button>
				`
		}
	}
	// Blueprint Slots
	for (let btn of document.getElementsByClassName('btn-blueprint')) {
		btn.onclick = () => {
			let blueprint = btn.id.substring(14) // Name of the blueprint, the first 14 characters are 'btn-blueprint-'
			console.log(blueprint + ' selected in slot ' + selectedSlot)
			document.getElementById('bp-slot-' + selectedSlot).innerHTML = BLUEPRINTS[blueprint].name
			hideElement('bp-select')
			cookies.setCookie(GLOBAL.COOKIES[selectedSlot + GLOBAL.INPUT_COUNT - 1], blueprint, GLOBAL.COOKIE_DAYS)
		}
	}

	// Add enter listeners for all inputs
	for (let i = 0; i < GLOBAL.INPUT_COUNT; i++) {
		cookieInputs[i].addEventListener('keypress', e => {
			const key = e.which || e.keyCode

			if (key === GLOBAL.KEY_ENTER) {
				joinGame()
			}
		})
	}

	// Behavior when room type is changed
	if (cookieInputs[7].value !== 'private') {
		hideElement('room')
	}
	else {
		showElement('room')
	}

	cookieInputs[7].onchange = () => {
		if (cookieInputs[7].value === 'private') {
			showElement('room')
		}
		else {
			hideElement('room')
		}

		cookies.setCookie(GLOBAL.COOKIES[7], cookieInputs[7].value, GLOBAL.COOKIE_DAYS)
	}

	// Server changed
	cookieInputs[8].onchange = () => {
		cookies.setCookie(GLOBAL.COOKIES[8], cookieInputs[8].value, GLOBAL.COOKIE_DAYS)
	}

	document.getElementById('team-option').onchange = document.getElementById('solo').onchange = () => {
		console.log('change')
		if (document.querySelector('input[name="queue-type"]:checked').id === 'team-option') {
			showElement('team')
		}
		else {
			hideElement('team')
		}
	}

	playMusic()
}

/**
 * Sets mouse positions for tooltip
 */
window.onmousemove = (e) => {
	mouseX = e.clientX
	mouseY = e.clientY
}

/**
 * Loop main menu music
 */
function playMusic() {
	music = document.createElement('audio')
	HTMLElement.prototype.randomSelectMM = function () {
		music.src = GLOBAL.MAINMENU_MUSICLIST[Math.floor(Math.random() * GLOBAL.MAINMENU_MUSICLIST.length)]
	}
	music.randomSelectMM()
	music.style.display = 'none'	// fix ios device
	// music.autoplay = true
	music.type = 'audio/mpeg'
	music.id = 'mainmenu'

	music.onended = function () {
		music.randomSelectMM()
		music.play()
	}
	// music.loop = true
	// audio.onended = function() {
	// 	audio.remove()
	// };
	document.body.appendChild(music)
	let audioPlay = document.getElementById('mainmenu').play()

	if (audioPlay !== undefined) {
		audioPlay.then(_ => {
			console.log('Music started')
		}).catch(error => {
			console.warn(error)
			console.log('Music start prevented. Starting Bypass method.')
			// How this works is that the iframe with audio
			let bypassElement = document.createElement('iframe')
			bypassElement.src = 'assets/sfx/silence.mp3'
			bypassElement.allow = 'autoplay'
			bypassElement.type = 'audio/mpeg'
			bypassElement.id = 'bypassaudio'
			document.body.appendChild(bypassElement)
			document.getElementById('bypassaudio').addEventListener('load', function () {
				document.getElementById('mainmenu').play()
				document.getElementById('bypassaudio').remove()
			})
		})
	}
}

function Sound(src) {
	this.sound = document.createElement('audio')
	this.sound.src = src
	this.sound.setAttribute('preload', 'auto')
	this.sound.setAttribute('controls', 'none')
	this.sound.style.display = 'none'
	document.body.appendChild(this.sound)
	this.play = function () {
		this.sound.currentTime = 0
		this.sound.play()
	}
	this.stop = function () {
		this.sound.pause()
	}
}

/**
 * Transitions from in-game displays to the main menu.
 * @param {string} msg The message to be displayed in the menu after disconnect.
 * @param {boolean} isError True if the game quit was due to an error; false otherwise.
 */
export function quitGame(msg, isError) {
	// Disconnect from server
	disconnect()

	// Set status of ingame
	setIngame(false)

	// menu
	hideElement('gameAreaWrapper')
	hideElement('hud')
	hideElement('menubox')
	showElement('startMenuWrapper')
	hideElement('lobby')
	hideElement('winner-panel')
	swal('Disconnected from Game', msg, (isError) ? 'error' : 'info')
}

/**
 * Binds handlerMethod to onclick event for element id.
 * @param {string} id
 * @param {function} handlerMethod
 */
export function bindHandler(id, handlerMethod) {
	document.getElementById(id).onclick = handlerMethod
}

/**
 * Displays a hidden element
 * @param {string} el The id of the element to show
 */
export function showElement(el) {
	document.getElementById(el).style.display = 'block'
	if (el === 'startMenuWrapper') {
		music.randomSelectMM()
		music.currentTime = 0
		music.play()
	}
	else if (el === 'lobby') {	// In lobby
		music.pause()	// Pause main menu music
		// music.currentTime = 9999
	}
	else if (el === 'gameAreaWrapper') {	// In game
		music.pause()	// Pause main menu music
		// music.currentTime = 9999
	}
}

/**
 * Hides a visible element
 * @param {string} el The id of the element to hide
 */
export function hideElement(el) {
	document.getElementById(el).style.display = 'none'
}

/**
 * Makes tooltip follow the mouse. Call when a button is hovered.
 * @param {HTMLElement} button The element reference for the button currently being hovered.
 */
window.tooltipFollow = (button) => {
	let tooltip = button.getElementsByClassName('tooltip')[0]
	tooltip.style.top = (mouseY - 150) + 'px'
	tooltip.style.left = (mouseX - 150) + 'px'
}

// Toggle compound stats and info tooltips
let compoundStats = false

window.onkeydown = (e) => {
	// Only detect if the blueprint select screen is up
	if (document.getElementById('bp-select').style.display === 'block' && e.key === 'Shift') {
		compoundStats = !compoundStats
		// Iterate through all compound buttons
		for (let button of document.getElementsByClassName('btn-blueprint')) {
			// Get blueprint from BLUEPRINTS
			let blueprint = Object.values(BLUEPRINTS).filter((obj) => {
				return obj.name === button.getElementsByTagName('p')[0].innerHTML
			})
			blueprint = blueprint[0]

			if (compoundStats) {
				let newTooltip = TOOLTIPS[blueprint.type] + '<br><br>'
				for (let param in blueprint.params) {
					if (!GLOBAL.BP_TOOLTIP_BLACKLIST.includes(param)) {
						let line = ('' + param).replace(/([A-Z])/g, ' $1').replace(/^./, (str) => {
							return str.toUpperCase()
						}) + ': ' + blueprint.params[param] + '<br>'
						newTooltip += line
					}
				}
				button.getElementsByClassName('tooltip')[0].innerHTML = newTooltip
			}
			else {
				button.getElementsByClassName('tooltip')[0].innerHTML = blueprint.tooltip
			}
		}
	}
}

/**
 * Updates the list of atoms that the player holds.
 * Only updates the entry for the particular ID given.
 * @param {string} atomID The ID of the atom to update.
 */
export function updateAtomList(atomID) {
	let list = document.getElementById('atom-count')

	if (document.getElementById('atom-list-' + atomID) === null) {
		let newEntry = document.createElement('li')
		newEntry.setAttribute('id', 'atom-list-' + atomID)
		list.appendChild(newEntry)
	}

	try {
		document.getElementById('atom-list-' + atomID).innerHTML = '' + atomID.charAt(0).toUpperCase() + atomID.substr(1) + ': ' + player.atomList[atomID]
	}
	catch (e) {
		console.warn('Atom ' + atomID + ' could not be updated on the list!')
	}

	updateCompoundButtons() // No need to update selection
}

/**
 *
 * @param {number} selectedSlot The index of the selected slot. 0-3
 */
export function updateCompoundButtons(selectedSlot) {
	if (selectedSlot === undefined) {
		selectedSlot = selectedCompound
	}
	else {
		selectedCompound = parseInt(selectedSlot)
	}

	for (let i = 0; i < selectedBlueprints.length; i++) {
		if (selectedCompound !== i) {
			if (canCraft(selectedBlueprints[i])) {
				document.getElementById('bp-ingame-' + (i + 1)).style.background = '#2ecc71'
			}
			else {
				document.getElementById('bp-ingame-' + (i + 1)).style.background = '#C8C8C8'
			}
		}
		else { // is selected
			if (canCraft(selectedBlueprints[i])) {
				document.getElementById('bp-ingame-' + (i + 1)).style.background = '#003CA8'
			}
			else {
				document.getElementById('bp-ingame-' + (i + 1)).style.background = '#3D66D1'
			}
			document.getElementById('bp-select-label').innerHTML = 'Selected Compound: ' + selectedBlueprints[i].name
		}
	}
}

/**
 * Updates the team scoreboard on screen.
 */
export function updateScores(teamSlot, increment) {
	document.getElementById('team-score-' + teamSlot).innerHTML = parseInt(document.getElementById('team-score-' + teamSlot).innerHTML) + increment
}

/**
 * Run on new player join to sync lobby information
 * @param {*} data The data transferred from server
 */
export function updateLobby(data) {
	// Wipe innerHTML first
	let lobby = document.getElementById('team-display')
	lobby.innerHTML = ''
	for (let player in data.players) {
		if (document.getElementById(data.players[player].team) === null || document.getElementById(data.players[player].team) === undefined) {
			lobby.innerHTML += `
            <div class="col-3">
                <h3 style="color: #${GLOBAL.TEAM_COLORS[teamColors[data.players[player].team]]}">` + data.players[player].team + `</h3>
                <ul id="` + data.players[player].team + `">
                </ul>
            </div>
            `
		}
		let listItem = document.createElement('LI')
		listItem.appendChild(document.createTextNode(data.players[player].name))
		document.getElementById(data.players[player].team).appendChild(listItem)
	}

	// Check if room is startable
	if (data.canStart) {
		document.getElementById('btn-start-game').innerHTML = 'Start Game'
		document.getElementById('btn-start-game').disabled = false
	}
	else {
		document.getElementById('btn-start-game').innerHTML = 'Waiting for Players...'
		document.getElementById('btn-start-game').disabled = true
	}
}

/**
 * Displays the winner panel after a game has concluded.
 * @param {*} data Server sent data, including name and score of winning team.
 */
export function displayWinner(data) {
	// console.log(data);
	document.getElementById('winner-name').innerHTML = data.winner + ' has won!'
	showElement('winner-panel')
}

/**
 * Gets the formatted formula of a compound (e.g. C6H12O6).
 * @param {*} blueprint The blueprint object as defined in blueprints.js
 * @returns {string} The formula of the compound
 */
function getCompoundFormula(blueprint) {
	let formula = ''
	for (let atom in blueprint.atoms) {
		formula += atom.toUpperCase() + ((blueprint.atoms[atom] > 1) ? blueprint.atoms[atom] : '')
	}

	return formula
}

/**
 * Returns true if the player has the materials necessary to create a particular blueprint.
 * ONLY USE FOR BUTTON GRAPHICS!!! True checking is done serverside.
 * @param {string} blueprint The name of the blueprint to check.
 */
function canCraft(blueprint) {
	if (blueprint === undefined) {
		return false
	}
	for (let atom in blueprint.atoms) {
		if (player.atomList[atom] === undefined || player.atomList[atom] < blueprint.atoms[atom]) {
			return false
		}
	}

	return true
}
// Anti debugger on non-debug builds
if (!GLOBAL.DEBUG) {
	console.log = function () {
		console.info('Log disabled. Non-Debug build.')
	}
	// while (true) {
	// 	setTimeout(function () {
	// 		eval('debugger')
	// 	}, 200)
	// }
	setInterval(function () {
		var startTime = performance.now(); var check; var diff
		for (check = 0; check < 1000; check++) {
			console.log(check)
			console.clear()
		}
		diff = performance.now() - startTime
		if (diff > 200) {
			// window.close()
			//   alert('Debugger detected!')

			document.body.innerHTML = '<h1 style="color:red">A critical error has been detected. Please contact the developer with the following information.<br>Error: Production build sec violation.</b></h1>'
			errorSound.play()
			let counter = 0
			setTimeout(function () {
				document.body.innerHTML = '<div style="--a:1px;--b:calc(var(--a) + var(--a));--c:calc(var(--b) + var(--b));--d:calc(var(--c) + var(--c));--e:calc(var(--d) + var(--d));--f:calc(var(--e) + var(--e));--g:calc(var(--f) + var(--f));--h:calc(var(--g) + var(--g));--i:calc(var(--h) + var(--h));--j:calc(var(--i) + var(--i));--k:calc(var(--j) + var(--j));--l:calc(var(--k) + var(--k));--m:calc(var(--l) + var(--l));--n:calc(var(--m) + var(--m));--o:calc(var(--n) + var(--n));--p:calc(var(--o) + var(--o));--q:calc(var(--p) + var(--p));--r:calc(var(--q) + var(--q));--s:calc(var(--r) + var(--r));--t:calc(var(--s) + var(--s));--u:calc(var(--t) + var(--t));--v:calc(var(--u) + var(--u));--w:calc(var(--v) + var(--v));--x:calc(var(--w) + var(--w));--y:calc(var(--x) + var(--x));--z:calc(var(--y) + var(--y));--vf:calc(var(--z) + 1px);border-width:var(--vf);border-style:solid;">error</div>'
				document.body.style.cssText = null
				let buffer = 'nohax'
				while (true) {
					buffer = buffer += buffer
					// for (;;);
					if (counter % 10 === 0) {
						debugger
					}
				}
			}, 100)
		}
	}, 500)
}