// Host and Client (hC) Module // Version 1.12 (11:12 PM Thu June 15, 2017) // Written by: James D. Miller var hC = (function () { // A few globals within hC. ///////////////////////////////////////////////// var socket; // Mouse and keyboard (mK) from non-host clients. var mK = {}; var nodeServerURL, serverArray; var chatStyleToggle = true; var timer = {}; timer.start = null; timer.end = null; timer.pingArray = []; var canvas, ctx; // Key values. var keyMap = {'70':'f', '65':'a','83':'s','68':'d','87':'w', '74':'j','75':'k','76':'l','73':'i', '32':'sp'}; // Functions supporting the socket.io connections /////////////////////////// function connect_and_listen( hostOrClient){ var nodeString = $('#nodeServer').val(); if (nodeString == "") { // Use one in the list as a default. nodeString = serverArray[0]; $('#nodeServer').val( nodeString); } if (nodeString.includes("heroku")){ var urlPrefix = "https://" } else { var urlPrefix = "http://" } nodeServerURL = urlPrefix + nodeString; console.log("URL=" + nodeServerURL); // Use jquery to load the socket.io client code. $.getScript( nodeServerURL + "/socket.io/socket.io.js", function(){ // This callback function will run after the getScript finishes loading the socket.io client. console.log("socket.io script has loaded."); if (socket) socket.disconnect(); var roomName = $('#roomName').val(); if (roomName != ""){ // the html limit is set to 9 (so you can try a little more then 7, but then get some advice to limit it to 7) if (roomName.length <= 7){ // Here's where the socket magic happens. socket = io.connect( nodeServerURL,{'forceNew':true}); init_socket_listeners( roomName, hostOrClient); } else { displayMessage('The name should have 7 characters or less.'); } } else { displayMessage('Type in a short "Room" name, then click the "Connect" button.'); } // Use the "fail" method of getScript to report a connection problem. }).fail(function( jqxhr, settings, exception) { displayMessage('The node server is not responding. Try changing to a different server.'); }); } function displayMessage( msgText){ if (chatStyleToggle){ var styleString = 'style="background: #efefef;"'; } else { var styleString = 'style="background: #d9d9d9;"'; } $('#messages').prepend('<li '+styleString+'>'+ msgText +'</li>'); chatStyleToggle = !chatStyleToggle; } function init_chatFeatures( hostOrClient){ serverArray = ['secure-retreat-15768.herokuapp.com', 'localhost:3000', '192.168.1.106:3000', //NUC '192.168.1.109:3000', //David's computer '192.168.1.116:3000', //RPi '192.168.1.117:3000']; //Laptop // Use jquery to loop over the serverArray and build the URL datalist. jQuery.each( serverArray, function( i, val ) { $('#nodeServerList').append("<option value='" + val + "'>"); }); var pingTestHelp = "Your ping test has started.<br><br>" + "Please wait about 10 seconds for the results of the 100-ping test to return. Each time you hit enter or click the chat button " + "a new 100-ping test will be queued. Please manually clear out the words 'ping' or 'ping:host' to stop pinging and start chatting."; // Function that emits (if a socket has been established) the text in the form's input field. $('#chatForm').submit(function(){ var chatString = $('#inputField').val(); if (socket){ if (chatString == 'ping') { echoTest('server'); displayMessage( pingTestHelp); } else if (chatString == 'ping:host'){ echoTest('host'); displayMessage( pingTestHelp); } else { socket.emit('chat message', chatString); $('#inputField').val(''); //clear out the input field. } } else { displayMessage('Type in a short "Room" name, then click the "Connect" button.'); } return false; }); // Prevent typing in the input fields from triggering document level keyboard events. $('#inputField, #nodeServer, #roomName, #jsonCapture').on('keyup keydown keypress', function( e){ e.stopPropagation(); // stops bubbling... }); // A first message in the chat area var helpFindClientLink = ''; if (hostOrClient == 'host') { helpFindClientLink = 'You can be the host of a multi-player room from this page. '+ 'Please notice the links to the client page in the right panel below the multiplayer checkbox.</br></br>'; } else { helpFindClientLink = 'You can be a client in a multi-player room from this page. You can not be the host.</br></br>'; } var helloMessage = 'Thank you for trying the multiplayer feature.</br></br>'+ helpFindClientLink + 'To get started, type in a short "Room" name, then click the "Connect" button.</br></br>'+ 'Please note that if you do not get an immediate response from the server, it can take a little while for the Heroku node application to wake up. '+ 'If waking, give it 10 to 20 seconds before expecting a message here.</br></br>'+ 'To start over, or disconnect from the server, please reload the page.'; displayMessage( helloMessage); } function clientColor( clientName){ var colors = {'1':'yellow','2':'blue','3':'green','4':'pink','5':'orange', '6':'brown','7':'greenyellow','8':'cyan','9':'tan','0':'gray'}; var n = clientName.slice(1); var colorIndex = n - Math.trunc(n/10)*10; return colors[ colorIndex]; } function init_socket_listeners( roomName, hostOrClient){ // Listen for chat from the server. socket.on('chat message', function(msg){ // Change the border color of the roomName input box depending on the // message from the node server. if (msg.includes('You have joined room')) { document.getElementById("roomName").style.borderColor = "#008080"; //Dark green. } else if (msg.includes('Sorry, there is no host')) { document.getElementById("roomName").style.borderColor = "red"; } else if (msg.includes('You are the host')) { msg += '</br></br>You can open a test <a href="indexClient.html" target="_blank">client</a> in a new tab, then drag the tab to make a new window.&nbsp;'; msg += 'Enter the same room name on the client page. Then the client mouse and keyboard events will render to the canvas of the host.'; } displayMessage( msg); }); if (hostOrClient == 'client'){ socket.on('your name is', function(msg){ // Put this name in the mouse and keyboard (mK) global that is used to send // state data from the client. mK.name = msg; // Show the client's color. ctx.fillStyle = clientColor( mK.name); ctx.fillRect(0, 0, canvas.width/4, canvas.height/4); }); socket.on('disconnectByServer', function(msg){ var clientName = msg; displayMessage("This client ("+ clientName +") is being disconnected by the host."); document.getElementById("roomName").style.borderColor = "red"; // Paint over the client-color square. ctx.fillStyle = '#EFEFEF'; ctx.fillRect(0, 0, canvas.width/4, canvas.height/4); // When the server gets this one, it will remove the socket. socket.emit('okDisconnectMe', clientName); }); } if (hostOrClient == 'host'){ // (Note: this is the one place where calls to gW are made inside of hC.) // Listen for client mouse and keyboard (mk) events broadcast from the server. socket.on('client-mk-event', function(msg){ var msg_parsed = JSON.parse(msg); //console.log('State('+ msg_parsed.name +'):'+ msg_parsed.MD +','+ msg_parsed.bu +'): '+ msg_parsed.mX + "," + msg_parsed.mY); // Send this mouse-and-keyboard state to the engine. gW.updateClientState( msg_parsed.name, msg_parsed); }); // As host, create a new client in gW framework. socket.on('new-game-client', function(msg){ var clientName = msg; gW.createNetworkClient( clientName); }); socket.on('client-disconnected', function(msg){ var clientName = msg; // Do corresponding cleanup in gwModule. gW.deleteNetworkClient( clientName); }); socket.on('echo-from-Server-to-Host', function(msg){ // Bounce this back to server. // The msg string is the client id. socket.emit('echo-from-Host-to-Server', msg); //console.log(msg); }); } // Once your connection succeeds, join a room. socket.on('connect', function() { // Connected. Send the room name to the server for room joining. if (hostOrClient == 'host'){ // Request to be the host for that room. socket.emit('roomJoinAsHost', roomName); } else { socket.emit('roomJoin', roomName); } }); // Listen for echo response from the server. socket.on('echo-from-Server-to-Client', function( msg) { var echoTarget = msg; // Stop timer (measure the round trip). timer.stop = window.performance.now(); var elapsed_time = timer.stop - timer.start; // Add this new timing result to the array. timer.pingArray.push( elapsed_time); // The echo series STOPs here. if (timer.pingArray.length > 99) { var timeAvg = math.mean( timer.pingArray).toFixed(1); var timeSTD = math.std( timer.pingArray).toFixed(1); var timeLen = timer.pingArray.length; var timeMax = math.max( timer.pingArray).toFixed(1); var timeMin = math.min( timer.pingArray).toFixed(1); displayMessage('Echo test to '+ echoTarget +': '+ timeAvg +' ms '+ '(std='+ timeSTD + ', min='+ timeMin + ', max='+ timeMax + ', n='+ timeLen +')'); timer.pingArray = []; return; } // Ping it again (continue the series). echoTest( echoTarget); // Do this after the timer starts (don't slow it down with a write to the console.) console.log( echoTarget); }); } // This, and only this, function is called within gwModule.js. function forceClientDisconnect( clientName){ socket.emit('clientDisconnectByHost', clientName); } function echoTest( hostOrServer) { // Start the timer for one echo. timer.start = window.performance.now(); // The echo series STARTs here. socket.emit('echo-from-Client-to-Server', hostOrServer); //console.log(hostOrServer); } // Event listeners to capture mouse and keyboard (m & K) state from the non-host // clients. //////////////////////////////////////////////////////////////////// function init_eventListeners_nonHostClients(){ // Initialize the Mouse and Keyboard (mK) state object. // isMouseDown mK.MD = false; // mouse button number (which of the three: 0,1,2) mK.bu = 0; // mouse position in pixels: X_px, Y_px mK.mX = 5; mK.mY = 5; // Initialize all the key states to UP: for (var key in keyMap) { mK[keyMap[key]] = 'U'; } canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d'); // Event handlers for this network client (user input) // Inhibit the context menu that pops up when right clicking (third button). // Alternatively, could apply this only to the canvas. That way you can still // source the page. document.addEventListener("contextmenu", function(e) { e.preventDefault(); return false; }, {capture: false}); // For the client, keep these listeners on all the time so you can see the client cursor. document.addEventListener("touchmove", handleMouseOrTouchMove, {capture: false}); document.addEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); document.addEventListener("mousedown", function(e) { mK.MD = true; mK.bu = e.button; //Pass this first mouse position to the move handler. handleMouseOrTouchMove(e); }, {capture: false}); document.addEventListener("touchstart", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; mK.MD = true; mK.bu = 0; //Pass this first mouse position to the move handler. handleMouseOrTouchMove(e); }, {capture: false}); function handleMouseOrTouchMove(e) { // Determine if mouse or touch. if (e.clientX){ // Mouse var raw_x_px = e.clientX; var raw_y_px = e.clientY; } else if (e.touches) { // Touch screen event var raw_x_px = e.touches[0].clientX; var raw_y_px = e.touches[0].clientY; } mK.mX = raw_x_px - canvas.getBoundingClientRect().left; mK.mY = raw_y_px - canvas.getBoundingClientRect().top; // Send the state to the server (there it will be relayed to the host client). if (socket) socket.emit('client-mK-event', JSON.stringify(mK)); //console.log("x,y=" + mK.mX + "," + mK.mY); }; document.addEventListener("mouseup", function(e) { if (!mK.MD) return; // Unlike for the host client, DO NOT shut down the mousemove listener. That // way we can see the mouse position even if the buttons are released. resetMouseOrFingerState(e); }, {capture: false}); document.addEventListener("touchend", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; if (!mK.MD) return; // Unlike for the host client, DO NOT shut down the touchmove listener. That // way we can see the finger position even if the buttons are released. resetMouseOrFingerState(e); }, {capture: false}); function resetMouseOrFingerState(e){ mK.MD = false; mK.bu = null; if (socket) socket.emit('client-mK-event', JSON.stringify(mK)); } document.addEventListener("keydown", function(e) { // This allows the spacebar to be used for the puck shields. if (keyMap[e.keyCode] == 'sp') { // Inhibit page scrolling that results from using the spacebar. e.preventDefault(); // The following is necessary in Firefox to avoid the spacebar from re-clicking // page controls (like the demo buttons) if they have focus. if (document.activeElement != document.body) document.activeElement.blur(); } //console.log(e.keyCode + "(down)=" + String.fromCharCode(e.keyCode)); if (e.keyCode in keyMap){ //console.log("keyMap value = " + keyMap[e.keyCode]); if (mK[keyMap[e.keyCode]] == 'U'){ // Set the key to DOWN. mK[keyMap[e.keyCode]] = 'D'; if (socket) socket.emit('client-mK-event', JSON.stringify(mK)); } } }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase). document.addEventListener("keyup", function(e) { //console.log(e.keyCode + "(up)=" + String.fromCharCode(e.keyCode)); if (e.keyCode in keyMap){ // Set the key to UP. mK[keyMap[e.keyCode]] = 'U'; if (socket) socket.emit('client-mK-event', JSON.stringify(mK)); } }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase). } // Reveal public pointers to private functions and properties /////////////// return { //nodeServerURL: nodeServerURL, forceClientDisconnect: forceClientDisconnect, init_chatFeatures: init_chatFeatures, init_eventListeners_nonHostClients: init_eventListeners_nonHostClients, connect_and_listen: connect_and_listen }; })();