// Host and Client (hC) Module// hostAndClient.js// Version 2.2.0 (9:50 PM Tue August 21, 2018)// Written by: James D. Miller// This module is dependent on gwModule.js (referenced here as gW).varhC=(function(){// To insist on tighter code: e.g. globals, etc..."use strict";// A few globals within hC. /////////////////////////////////////////////////varsocket=null;varnodeServerURL,serverArray;varchatStyleToggle=true;vartimer={};timer.start=null;timer.end=null;timer.pingArray=[];varclientCanvas,ctx;varclientCanvas_tt,ctx_tt;varvideoMirror,videoStream;varchkRequestStream,chkLocalCursor;varchkTwoThumbs,btnTwoThumbs,twoThumbs;varbtnFullScreen;varchkPlayer;varmyRequest;// Key values.varkeyMap={'49':'1','50':'2','51':'3','52':'4','53':'5','54':'6','55':'7','56':'8','57':'9','70':'f','65':'a','83':'s','68':'d','87':'w','74':'j','75':'k','76':'l','73':'i','16':'sh','32':'sp',//sh:shift, sp:space'191':'cl'};// cl (short for color), 191 is the question-mark key.// Mouse and keyboard (mK) from non-host clients.varmK={};mK.name=null;// Key values, cso (client side only) for use only by the client, not to be sent over network// to the host.varkeyMap_cso={'16':'key_shift','17':'key_ctrl','27':'key_esc','80':'key_p'}varmK_cso={};// The client name of this user. This global is only used on the client page and// is some increment of u1, u2, etc for network clients.varnewClientName=null;// cl is a global that points at a "Client" or clientlike object. For the host, this will point at the// "Client" object of the client that most recently attempts to connect. On the client page, // this will simply keep this structure and replace the 'notyetnamed' with the name of that// client.varcl={'name':null,'previous_name':null};varrtc_choke=false;varfileName="hostAndClient.js";// Pacifier (connecting status) string for connecting...varpacifier={};// Switch to enable debugging...vardb={};// ...of the WebRTC stuff.db.rtc=false;vargameReportCounter=0;////////////////////////////////////////////////// // Object prototypes////////////////////////////////////////////////// functionRTC(pars){this.user1=setDefault(pars.user1,null);this.user2=setDefault(pars.user2,null);this.streamRequested=setDefault(pars.streamRequested,null);this.pc=null;this.dataChannel=null;}RTC.prototype.shutdown=function(){//console.log('pc:'+JSON.stringify(this.pc));//console.log('dataChannel:'+JSON.stringify(this.dataChannel));// Close then nullify any references to the datachannel and the p2p connection.if(this.dataChannel){this.dataChannel.close();}if(this.pc){varsenders=this.pc.getSenders();if(senders.length>=1){//console.log('senders length = ' + senders.length);this.pc.removeTrack(senders[0]);senders=this.pc.getSenders();//console.log('senders length = ' + senders.length);}this.pc.close();}if(this.dataChannel){this.dataChannel=null;}if(this.pc){this.pc=null;}}// This method works only on the host side of the WebRTC connection. So, that's why there's a check here// to see if user1 is the host.RTC.prototype.turnVideoStreamOff=function(){if(this.pc&&(this.user1=='host')){varsenders=this.pc.getSenders();//console.log('senders length (before) = ' + senders.length);if(senders.length>=1){this.pc.removeTrack(senders[0]);//senders = this.pc.getSenders();//console.log('senders length (after) = ' + senders.length);}}}functionTwoThumbs(pars){// Not yet using this adjustment point feature (TBD).this.adjustmentPoint_2d=newgW.Vec2D(0,0);this.enabled=false;// Grid of rectangles.this.grid={'jet_360':{'active':false,'mK':'w','UL':null,'LR':null,'dir_2d':null},'gun_360':{'active':false,'mK':'i','UL':null,'LR':null,'dir_2d':null},'shield':{'active':false,'mK':'sp','UL':null,'LR':null},'color':{'active':false,'mK':'cl','UL':null,'LR':null},'alt':{'active':false,'mK':null,'UL':null,'LR':null},// Controls that are dependent on the alt rectangle being touched.'esc':{'active':false,'mK':null,'UL':null,'LR':null},'demo7':{'active':false,'mK':'7','UL':null,'LR':null},'demo8':{'active':false,'mK':'8','UL':null,'LR':null},'freeze':{'active':false,'mK':'f','UL':null,'LR':null},// Secondary control that fires the gun. Changes angle by controlling the rotation rate.'gun_scope':{'active':false,'mK':'ScTr','UL':null,'LR':null}};// This is the same for both the jet and the gun.this.dirDotRadius_fraction=0.020;// Control radius in units of screen fraction. The jet has four// strength levels: <1, >1 && <2, >2 && <3, >3.this.grid['jet_360'].cRadius_1_f=0.090;this.grid['jet_360'].cRadius_2_f=0.130;this.grid['jet_360'].cRadius_3_f=0.170;this.jetRadiusColor_3="rgb(255, 0, 0)";this.jetRadiusColor_2="rgb(200, 0, 0)";this.jetRadiusColor_1="rgb(140, 0, 0)";this.jetRadiusColor_0="rgb( 50, 0, 0)";// The gun has zero level, for bluffing. All touches outside that ring// are firing.this.grid['gun_360'].cRadius_0_f=0.060;this.gunRadiusColor_0="rgb(255, 0, 0)";this.bgColor='lightgray';this.gridColor='#232323';// very dark gray // #008080 dark greenclientCanvas_tt.style.borderColor=this.gridColor;// 0.10 uses 10% of the rectangle width for the dead spot.this.scopeShootSpot=0.20;this.updateAndDrawTouchGrid('updateOnly');}// Calculate point position in canvas coordinates as a function of fractional position.TwoThumbs.prototype.absPos_x_px=function(fraction){returnMath.round(fraction*clientCanvas_tt.width);}TwoThumbs.prototype.absPos_y_px=function(fraction){returnMath.round(fraction*clientCanvas_tt.height);}TwoThumbs.prototype.resetRectangle=function(rectName){varrect=this.grid[rectName];// The alt rectangle cases:if((rectName=='esc'||rectName=='demo7'||rectName=='demo8'||rectName=='freeze')){if(this.grid['alt'].active){this.updateDirectionDot(rectName,this.gridColor);}else{this.updateDirectionDot(rectName,this.bgColor);}// The others...}else{if((rectName=='alt')||(rectName=='shield')){this.updateDirectionDot(rectName,this.gridColor);}elseif(rectName=='color'){if(cl.name)this.colorClientRect(clientColor(cl.name));}elseif(rectName=='jet_360'){this.updateDirectionDot(rectName,this.gridColor);mK.jet_d=null;// jet angle in degrees}elseif(rectName=='gun_360'){this.updateDirectionDot(rectName,this.gridColor);mK.gun_d=null;// gun angle in degrees}elseif(rectName=='gun_scope'){this.updateDirectionDot(rectName,this.gridColor);// Rotation rate fraction.mK.ScRrf=0.00;}}// For all rectangles: deactivate and reset the primary mK attribute for that square.rect.active=false;if(rect.mK)mK[rect.mK]='U';}TwoThumbs.prototype.processMultiTouch=function(touchVectors_2d_px){for(varrectNameinthis.grid){varrect=this.grid[rectName];varatLeastOnePointInRect=false;for(vari=0,len=touchVectors_2d_px.length;i<len;i++){varp_2d=touchVectors_2d_px[i];if((p_2d.x>rect.UL.x)&&(p_2d.x<rect.LR.x)&&(p_2d.y>rect.UL.y)&&(p_2d.y<rect.LR.y)){this.updateRectangle(rectName,p_2d);atLeastOnePointInRect=true;break;}}if(!atLeastOnePointInRect){this.resetRectangle(rectName);}}handle_sending_mK_data(mK);}TwoThumbs.prototype.processSingleTouchRelease=function(touchVector_2d_px){for(varrectNameinthis.grid){varrect=this.grid[rectName];varp_2d=touchVector_2d_px;if((p_2d.x>rect.UL.x)&&(p_2d.x<rect.LR.x)&&(p_2d.y>rect.UL.y)&&(p_2d.y<rect.LR.y)){this.resetRectangle(rectName);// When you release the alt rectangle, show as sleeping (not listening),// those rectangles that are dependent on the alt rectangle.if(rectName=='alt'){this.updateDirectionDot('esc',this.bgColor);this.updateDirectionDot('demo7',this.bgColor);this.updateDirectionDot('demo8',this.bgColor);this.updateDirectionDot('freeze',this.bgColor);}break;}}}TwoThumbs.prototype.updateDirectionDot=function(rectName,dirDotColor){varrect=this.grid[rectName];// Draw a square over the prior direction dot. This prevents a jagged edge when the direction// dot is drawn. And is a little more efficient then drawing the whole rectangle for each update.ctx_tt.fillStyle=this.bgColor;// The eraser rectangle is one pixel larger than the dot on each side.// -----------upper left corner------------------------------------------------------------, -----width--------------------, ----height---------------------ctx_tt.fillRect(rect.center_2d.x-this.dirDotRadius_px-1,rect.center_2d.y-this.dirDotRadius_px-1,(this.dirDotRadius_px*2)+2,(this.dirDotRadius_px*2)+2);if(dirDotColor!=this.bgColor){// Draw the dot.gW.drawCircle(ctx_tt,rect.center_2d,{'radius_px':this.dirDotRadius_px,'fillColor':dirDotColor});}}TwoThumbs.prototype.updateRectangle=function(rectName,point_2d){varrect=this.grid[rectName];vardirDotColor;varrelativeToCenter_2d=point_2d.subtract(rect.center_2d);if(rectName=='jet_360'||rectName=='gun_360'){varrTC_lengthSquared=relativeToCenter_2d.length_squared();// Orient dir_2d to match the direction of relativeToCenter_2d// Note the negative sign correction (on the angle result) is necessary because of the // negative orientation of the y axis with the screen (pixels) representation (not world here).varangle_d=-rect.dir_2d.matchAngle(relativeToCenter_2d);// End point for drawing the orientation vectorvarendPoint_2d=rect.center_2d.add(rect.dir_2d);if(rectName=='jet_360'){// Check where the point is relative to the control rings.// Always use at least the minimum jet power.dirDotColor=this.jetRadiusColor_0;mK.jet_t=0.1;// Jet throttle// Stronger jetif(rTC_lengthSquared>Math.pow(this.grid['jet_360'].cRadius_1_px,2)){dirDotColor=this.jetRadiusColor_1;mK.jet_t=0.4;// Even stronger jetif(rTC_lengthSquared>Math.pow(this.grid['jet_360'].cRadius_2_px,2)){dirDotColor=this.jetRadiusColor_2;mK.jet_t=0.7;// Even stronger jetif(rTC_lengthSquared>Math.pow(this.grid['jet_360'].cRadius_3_px,2)){dirDotColor=this.jetRadiusColor_3;mK.jet_t=1.0;}}}// Update mK for sending to the host.mK.w='D';mK.jet_d=angle_d+0;// + 180 to reverse it...}elseif(rectName=='gun_360'){// Check is the point is outside the control ring...if(rTC_lengthSquared>Math.pow(this.grid['gun_360'].cRadius_0_px,2)){dirDotColor=this.gunRadiusColor_0;mK.i='D';}else{dirDotColor=this.gridColor;mK.i='U';}// Update mK for sending to the host.mK.gun_d=angle_d;}this.updateDirectionDot(rectName,dirDotColor);// Draw the direction line.gW.drawLine(ctx_tt,rect.center_2d,endPoint_2d,{'width_px':3,'color':'white'});}elseif(rectName=='shield'){if(rect.mK)mK[rect.mK]='D';this.updateDirectionDot(rectName,'yellow');}elseif(rectName=='color'){// mK.cl = 'D'if(rect.mK)mK[rect.mK]='D';this.colorClientRect(this.bgColor);}elseif(rectName=='gun_scope'){// Rotation rate fraction (Rrf) for the scope control (Sc), where x_fraction varies // from -1 to +1;varx_fraction=relativeToCenter_2d.x/((rect.LR.x-rect.UL.x)/2.0);varx_fraction_abs=Math.abs(x_fraction);if(x_fraction_abs>0){varx_fraction_sign=x_fraction/x_fraction_abs;}else{varx_fraction_sign=1.0;}// Shooting spot in the middle where it will only shoot, not rotate.if(x_fraction_abs<this.scopeShootSpot){varx_fraction_mapped=0.00;mK[rect.mK]='D';this.updateDirectionDot(rectName,'red');// The outer areas will only rotate, not shoot.}else{// Map the x_fraction value so that near the edge of the dead zone, the rate is small. At the// outer edge of the rectangle, the rate is 1.5 times the normal keyboard rotation rate.varx_fraction_mapped=x_fraction_sign*(x_fraction_abs-this.scopeShootSpot-0.01)*1.0;mK[rect.mK]='U';this.updateDirectionDot(rectName,'yellow');}mK['ScRrf']=x_fraction_mapped.toFixed(2);}elseif(rectName=='alt'){this.updateDirectionDot(rectName,'yellow');// Show the alt-dependent rectangles as awake (ready to receive a touch). Don't do this// if the alt rectangle is already active. This check is necessary to allow the alt keys// to show yellow after they are touched. Remember, the updateRectangle function fires// twice when using the alt feature.if(!this.grid['alt'].active){this.updateDirectionDot('esc',this.gridColor);this.updateDirectionDot('demo7',this.gridColor);this.updateDirectionDot('demo8',this.gridColor);this.updateDirectionDot('freeze',this.gridColor);}// Must use the alt button for these:}elseif(this.grid['alt'].active&&(rectName=='esc'||rectName=='demo7'||rectName=='demo8'||rectName=='freeze')){if(rectName=='esc'){clientCanvas_tt.width=videoMirror.width;clientCanvas_tt.height=videoMirror.height;// Note: the alt and esc rectangles get "released" in this call to changeDisplay.this.changeDisplay('exit');return;}if(rect.mK)mK[rect.mK]='D';this.updateDirectionDot(rectName,'yellow');}// No matter what, set this rectangle to be active.rect.active=true;}// Color the rectangle that indicates the client color.TwoThumbs.prototype.colorClientRect=function(color){// Draw this a little smaller than the actual rectangle.varshrink_px=8;varULx=this.grid['color'].UL.x+shrink_px;varULy=this.grid['color'].UL.y+shrink_px;varLRx=this.grid['color'].LR.x-shrink_px;varLRy=this.grid['color'].LR.y-shrink_px;varwidth_px=LRx-ULx;varheight_px=LRy-ULy;ctx_tt.fillStyle=color;ctx_tt.fillRect(ULx,ULy,width_px,height_px);}TwoThumbs.prototype.updateAndDrawTouchGrid=function(mode){ctx_tt.fillStyle=this.bgColor;ctx_tt.fillRect(0,0,clientCanvas_tt.width,clientCanvas_tt.height);this.adjustmentPoint_2d.x=this.absPos_x_px(0.47);this.adjustmentPoint_2d.y=this.absPos_y_px(0.90);this.dirDotRadius_px=this.absPos_x_px(this.dirDotRadius_fraction);this.grid['jet_360'].cRadius_1_px=this.absPos_x_px(this.grid['jet_360'].cRadius_1_f);this.grid['jet_360'].cRadius_2_px=this.absPos_x_px(this.grid['jet_360'].cRadius_2_f);this.grid['jet_360'].cRadius_3_px=this.absPos_x_px(this.grid['jet_360'].cRadius_3_f);this.grid['gun_360'].cRadius_0_px=this.absPos_x_px(this.grid['gun_360'].cRadius_0_f);// x position of the vertical lines (from left to right).varx0=this.absPos_x_px(0.00);varx0a=this.absPos_x_px(0.10);varx0b=this.absPos_x_px(0.20);varx0c=this.absPos_x_px(0.30);varx0d=this.absPos_x_px(0.315);varx0e=this.absPos_x_px(0.455);varx1=this.adjustmentPoint_2d.x;varx2=this.absPos_x_px(0.60);// ...1.00) - this.adjustmentPoint_2d.x;varx3=this.absPos_x_px(1.00);// Center +/- the half width of the scope spot.varx2a=(x3+x2)/2.0-((x3-x2)*this.scopeShootSpot/2.0);varx2b=(x3+x2)/2.0+((x3-x2)*this.scopeShootSpot/2.0);// y position of the horizontal lines (from top to bottom).vary0=this.absPos_y_px(0.00);vary0a=this.absPos_y_px(0.65);vary0b=this.absPos_y_px(0.85);vary1=this.adjustmentPoint_2d.y;vary2=this.absPos_y_px(1.00);// Define all the rectangles in the grid. UL: upper left, LR: lower right.this.grid['jet_360'].UL=newgW.Vec2D(x0,y0);this.grid['jet_360'].LR=newgW.Vec2D(x1,y1);this.grid['jet_360'].dir_2d=newgW.Vec2D(0,this.dirDotRadius_px);this.grid['gun_360'].UL=newgW.Vec2D(x2,y0);this.grid['gun_360'].LR=newgW.Vec2D(x3,y1);this.grid['gun_360'].dir_2d=newgW.Vec2D(0,this.dirDotRadius_px);this.grid['shield'].UL=newgW.Vec2D(x1,y0);this.grid['shield'].LR=newgW.Vec2D(x2,y0a);this.grid['color'].UL=newgW.Vec2D(x1,y0a);this.grid['color'].LR=newgW.Vec2D(x2,y0b);this.grid['freeze'].UL=newgW.Vec2D(x0,y1);this.grid['freeze'].LR=newgW.Vec2D(x0a,y2);this.grid['demo7'].UL=newgW.Vec2D(x0a,y1);this.grid['demo7'].LR=newgW.Vec2D(x0b,y2);this.grid['demo8'].UL=newgW.Vec2D(x0b,y1);this.grid['demo8'].LR=newgW.Vec2D(x0c,y2);this.grid['esc'].UL=newgW.Vec2D(x0d,y1);this.grid['esc'].LR=newgW.Vec2D(x0e,y2);this.grid['alt'].UL=newgW.Vec2D(x1,y1);this.grid['alt'].LR=newgW.Vec2D(x2,y2);this.grid['gun_scope'].UL=newgW.Vec2D(x2,y0b);this.grid['gun_scope'].LR=newgW.Vec2D(x3,y2);// Calculate the center point of each rectangle.for(varrectNameinthis.grid){varrect=this.grid[rectName];rect.center_2d=rect.UL.add(rect.LR).scaleBy(1.0/2.0);}if(mode=='draw'){// Draw grid...// Vertical linesgW.drawLine(ctx_tt,newgW.Vec2D(x0a,y1),newgW.Vec2D(x0a,y2),{'width_px':3,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x0b,y1),newgW.Vec2D(x0b,y2),{'width_px':3,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x0c,y1),newgW.Vec2D(x0c,y2),{'width_px':3,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x0d,y1),newgW.Vec2D(x0d,y2),{'width_px':3,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x0e,y1),newgW.Vec2D(x0e,y2),{'width_px':3,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x1,y0),newgW.Vec2D(x1,y2),{'width_px':5,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x2,y0),newgW.Vec2D(x2,y2),{'width_px':5,'color':this.gridColor});// Vertical lines in the scope rectanglegW.drawLine(ctx_tt,newgW.Vec2D(x2a,y0b),newgW.Vec2D(x2a,y2),{'width_px':1,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x2b,y0b),newgW.Vec2D(x2b,y2),{'width_px':1,'color':this.gridColor});// Draw the vertical gradient lines in the scope rectangle.varwidth_px=x2a-x2;varstep_px=Math.round(width_px/3);varlength_px=Math.round((y2-y0b)/3);for(vari=step_px;i<width_px;i+=step_px){gW.drawLine(ctx_tt,newgW.Vec2D(x2+i,y2-length_px),newgW.Vec2D(x2+i,y2),{'width_px':1,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x3-i,y2-length_px),newgW.Vec2D(x3-i,y2),{'width_px':1,'color':this.gridColor});step_px*=0.60;step_px=Math.round(step_px);if(step_px<3)step_px=3;}// Horizontal lines// First two run only the width of the shield rectangle.gW.drawLine(ctx_tt,newgW.Vec2D(x1,y0a),newgW.Vec2D(x2,y0a),{'width_px':5,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x1,y0b),newgW.Vec2D(x2,y0b),{'width_px':5,'color':this.gridColor});// The next pair do the main bottom line: second segment is at a higher y level for the scope rectangle.gW.drawLine(ctx_tt,newgW.Vec2D(x0,y1),newgW.Vec2D(x2,y1),{'width_px':5,'color':this.gridColor});gW.drawLine(ctx_tt,newgW.Vec2D(x2,y0b),newgW.Vec2D(x3,y0b),{'width_px':5,'color':this.gridColor});// Adjustment Point//gW.drawCircle( ctx_tt, this.adjustmentPoint_2d, {'fillColor': 'red', 'radius_px':5} );// Direction dotsgW.drawCircle(ctx_tt,this.grid['jet_360'].center_2d,{'fillColor':this.gridColor,'radius_px':this.dirDotRadius_px});gW.drawCircle(ctx_tt,this.grid['gun_360'].center_2d,{'fillColor':this.gridColor,'radius_px':this.dirDotRadius_px});gW.drawCircle(ctx_tt,this.grid['shield'].center_2d,{'fillColor':this.gridColor,'radius_px':this.dirDotRadius_px});gW.drawCircle(ctx_tt,this.grid['alt'].center_2d,{'fillColor':this.gridColor,'radius_px':this.dirDotRadius_px});gW.drawCircle(ctx_tt,this.grid['gun_scope'].center_2d,{'fillColor':this.gridColor,'radius_px':this.dirDotRadius_px});// Control ringgW.drawCircle(ctx_tt,this.grid['jet_360'].center_2d,{'fillColor':'noFill','radius_px':this.grid['jet_360'].cRadius_1_px,'borderWidth_px':3,'borderColor':this.jetRadiusColor_1});gW.drawCircle(ctx_tt,this.grid['jet_360'].center_2d,{'fillColor':'noFill','radius_px':this.grid['jet_360'].cRadius_2_px,'borderWidth_px':3,'borderColor':this.jetRadiusColor_2});gW.drawCircle(ctx_tt,this.grid['jet_360'].center_2d,{'fillColor':'noFill','radius_px':this.grid['jet_360'].cRadius_3_px,'borderWidth_px':3,'borderColor':this.jetRadiusColor_3});gW.drawCircle(ctx_tt,this.grid['gun_360'].center_2d,{'fillColor':'noFill','radius_px':this.grid['gun_360'].cRadius_0_px,'borderWidth_px':3,'borderColor':this.gunRadiusColor_0});// Text labelsctx_tt.font="25px Arial";ctx_tt.fillStyle=this.gridColor;// Scale the positioning on this text using the x-axis scaling (not a mix of x and y) and include// a pixel offset on the y coordinate to account for the pixel height of the font.ctx_tt.fillText('jet',this.grid['jet_360'].UL.x+this.absPos_x_px(0.018),this.grid['jet_360'].UL.y+this.absPos_x_px(0.018)+20);ctx_tt.fillText('pea shooter',this.grid['gun_360'].UL.x+this.absPos_x_px(0.018),this.grid['gun_360'].UL.y+this.absPos_x_px(0.018)+20);ctx_tt.font="20px Arial";ctx_tt.fillText('shield',this.grid['shield'].UL.x+this.absPos_x_px(0.018),this.grid['shield'].UL.y+this.absPos_x_px(0.018)+20);ctx_tt.font="15px Arial";ctx_tt.fillText('esc',this.grid['esc'].UL.x+this.absPos_x_px(0.009),this.grid['esc'].UL.y+this.absPos_x_px(0.018)+10);ctx_tt.fillText('7',this.grid['demo7'].UL.x+this.absPos_x_px(0.009),this.grid['demo7'].UL.y+this.absPos_x_px(0.018)+10);ctx_tt.fillText('8',this.grid['demo8'].UL.x+this.absPos_x_px(0.009),this.grid['demo8'].UL.y+this.absPos_x_px(0.018)+10);ctx_tt.fillText('f',this.grid['freeze'].UL.x+this.absPos_x_px(0.009),this.grid['freeze'].UL.y+this.absPos_x_px(0.018)+10);ctx_tt.fillText('alt',this.grid['alt'].UL.x+this.absPos_x_px(0.009),this.grid['alt'].UL.y+this.absPos_x_px(0.018)+10);ctx_tt.fillText('ccw',this.grid['gun_scope'].UL.x+this.absPos_x_px(0.009),this.grid['gun_scope'].UL.y+this.absPos_x_px(0.018)+10);ctx_tt.fillText('cw',x2b+this.absPos_x_px(0.009),this.grid['gun_scope'].UL.y+this.absPos_x_px(0.018)+10);if(cl.name)this.colorClientRect(clientColor(cl.name));}}// Functions supporting full-screen display modeTwoThumbs.prototype.changeDisplay=function(mode){if((mode=='fullScreen')||(mode=='normal')){if(window.innerWidth<window.innerHeight){varorientationMessage="The Two-Thumbs client requests that your phone be oriented for landscape viewing. Please turn it sideways, then try touching the Two-Thumbs button again."alert(orientationMessage);displayMessage(orientationMessage);return;}this.enabled=true;// If there's a stream active, shut it down.if(chkRequestStream.checked){chkRequestStream.click();}// Reveal the canvas.videoMirror.setAttribute("hidden",null);clientCanvas_tt.removeAttribute("hidden");// A reference to the HTML root element.//changeFullScreenMode( document.documentElement, 'on');if(mode=='fullScreen'){changeFullScreenMode(clientCanvas_tt,'on');// Delay is needed with FireFox.window.setTimeout(function(){clientCanvas_tt.width=window.innerWidth-10;clientCanvas_tt.height=window.innerHeight-10;},600);// Wait a little longer than the canvas-resize delay above.// Notice "this" context is passed in with bind.window.setTimeout(function(){this.updateAndDrawTouchGrid('draw');}.bind(this),700);}elseif(mode=='normal'){this.updateAndDrawTouchGrid('draw');}}elseif(mode=='exit'){changeFullScreenMode(clientCanvas_tt,'off');// De-activate the two rectangles that may have gotten you in here (remember, you didn't have to lift// your fingers). This effectively resets these rectangles (like releasing your touch).this.grid['esc'].active=false;this.grid['alt'].active=false;// Reveal the video element (and hide the canvas).videoMirror.removeAttribute("hidden");clientCanvas_tt.setAttribute("hidden",null);chkTwoThumbs.checked=false;this.enabled=false;}}////////////////////////////////////////////////// // Functions supporting the socket.io connections////////////////////////////////////////////////// functiondisableClientControls(diableMode){// diableMode: true (disable it) or falseif(diableMode){$('#ConnectButton').html('Wait');$('#ConnectButton').prop('disabled',true);$('#chkRequestStream').prop('disabled',true);$('#twoThumbsButton').prop('disabled',true);$('#ChatButton').prop('disabled',true);}else{// Change the label from 'Wait' to 'Connect'.$('#ConnectButton').html('Connect');$('#ConnectButton').prop('disabled',false);// Note: the streaming checkbox opens when the p2p data-channel opens (see cl.rtc.dataChannel.onopen).// the two-thumbs button opens when the room is successfully joined.// the chat button opens when the room is successfully joined.}}functioncheckForNickName(mode,hostOrClient){varnickName={'status':'ok','value':null};//return nickName;// Check the chat input field, e.g. nn:Jimbo (that's how the user inputs it).varchatString=$('#inputField').val();if(mode=='normal'){// New nick name in the chat input field.if(chatString.includes('nn:')||chatString.includes('Nn:')){nickName.value=chatString.slice(3,chatString.length);if(nickName.value.length>9){nickName.status="too long";returnnickName;}else{if(hostOrClient=='client'){// Make the nickName accessible from the client. Remember, this cl object// exists on the client.cl.nickName=nickName.value;}elseif(hostOrClient=='host'){gW.clients['local'].nickName=nickName.value;}// Clear out the input field where the nick name was entered.$('#inputField').val('');}// Nothing new, so use the current nick name if it's there. }else{if(hostOrClient=='client'){nickName.value=cl.nickName;}elseif(hostOrClient=='host'){nickName.value=gW.clients['local'].nickName;}}}elseif((mode=='re-connect')&&cl.nickName){nickName.value=cl.nickName;}returnnickName;}functionconnect_and_listen(hostOrClient,mode){// First, run some checks on the room name.varroomName=$('#roomName').val();// Gotta have something...if(roomName==""){varbuttonName=(hostOrClient=='client')?'"Connect"':'"Create"';displayMessage('Type in a short "Room" name, then click the '+buttonName+' button.');document.getElementById("roomName").style.borderColor="red";return;// 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)}elseif(roomName.length>7){displayMessage('The name should have 7 characters or less.');document.getElementById("roomName").style.borderColor="red";return;}// Check to see if there's a nickname in the chat input field.varnickName=checkForNickName(mode,hostOrClient);if(nickName.status=='too long'){displayMessage('Nicknames must have fewer than 10 characters. Shorten the name and then try connecting again.');return;}if(hostOrClient=='client'){// Disable some of the client controls to keep users from repeatedly// clicking the connect button.disableClientControls(true);refresh_P2P_indicator({'mode':'connecting'});// Open the connect button after 4 seconds. Sometimes there are network delays.// Note: most of the disabled controls open based on events. For example: the // streaming checkbox opens when the p2p data-channel opens (see cl.rtc.dataChannel.onopen).window.setTimeout(function(){disableClientControls(false);},4000);}else{displayMessage('Connecting as host. Please wait up to 20 seconds...');}varnodeString=$('#nodeServer').val();if(nodeString==""){// Use one in the list as a default.nodeString=serverArray[0];// [0] or [2]$('#nodeServer').val(nodeString);}if(nodeString.includes("heroku")){varurlPrefix="https://"}else{varurlPrefix="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 there are already active network connections, close them before making new ones. This is // the case if the client repeatedly clicks the connect button trying to get a preferred color.if(socket){if(hostOrClient!='host'){// Send a message to the host (via socket.io server) to shutdown RTC connections.if(newClientName){if(videoMirror.srcObject)videoMirror.srcObject=null;// Trigger client shutdown at the host.socket.emit('shutDown-p2p-deleteClient',newClientName);}}window.setTimeout(function(){// Close socket.io connection after waiting a bit for the p2p connections to close.socket.disconnect();},500);}// Delay this (connection to the server) even longer than the socket.disconnect() above (to be sure the disconnect is done).window.setTimeout(function(){// When starting a new normal connection, turn off the stream.if(mode=='normal'&&(hostOrClient!='host'))chkRequestStream.checked=false;// Here is where the socket.io client initiates it's connection to the server. The 'query' parameter// shows the form of the query string needed for a multi-parameter example. This is how you pass parameters// to the connection handler in server.js.if(nickName.value){varnickNameString='&nickName='+nickName.value;}else{varnickNameString='';}varqueryString='mode='+mode+'&currentName='+cl.name+nickNameString;socket=io.connect(nodeServerURL,{'forceNew':true,'query':queryString});init_socket_listeners(roomName,hostOrClient);},600);// 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.');document.getElementById("roomName").style.borderColor="red";refresh_P2P_indicator({'mode':'reset'});});}functiongetGameReportCounter(){returngameReportCounter;}functiondisplayMessage(msgText){if(msgText.includes("Game Summary")){gameReportCounter+=1;varidString=" id='gR"+gameReportCounter+"'";}else{varidString="";}// Every other line, toggle the background shading.if(chatStyleToggle){varstyleString="style='background: #efefef;'";}else{varstyleString="style='background: #d9d9d9;'";}$("#messages").prepend("<li "+styleString+idString+">"+msgText+"</li>");chatStyleToggle=!chatStyleToggle;}// Used for broadcasting a message to non-host players.functionchatToNonHostPlayers(msgTxt){if(socket)socket.emit('chat message but not me',msgTxt+'</br>');}functioninit_chatFeatures(hostOrClient){serverArray=['secure-retreat-15768.herokuapp.com','localhost:3000','192.168.1.106:3000','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+"'>");});varpingTestHelp="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(){varchatString=$('#inputField').val();if(socket){if(chatString=='ping'){echoTest('server');displayMessage(pingTestHelp);}elseif(chatString=='ping:host'){echoTest('host');displayMessage(pingTestHelp);}else{socket.emit('chat message',chatString);$('#inputField').val('');//clear out the input field.}}else{varbuttonName=(hostOrClient=='client')?'"Connect"':'"Create"';displayMessage('Type in a short "Room" name, then click the '+buttonName+' button.');}returnfalse;});// 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 areavarhelloMessage;if(hostOrClient=='host'){helloMessage=''+'This is the host page for multiplayer.</br></br>'+'Click the multiplayer checkbox to toggle between this chat panel and the discussion/help panel. Doing so will not disable connections.</br></br>'+'From here you can host a multiplayer room. '+'Please notice the links to the client page in the right panel below the multiplayer checkbox. '+'You can also get to the client page from the three-line menu icon in the upper left corner.</br></br>'+'To get started, type a short room name into the red box, then click the "Create" button.</br></br>'+'Please note, when setting up the room as host, you might 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 confimation message in this chat area.</br></br>'+'To start over, or disconnect from the server, please reload the page.';}else{helloMessage=''+'This is the client page for multiplayer.</br></br>'+'From here you can be a client in a multiplayer room. The room must be started (hosted) from the main www.timetocode.org page. '+'Generally, a separate computer is used for hosting. For testing, the host and multiple clients can be run in separate windows on the same computer.</br></br>'+'To connect as a client, type, into the red box (here, on this client page), the room name provided to you by the host, then click the "Connect" button.</br></br>'+'To start over, or disconnect from the server, please reload the page.';}displayMessage(helloMessage);}functionclientColor(clientName){varcolors={'1':'yellow','2':'blue','3':'green','4':'pink','5':'orange','6':'brown','7':'greenyellow','8':'cyan','9':'tan','0':'purple'};varn=clientName.slice(1);varcolorIndex=n-Math.trunc(n/10)*10;returncolors[colorIndex];}functioninit_socket_listeners(roomName,hostOrClient){// Listeners needed by both the client and the host.// Listen for chat being forwarded by the server.socket.on('chat message',function(msg){displayMessage(msg);});// Change the border color of the roomName input box depending on the // message from the node server. And add additional info to the message.socket.on('room-joining-message',function(msg){if(msg.includes('You have joined room')){// Some visual indicators that all is well.document.getElementById("roomName").style.borderColor="#008080";//Dark green.if(hostOrClient=='client')$('#twoThumbsButton').prop('disabled',false);$('#ChatButton').prop('disabled',false);// If the names are the same, it indicates the network client has rejoined with a video stream.if((hostOrClient=='client')&&(cl.name==cl.previous_name)){if(cl.nickName){varnNstring=' ('+cl.nickName+').';}else{varnNstring='.';}msg='You have reconnected with a video stream. Your name is still '+cl.name+nNstring;// Additional instructions if this is a non-host client}elseif(hostOrClient=='client'){msg+=''+"</br></br>"+"You are in <strong>normal desktop</strong> mode. Your mouse and keyboard events get sent to the host. You must have direct visual access to the host's monitor."+"</br></br>"+"Two other options:</br></br>"+"<strong>Stream:</strong> This is like normal mode, but the host's canvas is rendered in the video element here. "+"So you can play out-of-sight of the host's monitor, in a separate room, city, country...</br></br>"+"<strong>Two Thumbs:</strong> touch-screen interface for your phone. Similar to normal mode, this requires line-of-sight to the host's monitor. "+"However, you can start up a second client (on a second device) and stream to it if you don't have line-of-sight.</br>";}// Client might get this warning...}elseif(msg.includes('Sorry, there is no host')){document.getElementById("roomName").style.borderColor="red";refresh_P2P_indicator({'mode':'reset'});// A candidate host might get this warning... }elseif(msg.includes('Sorry, there is already a host')){document.getElementById("roomName").style.borderColor="red";// Additional instructions for the new host. This room-joining-message event will have to be triggered a second time to get this message to the host after// the "You have joined room" message above.}elseif(msg.includes('You are the host')){varopenWindowString='"'+"window.open('indexClient.html', '_blank', 'width=1320, height=650') "+'"';msg+=''+"</br></br>"+"You can open a test <a href='#' onClick="+openWindowString+"title='Open a client page in a new window.'>client</a> in a new window. "+"Connect using 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);});// Once your connection succeeds, join a room.socket.on('connect',function(){if(hostOrClient=='host'){socket.emit('roomJoin',JSON.stringify({'hostOrClient':hostOrClient,'roomName':roomName}));}elseif(hostOrClient=='client'){socket.emit('roomJoin',JSON.stringify({'hostOrClient':hostOrClient,'roomName':roomName,'player':chkPlayer.checked,'requestStream':chkRequestStream.checked}));}});// Listen for echo response from the server.socket.on('echo-from-Server-to-Client',function(msg){varechoTarget=msg;// Stop timer (measure the round trip).timer.stop=window.performance.now();varelapsed_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){vartimeAvg=Math.mean(timer.pingArray).toFixed(1);vartimeSTD=Math.std(timer.pingArray).toFixed(1);vartimeLen=timer.pingArray.length;vartimeMax=Math.max(timer.pingArray).toFixed(1);vartimeMin=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);});// WebRTC Signaling.// This handles signaling from both sides of the peer-to-peer connection.socket.on('signaling message',function(msg){// Convert it back to a usable object (parse it).varsignal_message=JSON.parse(msg);// Note that signalData needs to be in a stringified form when writing to the console.//console.log("signal message from " + signal_message.from + ", to " + signal_message.to + ": " + JSON.stringify(signal_message.signalData));// Offers and Answersif(signal_message.signalData.sdp){//console.log('sdp in signal from host: ' + JSON.stringify(signal_message.signalData));if(signal_message.signalData.type=='offer'){//console.log("an offer");handleOffer(signal_message.signalData);}elseif(signal_message.signalData.type=='answer'){//console.log("an answer");handleAnswer(signal_message.signalData);}else{console.log("Woooooo-HoHo-Hoooooo, something is screwed up. This can't be good.");}// ICE candidates}elseif(signal_message.signalData.candidate){// handle ICE stuff.cl.rtc.pc.addIceCandidate(signal_message.signalData).catch(function(reason){// An error occurred, so...console.log('Error while handling ICE stuff:'+reason);});//console.log('signaling state after handling ICE = ' + cl.rtc.pc.signalingState);}else{//No WebRTC stuff found in the signaling message. Maybe you are testing...console.log("In final else block of 'signaling message' handler.");}});socket.on('control message',function(msg){// General receiver of control messages. This can be used by either the host or a client to// receive messages from anyone.// Convert the raw msg back to a usable object (parse it).varmessage=JSON.parse(msg);// Control message directed to the host.if(message.to=='host'){if(message.data.videoStream=='off'){gW.clients[message.from].rtc.turnVideoStreamOff();}elseif(message.data.fullScreen=='off'){console.log('full screen requested off by client');// Tried to do something here, but browsers fullscreen API requires that the// a change starts with a gesture. The error: '...API can only be initiated by a user gesture.'}// Control message directed to a non-host client.}else{}});// Listeners needed by the client only.if(hostOrClient=='client'){socket.on('your name is',function(msg){varmessage=JSON.parse(msg);varname=setDefault(message.name,null);// Note: not (yet) doing anything with nickName that comes back from the socket.io server.// cl.nickName gets set for the client on the front end of the connection. Just including it// here for completeness.varnickName=setDefault(message.nickName,null);// Put this name in the mouse and keyboard (mK) global that is used to send// state data from the client.mK.name=name;// Put your name in this global (on the client side) for (possible) use by the WebRTC functions.newClientName=name;// Before updating cl.name with the new client name, store it's current value in previous_name.cl.previous_name=cl.name;cl.name=newClientName;console.log('names: current='+cl.name+', previous='+cl.previous_name+', nick='+nickName);// Initialize this global container for the WebRTC stuff.cl.rtc=newRTC({'user1':newClientName,'user2':'host'});});socket.on('disconnectByServer',function(msg){if(db.rtc)console.log('in client disconnectByServer, msg='+msg);varclientName=msg;displayMessage("This client ("+clientName+") is being disconnected by the host.");document.getElementById("roomName").style.borderColor="red";// When the server gets this one, it will remove the socket.socket.emit('okDisconnectMe',clientName);// Shutdown and delete the client side of the WebRTC p2p connection.cl.rtc.shutdown();initialize_mK();//mK = {};// Delay this so it takes effect after the p2p toggle finishes.window.setTimeout(function(){displayMessage("");displayMessage("Shutdown of the p2p connection for "+clientName+" has finished.");displayMessage("");displayMessage("");displayMessage("");},100);});socket.on('command-from-host-to-all-clients',function(msg){// Clients (only) do something based on the message from the host.varcommand_message=JSON.parse(msg);vartype=command_message.type;varcommand=command_message.command;if(type=='resize'){gW.adjustSizeOfChatDiv(command);if(command=='normal'){videoMirror.width=600,videoMirror.height=600;}else{videoMirror.width=1250,videoMirror.height=950;}}else{console.log("I don't recognize that command; hey, I'm just saying...");}});}// Listeners needed by the host only.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.// StH: Server to Hostsocket.on('client-mK-StH-event',function(msg){varmsg_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){varmsgParsed=JSON.parse(msg);varstreamRequested=msgParsed.requestStream;varclientName=msgParsed.clientName;varplayer=msgParsed.player;varnickName=msgParsed.nickName;gW.createNetworkClient({'clientName':clientName,'player':player,'nickName':nickName});// WebRTC. Start the p2p connection here (from the host) when we hear (from the server)// that a client is trying to connect to a room.// Make a global reference to this new (the most recent) client's RTC object.cl=gW.clients[clientName];cl.rtc.user1='host';cl.rtc.user2=clientName;cl.rtc.streamRequested=streamRequested;if(db.rtc)console.log('in new-game-client, cl.rtc.user2 = '+cl.rtc.user2);// Start the WebRTC signaling exchange with the new client.// Diagnostic tools: chrome://webrtc-internals (in Chrome) and about:webrtc (in Firefox)try{openDataChannel(true);// open as the initiatorcreateOffer();}catch(e){console.log("WebRTC startup: "+e);}// Someone just connected. Send the layout state to them (actually to everyone, but that// should, of course, cover the connecting user also). Delay it a bit...window.setTimeout(function(){resizeClients(gW.getChatLayoutState());},300);});socket.on('client-disconnected',function(msg){varclientName=msg;if(db.rtc)console.log('in client-disconnected, clientName='+clientName);// Null out any WebRTC references in c object (most recent connection on the host page) if it happens to be// this client.nullReferences_toRTC_on_c(clientName);// 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);});socket.on('shutDown-p2p-deleteClient',function(msg){if(db.rtc)console.log('in shutDown-p2p-deleteClient');varclientName=msg;// First check for the case where the host has reloaded their page and // then a client attempts to reconnect. In that case the clients map will be empty and// this clientName won't be found in there.if(gW.clients[clientName]){// Check for a puck controlled by this client. Delete it first.if(gW.clients[clientName].puck)gW.clients[clientName].puck.deleteThisOne({});// Then start shutting down the WebRTC connection.gW.deleteRTC_onClientAndHost(clientName);}});}}// end of init_socket_listeners// The following two functions are exposed for external use and are called from within gwModule.js.functionforceClientDisconnect(clientName){if(db.rtc)console.log('in forceClientDisconnect');socket.emit('clientDisconnectByHost',clientName);}functionresizeClients(command){if(socket){socket.emit('command-from-host-to-all-clients',JSON.stringify({'type':'resize','command':command}));}}functionechoTest(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);}////////////////////////////////////////////////// Functions supporting the WebRTC connections.//////////////////////////////////////////////// varconfiguration={'iceServers':[{'urls':'stun:stun1.l.google.com:19302'}]};functionopenDataChannel(isInitiator){cl.rtc.pc=newRTCPeerConnection(configuration);// send any ice candidates to the other peercl.rtc.pc.onicecandidate=function(evt){if(evt.candidate){varsignal_message={'from':cl.rtc.user1,'to':cl.rtc.user2,'signalData':evt.candidate};socket.emit('signaling message',JSON.stringify(signal_message));}};// Host-side data channelif(isInitiator){vardc_id=cl.rtc.user2.slice(1);vardc_options={'id':dc_id,'ordered':false,'maxRetransmits':1};vardc_label="dc-"+cl.rtc.user2;cl.rtc.dataChannel=cl.rtc.pc.createDataChannel(dc_label,dc_options);cl.rtc.dataChannel.onmessage=function(e){handle_RTC_message(e);};cl.rtc.dataChannel.onopen=function(){console.log("------ RTC DC(H) OPENED ------");};cl.rtc.dataChannel.onclose=function(){console.log("------ RTC DC(H) closed ------");};cl.rtc.dataChannel.onerror=function(){console.log("RTC DC(H) error.....");};if(cl.rtc.streamRequested){startVideoStream();}// Client-side data channel}else{// This side of the data channel gets established in response to the channel initialization // on the host side.cl.rtc.pc.ondatachannel=function(evt){cl.rtc.dataChannel=evt.channel;// Must set up an onmessage handler for the clients too.cl.rtc.dataChannel.onmessage=function(e){console.log("DC (@client) message:"+e.data);};cl.rtc.dataChannel.onopen=function(){console.log("------ RTC DC(C) OPENED ------");rtc_choke=false;$('#chkRequestStream').prop('disabled',false);refresh_P2P_indicator({});};cl.rtc.dataChannel.onclose=function(){console.log("------ RTC DC(C) closed ------");rtc_choke=true;};cl.rtc.dataChannel.onerror=function(){console.log("RTC DC(C) error.....");};}// Respond to a new track by sending the stream to the video element.cl.rtc.pc.ontrack=function(evt){videoMirror.srcObject=evt.streams[0];};}//console.log('signaling state after openDataChannel = ' + cl.rtc.pc.signalingState);}// This function is used (only) by the host when someone connects and wants a stream.functionstartVideoStream(){if(!videoStream){varhostCanvas=document.getElementById('hostCanvas');videoStream=hostCanvas.captureStream();//60}cl.rtc.pc.addTrack(videoStream.getVideoTracks()[0],videoStream);// The chkStream is on the host page only (index.html)document.getElementById("chkStream").checked=true;videoStream.getVideoTracks()[0].enabled=true;}functionsetCanvasStream(newState){if(videoStream){if(newState=='on'){videoStream.getVideoTracks()[0].enabled=true;}else{videoStream.getVideoTracks()[0].enabled=false;}}}functionhandle_RTC_message(msg){//var user2 = Object.assign({}, cl.rtc.user2);/* var user2 = JSON.stringify(cl.rtc.user2); console.log("I am (cl.rtc.user2) = " + user2); console.log("DC ID = " + JSON.stringify(cl.rtc.dataChannel.id)); console.log("DC (@host) message: " + e.data); */// Process mK events from the client on the other end of this peer-to-peer connection.varmK_string=msg.data;varmK_data=JSON.parse(mK_string);// Send this mouse-and-keyboard state to the engine.gW.updateClientState(mK_data.name,mK_data);}functioncreateOffer(){cl.rtc.pc.createOffer().then(function(offer){returncl.rtc.pc.setLocalDescription(offer);}).then(function(){varsignal_message={'from':cl.rtc.user1,'to':cl.rtc.user2,'signalData':cl.rtc.pc.localDescription};socket.emit('signaling message',JSON.stringify(signal_message));}).catch(function(reason){// An error occurred, so handle the failure to connectconsole.log('Error while creating offer:'+reason);});//console.log('signaling state after createOffer = ' + cl.rtc.pc.signalingState);}functionhandleOffer(msg){openDataChannel(false);// Open as NOT the initiatorcl.rtc.pc.setRemoteDescription(msg).then(function(){returncl.rtc.pc.createAnswer();}).then(function(answer){returncl.rtc.pc.setLocalDescription(answer);}).then(function(){// Send the answer (localDescription) to the remote peervarsignal_message={'from':cl.rtc.user1,'to':cl.rtc.user2,'signalData':cl.rtc.pc.localDescription};socket.emit('signaling message',JSON.stringify(signal_message));}).catch(function(reason){console.log('Error while handling offer:'+reason);});//console.log('signaling state after handleOffer = ' + cl.rtc.pc.signalingState);}functionhandleAnswer(answer){cl.rtc.pc.setRemoteDescription(answer).catch(function(reason){console.log('Error while handling answer:'+reason);});//console.log('signaling state after handleAnswer = ' + cl.rtc.pc.signalingState);}functionlogError(error){console.log(error.name+': '+error.message);}functionnullReferences_toRTC_on_c(clientName){// Check the global "c" pointer (to the most recently connected client) to see if it happens to// be pointed at this client.//console.log('cl.rtc='+JSON.stringify( cl.rtc) + ", newClientName=" + clientName);if(cl.rtc&&(cl.rtc.user2==clientName)){cl.rtc=newRTC({});}}functionrefresh_P2P_indicator(pars){varmode=setDefault(pars.mode,'p2p');// Stop the pacifier (note: pacifier is a global object)clearInterval(pacifier.intFunction);// If connected, there will be a name (assigned from the server)if((mode=='p2p')&&cl.name){// Show (flood/erase the canvas with) the client's color.ctx.fillStyle=clientColor(cl.name);ctx.fillRect(0,0,clientCanvas.width,clientCanvas.height);ctx.font="12px Arial";// Use dark letters for the lighter client colors.varlightColors=['yellow','greenyellow','pink','cyan','tan'];if(lightColors.includes(clientColor(cl.name))){ctx.fillStyle='black';}else{ctx.fillStyle='white';}// If choke, RTC, data channel, and readyState are ok, display the "P2P" text.if(!rtc_choke&&cl.rtc&&cl.rtc.dataChannel&&(cl.rtc.dataChannel.readyState=='open')){ctx.fillText('P2P',10,12);}else{ctx.fillText('socket.io',10,12);}}elseif(mode=='connecting'){ctx.fillStyle='darkgray';ctx.fillRect(0,0,clientCanvas.width,clientCanvas.height);ctx.font="12px Arial";ctx.fillStyle='white';ctx.fillText('CONNECTING',10,12);// Start the pacifierpacifier.string='';pacifier.intFunction=setInterval(function(){pacifier.string+='--';ctx.fillText(pacifier.string,95,12);},200);}elseif(mode=='reset'){// Light gray fill.ctx.fillStyle='#EFEFEF';ctx.fillRect(0,0,clientCanvas.width,clientCanvas.height);}}////////////////////////////////////////////////////////////////////////////////// Functions supporting canvas animation////////////////////////////////////////////////////////////////////////////////// Currently not using this steady animation loop approach. Instead, update the canvas // as input events get fired. Refer to the methods in the TwoThumbs class./* function canvasLoop( timeStamp_ms) { updateCanvas(); myRequest = window.requestAnimationFrame( canvasLoop); } function updateCanvas() { // Clear the canvas (from one corner to the other) if (ctx_tt.globalCompositeOperation == 'screen') { ctx_tt.clearRect(0,0, clientCanvas_tt.width, clientCanvas_tt.height); } else { ctx_tt.fillStyle = 'blue'; ctx_tt.fillRect(clientCanvas_tt.width/8, clientCanvas_tt.width/8, clientCanvas_tt.width/4, clientCanvas_tt.height/4); } if (twoThumbs.enabled) { // Draw the two-thumb state if (cl.name) { var circleColor = clientColor( cl.name); } else { var circleColor = 'white'; } gW.drawCircle( ctx_tt, {'x':mK.mX, 'y':mK.mY}, {'fillColor': circleColor} ); } } function startAnimation() { // Only start a game loop if there is no game loop running. if (myRequest === null) { // Start the canvas loop. myRequest = window.requestAnimationFrame( canvasLoop); } } function stopAnimation() { window.cancelAnimationFrame( myRequest); myRequest = null; } */////////////////////////////////////////////////////////////////////////////////// Misc functions////////////////////////////////////////////////////////////////////////////////functioninit_nonHostClients(){init_eventListeners_nonHostClients();init_chatFeatures('client');twoThumbs=newTwoThumbs({});}functionsetDefault(theValue,theDefault){// Return the default if the value is undefined.return(typeoftheValue!=="undefined")?theValue:theDefault;}functionchangeFullScreenMode(targetElement,mode){if(mode=='on'){if(targetElement.requestFullscreen){targetElement.requestFullscreen();}elseif(targetElement.mozRequestFullScreen){targetElement.mozRequestFullScreen();}elseif(targetElement.webkitRequestFullScreen){targetElement.webkitRequestFullScreen();}elseif(targetElement.msRequestFullscreen){targetElement.msRequestFullscreen();}}elseif(mode=='off'){if(document.exitFullscreen){document.exitFullscreen();}elseif(document.mozCancelFullScreen){document.mozCancelFullScreen();}elseif(document.webkitCancelFullScreen){document.webkitCancelFullScreen();}elseif(document.msExitFullscreen){document.msExitFullscreen();}}}functionhandle_sending_mK_data(mK){// Use WebRTC datachannel if availableif(cl.rtc&&cl.rtc.dataChannel&&(cl.rtc.dataChannel.readyState=='open')&&(rtc_choke==false)){cl.rtc.dataChannel.send(JSON.stringify(mK));// Otherwise use socket.io (WebSocket)}elseif(socket){socket.emit('client-mK-event',JSON.stringify(mK));}}////////////////////////////////////////////////////////////////////////////////// Event listeners to capture mouse and keyboard (m & K) state from the non-host // clients. ////////////////////////////////////////////////////////////////////////////////functioninitialize_mK(){// Initialize the Mouse and Keyboard (mK) state object.// isMouseDownmK.MD=false;// mouse button number (which of the three: 0,1,2)mK.bu=0;// mouse position in pixels: X_px, Y_pxmK.mX=5;mK.mY=5;// Use the keyMap to define and initialize all the key states (to UP) in the // mK (mouse and keyboard state) object that is sent to the host.for(varkeyinkeyMap){mK[keyMap[key]]='U';}for(varkeyinkeyMap_cso){mK_cso[keyMap_cso[key]]='U';}// Initialize non-keyboard attributes (for the Two Thumbs interface)// Gun scopemK['ScRrf']=0.00;mK['ScTr']='U';// Jet throttle (the full throttle)mK['jet_t']=1.0;}functioninit_eventListeners_nonHostClients(){initialize_mK();clientCanvas=document.getElementById('connectionCanvas');ctx=clientCanvas.getContext('2d');clientCanvas_tt=document.getElementById('twoThumbsCanvas');ctx_tt=clientCanvas_tt.getContext('2d');myRequest=null;videoMirror=document.getElementById('videoMirror');// 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();returnfalse;},{capture:false});// For the client, keep these listeners on all the time so you can see the client cursor.// To avoid scrolling behavior on the video element, had to set up specific event handlers// for that element (videoMirror) and use preventDefault. This wasn't necessary on the host// side because no video element there (just a canvas).videoMirror.addEventListener("touchmove",function(e){e.preventDefault();handleMouseOrTouchMove(e,'touchmove');},{capture:false});videoMirror.addEventListener("mousemove",function(e){e.preventDefault();handleMouseOrTouchMove(e,'mousemove');},{capture:false});clientCanvas_tt.addEventListener("touchmove",function(e){e.preventDefault();handleMouseOrTouchMove(e,'touchmove');},{capture:false});document.addEventListener("mousedown",function(e){// Keep mousedown from firing in the TwoThumbs interface. Necessary for Android where this// would fire after the touchstart and screw up the direction dot. This of course prevents// use of the mouse in TwoThumbs, but that's fine, need fingers there...if(twoThumbs.enabled)return;mK.MD=true;// Mouse DownmK.bu=e.button;// Mouse button//Pass this first mouse position to the move handler.handleMouseOrTouchMove(e,'mousedown');//if (cl.rtc && cl.rtc.dataChannel) cl.rtc.dataChannel.send( 'mouse-down event, id = ' + cl.rtc.dataChannel.id);},{capture:false});document.addEventListener("touchstart",function(e){// Note: the following canvas style is set:// touch-action: none;// This keep the canvas from sliding when flinging objects.// Prevent the mousedown event from firing. But in the end decided to put the check-and-return// statement in the first line of mousedown. That works in all devices and all browsers. The following// statement problematically blocked touch operations in off-canvas areas of the client when running in// Firefox.//e.preventDefault(); // works great (to prevent mousedown) for laptop but not Android.mK.MD=true;// Mouse DownmK.bu=0;// Mouse button//Pass this first mouse position to the move handler.handleMouseOrTouchMove(e,'touchstart');},{capture:false});functionhandleMouseOrTouchMove(e,fromListener){if(twoThumbs.enabled){vartouchPoints_2d_px=[];// Determine event type// Mouse (single contact point)if(e.clientX&&(mK.MD==true)){touchPoints_2d_px[0]=gW.screenFromRaw_2d_px(clientCanvas_tt,newgW.Vec2D(e.clientX,e.clientY));// Touch screen (possibly multiple contact points)}elseif(e.touches){// Tried this but can't. Must start with a gesture on the host.// Use 4-finger touch to toggle fullscreen on the host.if((e.touches.length==4)&&(fromListener!='touchmove')){varcontrol_message={'from':cl.name,'to':'host','data':{'fullScreen':'off'}};socket.emit('control message',JSON.stringify(control_message));}for(vari=0,len=e.touches.length;i<len;i++){touchPoints_2d_px[i]=gW.screenFromRaw_2d_px(clientCanvas_tt,newgW.Vec2D(e.touches[i].clientX,e.touches[i].clientY));}}// Interpret the touch and mouse events using the twoThumbs interface.twoThumbs.processMultiTouch(touchPoints_2d_px);}else{// Determine event type// Mouseif(e.clientX){varraw_x_px=e.clientX;varraw_y_px=e.clientY;// Touch screen}elseif(e.touches){// Only consider the first touch event.varraw_x_px=e.touches[0].clientX;varraw_y_px=e.touches[0].clientY;}// Convert the raw mouse position into coordinated relative to the corner of the imaging element.varscreen_2d_px=gW.screenFromRaw_2d_px(videoMirror,newgW.Vec2D(raw_x_px,raw_y_px));// Send the state to the server (there it will be relayed to the host client).mK.mX=screen_2d_px.x;mK.mY=screen_2d_px.y;handle_sending_mK_data(mK);}};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){// Don't seem to need this...//if (!mK.MD) return;// Note: e.preventDefault() not needed here if the following canvas style is set// touch-action: none;resetMouseOrFingerState(e);},{capture:false});functionresetMouseOrFingerState(e){if(e.changedTouches){varreleasePoint_2d_px=gW.screenFromRaw_2d_px(clientCanvas_tt,newgW.Vec2D(e.changedTouches[0].clientX,e.changedTouches[0].clientY));twoThumbs.processSingleTouchRelease(releasePoint_2d_px);}mK.MD=false;// Mouse DownmK.bu=null;// Mouse button numberhandle_sending_mK_data(mK);}document.addEventListener("keydown",function(e){//console.log("e.keyCode = " + e.keyCode);// 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.keyCodeinkeyMap_cso){console.log("keyMap value = "+e.keyCode+", "+keyMap_cso[e.keyCode]);if(mK_cso[keyMap_cso[e.keyCode]]=='U'){// Set the key to DOWN.mK_cso[keyMap_cso[e.keyCode]]='D';}}// Toggle the p2p connectionif((mK_cso.key_p=='D')&&(mK_cso.key_shift=='D')){rtc_choke=!rtc_choke;refresh_P2P_indicator({});// Esc out of full-screen mode (only mildly useful if the twothumbs checkbox is not hidden) // If you're in fullscreen mode, this one won't// be the first to fire. The fullscreenchange handler fires first. Then, after// a second esc key press, this block will execute.}elseif(keyMap_cso[e.keyCode]=='key_esc'){//console.log('in key_esc block');// Reveal the video element (and hide the canvas).videoMirror.removeAttribute("hidden");clientCanvas_tt.setAttribute("hidden",null);chkTwoThumbs.checked=false;twoThumbs.enabled=false;}if(e.keyCodeinkeyMap){//console.log("keyMap value = " + keyMap[e.keyCode]); if(mK[keyMap[e.keyCode]]=='U'){// Set the key to DOWN.mK[keyMap[e.keyCode]]='D';handle_sending_mK_data(mK);}}},{capture:false});//"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.keyCodeinkeyMap){// Set the key to UP.mK[keyMap[e.keyCode]]='U';handle_sending_mK_data(mK);}if(e.keyCodeinkeyMap_cso){// Set the key to UP.mK_cso[keyMap_cso[e.keyCode]]='U';}},{capture:false});//"false" makes this fire in the bubbling phase (not capturing phase).// Video stream checkbox.chkRequestStream=document.getElementById('chkRequestStream');chkRequestStream.checked=false;chkRequestStream.addEventListener("click",function(){// You checked it.if(chkRequestStream.checked){$('#FullScreen').prop('disabled',false);if($('#roomName').val()==""){displayMessage('');displayMessage('You must have a room name in the red box. Try again.');displayMessage('');chkRequestStream.checked=false;}else{if(chkTwoThumbs.checked){// Uncheck twoThumbs (but it's probably hidden unless I'm testing)chkTwoThumbs.click();}// re-negotiate the connection.window.setTimeout(function(){connect_and_listen('client','re-connect');},100);}// You unchecked it.}else{$('#FullScreen').prop('disabled',true);if(socket){varcontrol_message={'from':cl.name,'to':'host','data':{'videoStream':'off'}};socket.emit('control message',JSON.stringify(control_message));// Wait a bit for the above message to get to the host. Then clean out the// video element.window.setTimeout(function(){if(videoMirror.srcObject)videoMirror.srcObject=null;},200);}else{displayMessage('');displayMessage("If you haven't already, please connect to the host.");}}},{capture:false});// This control can be useful for testing but is normally hidden. Edit indexClient.html// to un-hide it.chkTwoThumbs=document.getElementById('chkTwoThumbs');chkTwoThumbs.checked=false;chkTwoThumbs.addEventListener("click",function(){if(chkTwoThumbs.checked){twoThumbs.changeDisplay('normal');}else{twoThumbs.changeDisplay('exit');}},{capture:false});// Button for entering the mobile client interfacebtnTwoThumbs=document.getElementById('twoThumbsButton');btnTwoThumbs.addEventListener("click",function(){twoThumbs.changeDisplay('fullScreen');},{capture:false});// Full screen button (on client)btnFullScreen=document.getElementById('FullScreen');btnFullScreen.addEventListener('click',function(){changeFullScreenMode(videoMirror,'on');},{capture:false});// Local cursor is handy if the engine is paused. Also give visual indicator of lag.chkLocalCursor=document.getElementById('chkLocalCursor');chkLocalCursor.checked=true;chkLocalCursor.addEventListener("click",function(){//console.log("chkLocalCursor.checked=" + chkLocalCursor.checked);//console.log('in chkLocalCursor, cursor=' + videoMirror.style.cursor + '|');if(chkLocalCursor.checked){videoMirror.style.cursor='default';clientCanvas_tt.style.cursor='default';}else{videoMirror.style.cursor='none';clientCanvas_tt.style.cursor='none';}},{capture:false});// Option for connecting without a puck.chkPlayer=document.getElementById('chkPlayer');chkPlayer.checked=true;// For handling the first press of the ESC key (exiting fullscreen mode)$(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange msfullscreenchange',function(e){// Check the fullscreen state.// Starting fullscreenif(document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.msFullscreenElement){console.log('fullscreen state: TRUE');// Exiting fullscreen}else{console.log('fullscreen state: FALSE');clientCanvas_tt.width=videoMirror.width;clientCanvas_tt.height=videoMirror.height;twoThumbs.changeDisplay('exit');}});}// Reveal public pointers to private functions and properties ///////////////return{//nodeServerURL: nodeServerURL,forceClientDisconnect:forceClientDisconnect,resizeClients:resizeClients,init_chatFeatures:init_chatFeatures,init_nonHostClients:init_nonHostClients,connect_and_listen:connect_and_listen,refresh_P2P_indicator:refresh_P2P_indicator,setCanvasStream:setCanvasStream,changeFullScreenMode:changeFullScreenMode,chatToNonHostPlayers:chatToNonHostPlayers,displayMessage:displayMessage,getGameReportCounter:getGameReportCounter,checkForNickName:checkForNickName,RTC:RTC};})();