// Game Window (gW) Module
// gwModule.js 
   console.log('GW version 2.42');
// 3:06 PM Thu June 18, 2020
// Written by: James D. Miller 

/*
Dependencies for gwModule.js:
   constructorsAndPrototypes.js (cP.)
   hostAndClient.js (hC.)
   utilities.js
*/

var gW = (function() {
   
   "use strict";
   
   // Short names for Box2D constructors and prototypes
   var b2Vec2 = Box2D.Common.Math.b2Vec2,   
      b2BodyDef = Box2D.Dynamics.b2BodyDef,   
      b2Body = Box2D.Dynamics.b2Body,   
      b2FixtureDef = Box2D.Dynamics.b2FixtureDef,   
      b2Fixture = Box2D.Dynamics.b2Fixture,   
      b2World = Box2D.Dynamics.b2World,   
      b2Manifold = Box2D.Collision.b2Manifold,
      b2WorldManifold = Box2D.Collision.b2WorldManifold,
      b2DistanceJointDef = Box2D.Dynamics.Joints.b2DistanceJointDef,   
      b2DistanceJoint = Box2D.Dynamics.Joints.b2DistanceJoint,   
      b2MassData = Box2D.Collision.Shapes.b2MassData,   
      b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape,   
      b2CircleShape = Box2D.Collision.Shapes.b2CircleShape,   
      b2AABB = Box2D.Collision.b2AABB;
   
   ////////////////////////////////////////////////////////////////////
   // Common variables inside of gW (game window) module //////////////
   ////////////////////////////////////////////////////////////////////
   
   // The Air Table (aT): a place to call home for pucks, pins, springs, and walls.
   var aT = {};
  
   aT.puckMap = {}; // keyed by puck name.
   aT.jelloPucks = []; // An array for use in testing for tangled jello.
   
   aT.pinMap = {};  // keyed by pin name.
   
   aT.springMap = {}; // keyed by spring name.
   
   aT.wallMap = {}; // keyed by wall name.
   
   aT.collisionCount = 0;
   aT.collisionInThisStep = false;
   
   cP.Puck.restitution_default_gOn =  0.7;
   cP.Puck.friction_default_gOn =  0.6;
   cP.Puck.restitution_default_gOff = 1.0;
   cP.Puck.friction_default_gOff = 0.1;
   cP.Puck.bulletAgeLimit_ms = 1000;
   
   // Make a separate container for constants (c) and control flags used by aT objects. This avoids
   // circular references (and associated problems) with JSON capturing.
   var c = {};
   c.g_mps2 = 9.8;
   c.g_ON = false;
   c.px_per_m = null;
   
   // This 60 corresponds with the selected (default) value on the index.html page.
   c.frameRate = 60.0;
   // Seconds per frame
   c.deltaT_s = 1.0/c.frameRate;
   c.dtFloating = false;
   
   c.fullScreenState = false;
   
   c.demoIndex = null;
   c.demoLoopIndex = 0;
   c.demoVersion = null;
   c.leaderBoardIndex = 0;
   
   //c.contactCounter = 0;
   c.jello = {};
   c.jello.tangleTimer_s = 0;
   c.jello.reported = false;
   c.jello.verifyingDeTangle = false;
   
   c.puckPopperTimer_s = 0;
   c.puckPopperPlayers = {'human':0,'drone':0};
   
   c.poolTimer_s = 0;
   c.poolTimer_limit_s = 0.2;
   
   c.npcSleep = false;
   c.npcSleepUsage = false;
   
   c.lastClientToScoreHit = null;
   c.territoryMarked = false;   
   
   c.chatLayoutState = 'notSetYet';
   
   c.singleStep = false;
   c.softConstraints_default = false;
   
   c.canvasColor = 'black';
   
   c.scoreTip = '+200: win,\n+100: pop client or drone,\n+50: pop regular puck,\n+10: hit a puck with your bullet,\n-10: get hit by somebody else's bullet,\n-1: bad shot';
   
   c.startingPosAndVels = [];
   
   c.piCalcs = {'enabled':true, 'usePiEngine':false, 'clacks':false};

   c.pauseErase = false;
   
   // Client map keyed by client name.
   var clients = {};
   
   var tableMap = new Map();  // Special map where keys can be objects.
   var world, worldAABB;
   var myRequest, time_previous, dt_frame_ms, dt_frame_previous_ms, dt_frame_s, resumingAfterPause;
   var canvas, canvasDiv, ctx;
   
   
   var sounds = {
      'lowPop':  new cP.SoundEffect("sounds/puckpop_lower.mp3", 5),
      'highPop': new cP.SoundEffect("sounds/puckpop.mp3", 5),
      'clack2':  new cP.SoundEffect("sounds/clack_long.wav", 35)
   };
   
   var messages = {
      'ppTimer': new cP.HelpMessage({'font':'14px Arial', 'color':'lightgray'}),
      'score':   new cP.HelpMessage({'font':'14px Arial', 'color':'lightgray'}),
      'help':    new cP.HelpMessage({'font':'20px Arial', 'color':'lightgray'}),
      'win':     new cP.HelpMessage({'font':'20px Arial', 'color':'yellow'}),
      'lowHelp': new cP.HelpMessage({'font':'20px Arial', 'color':'yellow'}),
      'gameTitle':      new cP.HelpMessage({'font':'50px Arial', 'color':'lightgray'}),
      'videoTitle':     new cP.HelpMessage({'color':'lightgray'}),
   };
   
   var hostSelectBox = new cP.SelectBox({});
   var hostMSelect = new cP.MultiSelect();
   
   var piCalcEngine;
   
   // function defined inside other functions (like "init") that need global scope.
   var key_ctrl_handler, key_l_handler, clickToClearMulti, mouseUp_handler, wheelEvent_handler;
   
   // Document Controls (dC).
   var dC = {};
   dC.gravity = null;
   dC.pause = null;
   dC.comSelection = null;
   dC.multiplayer = null;
   dC.stream = null;
   dC.editor = null;
   dC.localCursor = null;
   
   // Key values.
   var keyMap = {'48':'key_0', '49':'key_1', '50':'key_2', '51':'key_3', '52':'key_4', '53':'key_5', '54':'key_6', '55':'key_7', '56':'key_8', '57':'key_9',
                 '65':'key_a', '66':'key_b', '67':'key_c', '68':'key_d', '69':'key_e', '70':'key_f', '71':'key_g', 
                 '73':'key_i', '74':'key_j', '75':'key_k', '76':'key_l', '77':'key_m', '78':'key_n', '79':'key_o', 
                 '80':'key_p', '81':'key_q', '82':'key_r', '83':'key_s', 
                 '84':'key_t', '86':'key_v', '87':'key_w', '88':'key_x', '90':'key_z',
                 '16':'key_shift', '17':'key_ctrl', 
                 '18':'key_alt', // both left and right alt key on Windows
                 
                 '32':'key_space', '8':'key_backspace',
                 
                 // Note that default behavior is blocked on all these arrow-key type keys. Search on
                 // arrowKeysMap in the handler for the keydown event.
                 // Exceptions to this are the key_+ and key_- number-pad keys that are in the allowDefaultKeysMap.
                 // This allows the desired native zoom feature when using the ctrl key along with these keys.  
                 '33':'key_pageUp', '34':'key_pageDown', 
                 '37':'key_leftArrow', '38':'key_upArrow', '39':'key_rightArrow', '40':'key_downArrow',
                 // These are the number pad +/- keys.
                 '107':'key_+', '109':'key_-',
                 // These are the +/- keys on the main keyboard.
                 '187':'key_=+', '189':'key_-_', // Chrome
                 '61':'key_=+',  '173':'key_-_', // Firefox
                 
                 '188':'key_lt', '190':'key_gt',
                 
                 '191':'key_questionMark',
                 
                 '219':'key_[', '221':'key_]',
                 
                 '225':'key_alt'};   // right-side alt key, needed for RPi
   
   var fileName = "gwModule.js";
   
   // Switch to enable debugging...
   var db = {};
   // ...of the WebRTC stuff.
   db.rtc = false;
   
   ////////////////////////////////////////////////////////
   // Initialize a world in Box2D    
   ////////////////////////////////////////////////////////
   
   // Constraint on space in world
   worldAABB = new b2AABB();
   worldAABB.lowerBound.Set(-20.0, -20.0);
   worldAABB.upperBound.Set( 20.0,  20.0);
   
   // b2d world: set gravity vector to 0, allow sleep.
   world = new b2World( new b2Vec2(0, -0.0), true);
   
   // Event handlers for Box2D. Get collision information.
   
   var listener = new Box2D.Dynamics.b2ContactListener;
   listener.BeginContact = function( contact) {
      aT.collisionCount += 1;
      aT.collisionInThisStep = true;
      
      // Use the table map to get a reference back to a gW object.
      var body_A = tableMap.get( contact.GetFixtureA().GetBody());
      var body_B = tableMap.get( contact.GetFixtureB().GetBody());

      // Ghost puck sensor event associated with client cursor. (also see Client.prototype.drawGhostBall in constructorsAndPrototypes.js)
      if ((body_A.constructor.name == "Client") || (body_B.constructor.name == "Client")) {
         
         if (body_A.constructor.name == "Client") {
            // Yes, I put the client into the tableMap (wow!). The (ghost) b2d sensor is an attribute on the client.
            var clientTarget = body_B, client = body_A; 
         } else {
            var clientTarget = body_A, client = body_B;
         }
         // ignore contact between the sensor (the ghost) and the source puck (selected by client).
         if ((client.selectedBody) && (clientTarget.name != client.selectedBody.name)) {
            console.log("target and selected: " + clientTarget.name + "," + client.selectedBody.name);
            
            // if no longer in contact with the previous target (it's null now), identify this contact as the new target.
            if ( ! client.sensorTargetName) {
               console.log("new main target identified");
               client.sensorTargetName = clientTarget.name;
            }
         }

      // Set the wall color to that of the puck hitting it.
      } else if ((body_A.constructor.name == "Wall") || (body_B.constructor.name == "Wall")) {
         if (body_B.constructor.name == "Puck") {
            var body_Puck = body_B, body_Wall = body_A;
         } else {
            var body_Puck = body_A, body_Wall = body_B;               
         }
         
         // If it's a puck designated as a color source, use its client color for the wall.
         if (body_Puck.colorSource) {
            if (body_Puck.clientName && body_Wall.fence) {
               body_Wall.color = clients[body_Puck.clientName].color;
            } else if (body_Wall.fence) {
               body_Wall.color = body_Puck.color;
            }
         } else {
            // Reset the wall color to it's default.
            body_Wall.color = cP.Wall.color_default;               
         }
                  
      } else if ((body_A.constructor.name == "Puck") && (body_B.constructor.name == "Puck")) {
         // Handle the case where one body is a bullet and one is not.
         if ((body_A.bullet && !body_B.bullet) || (body_B.bullet && !body_A.bullet)) {
            
            if (body_A.bullet && !body_B.bullet) {
               var bullet = body_A, target = body_B;
            } else if (body_B.bullet && !body_A.bullet) {
               var bullet = body_B, target = body_A;
            }
            
            // Check for restrictions on friendly fire AND that both target and shooter are human.
            var friendlyFire = false;
            if (! dC.friendlyFire.checked) {
               if (target.clientName && !target.clientName.includes('NPC') && !bullet.clientNameOfShooter.includes('NPC')) {
                  friendlyFire = true;
               }
            }
            
            // Can't shoot yourself in the foot and can't be friendly fire.
            if ((bullet.clientNameOfShooter != target.clientName) && !friendlyFire) {
               if (!target.shield.ON || (target.shield.ON && !target.shield.STRONG)) {
                  target.hitCount += 1;
                  target.inComing = true;
                  target.flash = true;
                  bullet.atLeastOneHit = true;
                  
                  // Give credit to the shooter (owner of the bullet).
                  if (!cP.Client.winnerBonusGiven && clients[ bullet.clientNameOfShooter]) {
                     clients[ bullet.clientNameOfShooter].score += 10;
                     // Keep track of the last successful hit to a client. Useful with multiple players and when friendly fire is blocked.
                     if (target.clientName) c.lastClientToScoreHit = bullet.clientNameOfShooter;
                  }
                  target.whoShotBullet = bullet.clientNameOfShooter;
                  // Remove credit from the puck that got hit (the not-bullet body).
                  if (!cP.Client.winnerBonusGiven && target.clientName && clients[ target.clientName]) {
                     clients[ target.clientName].score -= 10;
                  }
               }
            }
         }
      } 
   }
   listener.PreSolve = function( contact) {
      var body_A = tableMap.get( contact.GetFixtureA().GetBody());
      var body_B = tableMap.get( contact.GetFixtureB().GetBody());

      // Ghost puck sensor event associated with client cursor. (also see Client.prototype.drawGhostBall in constructorsAndPrototypes.js)
      if ((body_A.constructor.name == "Client") || (body_B.constructor.name == "Client")) {
         
         if (body_A.constructor.name == "Client") {
            var clientTarget = body_B, client = body_A; 
         } else {
            var clientTarget = body_A, client = body_B;
         }
         // ignore contact between the sensor (the ghost) and the source puck (selected by client).
         if ((client.selectedBody) && (clientTarget.name != client.selectedBody.name)) {
            var worldManifold = new b2WorldManifold();
            contact.GetWorldManifold( worldManifold);
            
            var wM_normal = Object.assign({}, worldManifold.m_normal);
            var wM_points = Object.assign({}, worldManifold.m_points);
            var test = 0; //breakpoint
            //console.log("wM normal = " + worldManifold.m_normal.x + "," + worldManifold.m_normal.y + "," + worldManifold.m_points.length);
            client.sensorContact = {'normal_2d_m':Vec2D_from_b2Vec2( wM_normal),'points_2d_m':wM_points};
         }
         
         if (clientTarget.constructor.name == "Wall") {
            contact.SetEnabled( true); // Identical wall behavior if set this to be false.
         } else {
            contact.SetEnabled( false);
         }
      }   
   }
   listener.EndContact = function( contact) {
      var body_A = tableMap.get( contact.GetFixtureA().GetBody());
      var body_B = tableMap.get( contact.GetFixtureB().GetBody());
      
      if (body_A && body_B) {
         if ((body_A.constructor.name == "Client") || (body_B.constructor.name == "Client")) {
            if (body_A.constructor.name == "Client") {
               var client = body_A, clientTarget = body_B;
            } else {
               var client = body_B, clientTarget = body_A;
            }
            
            // ignore contact between ghost and selected puck. (also see Client.prototype.drawGhostBall in constructorsAndPrototypes.js)
            if ((client.selectedBody) && (clientTarget.name != client.selectedBody.name)) {
               console.log("ending contact with target: " + clientTarget.name);
               if (client.sensorTargetName == clientTarget.name) {
                  console.log("ending contact with main target");
                  client.sensorTargetName = null;
                  client.sensorContact = null;
               }
            }
         }
      }
   }
   world.SetContactListener( listener);
   
   /////////////////////////////////////////////////////////////////////////////
   ////
   ////  Object Prototypes
   ////
   /////////////////////////////////////////////////////////////////////////////

   // see utilities.js and constructorsAndPrototypes.js
   
   /////////////////////////////////////////////////////////////////////////////
   ////
   ////  Functions
   ////
   /////////////////////////////////////////////////////////////////////////////
   
   // Misc utility stuff
   
   function pointInCanvas( p_2d_px) {
      var theRectangle = { 'UL_2d':{'x':0,'y':0}, 'LR_2d':{'x':canvas.width,'y':canvas.height} };
      return pointInRectangle( p_2d_px, theRectangle);
   }
   
   function pointInRectangle( p_2d, rect) {
      // UL: upper left corner, LR: lower right corner.
      if ( (p_2d.x > rect.UL_2d.x) && (p_2d.x < rect.LR_2d.x) && (p_2d.y > rect.UL_2d.y) && (p_2d.y < rect.LR_2d.y) ) {
         return true;
      } else {
         return false;
      }
   }
  
   
   // Support for the network client ///////////////////////////////////////////
   
   function createNetworkClient( pars) {
      var clientName = setDefault( pars.clientName, 'theInvisibleMan');
      // "player" is true/false to indicate if the client is requesting that a player puck be 
      // added to the client instance.
      var player = setDefault( pars.player, true);
      var nickName = setDefault( pars.nickName, null);
      
      var n = clientName.slice(1);
      // Repeat the color index every 10 users (10 colors in cP.Client.colors)
      var colorIndex = n - Math.trunc(n/10)*10;
      
      var clientPars = {};
      clientPars.player = player;
      clientPars.nickName = nickName;
      clientPars.color = cP.Client.colors[ colorIndex];
      clientPars.name = clientName;
      
      // if client is joining a ghost-ball pool game that's underway, initialize these.
      if (c.demoVersion.slice(0,3) == "3.d") {
         clientPars.ctrlShiftLock = true;
         clientPars.poolShotLocked = true;
         clientPars.poolShotLockedSpeed_mps = 20;
      }
      
      new cP.Client( clientPars);
   }
   
   function deleteNetworkClient( clientName) {
      // This function does not directly remove the client socket at the node server, but
      // that does happen at the server...
      if (db.rtc) console.log('in gW.deleteNetworkClient, clientName=' + clientName + ", fileName="+fileName);
      
      if (clients[clientName]) {
         // If it's driving a puck. First, delete that.
         if (clients[clientName].puck) {
            var thePuck = clients[clientName].puck
            
            // Remove this puck and do associated clean-up.
            thePuck.jet = null;
            thePuck.gun = null;
            thePuck.shield = null;
            tableMap.delete( thePuck.b2d);
            world.DestroyBody( thePuck.b2d);
            delete aT.puckMap[ thePuck.name];
         }
         deleteRTC_onHost( clientName);
      }
   }
   
   function deleteRTC_onHost( clientName) {
      if (db.rtc) console.log('in deleteRTC_onHost');
   
      // Shutdown and nullify any references to the host side of this WebRTC p2p connection.
      if (clients[clientName].rtc) {
         clients[ clientName].rtc.shutdown();
      }
      
      // Remove the client in the clients map.
      if (clients[clientName]) {
         delete clients[ clientName];
      }
   }
   
   function deleteRTC_onClientAndHost( clientName) {
      if (db.rtc) console.log('in deleteRTC_onClientAndHost');
      
      // Remove network clients on the node server.
      // (Note: this is one of the several places where hC is used inside of gW.)
      if (clientName.slice(0,1) == 'u') {
         // Send message to the server and then to the client to disconnect.
         hC.forceClientDisconnect( clientName);
      }
      
      // Remove the client in the clients map.
      deleteRTC_onHost( clientName);
   }
   
   function updateClientState( clientName, state) {
      /*
      This is mouse, keyboard, and touch-screen input as generated from non-host-client (network)
      events. Note that this can happen at anytime as triggered by events on 
      the client. This is not fired each frame.
      
      Repetition can be an issue here as mouse movement will repeatedly send 
      the state. If you want to avoid repeating actions, it may be appropriate 
      here to compare the incoming state with the current client state (or 
      make use of a key_?_enabled properties) to stop after the first act. 
      This blocking of repetition does not necessarily need to happen here. 
      For an example of this, search on key_i_enabled.
      
      It is handy to do the blocking here because you have access to the incoming
      state and don't need the key_?_enabled properties. But for actions that are
      repeating each frame, you need to use the key_?_enabled approach.
      */
      
      if (clients[ clientName]) {
         var client = clients[ clientName];
         client.mouseX_px = state.mX;
         client.mouseY_px = state.mY;
         
         
         if ((state.MD) && ( ! client.isMouseDown)) {
            // Similar to how the host client can clear multi-select by clicking on an open space.
            clickToClearMulti( clientName);
         }
         if (( ! state.MD) && (client.isMouseDown)) {
            // Put this mouseUp handler here to try and improve the feeling of the puck fling, click-drag-release, for the
            // network clients. Just reproducing what is being done for the host client. Not sure it helped. I think
            // the "feel" issue is more related to latency.
            mouseUp_handler( clientName);
         }
         client.isMouseDown = state.MD;
         
         
         if (client.isMouseDown && pointInCanvas( client.mouse_2d_px) && (client.deviceType == 'desktop')) {
            // If there's been a click inside the canvas area, flag it as mouse usage.
            // Indirectly, this also prevents cell-phone users from getting flagged here unless they
            // touch the canvas before getting into virtual game pad.
            client.mouseUsage = true;
         }
         
         client.button = state.bu;
         client.mouse_2d_px = new cP.Vec2D(client.mouseX_px, client.mouseY_px);
         client.mouse_2d_m = worldFromScreen( client.mouse_2d_px);
         
         if (state.mW == 'F') wheelEvent_handler( clientName, {'deltaY':1});
         if (state.mW == 'B') wheelEvent_handler( clientName, {'deltaY':-1});
         
         
         client.key_a = state.a;
         client.key_s = state.s;  // uses key_s_enabled
         client.key_d = state.d;
         client.key_w = state.w;
         
         client.key_j = state.j;
         client.key_k = state.k;  // uses key_k_enabled
         if ((state['l'] == "D") && (client.key_l == "U")) {
            key_l_handler('keydown', clientName);
         }
         client.key_l = state.l;
         client.key_i = state.i;  // uses key_i_enabled
         
         client.key_space = state.sp;
         client.key_questionMark = state.cl; //cl short for color
         
         client.key_alt = "U";  // for now...
         
         // Compare incoming state with the current state. Only act if changing from U to D.
         if ((state['1'] == "D") && (client.key_1 == "U")) demoStart(1);
         client.key_1 = state['1'];
         
         if ((state['2'] == "D") && (client.key_2 == "U")) demoStart(2);
         client.key_2 = state['2'];
         
         if ((state['3'] == "D") && (client.key_3 == "U")) demoStart(3);
         client.key_3 = state['3'];
         
         if ((state['4'] == "D") && (client.key_4 == "U")) demoStart(4);
         client.key_4 = state['4'];
         
         if ((state['5'] == "D") && (client.key_5 == "U")) demoStart(5);
         client.key_5 = state['5'];
         
         if ((state['6'] == "D") && (client.key_6 == "U")) demoStart(6);
         client.key_6 = state['6'];
         
         if ((state['7'] == "D") && (client.key_7 == "U")) demoStart(7);
         client.key_7 = state['7'];
         
         if ((state['8'] == "D") && (client.key_8 == "U")) demoStart(8);
         client.key_8 = state['8'];
         
         if ((state['9'] == "D") && (client.key_9 == "U")) demoStart(9);
         client.key_9 = state['9'];
         
         if ((state['f'] == "D") && (client.key_f == "U")) freeze();
         client.key_f = state['f'];
         
         
         // Similar to how the ctrl events are handled for the host (local client).
         if ((state['ct'] == "D") && (client.key_ctrl == "U")) {
            //console.log('ctrl is DOWN');
            key_ctrl_handler('keydown', clientName);
         }
         if ((state['ct'] == "U") && (client.key_ctrl == "D")) {
            //console.log('ctrl is UP');
            key_ctrl_handler('keyup', clientName);
         }
         client.key_ctrl = state.ct;
         
         // Releasing the shift key (similar to event handler for local client).
         if ((state['sh'] == "U") && (client.key_shift == "D")) {
            // Done with the rotation action. Get ready for the next one.
            hostMSelect.resetCenter();
            client.modifyCursorSpring('dettach');
         }
         client.key_shift = state.sh;
         
         // Set pool shot speed.
         if ( (state.z == "D") && (client.key_z == "U") && (((client.key_shift == "D") && (client.key_ctrl == "D")) || (client.ctrlShiftLock)) ) {
            client.togglePoolShotLock();
         }
         client.key_z = state.z;
         
         // Specific angle being sent from client in TwoThumbs mode.
         if (client.puck && state['jet_d']) {
            client.puck.jet.rotateJetToAngle( state['jet_d']);
         }
         if (client.puck && state['gun_d']) {
            client.puck.gun.rel_position_2d_m.set_angle( state['gun_d']);
            // Flag this client as using the virtual game pad during this game.
            client.twoThumbsUsage = true;
         }
         
         // Special Two Thumbs controls.
         if (client.puck) {
            // Jet throttle
            client.puck.jet.throttle = state['jet_t'];
            
            // Gun Scope: rotation rate fraction   and   firing trigger 
            // Freeze the puck at the first press of the scope trigger or rotator. If external forces
            // move the puck after this freeze event, so be it.
            if ((client.puck.gun.scopeTrigger == 'U')     && (state['ScTr']  == 'D') ||
                (client.puck.gun.scopeRotRateFrac == 0.0) && (state['ScRrf'] != 0.0)) {
               
               // Check if it's moving before breaking (and drawing the break circle).
               var v_2d_mps = client.puck.velocity_2d_mps;
               if ((Math.abs( v_2d_mps.x) > 0) || (Math.abs( v_2d_mps.y) > 0)) {
                  client.puck.b2d.SetLinearVelocity( new b2Vec2(0.0,0.0));
                  client.puck.gun.scopeBreak = true;
               }
            }
            client.puck.gun.scopeRotRateFrac = state['ScRrf'];
            client.puck.gun.scopeTrigger = state['ScTr'];
         }
         /*         
         var stateString = "";
         for (var key in state) stateString += key + ":" + state[ key] + ",";
         console.log("stateString=" + stateString);
         */
      }
   }
   
   function setClientCanvasToMatchHost() {
      // This must run within the context of the host's browser (to get the host's canvas dimensions).
      hC.sendSocketControlMessage({'from':'host', 'to':'roomNoSender', 'data':{'canvasResize':{'width':canvas.width,'height':canvas.height}} });
   }
   
   
   // box2d functions to interact with the engine //////////////////////////////
   
   function b2d_getBodyAt( mousePVec_2d_m) {
      var x = mousePVec_2d_m.x;
      var y = mousePVec_2d_m.y;
      var aabb = new b2AABB();
      var size_m = 0.001;
      aabb.lowerBound.Set(x - size_m, y - size_m);
      aabb.upperBound.Set(x + size_m, y + size_m);
      
      // Query the world for overlapping bodies. Where the body's bounding box overlaps
      // with the aabb box defined above. Run the function provided to QueryAABB for each
      // body found to overlap the aabb box.

      var selectedBody = null;
      var userData = null;
      world.QueryAABB( function( fixture) {
         // Don't consider cursor pins.
         if (!tableMap.get( fixture.GetBody()).cursorPin) {
            // Take the first fixture where this point can be found locally on it.
            if (fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), mousePVec_2d_m)) {
               selectedBody = fixture.GetBody();
               userData = selectedBody.GetUserData();
               // Skip ghost sensors; try to keep looking for a table object.
               if (userData != "ghost-sensor") {
                  return false; // stop checking the query results
               }
            }
         }
         // return true to continue checking at the rest of the fixtures returned by the query
         return true;
      }, aabb);
      // If the last, or only object found is a ghost, don't return it.
      if (userData != "ghost-sensor") {
         return selectedBody;
      } else {
         return null;
      }
   }  

   function b2d_getPolygonVertices( b2d_body) {
      // Make an array that has the world vertices scaled to screen coordinates.
      var poly_px = [];
      for (var i = 0; i < b2d_body.m_fixtureList.m_shape.m_vertices.length; i++) {
         var p_2d_px = screenFromWorld( b2d_body.GetWorldPoint( b2d_body.m_fixtureList.m_shape.m_vertices[i]));
         poly_px.push( p_2d_px);
      }
      return poly_px;
   }
   
   
 
   // Functions called by the buttons //////////////////////////////////////////
   
   function toggleMultiplayerStuff() {
      // This double toggle has the effect of switching between the following two divs.
      toggleElementDisplay("multiPlayer", "table-cell");
      toggleElementDisplay("ttcIntro",    "table-cell");
      
      // This toggles (displays/hides) the client links.
      toggleElementDisplay("clientLinks", "inline");
   }
   
   function toggleElementDisplay( id, displayStyle) {
      var e = document.getElementById( id);
      // Use ternary operator (?):   condition ? expr1 : expr2
      // If the current style isn't equal to the incoming displayStyle, set it to be displayStyle. 
      // If it is equal, set it to 'none'. When the value is 'none', the element is hidden.
      // The effect of this function is that repeated calls to it, with the same displayStyle value, will
      // toggle the style between 'none' and the specified style value.
      e.style.display = (e.style.display != displayStyle) ? displayStyle : 'none';
   }
   function setElementDisplay( id, displayStyle) {
      var e = document.getElementById( id);
      e.style.display = displayStyle;
   }
   
   function toggleSpanValue( id, value1, value2) {
      var e = document.getElementById( id);
      e.innerText = (e.innerText == value1) ? value2 : value1; 
   }
   
   function getSpanValue( id) {  
      var e = document.getElementById( id);
      return e.innerText;
   }
   
   function resetFenceColor( newColor) {
      cP.Wall.applyToAll( wall => {
         if (wall.fence) {
            wall.color = newColor;
            wall.draw( ctx);
         }
      });
   }
   
   function fenceIsClientColor( clientName) {
      var theyMatch = true;
      cP.Wall.applyToAll( wall => {
         if (wall.fence) {
            if (wall.color != clients[clientName].color) {
                  theyMatch = false;
            }
         }
      });
      return theyMatch;
   }
   
   function setPauseState( e) {
      // Make the pause state agree with the check box.
      if (dC.pause.checked) {
         stopit();
         setElementDisplay("fps_wrapper", "none");
         setElementDisplay("stepper_wrapper", "inline");
      } else {
         startit();
         c.singleStep = false;
         setElementDisplay("fps_wrapper", "inline");
         setElementDisplay("stepper_wrapper", "none");
      }
   }
   
   function startit() {
      // Only start a game loop if there is no game loop running.
      if (myRequest === null) {
         resetFenceColor( "white");
         if (!c.singleStep) dC.pause.checked = false;

         // Start the game loop.
         myRequest = window.requestAnimationFrame( gameLoop);
      }
   }

   function stopit() {
      resetFenceColor( "red");
      aT.dt_RA_ms.reset();
      dC.fps.innerHTML = '0';

      window.cancelAnimationFrame( myRequest);
      myRequest = null;
      resumingAfterPause = true;
   }
   
   function stepAnimation() {
      dC.pause.checked = true;
      // Set flag to allow only one step.
      c.singleStep = true;
      startit();
   }
   
   function setFrameRateBasedOnDisplayRate() {
      console.log("fps=" + dC.fps.innerHTML);
      var current_fps = dC.fps.innerHTML;
      var fps_choices = [60,75,85,100,120,144,240];
      var min_diff = 1000;
      var min_diff_index = null;
      var len = fps_choices.length;
      for (var i = 0; i < len; i++) {
         var diff = Math.abs( fps_choices[i] - current_fps);
         if (diff < min_diff) {
            min_diff = diff;
            min_diff_index = i;
         }
      }
      var bestMatch = fps_choices[ min_diff_index];
      // Set the value in the pulldown control.
      $('#FrameRate').val( bestMatch);
      setFrameRate();
   }
   
   function setFrameRate() {
      var frameRate = $('#FrameRate').val();
      if (frameRate != 'float') {
         c.frameRate = frameRate;
         c.deltaT_s = 1.0 / frameRate;
         c.dtFloating = false;
      } else {
         c.dtFloating = true;
      }
   }
   
   function freeze() {      
      cP.Puck.applyToAll( puck => puck.b2d.SetLinearVelocity( new b2Vec2(0.0,0.0)) );
   }
   function stopRotation() {      
      cP.Puck.applyToAll( puck => puck.b2d.SetAngularVelocity( 0.0) );
   }
   function reverseDirection() {      
      cP.Puck.applyToAll( puck => {
         puck.b2d.SetAngularVelocity( -1 * puck.angularSpeed_rps);
         puck.b2d.SetLinearVelocity( b2Vec2_from_Vec2D( puck.velocity_2d_mps.scaleBy( -1)) );
      });
   }
   
   
   function json_scrubber( key, value) {
      /*
      Use this function to exclude the b2d objects in the stringify process. 
      Apparently the b2d and rtc objects have circular references that 
      stringify doesn't like. So have to regenerate the b2d objects in the 
      demo area when the json capture is restored. 

      Also have to avoid the client related addons: jet, gun, and shield. 
      These have references back their pucks, this too causes circular issues 
      for stringify. 

      Also remove keys like spo1 and spo2 (in Springs object) mainly to keep 
      the wordiness down; many keys are not needed in the reconstruction 
      process. 

      So be careful here: any key with a name in the OR list of json_scrubber 
      (see if block below) will be excluded from the capture. 
      */
      if ( (key == 'b2d') || (key == 'b2dSensor') || (key == 'rtc') || 
           (key == 'jet') || (key == 'gun') || (key == 'shield') || 
           (key == 'spo1') || (key == 'spo2') || 
           (key == 'parsAtBirth') || 
           (key == 'puck') || (key.includes('key_')) || (key.includes('_scaling')) || (key.includes('selectionPoint')) || 
           (key == 'position_2d_px') || (key == 'nonCOM_2d_N') ) {
         return undefined;
      } else {
         return value;
      }
   }
   
   function saveState( dataForCleaning = null) {
      var timeString = new Date();
         
      // Use an old capture, that is passed in via dataForCleaning, as the data sources.
      if (dataForCleaning) {
         if ( ! (dataForCleaning.startingPosAndVels)) dataForCleaning.startingPosAndVels = null;
         
         // Add some canvas dimensions if needed.
         if ( ! (dataForCleaning.canvasDimensions)) {
            if (dataForCleaning.demoIndex == 8) {
               dataForCleaning.canvasDimensions = {'width':1250, 'height':950};
            } else {
               dataForCleaning.canvasDimensions = {'width':600, 'height':600};
            }
         }
		 
         var tableState = {'demoIndex':dataForCleaning.demoIndex,
                           'demoVersion':dataForCleaning.demoVersion,
                           'date':timeString.toLocaleString(),
                           'canvasDimensions': {'width':dataForCleaning.canvasDimensions.width, 'height':dataForCleaning.canvasDimensions.height},
                           'gravity':dataForCleaning.gravity,
                           'globalCompositeOperation':dataForCleaning.globalCompositeOperation,
                           'wallMapData':dataForCleaning.wallMapData, 
                           'puckMapData':dataForCleaning.puckMapData, 
                           'pinMapData':dataForCleaning.pinMapData, 
                           'springMapData':dataForCleaning.springMapData,
                           'startingPosAndVels':dataForCleaning.startingPosAndVels,
                           'clients':dataForCleaning.clients};
                           
         if (dataForCleaning.piCalcs) {
            tableState = Object.assign({}, tableState, {'piCalcs':dataForCleaning.piCalcs} );
            if (dataForCleaning.piEngine) {
               tableState = Object.assign({}, tableState, {'piEngine':dataForCleaning.piEngine} );
            }
         } else {
            tableState = Object.assign({}, tableState, {'piCalcs':{}} );
         }
      
      // Get a fresh capture (i.e., using the live stuff as the data source).            
      } else {
         c.demoVersion = c.demoVersion + '.' + Math.floor((Math.random() * 1000) + 1);
         var tableState = {'demoIndex':c.demoIndex,
                           'demoVersion':c.demoVersion,
                           'date':timeString.toLocaleString(),
                           'canvasDimensions': {'width':canvas.width, 'height':canvas.height},
                           'gravity':c.g_ON,
                           'globalCompositeOperation':ctx.globalCompositeOperation,
                           'wallMapData':aT.wallMap, 
                           'puckMapData':aT.puckMap, 
                           'pinMapData':aT.pinMap, 
                           'springMapData':aT.springMap,
                           'startingPosAndVels':c.startingPosAndVels,
                           'clients':clients};
                           
         // For demos using the piCalcEngine, add the engine state to the capture.
         if ( ['1.c','1.d','1.e'].includes( demoVersionBase( c.demoVersion)) ) {
            var piCalcs = {};
            piCalcs['clacks'] = c.piCalcs.clacks;
            piCalcs['enabled'] = c.piCalcs.enabled;
            if (c.piCalcs.usePiEngine) {
               cP.PiEngine.state['lastCollidedWithWall']       = piCalcEngine['lastCollidedWithWall'];
               cP.PiEngine.state['atLeastOneCollisionInFrame'] = piCalcEngine['atLeastOneCollisionInFrame'];
               cP.PiEngine.state['nFinerTimeStepFactor'] = piCalcEngine['nFinerTimeStepFactor'];               
               piCalcs['p1_v_max']       = piCalcEngine['p1_v_max'];
               piCalcs['collisionCount'] = piCalcEngine['collisionCount'];
               piCalcs['usePiEngine'] = true;
            } else {
               piCalcs['p1_v_max']       = aT.puckMap['puck1'].vmax;
               piCalcs['collisionCount'] = aT.collisionCount;
               piCalcs['usePiEngine'] = false;
            }
            console.log("piCalcs['collisionCount'] = " + piCalcs['collisionCount'] + ", " + cP.PiEngine.state['atLeastOneCollisionInFrame']);
            tableState = Object.assign( {}, tableState, {'piCalcs':piCalcs}, {'piEngine':cP.PiEngine.state} );
         }
      }
      
      // Here you still have full state info. That's because this is before the json_scrubber is called. So if you want to keep
      // something that's about to be scrubbed out, do it here. As an example: the angle of the NPC casting rays,
      // that are in the gun attributes. This can be put into the rayCast_init_deg attribute of the NPC's puck. Then it
      // will be used when the NPC's puck and gun are restored (the gun gets this from its puck).
      for (var p_key in tableState.puckMapData) {
         var puck = tableState.puckMapData[ p_key];
         if ((puck.clientName) && (puck.clientName.slice(0,3) == 'NPC')) {
            puck.rayCast_init_deg = puck.gun.rayCastLine_2d_m.get_angle();
         }
      }
      
      // See comments in the json_scrubber function above.
      var table_JSON = JSON.stringify( tableState, json_scrubber, 3);
      
      // Parsing after JSON.stringify makes a deep copy, with no references back to the original objects. So can delete stuff without
      // mangling the current running demo.
      var tableState_copy = JSON.parse( table_JSON);
      
      // Remove some non-editable puck keys.
      var generalPuckKeys = ['tail','age_ms','ageLimit_ms','radius_px','mass_kg','half_height_px','half_width_px','tempInhibitGForce'];
      var simplePuckKeys =  ['rayCast_init_deg','rayRotationRate_dps','rayCastLineLength_m',
                             'disableJet','noRecoil','bulletAgeLimit_ms','bullet_restitution',
                             'sprDamp_force_2d_N','springOnly_force_2d_N','jet_force_2d_N','impulse_2d_Ns','navSpringOnly_force_2d_N',
                             'poorHealthFraction','whoShotBullet','flash','inComing','flashCount',
                             'hitCount','deleted','clientNameOfShooter'];
      var clientPuckKeys = ['angleLine'];
      var nonNPC_clientPuckKeys = ['disableJet'];
      
      for (var p_key in tableState_copy.puckMapData) {
         var puck = tableState_copy.puckMapData[ p_key];
         
         // Delete bullet pucks in Puck Popper captures
         if ((tableState_copy.demoIndex == 7) || (tableState_copy.demoIndex == 8)) {
            if (puck.bullet) {
               delete tableState_copy.puckMapData[ p_key];
               continue;
            }
         }
         // Delete keys on pucks:
         // All pucks
         for (var key of generalPuckKeys) {
            delete puck[ key];
         }
         // Simple pucks (no client controls)
         if ( ! puck.clientName) {
            for (var key of simplePuckKeys) {
               delete puck[ key];
            }
         // All client pucks
         } else {
            for (var key of clientPuckKeys) {
               delete puck[ key];
            }
            // All non-NPC client pucks
            if (puck.clientName.slice(0,3) != 'NPC') {
               for (var key of nonNPC_clientPuckKeys) {
                  delete puck[ key];
               }
            }
         }
      }
      
      // Remove some non-editable pin keys.  
      var pinKeys = ['radius_m'];
      for (var pin_key in tableState_copy.pinMapData) {
         var pin = tableState_copy.pinMapData[ pin_key];
         for (var key of pinKeys) {
            delete pin[ key];
         }
      }
      
      // Remove some non-editable wall keys.
      var wallKeys = ['deleted','half_height_px','half_width_px'];  // color
      for (var wall_key in tableState_copy.wallMapData) {
         var wall = tableState_copy.wallMapData[ wall_key];
         for (var key of wallKeys) {
            delete wall[ key];
         }
      }
      
      // Remove some non-editable spring keys.
      var springKeys = ['pinned','p1p2_separation_2d_m','p1p2_separation_m','p1p2_normalized_2d',
                        'spo1_ap_w_2d_m','spo1_ap_w_2d_px','spo2_ap_w_2d_m','spo2_ap_w_2d_px'];
      for (var spring_key in tableState_copy.springMapData) {
         var spring = tableState_copy.springMapData[ spring_key];
         
         // Don't capture the local ap (attach point) for pins.
         if (spring.p1_name.slice(0,3) == "pin") delete spring['spo1_ap_l_2d_m'];
         if (spring.p2_name.slice(0,3) == "pin") delete spring['spo2_ap_l_2d_m'];
         
         for (var key of springKeys) { 
            delete spring[ key];
         }
      }
      
      // For client objects, clean off all keys EXCEPT these (i.e. SAVE these): 
      var saveTheseClientKeys = ['color','name','player','nickName','NPC_pin_timer_s','NPC_pin_timer_limit_s'];
      for (var client_key in tableState_copy.clients) {
         var client = tableState_copy.clients[ client_key];
         if (client.name.slice(0,1) == 'u') {
            // Delete network clients...
            delete tableState_copy.clients[ client_key];
         } else {
            // Clean-up everyone else.
            for (var clientKey in client) {
               if ( ! saveTheseClientKeys.includes( clientKey)) {
                  delete client[ clientKey];
               }
            }
         }
      }
      
      // Exit if state data was passed in to be cleaned.
      if (dataForCleaning) return tableState_copy;
      //----------------------------------------------------------------------
      
      // Once again, put it in a string...
      table_JSON = JSON.stringify( tableState_copy, null, 3);
      
      // Write the json string to this visible input field.
      dC.json.value = table_JSON;
      // Wait 0.5 seconds, then scroll the input field to the top.
      window.setTimeout( function() { scrollCaptureArea();}, 500);
      
      // Select, copy to clipboard, and then remove focus from the input field.
      dC.json.select();
      document.execCommand('copy');
      window.getSelection().removeAllRanges(); // this is necessary for the blur method to work in MS Edge.
      dC.json.blur();      
   }
   
   function clearState() {
      dC.json.value = '';
   }
   
   function cleanCapture() {
      // Clean up an old capture
      // This can be run from a hidden button (to the right of the clear button) on the index page.
      if (c.demoVersion == '8.a') {
         var state_data = demo_8_fromFile;         
      } else if (c.demoVersion == '6.a') {
         var state_data = demo_6_fromFile;   
      } else {
         if (dC.json.value != "") {
            var state_data = JSON.parse( dC.json.value);
         } else {
            console.log('no capture to clean');
            return;
         }
      }
      
      // first, process (clean) the capture with saveState
      state_data = saveState( state_data);
      
      // Special loop for pucks.
      for (var p_key in state_data.puckMapData) {
         var puck = state_data.puckMapData[ p_key];
         
         if (puck.clientName) {
            puck.groupIndex = -puck.name.slice(4) - 1000;
         } else {
            if ((state_data.demoVersion == '3.b') || (state_data.demoVersion == '3.c')) {
               // leave these alone, puck-puck collisions have been inhibited on these pucks.
            } else {
               puck.groupIndex = 0;
            }
         }
      }
      
      // For all the maps.
      var mapList = ['puckMapData','pinMapData','springMapData','wallMapData','clients'];
      for (var map of mapList) {
         for (var key in state_data[ map]) {
            var element = state_data[ map][ key];
            
            delete element['parsAtBirth'];
            delete element['alsoThese'];
            delete element['popsound'];
            
            // Put the alsoThese key at the beginning of the object. Commented this
            // out for now. Could be useful if want to force an attribute to be recognized
            // in the capture.
            //state_data[ map][ key] = Object.assign({'alsoThese':[]}, element);
         }
      }
      
      dC.json.value = JSON.stringify( state_data, null, 3);
      
      // Select, copy to clipboard, and then remove focus from the input field.
      dC.json.select();
      document.execCommand('copy');
      window.getSelection().removeAllRanges(); // this is necessary for the blur method to work in MS Edge.
      dC.json.blur();      
   }
   
   function newBirth( captureObj, type) {
      // Update the birth object (based on the capture state) and use it for restoration.
      var newBirthState = {}, par_list;
      
      // If there's a parameter that is getting into the capture but should be blocked in the birth process:
      var forgetList = {
         'puck': ['position_2d_m','velocity_2d_mps'], // These are explicitly passed to constructor via arguments (so not needed in birth object)
         'wall': ['position_2d_m'],  // Position is passed via arguments. Velocity can be specified in birth object.
         'pin':  ['position_2d_m'],  // Position is passed via arguments. Velocity can be specified in birth object.
         's':    [],
         'NPC':  []
      };
      for (var birthParm in captureObj) {
         if (!forgetList[ type].includes( birthParm)) {
            newBirthState[ birthParm] = captureObj[ birthParm];
         }
      }
      
      // For all types, override the default naming process, specify a name in the birth parameters. This gives
      // the new object the name used in the capture object. This is needed in reconstructing 
      // springs (that use the original puck name). This is also needed if pucks are
      // deleted in a jello matrix.
      if (captureObj.name) {
         newBirthState.name = captureObj.name;
      }
      return newBirthState;
   }   
   
   function restoreFromState( state_data) {
      try {
         // return the template that is returned from restoreFromState_main
         return restoreFromState_main( state_data);
      } catch (err) {
         stopit();
         window.alert(c.demoVersion +
                     "\nUnable to restore this capture. " +
                     "\n   Looks like you've been boldly editing the JSON text. Good try!" +
                     "\n   Please refine your edits or start from a new capture." +
                     "\n" +
                     "\n" + err.name +
                     "\nmessage:  " + err.message);
         //clearState();  // clear out the JSON text in the capture cell.
         demoStart(0); // c.demoIndex
      }
   }
      
   function restoreFromState_main( state_data) {
      // Environmental parameters...
      
      // Must do canvas dimensions before setting ctx.globalCompositeOperation.
      if (typeof state_data.canvasDimensions !== "undefined") {
         //canvasDiv.style.width = state_data.canvasDimensions.width + "px";
         canvas.width =          state_data.canvasDimensions.width;
         
         //canvasDiv.style.height = state_data.canvasDimensions.height + "px";
         canvas.height =         state_data.canvasDimensions.height; 
      }
      
      if (state_data.globalCompositeOperation) {
         ctx.globalCompositeOperation = state_data.globalCompositeOperation;
      } else {
         ctx.globalCompositeOperation = 'source-over';
      }
      
      clearCanvas();
      
      if (typeof state_data.demoVersion !== "undefined") {
         c.demoVersion = state_data.demoVersion;
      }
      
      // Rebuild the walls from the capture data.
      for (var wallName in state_data.wallMapData) {
         // wall references one specific wall (from the captured state)
         var wall = state_data.wallMapData[ wallName];
         // Create the new Wall and add it to the wallMap (via its constructor).
         new cP.Wall( wall.position_2d_m, newBirth( wall, 'wall'));
      }
      // Establish the name of the top leg of the fence (for use by the PiEngine).
      if ((cP.Wall.topFenceLegName == null) && (aT.wallMap['wall1'])) {
         if (aT.wallMap['wall1'].fence) {
            cP.Wall.topFenceLegName = 'wall1';
            console.log("topFenceLegName=" + cP.Wall.topFenceLegName);
         } else {
            console.log("wall1 is not part of the fence.");
         }
      } else {
         console.log("topFenceLegName set by restore");
      }      
      
      // NPC clients...
      for (var clientName in state_data.clients) {
         var client = state_data.clients[ clientName];
         if (clientName.slice(0,3) == 'NPC') {
            new cP.Client( newBirth( client, 'NPC'));
         }
      }
      
      // Rebuild the pins.
      for (var pinName in state_data.pinMapData) {
         // "pin" is one pin (captured state)
         var pin = state_data.pinMapData[ pinName];
         // Create the new Pin and add it to the pinMap (via its constructor).
         new cP.Pin( pin.position_2d_m, newBirth( pin, 'pin'));
      }
      
      // Rebuild the pucks (and the puck map).
      var localHostPuckName = null, networkClientName = null, puckNameForTemplate = null;
      
      for (var p_key in state_data.puckMapData) {
         // puck is a single puck (captured state)
         var puck = state_data.puckMapData[ p_key];
         
         // If there's a puck for the local host, record the name for use in returning a puck template.
         // Also snag a puck name from the network clients as a second option.
         if (puck.clientName == 'local') {
            localHostPuckName = puck.name;
         } else if ((puck.clientName) && (puck.clientName.slice(0,1) == 'u')) {
            networkClientName = puck.name;
         }
         
         // Now create the puck and give it the old name (see the end of the newBirth function).
         // The "Host player" option must be checked to enable the creation of a puck for the local client.
         // Network-client pucks are not recreation here (because it depends on active network clients for assignment).
         if ( (!(puck.bullet && (c.demoIndex == 7 || c.demoIndex == 8))) &&   // NOT a game bullet AND 
              ( (puck.clientName == null) ||                                  // (Regular puck  OR
                (puck.clientName.slice(0,3) == 'NPC') ||                      //  Drone puck    OR
                ((puck.clientName == 'local') && (dC.player.checked)) ) ) {   //  Local host and puck requested)
            
            var newPuck = new cP.Puck( puck.position_2d_m, puck.velocity_2d_mps, newBirth( puck, 'puck'));
            
            if (puck.jello) aT.jelloPucks.push( newPuck);
         }
      }
      
      // For the count-to-pi demos.
      if ( (typeof state_data.piCalcs !== "undefined") && 
           (['1.c','1.d','1.e'].includes( demoVersionBase( c.demoVersion))) ) {
         c.piCalcs.enabled        = state_data.piCalcs.enabled;
         c.piCalcs.clacks         = state_data.piCalcs.clacks;
         c.piCalcs.usePiEngine    = state_data.piCalcs.usePiEngine;
         
         if (state_data.piCalcs.usePiEngine) {
            if (state_data.piEngine) {
               cP.PiEngine.state                = state_data.piEngine;
               cP.PiEngine.state.collisionCount = state_data.piCalcs.collisionCount;
               cP.PiEngine.state.p1_v_max       = state_data.piCalcs.p1_v_max;
            }
         } else {
            // box2d engine
            aT.puckMap['puck1'].vmax = state_data.piCalcs.p1_v_max;
            aT.collisionCount        = state_data.piCalcs.collisionCount;         
         }
      }
      
      // Rebuild the spring.
      for (var springName in state_data.springMapData) {
         var theSpring = state_data.springMapData[ springName];
         
         // Don't try and restore navigation springs. Those are created
         // when the NPC pucks are restored.
         if (!theSpring.navigationForNPC) {
            var p1_type = theSpring.p1_name.slice(0,3);
            if (p1_type == "pin") {
               var p1 = aT.pinMap[ theSpring.p1_name];
            } else {
               var p1 = aT.puckMap[ theSpring.p1_name];
            }
            
            var p2_type = theSpring.p2_name.slice(0,3);
            if (p2_type == "pin") {
               var p2 = aT.pinMap[ theSpring.p2_name];
            } else {
               var p2 = aT.puckMap[ theSpring.p2_name];
            }
            
            if ((p1) && (p2)) {
               new cP.Spring(p1, p2, newBirth( theSpring, 's'));
            } else {
               console.log('WARNING: Attempting to rebuild a spring with one or both connected objects missing.');
            }
            
         }
      }
      // Have this at the end because need the objects instantiated before setting the restitution values
      // in the pucks (side effect of setGravityRelatedParameters)
      c.g_ON = state_data.gravity;
      dC.gravity.checked = c.g_ON;
      setGravityRelatedParameters({});
      
      // Give priority to the host's puck for use as a template. If there was no host puck when
      // the capture was done, the network puck will be used.
      if (localHostPuckName) {
         puckNameForTemplate = localHostPuckName;
      } else {
         puckNameForTemplate = networkClientName;
      }
      
      if (puckNameForTemplate) {
         return state_data.puckMapData[ puckNameForTemplate];
      } else {
         // Looks like a capture was made after host and all network pucks were popped, savage battle.
         // So let's make a puck template from the default pars for the host puck.
         return Object.assign({}, {'position_2d_m':new cP.Vec2D(2.0, 2.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)}, cP.Puck.hostPars);
      } 
   }
   
   
   
   // Functions in support of the demos ////////////////////////////////////////
   
   function demoVersionBase( demoVersion) {
      var parts = demoVersion.split(".");
      return parts[0] + "." + parts[1];
   }
   
   function scrollDemoHelp( targetID) {
      var container = $('#helpScroller');
      var scrollTo  = $(targetID);
      var tweak_px = -6;
      /*
      console.log('--------');
      console.log('scrollTo.offset().top: '  + scrollTo.offset().top);
      console.log('container.offset().top: ' + container.offset().top);
      console.log('container.scrollTop(): '  + container.scrollTop());
      console.log('tweak_px: '               + tweak_px);
      */
      if (targetID == 'scroll-to-very-top') {
         container.animate( {scrollTop: 0}, 500);
      } else {
         container.animate( {scrollTop: scrollTo.offset().top - container.offset().top + container.scrollTop() + tweak_px}, 500 );
      }
   }
   
   function scrollCaptureArea() {
      dC.json.scrollTop = 30; 
      dC.json.scrollLeft = 130;
   }
   
   // Editor help toggle
   function openDemoHelp() {
      // Not using this anymore. A bit confusing. Might bring it back.
      
      if (dC.multiplayer.checked) {
         dC.multiplayer.checked = !dC.multiplayer.checked;
         toggleMultiplayerStuff();
      }
   
      toggleElementDisplay('outline1','block');
      
      toggleSpanValue('moreOrLess','More','Less');
      toggleSpanValue('moreOrLess2','More','Less');
      
      scrollDemoHelp('#editorMark');
   }
   
   function resetRestitutionAndFrictionParameters() {
      cP.Puck.restitution_gOn = cP.Puck.restitution_default_gOn;
      cP.Puck.friction_gOn = cP.Puck.friction_default_gOn;
      
      cP.Puck.restitution_gOff = cP.Puck.restitution_default_gOff;
      cP.Puck.friction_gOff = cP.Puck.friction_default_gOff;
   }
   
   function setGravityRelatedParameters( pars) {
      var showMessage = setDefault( pars.showMessage, false);
   
      if (c.g_ON) {
         // Box2D velocityThreshold setting is needed for settling stacks of pucks.
         Box2D.Common.b2Settings.b2_velocityThreshold = 1.0;
         cP.Puck.g_2d_mps2 = new cP.Vec2D(0.0, -c.g_mps2); // module-level
         var restitution = cP.Puck.restitution_gOn;
         var friction =    cP.Puck.friction_gOn;
      } else {
         // But here, with no gravity, it's better to turn the velocityThreshold setting off 
         // so pucks don't stick to walls.
         Box2D.Common.b2Settings.b2_velocityThreshold = 0.0; // 0.0
         cP.Puck.g_2d_mps2 = new cP.Vec2D(0.0, 0.0); // module-level
         var restitution = cP.Puck.restitution_gOff;
         var friction =    cP.Puck.friction_gOff;
      }
      if (showMessage) {
         messages['help'].newMessage('Gravity = ' + cP.Puck.g_2d_mps2.y, 1.0);
      }
      
      // If there are some existing pucks on the table:
      // If not fixed, set restitution and friction properties.
      cP.Puck.applyToAll( puck => {
         if (!puck.restitution_fixed) {
            puck.b2d.m_fixtureList.m_restitution = restitution;
            puck.restitution = restitution;
         }
         if (!puck.friction_fixed) {
            puck.b2d.m_fixtureList.m_friction    = friction;  
            puck.friction = friction;
         }
         //console.log(puck.name + ',rest,fric = ' + puck.restitution + puck.restitution_fixed + "," + puck.friction + puck.friction_fixed);
      });
   }
   
   function em( px) {
      // Convert to em units based on a font-size of 16px.
      return px/16.0;
   }
   
   function getChatLayoutState() {
      // This (revealed) function is needed to share this parameter with the other module.
      return c.chatLayoutState;
   }
   
   function getCanvasDimensions() {
      // This (revealed) function is needed to share these canvas parameters with other modules.
      // The canvas element cannot be revealed directly because of the page-load delay before its characteristics are established. 
      return {'width':canvas.width, 'height':canvas.height};
   }
   
   function fullScreenState( mode = 'get') {
      // Get or set the fullscreen state. This is revealed for use by other modules.
      if (mode == 'get') {
         return c.fullScreenState;
      } else if (mode == 'on') {
         //console.log('setting fullscreen indicator to true');
         c.fullScreenState = true;
      } else if (mode == 'off') {
         //console.log('setting fullscreen indicator to false');
         c.fullScreenState = false;
      } else {
         console.log('from fullScreenState');
      }
   }
   
   function adjustSizeOfChatDiv( mode) {
      // This is used for both the host and client pages. Any calls to getElementById
      // will return a null for elements not found on that page.
      
      if (mode == 'mobile') mode = 'small';
      
      // Input fields
      dC.nodeServer = document.getElementById('nodeServer');
      dC.roomName = document.getElementById('roomName');
      dC.inputField = document.getElementById('inputField');
      
      // The two divs that toggle
      dC.multiPlayer = document.getElementById('multiPlayer');
      dC.ttcIntro = document.getElementById('ttcIntro');
      
      // connectionCanvas is only on the client page.
      dC.connectionCanvas = document.getElementById('connectionCanvas');
      
      var divW_Large = em(540);
      var divW_Small = em(540-118);

      var tweek = -8;
      var nodeServer_Large = em(332+tweek);
      var roomName_Large   = em( 70+0);
      var inputField_Large = em(534+tweek); //536
      var connectionCanvas_Large_px = 518+tweek;  //518
      
      var shrink_px = 141;
      var shrink = em( shrink_px);
      
      var nodeServer_Small = nodeServer_Large - shrink;
      var roomName_Small   = roomName_Large   -  em(0);
      var inputField_Small = inputField_Large - shrink;
      var connectionCanvas_Small_px = connectionCanvas_Large_px - 117;
      
      if (mode == 'small') {
         dC.nodeServer.style.width = (nodeServer_Small) + 'em';
         dC.roomName.style.width   = (roomName_Small  ) + 'em';
         dC.inputField.style.width = (inputField_Small) + 'em';
         if (dC.connectionCanvas) {
            dC.connectionCanvas.width = connectionCanvas_Small_px;
            dC.connectionCanvas.height = 15;
            hC.refresh_P2P_indicator({'mode':'p2p','context':'sizeAdjust'});
         }
         
         dC.ttcIntro.style.maxWidth    = divW_Small + 'em';
         dC.ttcIntro.style.minWidth    = divW_Small + 'em';
         
         dC.multiPlayer.style.maxWidth = divW_Small + 'em';
         dC.multiPlayer.style.minWidth = divW_Small + 'em';
         
      } else {
         dC.nodeServer.style.width = (nodeServer_Large) + 'em';
         dC.roomName.style.width   = (roomName_Large  ) + 'em';
         dC.inputField.style.width = (inputField_Large) + 'em'; 
         if (dC.connectionCanvas) {
            dC.connectionCanvas.width = connectionCanvas_Large_px;
            dC.connectionCanvas.height = 15;
            hC.refresh_P2P_indicator({'mode':'p2p','context':'sizeAdjust'});
         }

         dC.ttcIntro.style.maxWidth    = divW_Large + 'em';
         dC.ttcIntro.style.minWidth    = divW_Large + 'em';
         
         dC.multiPlayer.style.maxWidth = divW_Large + 'em';
         dC.multiPlayer.style.minWidth = divW_Large + 'em';
      }
   }    

   function makeJello( pars) {
      var pinned = setDefault( pars.pinned, false);
      var gridsize = setDefault( pars.gridsize, 4);
   
      var offset_2d_m = new cP.Vec2D(2.0, 2.0);

      var spacing_factor_m = 0.9;
      
      var v_init_2d_mps = new cP.Vec2D(0.0, 0.0);
      
      var puckParms = {'radius_m':0.20, 'density':5.0, 'jello':true};
      
      var springParms = {
         'unstretched_width_m': 0.07,
         'strength_Npm': 350.0,          
         'length_m': spacing_factor_m * 1.0,
         'damper_Ns2pm2': 5.0};

      // Grid of pucks.
      for (var j = 0; j < gridsize; j++) {
         for (var k = 0; k < gridsize; k++) {
            if ((j==2) && (k==2)) {
               puckParms.color = "orange";
            } else {
               puckParms.color = undefined;  // use default
            }
            var pos_2d_m = new cP.Vec2D( spacing_factor_m * j, spacing_factor_m * k);
            pos_2d_m.addTo( offset_2d_m);
            aT.jelloPucks.push( new cP.Puck( Object.assign({}, pos_2d_m), Object.assign({}, v_init_2d_mps), Object.assign({}, puckParms)));
         }
      }
      // Horizontal springs (between neighbors)
      for (var m = 0; m < gridsize*(gridsize-1); m++) {
         springParms.color = "blue";
         // Note: Object.assign is used here to make a copy of the springParms object (mutable). This avoids the multiple reference to springParms
         // and any associated mutation side effects (from this and the following color changes) when the state is captured.
         new cP.Spring(aT.jelloPucks[m], aT.jelloPucks[m+gridsize], Object.assign({}, springParms));
      }
      // Vertical springs
      for (var m = 0; m < gridsize-1; m++) {
         for (var n = 0; n < gridsize; n++) {
            var o_index = m + (n * gridsize);
            springParms.color = "blue";
            new cP.Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index+1], Object.assign({}, springParms));
         }
      }
      // Diagonal springs (yellow)
      for (var m = 0; m < gridsize-1; m++) {
         for (var n = 1; n < gridsize; n++) {
            var o_index = m + (n * gridsize);
            springParms.color = "yellow";
            springParms.length_m = spacing_factor_m * 1.41;  // A diagonal
            new cP.Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index-(gridsize-1)], Object.assign({}, springParms));
         }
      }
      // Diagonal springs (perpendicular to the other diagonals)
      for (var m = 0; m < gridsize-1; m++) {
         for (var n = 0; n < gridsize-1; n++) {
            var o_index = m + (n * gridsize);
            springParms.color = "yellow";
            springParms.length_m = spacing_factor_m * 1.41; // A diagonal
            new cP.Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index+(gridsize+1)], Object.assign({}, springParms));
         }
      }
      
      // Add two pinned springs.
      if (pinned) {
         var corner_puck = (gridsize * gridsize) - 1;
         new cP.Spring(aT.jelloPucks[ 0], new cP.Pin( new cP.Vec2D( 0.5, 0.5), {radius_px:4}), {strength_Npm:800.0, unstretched_width_m:0.3, color:'brown',damper_Ns2pm2:5.0});
         new cP.Spring(aT.jelloPucks[ corner_puck], new cP.Pin( new cP.Vec2D( 9.0, 9.0), {radius_px:4}), {strength_Npm:800.0, unstretched_width_m:0.3, color:'brown',damper_Ns2pm2:5.0});
      }
   }
   
   function checkForJelloTangle() {
      // Determine if tangled by looking for balls that are fairly close to 
      // each other. This does not require puck contact to detect a tangle.
      
      // A looping structure that avoids self reference and repeated puck-otherpuck references.
      var stillTangled = false;
      for (var j = 0, len = aT.jelloPucks.length; j < len; j++) {
         for (var k = j+1; k < len; k++) {
            // Check distance between j and k pucks.
            var diff_2d_m = aT.jelloPucks[j].position_2d_m.subtract( aT.jelloPucks[k].position_2d_m);
            
            // Square of the vector length.
            var lenSquared = diff_2d_m.length_squared();
            
            // Make the separation test a little more than the sum of the radii (add 30% of the radius of the smaller puck).
            // Then square it for comparison with the length squared.
            var radiiSum_m = aT.jelloPucks[j].radius_m + aT.jelloPucks[k].radius_m;
            var minRadius_m = Math.min( aT.jelloPucks[j].radius_m, aT.jelloPucks[k].radius_m )
            var separation_check = Math.pow(radiiSum_m + (minRadius_m * 0.30), 2);
            
            if (lenSquared < separation_check) {
               // This one is too close to be in a non-tangled jello block.
               stillTangled = true;
               c.jello.tangleTimer_s += c.deltaT_s;
               j = k = 10000; // break out of the two loops.
            }
         }
      }
      ctx.font = "25px Arial";
      ctx.fillStyle = 'lightgray';
      ctx.fillText(c.jello.tangleTimer_s.toFixed(2),15,40);
      
      if (!stillTangled) {
         // Get a timestamp for use in verification.
         if (!c.jello.verifyingDeTangle) {
            c.jello.timerAtDetangle_s = c.jello.tangleTimer_s;
         }
         // Wait 1.000 seconds and verify (that there has been no timer change).
         if (!c.jello.reported && !c.jello.verifyingDeTangle) {
            c.jello.verifyingDeTangle = true;
            //console.log('a new verification');
            //console.log('timer=' + c.jello.tangleTimer_s.toFixed(3) + ", [email protected]=" + c.jello.timerAtDetangle_s.toFixed(3));
            window.setTimeout( function() { 
               // If the timer hasn't advanced, must still be detangled.
               if (c.jello.tangleTimer_s == c.jello.timerAtDetangle_s) {
                  if (!c.jello.reported) {
                     
                     // leaderboard stuff
                     cP.Client.applyToAll( client => { 
                        client.addScoreToSummary( c.jello.tangleTimer_s.toFixed(2), c.demoIndex, c.npcSleepUsage);
                     });
                     reportGameResults();
                     // Send a score for each human player to the leaderboard. Build leaderboard report at the end.
                     submitScoresThenReport();
                     // Open up the multi-player panel so you can see the leader-board report.
                     if (!dC.multiplayer.checked) {  
                        dC.multiplayer.checked = !dC.multiplayer.checked;
                        toggleMultiplayerStuff();
                     }
                     
                     console.log('jello game over, ' + c.jello.tangleTimer_s.toFixed(2));
                     //console.log('timer=' + c.jello.tangleTimer_s.toFixed(3) + ", [email protected]=" + c.jello.timerAtDetangle_s.toFixed(3));
                     // Make sure this gets reported only once (per demo #6 start).
                     c.jello.reported = true;
                     messages['win'].newMessage("That's better. Thank you.", 3.5);
                  }
               } else {
                  console.log('not sustainably detangled...');
                  //console.log('timer=' + c.jello.tangleTimer_s.toFixed(3) + ", [email protected]=" + c.jello.timerAtDetangle_s.toFixed(3));
               }
               c.jello.verifyingDeTangle = false;
            }, 1000);
         }
      }
   }
   
   function setNickNameWithoutConnecting() {
      var nickName = hC.checkForNickName('normal','host');
      if (nickName.status == 'too long') {
         hC.displayMessage('Nicknames must have 10 characters or less. Please shorten it and then try again.');
      } else if (nickName.value) {
         hC.displayMessage('Your nickname is ' + nickName.value + '.');
      }
   }
   
   function leaderBoardReport( lbResp, gameVersion) {
      c.leaderBoardIndex += 1;
      var scoreCell_id = 'scoresCell' + c.leaderBoardIndex;
      var timeCell_id = 'timesCell' + c.leaderBoardIndex;
      var scoreOrTime_id = 'scoreOrTime' + c.leaderBoardIndex;
      
      // Simplify the reporting for Jello Madness because there is only the time-based result (no scoring result).
      if (c.demoIndex == 6) {
         var rankString = "";
         rankString = "On a time basis, " + lbResp.userName + " placed " + lbResp.timeSortedResults.userRank + ' of ' + lbResp.timeSortedResults.scoreCount + 
                             ", " + lbResp.timeSortedResults.winTime + " seconds.</br><br class='score'>";
         var leaderBoardReportHTML = "Leader Board Report: " + gameVersion + "</br><br class='score'>" + rankString;
         
      } else {
         if (lbResp.userRank != 'mouse or npcSleep usage') {
            var rankString = "Highest human scorer, " + lbResp.userName + ', placed ' + lbResp.userRank + ' of ' + lbResp.scoreCount + ' with a score of ' + lbResp.userScore + ". ";
            if (lbResp.timeSortedResults.winTime != '') {
               rankString += "On a time basis, placed " + lbResp.timeSortedResults.userRank + ' of ' + lbResp.timeSortedResults.scoreCount + 
                             ", " + lbResp.timeSortedResults.winTime + " seconds.";
            }
         } else {
            var rankString = "Highest human scorer, " + lbResp.userName + ', scored ' + lbResp.userScore + " (mouse or npc-sleep used).";
         }
         
         rankString += "</br><br class='score'>";
         // Note the use of the escape \ to get three levels of quotations in the following string.     
         var leaderBoardReportHTML = "Leader Board Report: " + gameVersion + "&nbsp;&nbsp;&nbsp;(" + 
              "<a title = 'toggle between low-time and high-score based queries' onclick=\"gW.toggleElementDisplay('" + timeCell_id +  "','block'); " + 
                           "gW.toggleElementDisplay('" + scoreCell_id + "','block'); " +
                           "gW.toggleSpanValue('" + scoreOrTime_id + "','time','score');\">" + 
              "<span id='" + scoreOrTime_id + "'>score</span></a>" + 
              ")</br><br class='score'>" + rankString;
      }
      
      // Add the tables
      var scoreTable = leaderBoardTable( "score",   lbResp,                   gameVersion);
      var timeTable  = leaderBoardTable( "winTime", lbResp.timeSortedResults, gameVersion);
      leaderBoardReportHTML += 
         "<table><tr>" + 
         "<td id='" + scoreCell_id + "' style='display:none'>" + scoreTable + "</td>" + 
         "<td id='" + timeCell_id +  "'  style='vertical-align:text-top; display:block'>" + timeTable + "</td>" + 
         "</tr></table>";
      
      // Find the most recent game report element (in the chat panel).
      var gameReportElement = document.getElementById("gR" + hC.gb.gameReportCounter);
      // Append the leader-board report to the game report.
      gameReportElement.innerHTML = gameReportElement.innerHTML + "<br>" + leaderBoardReportHTML;
      
      // Send this, the combo of the game summary and leader-board report, to everyone else in the 
      // room so they can see it in their chat panel.
      hC.chatToNonHostPlayers( gameReportElement.innerHTML);
   }
   
   function checkIfInGameTable( userName, winTime, userScore, index) {
      // This compares one row from the leaderboard report to each row in the game table.
      var inTable = false;
      for (let scoreRecord of cP.Client.scoreSummary) {
         if ((scoreRecord['name'] == userName) && (scoreRecord['winner'] == winTime) && (scoreRecord['score'] == userScore) && (scoreRecord['randomIndex'] == index)) {
            inTable = true;
         } 
      }
      return inTable;
   }
   
   function leaderBoardTable( mode, lbResp, gameVersion) {
      var rowIndex = 1;
      
      // If no records in the report, return with this simple warning.
      if (lbResp.users.length < 1) return "(no " + mode + " records)";
      
      var colHighLightStyle = "style='background-color:#ffffef;'"; // #FFFFFF #e2e2b7 #f7f7d7 #f9f9e5 #ffffef
      var rowHighLightStyle = "style='background-color:darkgray; color:white'";
      if (mode == 'score') {
         var style_score = colHighLightStyle;
         var style_winTime = "";
         var tableClass = "score";
      } else {
         var style_winTime = colHighLightStyle;
         var style_score = "";
         var tableClass = "score";
      }
      if (c.demoIndex == 6) {
         var tableString = "<table class='" + tableClass + "'><tr align='right'>" +
            "<td class='leaderboardHeader'></td>" +
            "<td class='leaderboardHeader' title='client name \n or \nnickname (client name)'>name</td>" +
            "<td class='leaderboardHeader' title='time (seconds) to untangle the jello (separate the pucks)' " +style_winTime+ ">time</td>" +
            "<td class='leaderboardHeader' title='human players'>p</td>" +
            "<td class='leaderboardHeader' title='monitor frames per second'>fps</td>" +
            "<td class='leaderboardHeader' title='inverse of the physics timestep'>ipt</td>" +
            "</tr>";
      } else {
         var tableString = "<table class='" + tableClass + "'><tr align='right'>" +
            "<td class='leaderboardHeader'></td>" +
            "<td class='leaderboardHeader' title='client name \n or \nnickname (client name)'>name</td>" +
            "<td class='leaderboardHeader' title='time (seconds) to win game (last puck standing)' " +style_winTime+ ">time</td>" +
            "<td class='leaderboardHeader' title='" +c.scoreTip+ "' " +style_score+ ">score</td>" +
            "<td class='leaderboardHeader' title='human players'>p</td>" +
            "<td class='leaderboardHeader' title='drones'>d</td>" +
            "<td class='leaderboardHeader' title='monitor frames per second'>fps</td>" +
            "<td class='leaderboardHeader' title='inverse of the physics timestep'>ipt</td>" +
            "<td class='leaderboardHeader' title='virtual gamepad was used during game'>vgp</td>" +
            "<td class='leaderboardHeader' title='friendly fire was prevented during game'>nff</td>" +
            "</tr>";
         
      }
      
      for (let score of lbResp.users) {
         // Highlight each row in the leader-board report that matches any row in the game result report.
         if ( checkIfInGameTable( score['userName'], score['winTime'], score['score'], score['index']) ) {
            var rowStyle = rowHighLightStyle;
            var style_score_td = "";
            var style_winTime_td = "";
         } else {
            var rowStyle = "";
            var style_score_td = style_score;
            var style_winTime_td = style_winTime;
         }
         
         if (typeof score['winTime'] == 'number') {
            if (mode == 'score') {
               var timeResult = score['winTime'].toFixed(2);
            } else {
               var timeResult = score['winTime'].toFixed(2);
            }
         } else {
            var timeResult = score['winTime'];
         }
         
         if (c.demoIndex == 6) {
            tableString += "<tr align='right' " + rowStyle + ">" + 
               "<td class='leaderboardIndex'>" + rowIndex + "</td>" +
               "<td class='leaderboardName'                        >" + score['userName'].replace('(host)','(h)') + "</td>" +
               "<td class='leaderboardScore' " +style_winTime_td+ ">" + timeResult +                                "</td>" +
               "<td class='leaderboardScore'                       >" + score['nPeople'] + "</td>" +
               "<td class='leaderboardScore'                       >" + score['frMonitor'] + "</td>" +
               "<td class='leaderboardScore'                       >" + score['hzPhysics'] + "</td>" +
               "</tr>";
         } else {
            tableString += "<tr align='right' " + rowStyle + ">" + 
               "<td class='leaderboardIndex'>" + rowIndex + "</td>" +
               "<td class='leaderboardName'                        >" + score['userName'].replace('(host)','(h)') + "</td>" +
               "<td class='leaderboardScore' " +style_winTime_td+ ">" + timeResult +                                "</td>" +
               "<td class='leaderboardScore' " +style_score_td+   ">" + score['score'] +                            "</td>" +
               "<td class='leaderboardScore'                       >" + score['nPeople'] + "</td>" +
               "<td class='leaderboardScore'                       >" + score['nDrones'] + "</td>" +
               "<td class='leaderboardScore'                       >" + score['frMonitor'] + "</td>" +
               "<td class='leaderboardScore'                       >" + score['hzPhysics'] + "</td>" +
               "<td class='leaderboardScore'                       >" + score['virtualGamePad'] + "</td>" +
               "<td class='leaderboardScore'                       >" + score['noFriendlyFire'] + "</td>" +
               "</tr>";
         }
         rowIndex += 1;
      }
      tableString += "</table>";
      return tableString;
   }
   
   function logEntry( eventDescription, mode='normal') {
      // If this page is coming from the production server...
      var pageURL = window.location.href;
      if (pageURL.includes("timetocode")) {
         var sheetURL = 'https://script.google.com/macros/s/AKfycbymaDOxbOAtZAzgxPwm6yIvWG8Euw8jcHM1weyQ_caVSL0BkBI/exec';
         // AJAX
         var xhttp = new XMLHttpRequest();
         xhttp.open('GET', sheetURL + '?mode=' + mode + '&eventDesc=' + eventDescription, true);
         xhttp.send();
      }
   }
   
   function submitScoresThenReport() {
      var nR = 0;
      var peopleClients = [];
      // Define the spreadsheet function within this submitScoresThenReport scope so it has access to nR
      // and peopleClients.
      function sendScoreToSpreadSheet( mode, userName, userScore, gameVersion, winner, mouse, npcSleep, n_people, n_drones, frameRate_monitor, frameRate_physics, virtualGamePad, noFriendlyFire, index) {
         var sheetURL = 'https://script.google.com/macros/s/AKfycbz2DWA7VNas0M4ZwIADjPBSxF9SLqX64PxnwpF-bbM0xECDZrhS/exec';
         
         // AJAX
         var xhttp = new XMLHttpRequest();
         xhttp.open('GET', sheetURL + '?mode=' + mode + 
                                      '&userName=' + userName + '&score=' + userScore +  '&gameVersion=' + gameVersion + 
                                      '&winTime=' + winner +    '&mouse=' + mouse +      '&npcSleep=' + npcSleep +
                                      '&nPeople=' + n_people +  '&nDrones=' + n_drones + '&frMonitor=' + frameRate_monitor + '&hzPhysics=' + frameRate_physics + 
                                      '&virtualGamePad=' + virtualGamePad + '&noFriendlyFire=' + noFriendlyFire + '&index=' + index, true);
         xhttp.send();
         xhttp.onreadystatechange = function () {
            // If there is a response from the spreadsheet:
            if (this.readyState == 4 && this.status == 200) {
               // lbResp is short for leaderBoardResponse
               var lbResp = JSON.parse( this.responseText);
               
               if (lbResp.result == 'report') {
                  /*
                  // useful for testing:
                  console.log('You, ' + lbResp.userID + ', placed ' + lbResp.userRank + ' of ' + lbResp.scoreCount + ' with a score of ' + lbResp.userScore);
                  for (var i = 0; i < lbResp.users.length; i++) {
                     // Convert the date so can display it.
                     var recordDate = new Date(lbResp.users[i].date);
                     var recordDateString = recordDate.getDate() +'/'+ (recordDate.getMonth() + 1) +'/'+ recordDate.getFullYear() +' '+ recordDate.getHours() +':'+ recordDate.getMinutes();         
                     console.log(recordDateString + ', ' + lbResp.users[i].id + ', ' + lbResp.users[i].score);
                  } 
                  */
                  
                  // Assemble the html needed to display the leaderboard query results in the chat panel.
                  leaderBoardReport( lbResp, gameVersion);
                  
               } else {
                  console.log( lbResp.result);
                  if (lbResp.error) console.log( lbResp.error);
               }
               
               // Keep (recursively) sending data until the last score (highest), ask for a report for that last one. 
               nR += 1;
               console.log('rC='+nR);
               
               if (nR < n_people-1) {
                  // Make another non-report entry
                  sendScoreToSpreadSheet( 'noReport', peopleClients[ nR]['name'], peopleClients[ nR]['score'], c.demoVersion, 
                                          peopleClients[ nR]['winner'], peopleClients[ nR]['mouse'], peopleClients[ nR]['npcSleep'], 
                                          n_people, n_drones, frameRate_monitor, frameRate_physics, peopleClients[ nR]['virtualGamePad'], noFriendlyFire, peopleClients[ nR]['randomIndex']);
                  
               } else if (nR == n_people-1) {
                  // Do a final submission, and ask for a report (see first parameter) from the spreadsheet this time.
                  sendScoreToSpreadSheet( 'report',   peopleClients[ nR]['name'], peopleClients[ nR]['score'], c.demoVersion,
                                          peopleClients[ nR]['winner'], peopleClients[ nR]['mouse'], peopleClients[ nR]['npcSleep'], 
                                          n_people, n_drones, frameRate_monitor, frameRate_physics, peopleClients[ nR]['virtualGamePad'], noFriendlyFire, peopleClients[ nR]['randomIndex']);
               }
            }
         }
      }
      
      // Ascending sort (this way the report gets issued on the highest score, last one.)
      cP.Client.scoreSummary.sort((a, b) => a['score'] - b['score']);
      
      // Make a subset of the scores to only include real people.
      for (let score of cP.Client.scoreSummary) {
         // Filter out the NPC pucks here.
         if ( ! score['name'].includes('NPC')) {
            peopleClients.push( score);
         }
      }
      var n_people = peopleClients.length;
      var n_drones = cP.Client.scoreSummary.length - n_people;
      var frameRate_monitor = dC.fps.innerHTML; //current observed refresh rate of the monitor
      var frameRate_physics = $('#FrameRate').val(); //timestep for engine
      var noFriendlyFire = (dC.friendlyFire.checked) ? '':'x'; 
      
      // Recursively send the scores. If only one player, go right to 'report' mode.
      if (n_people > 0) {
         var reportMode = (n_people == 1) ? 'report':'noReport'; 
         sendScoreToSpreadSheet( reportMode, peopleClients[0]['name'], peopleClients[0]['score'], c.demoVersion, 
                                             peopleClients[0]['winner'], peopleClients[0]['mouse'], peopleClients[0]['npcSleep'], 
                                             n_people, n_drones, frameRate_monitor, frameRate_physics, peopleClients[0]['virtualGamePad'], noFriendlyFire, peopleClients[0]['randomIndex']);
      }
   }
   
   function reportGameResults() {
      if (c.demoIndex == 6) {
         var summaryString = "Game Summary: " + c.demoVersion + "</br><br class='score'>" + 
            "<table class='score'><tr align='right'>" +
            "<td class='scoreHeader' title='client name \n or \nnickname (client name)'>name</td>" +
            "<td class='scoreHeader' title='time (seconds) to untangle the jello (separate the pucks)'>time</td>" +
            "</tr>";
         for (let score of cP.Client.scoreSummary) {
            summaryString += "<tr align='right'>" + 
            "<td class='score'>" + score['name']     + "</td>" + 
            "<td class='score'>" + score['winner']   + "</td>" + 
            "</tr>";
         }
         
      } else {
         cP.Client.scoreSummary.sort((a, b) => b['score'] - a['score']);
         var summaryString = "Game Summary: " + c.demoVersion + "</br><br class='score'>" + 
            "<table class='score'><tr align='right'>" +
            "<td class='scoreHeader' title='client name \n or \nnickname (client name)'>name</td>" +
            "<td class='scoreHeader' title='time (seconds) to win game (last puck standing)'>time</td>" +
            "<td class='scoreHeader' title='" +c.scoreTip+ "'>score</td>" +
            "<td class='scoreHeader' title='mouse usage in the canvas area'>m</td>" +
            "<td class='scoreHeader' title='NPCs have been sleeping (ctrl-q)'>s</td>" +
            "<td class='scoreHeader' title='virtual gamepad used during game'>vgp</td>" + 
            "</tr>";
         // Check for any mouse usage by the players as you write out the rows.
         var someMouseFunnyBz = false;
         for (let score of cP.Client.scoreSummary) {
            if (score['mouse'] == 'x') someMouseFunnyBz = true;
            summaryString += "<tr align='right'>" + 
            "<td class='score'>" + score['name']     + "</td>" + 
            "<td class='score'>" + score['winner']   + "</td>" + 
            "<td class='score'>" + score['score']    + "</td>" + 
            "<td class='score'>" + score['mouse']    + "</td>" +
            "<td class='score'>" + score['npcSleep'] + "</td>" +
            "<td class='score'>" + score['virtualGamePad'] + "</td>" +
            "</tr>";
         }
      }
      
      // Now report the sorted score summary (pass in function to give descending numeric sort)
      summaryString += "</table>"
      hC.displayMessage( summaryString);
      
      // If any of the players or the host (without a puck player) used the mouse, mark everyone 
      // as suspect before doing the submission to the leaderboard. This appropriately blocks the case where the host
      // turns off his player and uses his mouse to delete the drones and lets one network player win.
      // That's clever, but that's not allowed.
      // (Notice the word "of" here. This type of for-of loop works nicely on arrays, and presents the item, not simply the index.)
      for (let score of cP.Client.scoreSummary) {
         // For Jello Madness, don't check for mouse usage. Mouse is always used.
         if (c.demoIndex == 6) {
            score['mouse'] = '';
         } else {
            if (someMouseFunnyBz || clients['local'].mouseUsage) score['mouse'] = 'x';
         }
      }
   }
   
   // Note that this check gets called every frame (if running #7 or #8).
   function checkForPuckPopperWinnerAndReport() {
      // Check for a puck-popper winner. Do this check on the pucks because the human clients
      // are not removed when their pucks are popped.
      if ((cP.Puck.playerCount == 1) || (!dC.friendlyFire.checked && (cP.Puck.npcCount == 0))) {
         // Get the name of the client scoring the last hit. The check, to see if the winner (last client to produce a hit)
         // is still there, prevents a failed reference (to nickname) if the host uses the mouse to delete the last NPC. Usually, with
         // mouse deletion of the NPC, they are the last hitter, and so the applyToAll loop will run.
         var winnerClientName = c.lastClientToScoreHit;
         var winnerDescString = 'scoring winning hit';
         if ( ! clients[ winnerClientName]) {
            // Looks like the last hitting client is not there (host is probably using the mouse for NPC deletes). 
            // There might be multiple players left (if friendly fire is off). So take the highest scorer as the winner.
            var highestScore = -10000;
            cP.Puck.applyToAll( puck => {
               if (puck.clientName) {
                  if (clients[ puck.clientName].score > highestScore) {
                     winnerClientName = puck.clientName;
                     highestScore = clients[ puck.clientName].score;
                  }
               }
            });
            winnerDescString = 'with highest score';
         }
         
         if (clients[ winnerClientName]) {
            var winnerNickName = clients[ winnerClientName].nickName;
         } else {
            // Still having trouble establishing a winner. Let's exit.
            return;
         }
         
         // If the winner is still around (hasn't disconnected)
         if (clients[ winnerClientName] || (winnerClientName == 'Team')) {
            
            if (winnerNickName) {
               var displayName = winnerNickName + ' (' + cP.Client.translateIfLocal( winnerClientName) + ')';
            } else {
               var displayName = cP.Client.translateIfLocal( winnerClientName);
            }
            
            if (cP.Client.puckCountAtGameStart > 1) {
               
               // Give a bonus (only once, not every frame) for winning.
               if ( ! cP.Client.winnerBonusGiven) {
                  cP.Client.winnerBonusGiven = true;
                  
                  // Yes, now add the winner(s) to the summary too. The losers got added when their puck was popped.
                  if (dC.friendlyFire.checked) {
                     // Can only be one puck standing in this case.
                     clients[ winnerClientName].score += 200;
                     clients[ winnerClientName].addScoreToSummary( c.puckPopperTimer_s.toFixed(2), c.demoIndex, c.npcSleepUsage);
                  } else {
                     // Assign the winning time to all the client pucks on the no-friendly-fire team.
                     cP.Puck.applyToAll( puck => {
                        if (puck.clientName) {
                           clients[ puck.clientName].score += 200;
                           clients[ puck.clientName].addScoreToSummary( c.puckPopperTimer_s.toFixed(2), c.demoIndex, c.npcSleepUsage);
                        }
                     });
                  }
                  
                  reportGameResults();
                  
                  // Send a score to the leaderboard for each human player. Build leaderboard report at the end.
                  submitScoresThenReport();
                  
                  // Open up the multi-player panel so you can see the leader-board report.
                  if (!dC.multiplayer.checked) {  
                     // Note: to directly call the click handler function, toggleMultiplayerStuff, it must be put in
                     // module-level scope, which has been done. So this can be explicitly controlled as follows:
                     dC.multiplayer.checked = !dC.multiplayer.checked;
                     toggleMultiplayerStuff();
                     /*
                     // Another approach is to get at the function via the module-level, dC.multiplayer. The following
                     // are alternate ways to do what the two statements above do. These approaches to not
                     // require toggleMultiplayerStuff to be in the module-level scope.
                     dC.multiplayer.click();
                     $("#chkMultiplayer").trigger("click");
                     // Note: the following also works. But don't think it's necessary to provide
                     // the 'this' context here.
                     dC.multiplayer.click.apply( dC.multiplayer);
                     */
                  }
                  // only displayed once per win (because this block only runs once per win)
                  messages['help'].resetMessage();
                  
                  if (winnerClientName.includes('NPC')) {
                     var congratsString = "Only one player remaining...";
                     var summaryString = "Computer wins (oh man, that's not good)" +
                                         "\\   color = " + clients[ winnerClientName].color + 
                                         "\\   time = " + c.puckPopperTimer_s.toFixed(2) + "s" +
                                         "\\   score = " + clients[ winnerClientName].score;
                  } else {
                     if (dC.friendlyFire.checked) {
                        var congratsString = "Only one player remaining...";
                        var summaryString = "" + displayName + " wins" + 
                                            "\\   color = " + clients[ winnerClientName].color + 
                                            "\\   time = " + c.puckPopperTimer_s.toFixed(2) + "s" +
                                            "\\   score = " + clients[ winnerClientName].score;
                     } else {
                        var congratsString = "Only good guys remaining...";
                        var summaryString = "The team wins" + 
                                            "\\   name of player " + winnerDescString + " = " + displayName + 
                                            "\\   color that player = " + clients[ winnerClientName].color + 
                                            "\\   time to win = " + c.puckPopperTimer_s.toFixed(2) + "s";
                     }
                  }
                  
                  var theSeries = {
                     1:{'tL_s':2.0, 'message':congratsString},
                     2:{'tL_s':2.5, 'message':"...so that's a win!"},
                     3:{'tL_s':1.0, 'message':"Summary:"},
                     4:{'tL_s':5.0, 'message': summaryString},
                     5:{'tL_s':2.0, 'message':"Reports are in the left panel."},
                     6:{'tL_s':4.0, 'message':"Click the \"multiplayer\" checkbox (or use the m key) \\to toggle back to the help."}};
                  if ( (!winnerClientName.includes('NPC')) && dC.friendlyFire.checked && (!c.territoryMarked) ) {
                     Object.assign( theSeries, { 
                        7:{'tL_s':1.0, 'message':"One last thing to try..."},
                        8:{'tL_s':2.5, 'message':"pop any left-over pucks..."},
                        9:{'tL_s':2.0, 'message':"then navigate..."},
                       10:{'tL_s':3.0, 'message':"to bounce your puck off the four walls."}
                     });
                  }
                  Object.assign( theSeries, { 
                     12:{'tL_s':1.0, 'message':"That's it..."},
                     13:{'tL_s':1.0, 'message':"...the end."},
                     15:{'tL_s':1.0, 'message':"."},
                     16:{'tL_s':1.0, 'message':".."},
                     17:{'tL_s':1.0, 'message':"..."},
                     18:{'tL_s':1.0, 'message':"...."},
                     19:{'tL_s':1.0, 'message':"....."},
                     20:{'tL_s':3.0, 'message':"You're still there?"},
                     21:{'tL_s':2.0, 'message':"Till next time."}
                  });
                  messages['win'].newMessageSeries( theSeries);
               }
               
               // Turn off (zero out the message string) the little score display for the local client (if they're the winner).
               if (winnerClientName == 'local') messages['score'].newMessage("", 0.0);
               
               // This marked condition is checked every frame after a win. So use c.territoryMarked to post this
               // 5 second message only once per onset of a marked territory. That way it turns off after 5s.
               if (fenceIsClientColor( winnerClientName)) {
                  if ( ! c.territoryMarked) {
                     messages['lowHelp'].newMessage("...nice job marking your territory...", 5.0);
                     c.territoryMarked = true;
                  }
               } else {
                  c.territoryMarked = false;
               }
            }
         }
      } else if (cP.Puck.playerCount > 1) {
         // Display host score as long as the corresponding puck remains...
         if (clients['local'].puck) {
            messages['score'].newMessage('host score = ' + clients['local'].score, 0.2);
            messages['score'].displayIt( c.deltaT_s, ctx);
         }
      
         c.puckPopperTimer_s += c.deltaT_s;
         messages['ppTimer'].newMessage( c.puckPopperTimer_s.toFixed(2), 0.2);
         messages['ppTimer'].displayIt( c.deltaT_s, ctx);
      }
   }
   
   /*
   Tried using the B2D contact listener to detect tangle. But this
   approach fails to deal with a tangled state where the balls are not quite
   touching... So the approach above is used.
   
   function checkForJelloTangle2() {
      if (c.contactCounter > 0) {
         c.jello.tangleTimer_s += c.deltaT_s;
      }
      ctx.font = "30px Arial";
      ctx.fillStyle = 'yellow';
      ctx.fillText(c.jello.tangleTimer_s.toFixed(2),10,50);
   }
   */
   
   // For loading and running a capture from a web page link.
   function demoStart_fromCapture( index, pars) {
      var fileName = setDefault( pars.fileName, 'null');
      $.getScript( fileName, function() {
         console.log('fetching '+ fileName +' from main server');
         // Note: demo_capture is a page level global and is assigned a value, the capture object, in the first line of the loading file.
         // Put the capture into the capture input box on the page.
         dC.json.value = JSON.stringify( demo_capture, null, 3);
         window.setTimeout( function() { scrollCaptureArea();}, 500);
         demoStart( index);
      }).fail( function() {
         // Try the local web server. Maybe the file hasn't been published yet. This will
         // only work for the developer (that's me).
         console.log('fetching '+ fileName +' from local server');
         $.getScript( 'http://localhost/ttc-root/'+fileName, function() {
            dC.json.value = JSON.stringify( demo_capture, null, 3);
            window.setTimeout( function() { scrollCaptureArea();}, 500);
            demoStart( index);
         }).fail( function() {
            console.log('capture file not found');
            demoStart( 1);
         });
      });
   }
   
   
   function createPucksForNetworkClients( networkPuckTemplate, startingPosAndVels) {
      /*
      Make a controlled puck for each client (that wants one). Copy attributes from
      the host puck or the provided template.
      */
      var position_2d_m, velocity_2d_mps;
      var networkClientIndex = 1;
      
      function randomPandV( position_2d_m, velocity_2d_mps) {
         // Randomize the position as constrained by the boundary of the canvas.
         position_2d_m.x = (meters_from_px( canvas.width) -0.3) * Math.random();
         position_2d_m.y = (meters_from_px( canvas.height)-0.3) * Math.random();
         // Randomize the initial velocity 
         velocity_2d_mps.x = 5.0 * (Math.random() - 0.5);
         velocity_2d_mps.y = 5.0 * (Math.random() - 0.5);
      }
      
      function initializePuckPosAndVel( puckOrTemplate, clientName) {
         if ((networkClientIndex - 1) <= (startingPosAndVels.length - 1)) {
            // Use the array of starting positions
            position_2d_m = startingPosAndVels[ networkClientIndex - 1].position_2d_m;
            velocity_2d_mps = startingPosAndVels[ networkClientIndex - 1].velocity_2d_mps;
         } else {
            randomPandV( position_2d_m, velocity_2d_mps);
         }
         
         new cP.Puck( position_2d_m, velocity_2d_mps, {'color':'black', 'colorSource':true, 'clientName':clientName, 
             'radius_m':    puckOrTemplate.radius_m,
             'hitLimit':    puckOrTemplate.hitLimit,
             'linDamp':     puckOrTemplate.linDamp,
             'angDamp':     puckOrTemplate.angDamp,
             'restitution': puckOrTemplate.restitution,
             'friction':    puckOrTemplate.friction });
      }
      
      // First, check to make sure there is a puck for the host, if it's requested (checked).
      if (dC.player.checked && !(clients['local'].puck)) {
         // Make the requested puck for the host
         position_2d_m = startingPosAndVels[ networkClientIndex - 1].position_2d_m;
         velocity_2d_mps = startingPosAndVels[ networkClientIndex - 1].velocity_2d_mps;
         new cP.Puck( position_2d_m, velocity_2d_mps, cP.Puck.hostPars);
         networkClientIndex++;
      }
      
      cP.Client.applyToAll( client => {
         var position_2d_m = new cP.Vec2D(0,0);
         var velocity_2d_mps = new cP.Vec2D(0,0);
         
         if ( (client.name.slice(0,1) == 'u') && (client.player) ) {
            
            // If the host has a puck for keyboard play, use the local puck as a template.
            if (clients['local'].puck) {
               initializePuckPosAndVel( clients['local'].puck, client.name);
                   
            // If the host is using the virtual game pad and a template has been established (e.g. from the 'local' puck in the capture).
            } else if (networkPuckTemplate) {  
               initializePuckPosAndVel( networkPuckTemplate, client.name);
                               
            } else {
               console.log('can not find anything to use as a puck template.');
            }
            
            // Update the TwoThumbs interface to show there is a client puck.
            var control_message = {'from':'host', 'to':client.name, 'data':{'puckPopped':{'value':false}} };
            hC.sendSocketControlMessage( control_message);
            
            // Send out the gun angle of the new puck so that sweep operations on a new puck start out 
            // smoothly (instead seeing a jump-to-sync at the client).
            client.gunAngleFromHost( 0.0, true); // true --> bypasses limits and executes immediately
            // Also sync up the jet angle on the client.
            client.jetAngleFromHost();
            
            // Increment only for network clients (inside the if block)
            networkClientIndex++;
         }
      });
   }
   
   function hL( id) {
      // hL is short for highlighting...
      // For inserting a style string in the links of the "Plus" row below the button cluster.
      if (c.demoVersion == id) {
         return "style='color:white; background-color:gray;'";     
      } else {
         return "";
      }
   }
   
   function demoStart( index, scrollCA=true, scrollHelp=true) {
      var v_init_2d_mps, buttonColor, buttonTextColor;
      var p1, p2, p3, p4;
      
      aT.collisionCount = 0;
      aT.collisionInThisStep = false;
      
      // by default no blending
      ctx.globalCompositeOperation = 'source-over';
      
      // So you can see the name of the capture if it's there.
      // However, nice to be able to edit the capture and run it without losing the spot where
      // you're working. In that case, set scrollCA to be false.
      if (scrollCA) scrollCaptureArea();
      
      // Set this module-level value to support the JSON capture.
      c.demoIndex = index;
      var networkPuckTemplate = null;
      
      dC.extraDemos.innerHTML = '';
      
      // Scaling factor between the Box2d world and the screen (pixels per meter)
      c.px_per_m = 100;  // a module-level value
      
      
      canvas.width = 600, canvas.height = 600;
      c.canvasColor = 'black';
      ctx.scale(1, 1);
      clearCanvas();
      
      canvas.style.borderColor = '#008080';
      
      cP.Wall.topFenceLegName = null; // For use in piCalcEngine
      
      adjustSizeOfChatDiv('normal');
      hC.resizeClients('normal');
      // Set this module-level value to help new connecting clients adjust their layout.
      c.chatLayoutState = 'normal';
                
      // Change the color of the demo button that was clicked.
      for (var j = 1; j <= 9; j++) {
         if (j == index) {
            buttonColor = "yellow";
            buttonTextColor = "black";
         } else {
            // Darkgray (with white text) for the game buttons
            if ((j == 3) || (j == 6) || (j == 7) || (j == 8)) {
               buttonColor = "darkgray";
               buttonTextColor = "white";
            } else {
               buttonColor = "lightgray";
               buttonTextColor = "black";
            }
         }
         document.getElementById('b'+j).style.backgroundColor = buttonColor;
         document.getElementById('b'+j).style.color = buttonTextColor;
      }
      
      // Delete pucks (and references to them) from the previous demo.
      cP.Puck.deleteAll();
      
      // Clean out the old springs.
      cP.Spring.deleteAll();
      c.springNameForPasting = null;
      
      // Clean out the non-player clients
      cP.Client.deleteNPCs();
      
      // Clean out the old pins and their representation in the b2d world.
      cP.Pin.deleteAll();
      
      // Clean out the old walls and their representation in the b2d world.
      cP.Wall.deleteAll();
      
      // De-select anything still selected.
      clients['local'].selectedBody = null;
      hostMSelect.resetAll();
            
      resetFenceColor( "white");
      if (dC.pause.checked) {
         dC.pause.checked = false;
      }
      setPauseState();
      
      // Turn gravity off by default.
      if (c.g_ON) {
         c.g_ON = false;
         dC.gravity.checked = false;
      }
      
      resetRestitutionAndFrictionParameters();
      setGravityRelatedParameters({});
      
      cP.Puck.bulletAgeLimit_ms = 1000;
      
      // reset the pi stuff back to defaults
      c.piCalcs = {'clacks':false, 'usePiEngine':false};
      
      // These message resets shut down any lingering messages from prior demos.
      messages['help'].resetMessage();
      messages['help'].loc_px = {'x':15,'y':30}; // The help location for all the non-game demos.
      messages['win'].resetMessage();
      messages['lowHelp'].resetMessage();
      messages['gameTitle'].resetMessage();
      if (messages['videoTitle']) messages['videoTitle'].resetMessage();
      
      // By default, use "a" for the demoVersion. 
      // (Loading a capture will overwrite this default value, as it should.)
      // When a capture is taken, its name will be based on (added to) this demo version name.
      c.demoVersion = index + '.a';
      
      // Convert (parse) the json capture into a local object.
      if (dC.json.value != '') {
         try {
            var state_capture = JSON.parse( dC.json.value);
         } catch (err) {
            var state_capture = null;
            window.alert("There's a formatting error in the state capture. Try clicking the 'Clear' button.");
         }
      } else {
         var state_capture = null;
      }
      
      // pool game locks and settings
      if ( (state_capture) && (index == 3) && (state_capture.demoVersion.slice(0,3) == "3.d") ) {
         // Initiate new clients that don't have any pool-game locks set. Restarting the
         // pool game will not reset values for a continuing player.
         cP.Client.applyToAll( client => {
            if (!((client.ctrlShiftLock) || (client.poolShotLocked))) {
               client.ctrlShiftLock = true;
               client.poolShotLocked = true;
               client.poolShotLockedSpeed_mps = 20;
            }
         });
      } else {
         // Turn off the pool game locks when starting all other demos.
         cP.Client.applyToAll( client => {
            client.ctrlShiftLock = false;
            client.poolShotLocked = false;
            client.poolShotLockedSpeed_mps = 0;
         });
      }
      
      if (index == 0) {
         clearState();
         if (document.fullscreenElement) hC.changeFullScreenMode( canvas, 'off');
         scrollDemoHelp('scroll-to-very-top');
         
         // Normally, keep the "0" demo blank for observing the framerate.
         
         /*
         // The following is an animation that was used in the beginning of the Puck Popper video.
         //canvas.width = 1250, canvas.height = 950;
         //canvas.width = 1920, canvas.height = 1080;
         canvas.width = 1850, canvas.height = 1060;
         cP.Wall.makeFence({'tOn':false,'rOn':false}, canvas); // Turn top and right walls off.
         
         messages['videoTitle'].font = "35px Arial";
         messages['videoTitle'].loc_px = {'x':300,'y':400};
         messages['videoTitle'].popAtEnd = false;
         var theSeries = {
            1:{'tL_s':1.5, 'message':"an introduction..."},
            2:{'tL_s':1.5, 'message':"maybe less...",            'loc_px':{'x':300,'y':400} },
            3:{'tL_s':1.5, 'message':"maybe more...",            'loc_px':{'x':300,'y':450} },
            4:{'tL_s':1.5, 'message':"than you should know...",  'loc_px':{'x':300,'y':400} },
            6:{'tL_s':1.5, 'message':"about...",                 'loc_px':{'x':300,'y':450},                      'popAtEnd':true},
            7:{'tL_s':1.3, 'message':"Puck",                     'loc_px':{'x':250,'y':350}, 'font':"90px Arial", 'popAtEnd':true},
            8:{'tL_s':1.5, 'message':"Popper",                   'loc_px':{'x':300,'y':450},                      'popAtEnd':false},
            
            9:{'tL_s':1.0, 'message':"...",                                 'loc_px':{'x':300,'y':450}, 'font':"35px Arial"},
            10:{'tL_s':1.5, 'message':"but first...",                       'loc_px':{'x':300,'y':450}, 'font':"35px Arial"},
            11:{'tL_s':3.0, 'message':"a game of the #8c version...", 'loc_px':{'x':300,'y':450} },
         };
         messages['videoTitle'].newMessageSeries( theSeries);
         
         var nBalls = 36; //100 36 180
         var angle_step_deg = 360.0 / nBalls;
         var v_2d_mps = new cP.Vec2D(0, 2.0);
         // 12.5/2, 9.5/2
         for (var i = 1; i <= nBalls; i++) {
               new cP.Puck(new cP.Vec2D(3.0, 3.0), v_2d_mps, {'radius_m':0.1, 'groupIndex':-1, 'color':'white', 'friction':0.0});
               // Rotate for the next ball.
               v_2d_mps.rotated_by( angle_step_deg);
         }
         */
         
      } else if (index == 1) {
         
         if (scrollHelp) scrollDemoHelp('#d1');
         
         if ((state_capture) && (state_capture.demoIndex == 1)) {
            restoreFromState( state_capture);
            
            if ( ['1.c','1.d','1.e','1.f'].includes( demoVersionBase( c.demoVersion)) ) {
               var enginePars = Object.assign({}, cP.PiEngine.state, c.piCalcs);
               piCalcEngine = new cP.PiEngine( aT.puckMap['puck1'], aT.puckMap['puck2'], sounds['clack2'], enginePars);
               cP.PiEngine.state = {}; // done with pi engine state data from the restore of the capture...
               
               if (c.piCalcs.enabled) {
                  var massRatio = Math.round( aT.puckMap['puck2'].mass_kg / aT.puckMap['puck1'].mass_kg);
                  var massRatio_string = massRatio.toLocaleString(); // commas in the string
                  messages['lowHelp'].newMessage("Mass ratio = " + massRatio_string, 3.0);
                  
                  if (c.piCalcs.usePiEngine) {
                     var initialCount = piCalcEngine.collisionCount;
                  } else {
                     var initialCount = aT.collisionCount;
                  }
                  // This initial message is updated in the engines (box2d and piCalcs).
                  messages['help'].newMessage("count = " + initialCount, 30.0);
               }
            }
            
         } else {
            cP.Wall.makeFence({}, canvas);
            
            var v_init_2d_mps = new cP.Vec2D(0.0, -2.0);
            new cP.Puck( new cP.Vec2D(2.0, 3.99),       v_init_2d_mps, {'radius_m':0.10, 'color':'GoldenRod', 'colorSource':true, 'bullet':true});
            new cP.Puck( new cP.Vec2D(2.0, 3.00),       v_init_2d_mps, {'radius_m':0.80                                         , 'bullet':true});
            
            var v_init_2d_mps = new cP.Vec2D(0.0,  2.0);
            new cP.Puck( new cP.Vec2D(5.00, 1.60+1.5*2), v_init_2d_mps, {'radius_m':0.35                                         , 'bullet':true});
            new cP.Puck( new cP.Vec2D(5.00, 1.60+1.5),   v_init_2d_mps, {'radius_m':0.35, 'color':'GoldenRod', 'colorSource':true, 'bullet':true});
            new cP.Puck( new cP.Vec2D(5.00, 1.60),       v_init_2d_mps, {'radius_m':0.35                                         , 'bullet':true});
            
            new cP.Puck( new cP.Vec2D(0.50, 5.60), new cP.Vec2D(0.40, 0.00), {'radius_m':0.15, 'bullet':true});
         }
         
         /*
         if ((getSpanValue('moreOrLess') == 'More') && (!dC.multiplayer.checked) && (!c.piCalcs.clacks)) {
            console.log(c.demoVersion);
            messages['help'].newMessageSeries({
               1:{'tL_s':2.0, 'message':"To learn more about the demos and the games..."},
               2:{'tL_s':2.0, 'message':"click on the 'More' link in the left panel..."},
               3:{'tL_s':2.0, 'message':"then restart the demo (click it's number again)."}
            });
         }
         */
         
         dC.extraDemos.innerHTML = 
            "<a title='big and little'     " + hL('1.a') + " onclick=\"gW.clearState(); gW.demoStart(1)\">1a,</a>" +
            "<a title='a gentle landing' "   + hL('1.b') + " onclick=\"gW.demoStart_fromCapture(1, {'fileName':'demo1b.js'})\">&nbsp;b,</a>" +
            "<a title='calculating the first two digits of pi with collisions' " + hL('1.c') + " onclick=\"gW.demoStart_fromCapture(1, {'fileName':'demo1c.js'})\">&nbsp;c,</a>" +
            "<a title='three digits of pi' " + hL('1.d') + " onclick=\"gW.demoStart_fromCapture(1, {'fileName':'demo1d.js'})\">&nbsp;d,</a>" +
            "<a title='five digits of pi' " + hL('1.e') + " onclick=\"gW.demoStart_fromCapture(1, {'fileName':'demo1e.js'})\">&nbsp;e&nbsp;</a>";
         
      } else if (index == 2) {
         
         if (scrollHelp) scrollDemoHelp('#d2');
         
         cP.Puck.restitution_gOn =  0.7;
         cP.Puck.friction_gOn =  0.6;
         
         cP.Puck.restitution_gOff = 1.0;
         cP.Puck.friction_gOff = 0.0;
         
         if ((state_capture) && (state_capture.demoIndex == 2)) {
            restoreFromState( state_capture);
            
         } else {
            cP.Wall.makeFence({}, canvas);
            new cP.Puck( new cP.Vec2D(4.5, 4.5), new cP.Vec2D(-4.0, 4.0), {'radius_m':0.20, 'friction':0.0, 'angleLine':false, 'color':'yellow', 'colorSource':true,
                                                                  'createTail':true, 
                                                                  'tailPars':{
                                                                     'propSpeed_ppf_px':2, 'length_limit':35,
                                                                     'color':'lightgray',
                                                                     'rainbow':false, 'rbSaturation': 75, 'rbLightness': 40,
                                                                     'machSwitch':false, 'machValue':0} });
                                                                  
            new cP.Puck( new cP.Vec2D(3.0, 3.0), new cP.Vec2D( 0.0, 0.0), {'radius_m':0.60, 'friction':0.0, 'angleLine':false, 'color':'GoldenRod', 'colorSource':true });
                                                                  
            new cP.Puck( new cP.Vec2D(1.5, 1.5), new cP.Vec2D( 0.0, 0.0), {'radius_m':0.20, 'friction':0.0, 'angleLine':false, 'color':'blue', 'colorSource':true,
                                                                  'createTail':true, 
                                                                  'tailPars':{
                                                                     'propSpeed_ppf_px':2, 'length_limit':35,
                                                                     'color':'lightgray',
                                                                     'rainbow':false, 'rbSaturation': 75, 'rbLightness': 40,
                                                                     'machSwitch':false, 'machValue':0} });
         }
         
         dC.extraDemos.innerHTML = 
            "<a title='sound field'                          " + hL('2.a') + " onclick=\"gW.clearState(); gW.demoStart(2)\">2a,</a>" +
            "<a title='pretty'                               " + hL('2.b') + " onclick=\"gW.demoStart_fromCapture(2, {'fileName':'demo2b.js'})\">&nbsp;b,</a>" +
            "<a title='Mach speeds of 1.0, 1.4, and 2.0'     " + hL('2.c') + " onclick=\"gW.demoStart_fromCapture(2, {'fileName':'demo2c.js'})\">&nbsp;c,</a>" +
            "<a title='tag'                                  " + hL('2.d') + " onclick=\"gW.demoStart_fromCapture(2, {'fileName':'demo2d.js'})\">&nbsp;d,</a>" +
            "<a title='rainbow'                              " + hL('2.e') + " onclick=\"gW.demoStart_fromCapture(2, {'fileName':'demo2e.js'})\">&nbsp;e&nbsp;</a>";
            
         if (c.demoVersion.slice(0,3) == "2.e") {
            var messageString =                                 'Play with the rainbow tail:';
            if ( ! document.fullscreenElement) messageString += '\\    click the right full-screen button, then...';
            messageString +=                                    '\\    click and drag the black ball.';
            messages['help'].newMessage( messageString, 3.0);
         }
         
      } else if (index == 3) {
         
         cP.Puck.restitution_gOn =  0.7;
         cP.Puck.friction_gOn =  0.6;
         
         cP.Puck.restitution_gOff = 1.0;
         cP.Puck.friction_gOff = 0.0;
         
         v_init_2d_mps = new cP.Vec2D(0.0, 2.0); 
         
         if ((state_capture) && (state_capture.demoIndex == 3)) {
            restoreFromState( state_capture);
            
            if (c.demoVersion.slice(0,3) == "3.d") {
               c.canvasColor = '#2b473b'; // #36594a
               
               messages['gameTitle'].loc_px = {'x':75,'y':240};
               messages['gameTitle'].popAtEnd = false;
               messages['gameTitle'].newMessage("ghost-ball\\  pool", 2.0);
               
               messages['help'].loc_px = {'x':75,'y':90};
               messages['help'].newMessage("Release the ghost:\\    press control and shift key then drag the cue ball.", 3.0);

               messages['help'].newMessageSeries({
                  1:{'tL_s':5.0, 'message':'release the ghost:' + 
                                         '\\    use the mouse to drag the ghost ball out of the cue ball'},
                  2:{'tL_s':5.0, 'message':"aim your shot:" + 
                                         "\\    touch the ghost ball against an object ball or cushion for alignment aids"},
                  4:{'tL_s':5.0, 'message':'shoot the cue ball:' + 
                                         '\\    release the mouse button ' + 
                                         '\\    (release over the cue ball to cancel the shot)'},
                  5:{'tL_s':7.0, 'message':'adjust the cue ball speed:' +
                                         '\\    tap the "z" key while dragging the ghost ball' +
                                         "\\    (speed value is based on ghost-cue separation)" +
                                         "\\    alternately use the mouse wheel"},
                  6:{'tL_s':5.0, 'message':'full-screen view:' +
                                         '\\    press the "v" key to fill the screen with the pool table' +
                                         '\\    press the "esc" key to return to the normal view'},
                  7:{'tL_s':5.0, 'message':"restart the game (and this help):" + 
                                         "\\    press #3 key " + 
                                         "\\    (above the w, not on keypad)"},
                  8:{'tL_s':2.0, 'message':"play some pool..."}
               });
            }
            
         } else {
            cP.Wall.makeFence({}, canvas);
            
            var grid_order = 7;
            var grid_spacing_m = 0.45;
            var startPosition_2d_m = new cP.Vec2D(0.0, 0.0);
            
            for (var i = 1; i <= grid_order; i++) {
               for (var j = 1; j <= grid_order; j++) {
                  var delta_2d_m = new cP.Vec2D( i * grid_spacing_m, j * grid_spacing_m);
                  var position_2d_m = startPosition_2d_m.add( delta_2d_m);
                  new cP.Puck(position_2d_m, v_init_2d_mps, {'radius_m':0.10, 'groupIndex':0});
               }
            }
            
            v_init_2d_mps = new cP.Vec2D(0.2, 0.0);
            new cP.Puck( new cP.Vec2D(5.5, 3.5), v_init_2d_mps, {'radius_m':0.10, 'color':'GoldenRod', 'colorSource':true, 'groupIndex':0} );
            
            /*
            // Expanding ring of non-colliding balls.
            var nBalls = 36; //100 36 180
            var angle_step_deg = 360.0 / nBalls;
            var v_2d_mps = new cP.Vec2D(0, 2.0);
            for (var i = 1; i <= nBalls; i++) {
                  new cP.Puck(new cP.Vec2D(3, 3), v_2d_mps, {'radius_m':0.1, 'groupIndex':-1, 'color':'white'});
                  // Rotate for the next ball.
                  v_2d_mps.rotated_by( angle_step_deg);
            }
            window.setTimeout( function() {
               saveState();
            }, 1);
            */
         }
         
         dC.extraDemos.innerHTML = 
            "<a title='order and disorder'      " + hL('3.a') + " onclick=\"gW.clearState(); gW.demoStart(3)\">3a,</a>" +
            "<a title='no puck-puck collisions' " + hL('3.b') + " onclick=\"gW.demoStart_fromCapture(3, {'fileName':'demo3b.js'})\">&nbsp;b,</a>" +
            "<a title='no puck-puck collisions' " + hL('3.c') + " onclick=\"gW.demoStart_fromCapture(3, {'fileName':'demo3c.js'})\">&nbsp;c,</a>" +
            "<a title='pool shots' "              + hL('3.d') + " onclick=\"gW.demoStart_fromCapture(3, {'fileName':'demo3d.js'})\">&nbsp;d&nbsp;</a>";
            
         if (scrollHelp) {
            if ( ['3.d'].includes( demoVersionBase( c.demoVersion)) ) {               
               scrollDemoHelp('#d3d');
            } else {
               scrollDemoHelp('#d3');
            }
         }
         
      } else if (index == 4) {
         
         if (scrollHelp) scrollDemoHelp('#d4');
         
         cP.Puck.restitution_gOn =  0.7;
         cP.Puck.friction_gOn =  0.6;
         
         cP.Puck.restitution_gOff = 1.0;
         cP.Puck.friction_gOff = 0.0;
                 
         if ((state_capture) && (state_capture.demoIndex == 4)) {
            restoreFromState( state_capture);
            
         } else {
            cP.Wall.makeFence({}, canvas);
            
            new cP.Puck( new cP.Vec2D(3.00, 3.00), new cP.Vec2D( 0.0, 0.0), 
               {'radius_m':0.40, 'color':'GoldenRod', 'colorSource':true , 'shape':'rect', 'angularSpeed_rps':25.0});
            
            new cP.Puck( new cP.Vec2D(0.25, 3.00), new cP.Vec2D( 2.0, 0.0), 
               {'radius_m':0.15, 'shape':'rect', 'aspectR':4.0, 'angularSpeed_rps':0, 'angle_r': Math.PI/2});
            new cP.Puck( new cP.Vec2D(5.75, 3.00), new cP.Vec2D(-2.0, 0.0), 
               {'radius_m':0.15, 'shape':'rect', 'aspectR':4.0, 'angularSpeed_rps':0, 'angle_r': Math.PI/2});
               
            // Include two pins and a spring as a source for replicating. 
            new cP.Spring( new cP.Pin( new cP.Vec2D( 0.1, 0.2),{}), new cP.Pin( new cP.Vec2D( 0.1, 1.2),{}), 
                 {'length_m':1.5, 'strength_Npm':10.0, 'unstretched_width_m':0.1, 'color':'yellow', 'damper_Ns2pm2':1.0});
         }
         
         dC.extraDemos.innerHTML = 
            "<a title='rectangular symmetry'                " + hL('4.a') + " onclick=\"gW.clearState(); gW.demoStart(4)\">4a,</a>" +
            "<a title='conservation of angular momentum...' " + hL('4.b') + " onclick=\"gW.demoStart_fromCapture(4, {'fileName':'demo4b.js'})\">&nbsp;b,</a>" +
            "<a title='no surface friction or y momentum' "   + hL('4.c') + " onclick=\"gW.demoStart_fromCapture(4, {'fileName':'demo4c.js'})\">&nbsp;c,</a>" +
            "<a title='little moves big' "                    + hL('4.d') + " onclick=\"gW.demoStart_fromCapture(4, {'fileName':'demo4d.js'})\">&nbsp;d&nbsp;</a>";
         
      } else if (index == 5) {
         
         cP.Puck.restitution_gOn =  0.7;
         cP.Puck.friction_gOn =  0.6;
         
         cP.Puck.restitution_gOff = 1.0;
         cP.Puck.friction_gOff = 0.0;

         v_init_2d_mps = new cP.Vec2D(0.0,0.0);         
         
         if ((state_capture) && (state_capture.demoIndex == 5)) {
            restoreFromState( state_capture);
            
         } else {
            cP.Wall.makeFence({}, canvas);
            
            // Spring triangle.
            var tri_vel_mps = new cP.Vec2D( 5.0, 0.0);
            var d5_puckPars_triangle = {'radius_m':0.20, 'restitution':0.0, 'friction':1.0}
            new cP.Puck( new cP.Vec2D(1.00, 0.80 + Math.sin(60.0*Math.PI/180)), tri_vel_mps, Object.assign({}, d5_puckPars_triangle, {'name':'puck1'}));
            
            tri_vel_mps.rotated_by(-240.0);
            new cP.Puck( new cP.Vec2D(0.50, 0.80                             ), tri_vel_mps, Object.assign({}, d5_puckPars_triangle, {'name':'puck2'}));
            
            tri_vel_mps.rotated_by(-240.0);
            new cP.Puck( new cP.Vec2D(1.50, 0.80                             ), tri_vel_mps, Object.assign({}, d5_puckPars_triangle, {'name':'puck3'}));
            
            var springColor1 = 'blue';
            new cP.Spring(aT.puckMap['puck1'], aT.puckMap['puck2'], 
                                        {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':springColor1});
            new cP.Spring(aT.puckMap['puck1'], aT.puckMap['puck3'], 
                                        {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':springColor1});
            new cP.Spring(aT.puckMap['puck2'], aT.puckMap['puck3'], 
                                        {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':springColor1});
            
            var springColor2 = 'yellow';
            
            // Single puck with two springs and pins.
            new cP.Puck( new cP.Vec2D(4.0, 5.0), new cP.Vec2D(0.0, 0.0), {'radius_m':0.55, 'name':'puck4', 'restitution':0.0, 'angDamp':0.0, 'linDamp':2.0, 'friction':1.0});
            var d5_springPars_onePuck = {'strength_Npm':20.0, 'unstretched_width_m':0.2, 'color':springColor2, 'damper_Ns2pm2':0.0, 'drag_c':0.0};
            new cP.Spring(aT.puckMap['puck4'], new cP.Pin( new cP.Vec2D( 3.0, 5.0),{borderColor:'yellow'}), 
                  Object.assign({}, d5_springPars_onePuck, {'spo1_ap_l_2d_m':new cP.Vec2D( 0.54, 0.01)}) );
            new cP.Spring(aT.puckMap['puck4'], new cP.Pin( new cP.Vec2D( 5.0, 5.0),{borderColor:'yellow'}), 
                  Object.assign({}, d5_springPars_onePuck, {'spo1_ap_l_2d_m':new cP.Vec2D(-0.54, 0.00)}) );
                                        
            // Two pucks (one bigger than the other) on spring orbiting each other (upper left corner)
            new cP.Puck( new cP.Vec2D(0.75, 5.00), new cP.Vec2D(0.0, -5.00                          * 1.2), {'radius_m':0.15, 'name':'puck5'});
            // Scale the y velocity by the square of the radius ratio. This gives a net momentum of zero (so it stays in one place as it spins).
            new cP.Puck( new cP.Vec2D(1.25, 5.00), new cP.Vec2D(0.0, +5.00 * Math.pow(0.15/0.25, 2) * 1.2), {'radius_m':0.25, 'name':'puck6'});
            new cP.Spring(aT.puckMap['puck5'], aT.puckMap['puck6'], 
                                        {'length_m':0.5, 'strength_Npm':5.0, 'unstretched_width_m':0.05, 'color':springColor2});
                                        
            // Same thing (lower right corner)
            new cP.Puck( new cP.Vec2D(4.70, 0.55), new cP.Vec2D(+4.90, 0.0), {'radius_m':0.20, 'name':'puck7'});
            new cP.Puck( new cP.Vec2D(4.70, 1.55), new cP.Vec2D(-4.90, 0.0), {'radius_m':0.20, 'name':'puck8'});
            new cP.Spring(aT.puckMap['puck7'], aT.puckMap['puck8'], 
                                        {'length_m':0.5, 'strength_Npm':5.0, 'unstretched_width_m':0.05, 'color':springColor2});
                                        
         }
         
         dC.extraDemos.innerHTML = 
            "<a title='stretchy things'          " + hL('5.a') + " onclick=\"gW.clearState(); gW.demoStart(5)\">5a,</a>" +
            "<a title='Rube would like this...'  " + hL('5.b') + " onclick=\"gW.demoStart_fromCapture(5, {'fileName':'demo5b.js'})\">&nbsp;b,</a>" +
            "<a title='spring pendulum'          " + hL('5.c') + " onclick=\"gW.demoStart_fromCapture(5, {'fileName':'demo5c.js'})\">&nbsp;c,</a>" +
            "<a title='dandelion seeds'          " + hL('5.d') + " onclick=\"gW.demoStart_fromCapture(5, {'fileName':'demo5d.js'})\">&nbsp;d,</a>" +
            "<a title='traditional springs (left side) and \nBox2D distance joints (right side)' " +
                                                     hL('5.e') + " onclick=\"gW.demoStart_fromCapture(5, {'fileName':'demo5e.js'})\">&nbsp;e&nbsp;</a>";
         
         // Scroll AFTER loading the capture (and setting c.demoVersion) so can scroll to the special help for the 5d demo.
         if (scrollHelp) {
            if ( ['5.e'].includes( demoVersionBase( c.demoVersion)) ) {               
               scrollDemoHelp('#d5e');
            } else {
               scrollDemoHelp('#d5');
            }
         }
      
      } else if (index == 6) {
         
         if (scrollHelp) scrollDemoHelp('#d6');
      
         setNickNameWithoutConnecting();
      
         c.g_ON = false;
         dC.gravity.checked = false;
         
         cP.Puck.restitution_gOn =  0.0;
         cP.Puck.friction_gOn =  0.6;
         
         cP.Puck.restitution_gOff = 0.0;
         cP.Puck.friction_gOff = 0.6;
         
         cP.Client.resetScores();
         
         if ((state_capture) && (state_capture.demoIndex == 6)) {
            restoreFromState( state_capture);
         
         } else if ( demo_6_fromFile) {
            restoreFromState( demo_6_fromFile);
            
         } else {
            cP.Wall.makeFence({}, canvas);
            makeJello({});
         }
         
         // For 6.a or 6.d or any capture based on them, run them like the Jello game.
         messages['help'].loc_px = {'x':15,'y': 75};
         messages['win'].loc_px =  {'x':15,'y':100};
         c.jello.reported = true;
         c.jello.tangleTimer_s = 0.0;
         if ((c.demoVersion.slice(0,3) == "6.a") || (c.demoVersion.slice(0,3) == "6.d")) {
            messages['help'].newMessage("Detangle the jello:\\    Try the f key. Try right-click mouse drags.", 3.0);
            
            messages['gameTitle'].newMessage("Jello Madness", 1.0);
            messages['gameTitle'].loc_px = {'x':15,'y':200};
            messages['gameTitle'].popAtEnd = false;
            
            c.jello.reported = false;
            c.jello.verifyingDeTangle = false;
         }
         
         setGravityRelatedParameters({});
      
         // An extra puck to play with.
         //puckParms.restitution = 0.0;
         //new cP.Puck( 3.8, 5.5, v_init_2d_mps, puck_radius_m * 2.8, puckParms);
         
         dC.extraDemos.innerHTML = 
            "<a title='Jello Madness'                            " + hL('6.a') + " onclick=\"gW.clearState(); gW.demoStart(6)\">6a,</a>" +
            "<a title='the editor turned the jello into this...' " + hL('6.b') + " onclick=\"gW.demoStart_fromCapture(6, {'fileName':'demo6b.js'})\">&nbsp;b,</a>" +
            "<a title='the editor turned the jello into this...' " + hL('6.c') + " onclick=\"gW.demoStart_fromCapture(6, {'fileName':'demo6c.js'})\">&nbsp;c,</a>" +
            "<a title='a tough tangle...' " + hL('6.d') + " onclick=\"gW.demoStart_fromCapture(6, {'fileName':'demo6d.js'})\">&nbsp;d&nbsp;</a>";
         
      } else if (index == 7) {
         if (scrollHelp) scrollDemoHelp('#d7');
         
         messages['help'].loc_px    = {'x':15,'y':75};
         
         messages['gameTitle'].loc_px =      {'x':15,'y':200};
         messages['gameTitle'].popAtEnd = true;
         
         messages['score'].loc_px =   {'x':15,'y': 25};
         messages['ppTimer'].loc_px = {'x':15,'y': 45};
         messages['win'].loc_px =     {'x':15,'y':125};
         messages['lowHelp'].loc_px = {'x':15,'y':325};
         
         //hC.clearInputDefault( document.getElementById('inputField'));
         setNickNameWithoutConnecting();
         
         cP.Puck.restitution_gOn =  0.6; 
         cP.Puck.friction_gOn =  0.0;
         
         cP.Puck.restitution_gOff = 0.6; 
         cP.Puck.friction_gOff = 0.0;
         
         cP.Puck.bulletAgeLimit_ms = 1000;
         
         if ((state_capture) && (state_capture.demoIndex == 7)) {
            networkPuckTemplate = restoreFromState( state_capture);
            
         } else {
            cP.Wall.makeFence({}, canvas);
            
            // Normal pucks
            new cP.Puck( new cP.Vec2D(0.35, 0.35), new cP.Vec2D( 0.0, 4.0), {'radius_m':0.25}); //   , 'categoryBits':'0x0000', 'maskBits':'0x0000', 'color':'pink'
            new cP.Puck( new cP.Vec2D(5.65, 0.35), new cP.Vec2D( 0.0, 4.0), {'radius_m':0.25}); //   , 'categoryBits':'0x0000', 'maskBits':'0x0000', 'color':'pink'
            
            new cP.Puck( new cP.Vec2D(2.75, 0.35), new cP.Vec2D(+2.0, 0.0), {'radius_m':0.25});
            new cP.Puck( new cP.Vec2D(3.25, 0.35), new cP.Vec2D(-2.0, 0.0), {'radius_m':0.25});
            
            new cP.Puck( new cP.Vec2D(0.35, 5.65), new cP.Vec2D(+2.0, 0.0), {'radius_m':0.25});
            new cP.Puck( new cP.Vec2D(5.65, 5.65), new cP.Vec2D(-2.0, 0.0), {'radius_m':0.25});
            
            // Shelter
            //    Vertical part
            new cP.Wall( new cP.Vec2D( 3.0, 3.0), {'half_width_m':0.02, 'half_height_m':0.50});
            //    Horizontal part
            new cP.Wall( new cP.Vec2D( 3.0, 3.0), {'half_width_m':0.50, 'half_height_m':0.02});
            
            // Note the 'bullet_restitution':0.85 in what follows for the local and NPC client pucks. I have
            // also changed the 7b,c,d (captures) to include this parameter and value for all the driven pucks.
            
            // Puck for the local client (the host) to drive.
            var position_2d_m = new cP.Vec2D(3.0, 4.5);
            var velocity_2d_mps = new cP.Vec2D(0.0, 0.0);
            if (dC.player.checked) {
               // Make the requested puck for the host
               new cP.Puck( position_2d_m, velocity_2d_mps, cP.Puck.hostPars);
            } else {
               // Don't actually create a puck for the host. But collect parameters needed for creating the network pucks in a
               // way that reflects the birth parameters here.
               networkPuckTemplate = Object.assign({}, {'position_2d_m':position_2d_m, 'velocity_2d_mps':velocity_2d_mps}, cP.Puck.hostPars);
            }
            
            // A 4-pin track for NPC client navigation.
            var pinRadius = 3;
            var e1 = 1.5, e2 = 4.5;
            p1 = new cP.Pin( new cP.Vec2D( e1, e1), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin4', 'name':'pin1', 'nextPinName':'pin2'});
            p2 = new cP.Pin( new cP.Vec2D( e2, e1), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin1', 'name':'pin2', 'nextPinName':'pin3'});
            p3 = new cP.Pin( new cP.Vec2D( e2, e2), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin2', 'name':'pin3', 'nextPinName':'pin4'});
            p4 = new cP.Pin( new cP.Vec2D( e1, e2), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin3', 'name':'pin4', 'nextPinName':'pin1'});
            
            // Add local non-player clients (NPC, aka drones) and associated pucks to drive. Assign
            // a starting pin.
            new cP.Client({'name':'NPC1', 'color':'purple'});
            new cP.Puck( p1.position_2d_m, new cP.Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC1', 'hitLimit':20, 'pinName':'pin1', 'rayCast_init_deg':100,
                'bullet_restitution':0.85, 'linDamp':1.0} );
            //new cP.Client({'name':'NPC2', 'color':'purple'});
            //new cP.Puck( p3.position_2d_m, new cP.Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC2', 'linDamp':1.0, 
            //                                                        'hitLimit':20, 'pinName':'pin3', 'rayCast_init_deg':-90} );
            
            // A 2-pin navigation track for a single client.
            //var p5 = new cP.Pin( new cP.Vec2D( 5.0, 2.5), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin6', 'name':'pin5', 'nextPinName':'pin6'});
            //var p6 = new cP.Pin( new cP.Vec2D( 5.0, 3.5), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin5', 'name':'pin6', 'nextPinName':'pin5'});
            //new cP.Client({'name':'NPC3', 'color':'purple'});
            //new cP.Puck( new cP.Vec2D( 5.0, 2.5), new cP.Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC3', 'linDamp':1.0, 
            //                                                               'hitLimit':20, 'pinName':'pin5', 'rayCast_init_deg':0} );
            
            // Make a one single-pin track and corresponding NPC client.
            //cP.Client.makeNPCtracks(1, cP.Pin.nameIndex + 1, cP.Client.npcIndex + 1, new cP.Vec2D( 1.0, 1.0));
         }
            
         cP.Client.resetScores();
         
         if (state_capture && (state_capture.demoIndex == 7) && state_capture.startingPosAndVels) {
            c.startingPosAndVels = state_capture.startingPosAndVels;
         } else {
            c.startingPosAndVels = [ {'position_2d_m':new cP.Vec2D(2.6, 3.4), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                     {'position_2d_m':new cP.Vec2D(3.4, 3.4), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                     {'position_2d_m':new cP.Vec2D(3.4, 2.6), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                     {'position_2d_m':new cP.Vec2D(2.6, 2.6), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)} ];
         }
         createPucksForNetworkClients( networkPuckTemplate, c.startingPosAndVels);
         cP.Client.setPuckCountAtGameStart(); 
         
         if (cP.Client.getCountHumanPucks() > 0) {
            // Looks like someone is ready to play. Label this as a game.
            messages['gameTitle'].newMessage("Puck \\Popper", 1.0);
         }
         if ( ( ! cP.Client.thereIsAnActiveTTclient()) && clients['local'].puck) {
            messages['help'].newMessageSeries({
               1:{'tL_s':2.0, 'message':"Pop the other pucks..."},
               2:{'tL_s':2.0, 'message':"Use your keyboard to move and shoot..."},
               3:{'tL_s':5.0, 'message':"move (w, a,d, s)\\  shoot (i, j,l, k)\\    shield (spacebar)\\      find you (?)..."},
               4:{'tL_s':3.0, 'message':"Place your middle fingers \\  on the \"w\" and \"i\" keys."}
            });
         }
         
         dC.extraDemos.innerHTML = 
            "<a title='Puck Popper (1 drone on 4 pins)'  " + hL('7.a') + "  onclick=\"gW.clearState(); gW.demoStart(7)\">7a,</a>" +
            "<a title='2 drones on 4 pins'               " + hL('7.b') + "  onclick=\"gW.demoStart_fromCapture(7, {'fileName':'demo7b.js'})\">&nbsp;b,</a>" +
            "<a title='4 drones on 5 pins'               " + hL('7.c') + "  onclick=\"gW.demoStart_fromCapture(7, {'fileName':'demo7c.js'})\">&nbsp;c,</a>" +
            "<a title='1 drone on 2 pins'                " + hL('7.d') + "  onclick=\"gW.demoStart_fromCapture(7, {'fileName':'demo7d.js'})\">&nbsp;d&nbsp;</a>";
         
      } else if (index == 8) {
         
         canvas.width = 1250, canvas.height = 950;
         adjustSizeOfChatDiv('small');         
         hC.resizeClients('small');
         // Set this module-level value to help new connecting clients adjust their layout.
         c.chatLayoutState = 'small';
         
         // Must do this AFTER the chat-div adjustment.
         if (scrollHelp) scrollDemoHelp('#d8');
         
         messages['help'].loc_px    = {'x':55,'y': 84};
         
         messages['gameTitle'].loc_px =      {'x':55,'y':200};
         messages['gameTitle'].popAtEnd = true;
         
         messages['score'].loc_px   = {'x':55,'y': 35};
         messages['ppTimer'].loc_px = {'x':55,'y': 55};
         messages['win'].loc_px =     {'x':55,'y':120};
         messages['lowHelp'].loc_px = {'x':55,'y':325};
         
         hC.clearInputDefault( document.getElementById('inputField'));
         setNickNameWithoutConnecting();
         
         c.g_ON = false;
         dC.gravity.checked = false;
         
         cP.Puck.restitution_gOn =  0.7;  //0.7
         cP.Puck.friction_gOn =  0.6;
         
         // Keep the restitution 0.0 for gOff operation in all the 8 version demos. That way the drones fly
         // smoothly through the navigation channels in the terrain. setGravityRelatedParameters runs after
         // the drones are restored.
         cP.Puck.restitution_gOff = 0.0;  //1.0
         cP.Puck.friction_gOff = 0.6;
         
         //setGravityRelatedParameters({});
         
         cP.Puck.bulletAgeLimit_ms = 1500;
         
         if ((state_capture) && (state_capture.demoIndex == 8)) {
            networkPuckTemplate = restoreFromState( state_capture);
         
         } else if (demo_8_fromFile) {
            // Don't need to parse here because read in from a file.
            networkPuckTemplate = restoreFromState( demo_8_fromFile);
            
            // Some little walls in the middle.
            /*
            new cP.Wall( new cP.Vec2D( 2.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14});
            new cP.Wall( new cP.Vec2D( 3.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 4.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2});
            new cP.Wall( new cP.Vec2D( 5.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 6.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14});
            new cP.Wall( new cP.Vec2D( 7.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 8.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2});            
            */
            
            /*
            // Puck for the local client (the host) to drive.
            if (dC.player.checked) {
               new cP.Puck( new cP.Vec2D(3.0, 4.5), new cP.Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'black', 'colorSource':true, 'clientName':'local', 'linDamp':1.0, 'hitLimit':20} );
            }
            
            var pinRadius = 3;
            p1 = new cP.Pin( new cP.Vec2D( 1.0, 2.0), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin103', 'name':'pin101', 'nextPinName':'pin102'});
            p2 = new cP.Pin( new cP.Vec2D( 1.0, 4.0), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin101', 'name':'pin102', 'nextPinName':'pin103'});
            p3 = new cP.Pin( new cP.Vec2D( 1.0, 5.0), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin102', 'name':'pin103', 'nextPinName':'pin101'});
            */
            
            /*
            // Add some local non-player clients (NPCs)
            new cP.Client({'name':'NPC3', 'color':'purple'});
            new cP.Client({'name':'NPC4', 'color':'purple'});
            
            // Controllable pucks for these NPC clients; assign a starting pin.
            new cP.Puck( new cP.Vec2D( 1.0, 2.0), new cP.Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC3', 'linDamp':1.0, 'hitLimit':20, 'pinName':'pin102'} );
            new cP.Puck( new cP.Vec2D( 1.0, 2.0), new cP.Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC4', 'linDamp':1.0, 'hitLimit':20, 'pinName':'pin103'} );
            */
            
            // Make a set of drones and single-pin navigation tracks (use editor to add more pins if wanted). 
            //cP.Client.makeNPCtracks(3, cP.Pin.nameIndex + 1, cP.Client.npcIndex + 1, new cP.Vec2D( 1.0, 1.0));
            
         } else {
            
            makeJello({'pinned':true, 'gridsize':4});
          
            cP.Wall.makeFence({}, canvas);
            
            // Some little walls in the middle.
            new cP.Wall( new cP.Vec2D( 2.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2});
            new cP.Wall( new cP.Vec2D( 3.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 4.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 5.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 6.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 7.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            new cP.Wall( new cP.Vec2D( 8.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02});
            
         }
         
         cP.Client.resetScores();
         
         if (state_capture && (state_capture.demoIndex == 8) && state_capture.startingPosAndVels) {
            c.startingPosAndVels = state_capture.startingPosAndVels;
         } else {
            if        (c.demoVersion == '8.a') {
               c.startingPosAndVels = [ {'position_2d_m':new cP.Vec2D( 9.34, 5.23), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(10.21, 7.61), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(10.21, 4.46), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D( 9.34, 6.84), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)} ];
            } else if (c.demoVersion == '8.b') {
               c.startingPosAndVels = [ {'position_2d_m':new cP.Vec2D(1.3, 2.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(2.0, 2.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(3.0, 2.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(4.0, 2.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)} ];
            } else if (c.demoVersion == '8.c') {
               c.startingPosAndVels = [ {'position_2d_m':new cP.Vec2D(2.77, 4.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(2.77, 3.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(2.77, 2.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(2.77, 1.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)} ];
            } else if (c.demoVersion == '8.d') {
               c.startingPosAndVels = [ {'position_2d_m':new cP.Vec2D(4.95, 4.91), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(5.95, 4.91), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(6.95, 4.91), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(7.95, 4.91), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)} ];
            } else if (c.demoVersion == '8.e') {
               c.startingPosAndVels = [ {'position_2d_m':new cP.Vec2D(2.0, 5.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(3.0, 5.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(4.0, 5.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(5.0, 5.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)} ];
            } else {
               c.startingPosAndVels = [ {'position_2d_m':new cP.Vec2D(2.0, 6.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(3.0, 6.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(4.0, 6.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)},
                                        {'position_2d_m':new cP.Vec2D(5.0, 6.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)} ];
            }
         }
         createPucksForNetworkClients( networkPuckTemplate, c.startingPosAndVels);
         cP.Client.setPuckCountAtGameStart();
         
         if (cP.Client.getCountHumanPucks() > 0) {
            // Looks like someone is ready to play. Label this as a game.
            messages['gameTitle'].newMessage("Puck \\Popper", 1.0);
         }
         if ( ( ! cP.Client.thereIsAnActiveTTclient()) && clients['local'].puck) {
            messages['help'].newMessage("move (w, a,d, s)   shoot (i, j,l, k)   shield (spacebar)   find you (?)", 3.0);
         }
         
         // Removing the old version of 8c (similar to 8b). 
         // File is still out there for running from a URL query string. Old one runs as 8f now.
         dC.extraDemos.innerHTML = 
           "<a title='Puck Popper (with jello)' " + hL('8.a') + " onclick=\"gW.clearState(); gW.demoStart(8)\" style='cursor: pointer'>8a,</a>" +
           "<a title='high-noon maze' " + hL('8.b') + " onclick=\"gW.demoStart_fromCapture(8, {'fileName':'demo8b.js'})\">&nbsp;b,</a>" +
           "<a title='wide open spaces (no drag)' " + hL('8.c') + " onclick=\"gW.demoStart_fromCapture(8, {'fileName':'demo8c.js'})\">&nbsp;c,</a>" +
           "<a title='bullet energy (no drag, and elastic collisions)' " + hL('8.d') +
                                                   " onclick=\"gW.demoStart_fromCapture(8, {'fileName':'demo8d.js'})\">&nbsp;d,</a>" +
           "<a title='target-leading demo (no recoil, no drag, and elastic collisions)' " + hL('8.e') +
                                                   " onclick=\"gW.demoStart_fromCapture(8, {'fileName':'demo8e.js'})\">&nbsp;e&nbsp;</a>";
                  
      } else if (index == 9) {
         if (scrollHelp) scrollDemoHelp('#d9');
         
         canvas.style.borderColor = 'black';
         
         cP.Puck.restitution_gOn =  0.7;
         cP.Puck.friction_gOn =  0.6;
         
         cP.Puck.restitution_gOff = 1.0;
         cP.Puck.friction_gOff = 0.6;
         
         if ((state_capture) && (state_capture.demoIndex == 9)) {
            restoreFromState( state_capture);
            
         } else {            
            cP.Wall.makeFence({}, canvas);
            
            // To simulate additive color mixing.
            ctx.globalCompositeOperation = 'screen'; // 'source-over' 'screen'
            
            // pucks
            var puckStart_2d_m = new cP.Vec2D( 3.0, 3.0);
            var puckBasePars = {'radius_m':1.1, 'borderWidth_px':0, 'angleLine':false, 'colorSource':true, 'linDamp':1.0, 'angDamp':0.2, 'friction':1.0};
            // Green, Red, and Blue
            // Use Object.assign to make an independent pars object (a copy) that builds off the puckBasePars object. Note: it is important to
            // have the {} target in order to make a copy. If you use puckBasePars as the target, you'll just keep updating the reference to 
            // puckBasePars (not good).
            new cP.Puck( puckStart_2d_m, new cP.Vec2D(+0.08, -0.04),   Object.assign({}, puckBasePars, {'name':'puck1', 'color':'#00ff00'}));
            new cP.Puck( puckStart_2d_m, new cP.Vec2D(-0.08, -0.04),   Object.assign({}, puckBasePars, {'name':'puck2', 'color':'#ff0000'}));
            new cP.Puck( puckStart_2d_m, new cP.Vec2D( 0.00,  0.0894), Object.assign({}, puckBasePars, {'name':'puck3', 'color':'#0000ff'}));
            
            // Springs between the three pucks
            var springPars = {'length_m':1.0, 'strength_Npm':25.0, 'unstretched_width_m':0.125, 'visible':false, 'damper_Ns2pm2':0.5, 
                              'softConstraints':true, 'collideConnected':false, 'color':'white'};
            new cP.Spring( aT.puckMap['puck1'], aT.puckMap['puck2'], springPars);
            new cP.Spring( aT.puckMap['puck2'], aT.puckMap['puck3'], springPars);
            new cP.Spring( aT.puckMap['puck3'], aT.puckMap['puck1'], springPars);
            
            // Wait 5 seconds and then print out all the puck positions.
            //window.setTimeout(()=> cP.Puck.applyToAll( (p) => console.log(p.name + ":" + p.position_2d_m.x + "," + p.position_2d_m.y) ), 5000);
            
            // Three weaker springs (on final-position pins) that bring the triangle back to a nice center position.
            var centeringSpringPars = {'length_m':0.0, 'strength_Npm':10.0, 'unstretched_width_m':0.05, 'visible':false, 'damper_Ns2pm2':0.5, 
                                       'softConstraints':true, 'collideConnected':false, 'color':'white'};
            p1 = new cP.Pin( new cP.Vec2D( 3.5, 2.711), {'visible':false, 'borderColor':'white', 'fillColor':'black'});
            p2 = new cP.Pin( new cP.Vec2D( 2.5, 2.711), {'visible':false, 'borderColor':'white', 'fillColor':'black'});
            p3 = new cP.Pin( new cP.Vec2D( 3.0, 3.577), {'visible':false, 'borderColor':'white', 'fillColor':'black'});
            new cP.Spring( aT.puckMap['puck1'], p1, centeringSpringPars);
            new cP.Spring( aT.puckMap['puck2'], p2, centeringSpringPars);
            new cP.Spring( aT.puckMap['puck3'], p3, centeringSpringPars);
         }
         
         dC.extraDemos.innerHTML = 
            "<a title='color mixer' " + hL('9.a') + " onclick=\"gW.clearState(); gW.demoStart(9)\">9a,</a>" +
            "<a title='colorful' " + hL('9.b') + " onclick=\"gW.demoStart_fromCapture(9, {'fileName':'demo9b.js'})\">&nbsp;b&nbsp;</a>";
         
      }
      console.log('c.demoVersion=' + c.demoVersion);
      // If any demo uses special canvas dimensions, now is a good time to let the clients know.
      setClientCanvasToMatchHost();
      logEntry( c.demoVersion);
      
      // If no nickname, put the tip back into the chat input field.
      if ( ! (clients['local'].nickName)) hC.restoreInputDefault( document.getElementById('inputField'));
   }
   
   
   ///////////////////////////////////////////////////////
   // Initialize almost everything ///////////////////////
   ///////////////////////////////////////////////////////
   
   // init() is called from the index.html page. This delays the
   // execution of this code until after all the page elements have loaded in.
   // Because of this delay, no objects can be initialized here that need to be revealed
   // in the public pointers at the end of this file.
   
   function init() {
      
      // Demo specified in URL query string.
      // Take the first part of the string (ignore, for now, anything after the & character).
      var queryStringInURL = window.location.search.split("&")[0];
      var demoFromURL = {};
      var scrollTargetAtStart = null;
      // e.g. www.timetocode.org/?7b  or  www.timetocode.org/index.html?7b
      if (queryStringInURL.length == 3) {
         // Take everything after the ?
         demoFromURL.file = 'demo' + queryStringInURL.slice(1) + '.js';
         // Take only the first character after the ?
         demoFromURL.index = queryStringInURL.slice(1,2);
      
      // e.g. www.timetocode.org/?7
      } else if (queryStringInURL.length == 2) {
         demoFromURL.index = queryStringInURL.slice(1,2);
      
      // Open to a particular help topic, e.g. www.timetocode.org/?codeLinks
      } else if (queryStringInURL.length > 3) {
         scrollTargetAtStart = "#" + queryStringInURL.slice(1);
      }
            
      // Initialize the canvas display window.
      
      myRequest = null;
      resumingAfterPause = false;
      time_previous = performance.now(); // Initialize the previous time variable to now.
      canvas = document.getElementById('hostCanvas');
      canvasDiv = document.getElementById('hostCanvasDiv');
      
      ctx = canvas.getContext('2d');
      
      // Miscellaneous pointers to DOM elements
      
      dC.json = document.getElementById('jsonCapture');
      
      
      /////////////////////////////////////////////////////
      // Event handlers for local client (user input)
      /////////////////////////////////////////////////////
      
      // Inhibit the context menu that pops up when right clicking (third button).
      // Do this on mainDiv to prevent the menu from appearing when you drag the
      // mouse off the canvas.
      var mainDiv = document.getElementById('mainDiv');
      mainDiv.addEventListener("contextmenu", function(e) {
         //console.log('contextmenu event');
         e.preventDefault();
         return false;
      }, {capture: false});
      
      /*
      // Added this (11:17 AM Fri May 29, 2020) as workaround to a Chromium bug.
      // https://bugs.chromium.org/p/chromium/issues/detail?id=1087488
      // Should not need this!
      // Bug is fixed in Chrome Version 83.0.4103.97
      const resizeHandler = new ResizeObserver( entries => {
         for (let entry of entries) {
            const cr = entry.contentRect;
            console.log('Element:', entry.target.id);
            console.log(`Element size: ${cr.width}px x ${cr.height}px`);
            if (entry.target.id == 'hostCanvas') {
               canvasDiv.style.width = cr.width + "px";
               canvasDiv.style.height = cr.height + "px";
            }
         }
      });
      resizeHandler.observe( canvas);
      */
      
      wheelEvent_handler = function(clientName, e) {
         var client = clients[ clientName];
         
         // Adjust pool-shot speed value.         
         if (client.poolShotLocked) {
            client.poolShotLockedSpeed_mps = Math.round( client.poolShotLockedSpeed_mps);
            if (e.deltaY < 0) {
               client.poolShotLockedSpeed_mps += 1.0;
            } else {
               client.poolShotLockedSpeed_mps -= 1.0;
            }
            messages['help'].newMessage(client.nameString() + ", shot speed locked: " + client.poolShotLockedSpeed_mps.toFixed( 1) + " mps", 1.0);
         }
      }

      canvasDiv.addEventListener('wheel', function(e) {
         // note: see comments in the wheel event listener in hostAndClient.js 
         if (clients['local'].poolShotLocked) e.preventDefault();
         wheelEvent_handler('local', e);
      }, {passive: false, capture: false});
      
      // Start a listener for the mousemove event.
      //
      // Note: This call to addEventListener could be put (and was for a while) inside the mousedown handler. 
      // Then, if there is a corresponding removeEventListen for this in the mouseup handler, effectively the
      // the mousemove listener would only run while a mouse button is down. That works out 
      // nicely if you are using the native Windows cursor. But if you are drawing a cursor into
      // the canvas, you need to keep track of it even if the mouse isn't clicked down. I've commented
      // out the corresponding removeEventListen (in the mouseup handler) that is no longer in use.
      document.addEventListener("mousemove", handleMouseOrTouchMove, {capture: false});
      
      clickToClearMulti = function(clientName) {
         var client = clients[ clientName];
         
         // Check for body at the mouse position. If nothing there, and shift (and alt) keys are UP, reset the
         // multi-select map. So, user needs to release the shift (and alt) key and click on open area to 
         // flush out the multi-select.
         var selected_b2d_Body = b2d_getBodyAt( client.mouse_2d_m);
         var selectedBody = tableMap.get( selected_b2d_Body);
            
         if ((client.key_shift == "U") && (client.key_alt == "U") && (client.key_ctrl == "U")) {
            // Un-dash all the springs.
            cP.Spring.findAll_InMultiSelect( spring => spring.dashedLine = false);
            
            // Clicked on blank space on air table (un-selecting everything)
            if (!selected_b2d_Body) {
               // Un-select everything in the multi-select map.
               hostMSelect.resetAll();
            }
         } 
      }
      
      canvas.addEventListener("mousedown", function(e) {
         clients['local'].isMouseDown = true;
         
         // If there's been a click inside the canvas area, flag it as mouse usage for the local user (host).
         // Indirectly, this also prevents cell-phone users from getting flagged here unless they
         // touch the canvas before getting into virtual game pad.
         if ( pointInCanvas( clients['local'].mouse_2d_px)) clients['local'].mouseUsage = true;
         
         clients['local'].button = e.button;
         
         // Pass this first mouse position to the move handler. This will establish
         // the world position of the mouse.
         handleMouseOrTouchMove( e);
      
         // (Note: also see the checkForMouseSelection method in the cP.Client prototype.)
         // Clear out the multi-select map, if user clicks in open area.
         clickToClearMulti('local');
         
         // start a cursor-based selection box.
         if ((clients['local'].key_alt == 'D') && (clients['local'].key_ctrl == 'U') && ([0,1,2].includes(clients['local'].button)) && (!hostSelectBox.enabled)) {
            hostSelectBox.start();
            hostSelectBox.update();
         }
         
         // This prevents the middle mouse button from doing scrolling operations.
         e.preventDefault();
                  
      }, {capture: false});
      
      canvas.addEventListener("touchstart", function(e) {
         // Note: e.preventDefault() not needed here if the following canvas style is set
         // touch-action: none;
         
         clients['local'].isMouseDown = true;
         clients['local'].button = 0;
         
         // Start a listener for the touchmove event.
         document.addEventListener("touchmove", handleMouseOrTouchMove, {passive: true, capture: false});
         //Pass this first mouse position to the move handler.
         handleMouseOrTouchMove(e);
         
      }, {passive: true, capture: false});
         
      function handleMouseOrTouchMove( e) {
         // Determine if mouse or touch.
         // Mouse
         if (e.clientX) {
            var raw_x_px = e.clientX;
            var raw_y_px = e.clientY;
         // Touch
         } else if (e.touches) {
            var raw_x_px = e.touches[0].clientX;
            var raw_y_px = e.touches[0].clientY;
         }
         // Prevent NaN (non numeric) values from getting processed. This can happen when the mouse is moved over
         // the left edge of the screen. A rapid mouse fling can lead to this.
         if (!isNaN( raw_x_px) && !isNaN( raw_y_px)) {
            
            clients['local'].mouse_2d_px = screenFromRaw_2d_px( canvas, new cP.Vec2D( raw_x_px, raw_y_px));
            //messages['help'].newMessage('x,y='+clients['local'].mouse_2d_px.x+","+clients['local'].mouse_2d_px.y,0.1);
            clients['local'].mouseX_px = clients['local'].mouse_2d_px.x;
            clients['local'].mouseY_px = clients['local'].mouse_2d_px.y;
            
            clients['local'].mouse_2d_m = worldFromScreen( clients['local'].mouse_2d_px);
         }
      };
      
      mouseUp_handler = function( clientName) {
         resetMouseOrFingerState( clientName);
         
         var client = clients[ clientName];
         if (client.cursorSpring) {
            
            // Shoot the (single-selected) puck with the cursor spring energy.
            var tryingToShoot = ((client.key_ctrl == 'D') && (client.key_shift == 'D')) || (client.ctrlShiftLock);
            if ((tryingToShoot) && (client.selectedBody)) {
               if (client.selectedBody.constructor.name == 'Puck') {
                  // This restriction on shooting is a way for the user to NOT shoot (cancel a shot): 
                  // just move the cursor inside the "cue" ball before launching it.
                  var selected_b2d_Body = b2d_getBodyAt( client.mouse_2d_m);
                  var selectedBody = tableMap.get( selected_b2d_Body);
                  if ((!selectedBody) || ((selectedBody) && (selectedBody.name != client.selectedBody.name))) {
                     client.poolShot();
                     messages['help'].resetMessage(); // stop the help for experienced pool players
                  }
               }
            }
            client.modifyCursorSpring('dettach');
         }
         
         // Close the selection box.
         hostSelectBox.stop();
         
         // Done with rotation action.
         hostMSelect.resetCenter();
         
         // just to sure, clear out the cursor sensor
         client.sensorTargetName = null;
         client.sensorContact = null;
      }
         
      document.addEventListener("mouseup", function(e) {
         
         // Remove focus from checkboxes after use (release mouse button). This is needed for 
         // the canvas to get immediate attention when using the control, shift, and alt keys.
         // (for example: multi-select using the alt key after enabling the editor)
         $(":checkbox").blur();
         
         // This is necessary for MS Edge. Some buttons were staying depressed.
         $(":button").blur();
         
         // To get past here, isMouseDown state must be down (true).
         if ( ! clients['local'].isMouseDown) return;
         
         // Stop (using cpu) watching the mouse position.
         // Note the following code line is necessarily commented now that the mousemove listener is created 
         // outside of the mousedown event.
         //document.removeEventListener("mousemove", handleMouseOrTouchMove, {capture: false});
         
         mouseUp_handler('local');
         
      }, {capture: false});
      
      canvas.addEventListener("touchend", function(e) {
         // Note: e.preventDefault() not needed here if the following canvas style is set
         // touch-action: none;
         
         if (!clients['local'].isMouseDown) return;
         
         // Stop (using cpu) watching the position.
         document.removeEventListener("touchmove", handleMouseOrTouchMove, {passive: true, capture: false});
         
         //resetMouseOrFingerState('local');
         mouseUp_handler('local');
         
      }, {passive: true, capture: false});
      
      function resetMouseOrFingerState( clientName) {
         var client = clients[ clientName];
         
         client.isMouseDown = false;
         client.button = null;         
         client.mouseX_m = null;
         client.mouseY_m = null;  
      }
          
      var arrowKeysMap = {'key_leftArrow':'thinner', 'key_rightArrow':'wider', 'key_upArrow':'taller', 'key_downArrow':'shorter',
                          'key_[':'lessDamping', 'key_]':'moreDamping',
                          'key_-':'lessFriction',  'key_+':'moreFriction',
                          'key_-_':'lessFriction', 'key_=+':'moreFriction',
                          'key_lt':'lessDrag',     'key_gt':'moreDrag'};
      var allowDefaultKeysMap = {'key_-':null, 'key_+':null, 'key_-_':null, 'key_=+':null};
      
      document.addEventListener("keydown", function(e) {
         // Uncomment the following line for an easy test to see if the default key behavior can be inhibited.
         //e.preventDefault();
         
         //console.log(e.keyCode + " down/repeated, " + keyMap[e.keyCode]);
         
         // The following is necessary in Firefox to avoid the spacebar from re-clicking 
         // page controls (like the demo buttons) if they have focus.
         // This also prevents some unwanted spacebar-related button behavior in Chrome.
         if ((document.activeElement.tagName != 'BODY') && (document.activeElement.tagName != 'INPUT')) {
            document.activeElement.blur();
         }
         //console.log("activeElement tagName = " + document.activeElement.tagName);
         
         /*
         Anything in this first group of blocks will repeat if the key is held down for a 
         while. Holding it down will fire the keydown event repeatedly. Of course 
         this area only affects the local client. Note there is another area in 
         this code where repetition is avoided though use of the key_?_enabled 
         attributes; search on key_s_enabled for example. That repetition is of 
         a different nature in that it comes from action triggered by observing 
         the key state (up/down) each frame. 
         */
         
         // Note: the activeElement clause avoids acting on keystrokes while typing in the input cells in MS Edge.
         if ((e.keyCode in keyMap) && (document.activeElement.tagName != 'INPUT')) {
            // If you want down keys to repeat, put them here.
            
            // Inhibit default behaviors.
            if (['key_space', 'key_s', 'key_q', 'key_alt', 'key_questionMark'].includes( keyMap[e.keyCode])) {
               // Inhibit page scrolling that results from using the spacebar (when using puck shields)
               // Also inhibit repeat presses of the demo keys when using the spacebar.
               // Inhibit ctrl-s behavior in Firefox (save page).
               // Inhibit ctrl-q behavior in Edge (history panel).
               // Inhibit questionMark key behavior in Firefox (brings up text-search box)
               // Inhibit alt key behavior. Prevents a problem where if the alt key is depressed during the middle of a mouse drag, it
               // prevents the box select from working on the next try.
               e.preventDefault();
                
            } else if ((keyMap[e.keyCode] in arrowKeysMap) && !(keyMap[e.keyCode] in allowDefaultKeysMap)) {
               // Prevent page scrolling when using the arrow keys in the editor.
               e.preventDefault();
            
            } else if (keyMap[e.keyCode] == 'key_o') {
               if (! dC.pause.checked) {
                  setElementDisplay("fps_wrapper", "none");
                  setElementDisplay("stepper_wrapper", "inline");
               }
               stepAnimation();
            
            // Change body rotation when editing.
            } else if ((keyMap[e.keyCode] == 'key_t')) {
               
               hostMSelect.applyToAll( function( tableObj) {
                  if (clients['local'].key_shift == 'D') {
                     // Increase rate counterclockwise
                     var rotRate_change_dps = +5; // degrees per second
                  } else {
                     // Increase rate clockwise
                     var rotRate_change_dps = -5;
                  }
                  var current_rotRate_rps = tableObj.b2d.GetAngularVelocity();
                  var new_rotRate_rps = current_rotRate_rps + rotRate_change_dps*(Math.PI/180);
                  /*
                  If not currently rotating, will need to delete and recreate the body. 
                  This is an oddity of b2d in that you can't change the rotation rate on 
                  an existing kinematic body that currently is NOT rotating. 
                  */
                  if ((tableObj.constructor.name == "Wall") && (current_rotRate_rps == 0.0)) {
                     // Make a temporary reference to the selected body.
                     var oldWall = tableObj;
                     // Delete the selected wall (this will delete it from the multi-select map also)
                     tableObj.deleteThisOne({});
                     // Point the client reference to a new wall. Rebuild the wall at the new rotational rate (all other parameters are equal to those of the old wall).
                     tableObj = new cP.Wall( oldWall.position_2d_m, 
                        {'half_width_m':oldWall.half_width_m, 'half_height_m':oldWall.half_height_m, 'angle_r':oldWall.angle_r, 'angularSpeed_rps':new_rotRate_rps});
                     // Add this new wall to the multi-select map.   
                     hostMSelect.map[ tableObj.name] = tableObj;
                        
                  } else {
                     tableObj.angularSpeed_rps = new_rotRate_rps;
                     tableObj.b2d.SetAngularVelocity( new_rotRate_rps);
                  }
               });
            }
            
            // Use the keys in the arrow-keys map to change the characteristics of the selected body.
            if (keyMap[e.keyCode] in arrowKeysMap) {
               
               // Multi-select
               if (hostMSelect.count() > 0) {
                  // Direct the edit actions at the springs (s key down)
                  if (clients['local'].key_s == 'D') {
                     // Arrow keys and page-up/page-down.
                     var mode = arrowKeysMap[ keyMap[e.keyCode]];
                     cP.Spring.findAll_InMultiSelect( spring => spring.modify_fixture( mode));
                  // All other object types
                  } else {
                     hostMSelect.applyToAll( msObject => {
                        if (msObject.constructor.name != "Pin") {
                           msObject.modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]);
                        }
                     });
                  }
               }
               
               // Single-body selection (client spring)
               if (clients['local'].selectedBody) {
                  if (clients['local'].selectedBody.constructor.name != "Pin") {
                     clients['local'].selectedBody.modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]);
                  }
               }
            }
            
            /*
            Keys that are held down will NOT repeat in this next block. Current key 
            state must be UP before it will change the state to DOWN and perform the 
            action. This is for cases where you are toggling the state of the 
            client's key parameter. Also see comment paragraph on repetition above.
            */
            
            // If the current key state is UP...
            if (clients['local'][keyMap[e.keyCode]] == 'U') {
               
               // Set the key state to be DOWN.
               clients['local'][keyMap[e.keyCode]] = 'D';
               //console.log(e.keyCode + "(down)=" + keyMap[e.keyCode]);
               
               // Immediate execution on keydown (that's the event that got you in here):
               
               if (keyMap[e.keyCode] == 'key_ctrl') {
                  key_ctrl_handler('keydown', 'local');
                  
               } else if (keyMap[e.keyCode] == 'key_l') {
                  key_l_handler('keydown', 'local');
               
               // For showing all the count-to-pi demos without exiting full-screen view (for making videos).
               } else if ((clients['local'].key_alt == 'D') && (keyMap[e.keyCode] == 'key_pageDown')) {
                  if (c.demoLoopIndex == 0) {
                     demoStart_fromCapture(1, {'fileName':'demoSeries1b.js'});
                  } else if (c.demoLoopIndex == 1)  {
                     demoStart_fromCapture(1, {'fileName':'demoSeries1c.js'});
                  } else if (c.demoLoopIndex == 2)  {
                     demoStart_fromCapture(1, {'fileName':'demoSeries1d.js'});
                  } else if (c.demoLoopIndex == 3)  {
                     demoStart_fromCapture(1, {'fileName':'demoSeries1e.js'});
                  }
                  if (c.demoLoopIndex == 3) {
                     c.demoLoopIndex = 0;
                  } else {
                     c.demoLoopIndex += 1;
                  }
                                   
               } else if ((keyMap[e.keyCode] == 'key_c') && (clients['local'].key_ctrl != 'D')) {
                  // Only local host can change the COM selection checkbox.
                  dC.comSelection.checked = !dC.comSelection.checked;
                  comSelection_Toggle();
                  
               } else if ((keyMap[e.keyCode] == 'key_backspace') && (clients['local'].key_ctrl == 'D')) {
                  reverseDirection();
                  
               } else if (keyMap[e.keyCode] == 'key_f') { 
                  freeze();
               
               } else if (keyMap[e.keyCode] == 'key_r') { 
                  stopRotation();
               
               } else if (keyMap[e.keyCode] == 'key_g') { 
                  c.g_ON = !c.g_ON;
                  if (c.g_ON) {
                     dC.gravity.checked = true;
                  } else {
                     dC.gravity.checked = false;
                  }
                  setGravityRelatedParameters({'showMessage':true});
                  
                  /*
                  // If there is only one fixture, m_fixtureList (a linked list) is a reference to that single fixture.
                  console.log(' ');
                  console.log("fixture count=" + aT.wallMap['wall1'].b2d.m_fixtureCount);
                  // also might want to look here: m_fixtureList, m_fixtureList.m_shape, m_fixtureList.m_shape.m_vertices
                  for (var x in aT.wallMap['wall1'].b2d.m_fixtureList) {
                     console.log("name=" + x);
                  }
                  */               
               
               } else if (keyMap[e.keyCode] == 'key_m') { 
                  dC.multiplayer.checked = !dC.multiplayer.checked;
                  toggleMultiplayerStuff();
                  
               } else if (keyMap[e.keyCode] == 'key_n') { 
                  dC.fullCanvas.click();
                  
               } else if ((keyMap[e.keyCode] == 'key_v') && (clients['local'].key_ctrl != 'D')) { 
                  hC.changeFullScreenMode( canvas, 'on');
                  
               } else if (keyMap[e.keyCode] == 'key_e') { 
                  dC.editor.checked = !dC.editor.checked;
                  toggleEditorStuff();
               
               } else if ((keyMap[e.keyCode] == 'key_p') && (clients['local'].key_shift != 'D') && (clients['local'].key_alt != 'D')) { 
                  dC.pause.checked = !dC.pause.checked;
                  setPauseState();
               
               } else if ((keyMap[e.keyCode] == 'key_p') && (clients['local'].key_alt == 'D')) { 
                  clearCanvas();
                  c.pauseErase = ! c.pauseErase;
               
               // Toggle the default spring type
               } else if ((keyMap[e.keyCode] == 'key_s') && (clients['local'].key_shift == 'D')) {
                  c.softConstraints_default = !c.softConstraints_default;
                  if (c.softConstraints_default) {
                     messages['help'].newMessage("springs: DISTANCE JOINT (soft constraints)", 1.0);
                  } else {
                     messages['help'].newMessage("springs: TRADITIONAL (Hooke's law)", 1.0);
                  }
               
               // Toggle the lock on the poolshot and set the speed value (z key pressed, while control and shift are down).
               } else if ( (keyMap[e.keyCode] == 'key_z') && (((clients['local'].key_shift == 'D') && (clients['local'].key_ctrl == 'D')) || (clients['local'].ctrlShiftLock)) ) {
                     clients['local'].togglePoolShotLock();
               
               // Pause NPC navigation.
               } else if ((keyMap[e.keyCode] == 'key_q') && (clients['local'].key_ctrl == 'D')) {
                  c.npcSleep = !c.npcSleep;
                  if (c.npcSleep) {
                     // Keep track of this during game play.
                     c.npcSleepUsage = true;
                     messages['help'].newMessage("drones are sleeping", 1.0);
                     // Make sure their i keys are up, i.e. stop trying to shoot (and calling the bullet stream updater).
                     cP.Client.applyToAll( client => {if (client.name.slice(0,3) == 'NPC') client.key_i = "U" });
                  } else {
                     messages['help'].newMessage("drones are awake", 1.0);
                  }
               
               // Delete stuff
               } else if ((keyMap[e.keyCode] == 'key_x') && (clients['local'].key_ctrl == 'D')) {
                  
                  // First process multi-select
                  var foundSpring = false;
                  if (hostMSelect.count() > 0) {
                     
                     // Delete each spring that has both it's pucks (or pins) in the multi-select.
                     cP.Spring.findAll_InMultiSelect( spring => {
                        spring.deleteThisOne({});
                        // This function includes the scope of the function in which is being defined.
                        // So foundSpring, defined in the surrounding function, is accessible (and changeable) here.
                        foundSpring = true; 
                     });
                     
                     // If springs have been cleared during first delete, now remove pucks, pins and walls that are still selected.
                     if (!foundSpring) {
                        hostMSelect.applyToAll( msObject => msObject.deleteThisOne({}) );
                     }
                     
                  } else if (clients['local'].selectedBody) {
                     // A single-object selection.
                     if (clients['local'].selectedBody.constructor.name == 'Puck') clients['local'].deleteBox2dSensor();
                     clients['local'].selectedBody.deleteThisOne({'deleteMode':'fromEditor'}); // Pucks, pins, and walls all have there own version of this method.
                     clients['local'].selectedBody = null;
                     clients['local'].cursorSpring.deleteThisOne({});
                     clients['local'].cursorSpring = null;
                  }
                  
               // Copy stuff
               } else if ((keyMap[e.keyCode] == 'key_c') && (clients['local'].key_ctrl == 'D')) {
                  if ((hostMSelect.count() > 0) && (hostMSelect.count() != 2)) {
                     messages['help'].newMessage( hostMSelect.count() + " selected; need 2 to select a spring", 1.0);
                  }
                  // Clear this out each time ctrl-c is used.
                  c.springNameForPasting = null;
                  
                  // Copy a Spring for pasting.
                  // First deal with multi-select case (a length of 2 indicates trying to copy a spring)
                  if (hostMSelect.count() == 2) {
                     // Make a copy of the spring (if there is one connected to these two objects).
                     // Note: this message will be overwritten (immediately) if a spring is found in the block below.
                     messages['help'].newMessage("2 selected, but no spring", 1.0); 
                     cP.Spring.findAll_InMultiSelect( spring => {
                        // Make a reference to this existing spring.
                        c.springNameForPasting = spring.name;
                        messages['help'].newMessage("2 selected, spring = " + spring.name, 3.0);
                        // De-select the source spring and its pucks (so the user doesn't have to click on empty space).
                        aT.springMap[ c.springNameForPasting].dashedLine = false;
                        hostMSelect.resetAll();
                     });
                     
                  // Normal copying of an object that is identified by single-object selection
                  } else if (clients['local'].selectedBody) {
                     // Put this message lower on the screen than the normal help because use of the control key
                     // will display a help message listing puck attributes.
                     messages['lowHelp'].newMessage("If you would like to replicate a single object, try ctrl-v.", 1.0);
                  }
               
               // Paste a spring onto a pair of pucks.
               } else if ((keyMap[e.keyCode] == 'key_s') && (clients['local'].key_ctrl == 'D')) {
                  
                  var p = [];
                  hostMSelect.applyToAll( msObject => {
                     // Unselect the walls (don't allow the user to attach springs to the walls).
                     if (msObject.constructor.name == 'Wall') {
                        delete hostMSelect.map[ msObject.name];
                     } else {
                        // Populate the p array so you can pass the pucks and pins as parameters (see call to copyThisOne).
                        p.push( msObject);
                     }
                  });
                  
                  // Only consider the case where there are two pucks selected.
                  if (hostMSelect.count() == 2) {
                     var deleteWarning = "";
                     var springToDelete = null;
                     if (c.springNameForPasting in aT.springMap) {
                        // Check each spring, between these two pucks in the multi-select, to see if trying to paste 
                        // onto the same attachment points of an existing spring (don't allow multiple springs on the same points).
                        // Note: had to put the areEqual function at the module level because these point objects were sometimes
                        // losing their methods (it's a mystery).
                        cP.Spring.findAll_InMultiSelect( spring => {
                           if (( Vec2D.areEqual( spring.spo1_ap_l_2d_m, p[0].selectionPoint_l_2d_m) &&  Vec2D.areEqual( spring.spo2_ap_l_2d_m, p[1].selectionPoint_l_2d_m) ) ||
                               ( Vec2D.areEqual( spring.spo2_ap_l_2d_m, p[0].selectionPoint_l_2d_m) &&  Vec2D.areEqual( spring.spo1_ap_l_2d_m, p[1].selectionPoint_l_2d_m) ) ) {
                              deleteWarning = spring.name + " deleted, ";
                              springToDelete = spring.name;
                           }
                        });
                        // Delete any spring attached in the same spots.
                        if (springToDelete) aT.springMap[ springToDelete].deleteThisOne({});
                        // Paste a copy of the source spring onto these two selected pucks (or pins).
                        var newSpringName = aT.springMap[ c.springNameForPasting].copyThisOne( p[0], p[1], "pasteSingle");
                        
                        // If one of these is a NPC puck and the other a NPC navigation pin, supply the puck attributes needed for navigation.
                        if ((p[0].clientName) && (p[0].constructor.name == 'Puck') && (p[0].clientName.slice(0,3) == 'NPC') && (p[1].NPC)) {
                           p[0].navSpringName = newSpringName;
                           p[0].pinName = p[1].name;
                        } else if ((p[1].clientName) && (p[1].constructor.name == 'Puck') && (p[1].clientName.slice(0,3) == 'NPC') && (p[0].NPC)) {
                           p[1].navSpringName = newSpringName;
                           p[1].pinName = p[0].name;
                        }
                        messages['help'].newMessage(deleteWarning + newSpringName+' copied from '+c.springNameForPasting, 2.0);
                        // De-select the pasted spring (and other selected springs) and its pucks (so the user doesn't have to click on empty space).
                        cP.Spring.findAll_InMultiSelect( spring => spring.dashedLine = false);
                        hostMSelect.resetAll();
                        
                     } else {
                        messages['help'].newMessage('No spring was selected (maybe deleted)', 1.0);
                        c.springNameForPasting = null;
                     }
                  } else if ((hostMSelect.count() != 2) && (c.springNameForPasting in aT.springMap)) {
                     messages['help'].newMessage("Need 2 pucks to paste a spring; "+hostMSelect.count()+" selected", 1.0);
                  }
               
               // A general copy and paste of the selected bodies.
               } else if ((keyMap[e.keyCode] == 'key_v') && (clients['local'].key_ctrl == 'D')) {
                  // Single object or a group as selected using the techniques of multiselect.
                  if (hostMSelect.count() > 0) {
                     hostMSelect.pasteCopyAtCursor();
                  
                  // A single object as selected using single select (direct host-cursor selection)
                  } else if (clients['local'].selectedBody) {
                     var cn = clients['local'].selectedBody.constructor.name;
                     if ((cn == "Wall") || (cn == "Pin") || (cn == "Puck")) {
                        // Put the copy a little to the right of the original. The engine will separate them
                        // if they overlap (colliding).
                        var pos_forCopy_2d_m = clients['local'].selectedBody.position_2d_m.addTo( new cP.Vec2D(0.1, 0.0));
                        clients['local'].selectedBody.copyThisOne({'position_2d_m':pos_forCopy_2d_m});
                     }
                  }
                     
               } else if ((clients['local'].key_shift == 'D') && (clients['local'].key_p == 'D') && (clients['local'].key_d == 'D')) {
                     // Make a single-pin drone track at the cursor location (for Puck Popper demos only).
                     if (c.demoIndex == 7 || c.demoIndex == 8) {
                        cP.Client.makeNPCtracks(1, cP.Pin.nameIndex + 1, cP.Client.npcIndex + 1, clients['local'].mouse_2d_m);
                     } else {
                        messages['help'].newMessage('This feature is only available for demos 7 and 8 (Puck Popper).', 1.0);
                     }
                  
               // numbers 0 to 9, run a demo
               } else if ((e.keyCode >= 48) && (e.keyCode <= 57)) {
                  if (document.activeElement.tagName == 'BODY') {
                     demoStart(e.keyCode - 48, false);
                  }
               }
            }            
         }
      }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase).
      
      
      // functions with global scope (defined at beginning of this file) that can be used outside of init (e.g. in updateClientState)
      
      key_ctrl_handler = function( mode, clientName) {
         var client = clients[ clientName];
         
         if (mode == 'keydown') {
            // When ctrl is depressed, set cursor spring attachment point to the original selection point (not COM).
            if (client.selectedBody) client.cursorSpring.spo2_ap_l_2d_m = client.selectionPoint_l_2d_m;
            
            if (c.demoVersion.slice(0,3) == "3.d") messages['help'].newMessage( client.nameString() + " has ball-in-hand ON", 2.0);
            
         } else if (mode == 'keyup') {
            if (c.demoVersion.slice(0,3) == "3.d") messages['help'].newMessage( client.nameString() + " turned ball-in-hand OFF", 0.5); 
            
            // Done with the rotation action. Get ready for the next one.
            hostMSelect.resetCenter();
            
            // When releasing the ctrl key, change the cursor spring attachment point according to the
            // COM selection control.
            if (dC.comSelection.checked) {
               if (client.selectedBody) {
                  client.cursorSpring.spo2_ap_l_2d_m = new cP.Vec2D(0,0);
               }
            } else {
               if (client.selectedBody) client.cursorSpring.spo2_ap_l_2d_m = client.selectionPoint_l_2d_m;
            }
            
            // Detach the cursor spring. This prevents unintended movement when releasing the control key.
            if (client.key_shift == "D") client.modifyCursorSpring('dettach');
            
         } else {
            console.log("not good to be in here...");
         }
      }
      
      key_l_handler = function( mode, clientName) {
         var client = clients[ clientName];
         
         if (mode == 'keydown') {
            if ((client.key_ctrl == "D") && (client.key_shift == "D")) {
               client.ctrlShiftLock = !client.ctrlShiftLock;
               if (client.ctrlShiftLock) {var mS = 'ON'} else {var mS = 'OFF'};
               messages['help'].newMessage( clients[ clientName].nameString() + ' set ctrl-shift LOCK ' + mS, 1.0);
            }
         } else if (mode == 'keyup') {
         } else {
            console.log("not good to be in here...");
         }
      }
      
      
      document.addEventListener("keyup", function(e) {
         if (e.keyCode in keyMap) {
            // Set the key state to be UP.
            clients['local'][keyMap[e.keyCode]] = 'U';               
            //console.log(e.keyCode + "(up)=" + keyMap[e.keyCode]);
         
         // numbers 0 to 9
         } else if ((e.keyCode >= 48) && (e.keyCode <= 57)) {
            
         }
         
         // Some specific actions.
         
         // Done with box-based selection.
         if (keyMap[e.keyCode] == 'key_alt') {
            hostSelectBox.stop();
            clients['local'].modifyCursorSpring('dettach');
            
         } else if (keyMap[e.keyCode] == 'key_ctrl') {
            /*
            */
            // Detach the cursor spring. This prevents unintended movement when releasing the control key.
            //clients['local'].modifyCursorSpring('dettach');
            
            key_ctrl_handler('keyup', 'local');
            
         } else if (keyMap[e.keyCode] == 'key_shift') {
            // Done with the rotation action. Get ready for the next one.
            hostMSelect.resetCenter();
            clients['local'].modifyCursorSpring('dettach');
         }
         
      }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase).
      
      
      // Gravity toggle
      dC.gravity = document.getElementById('chkGravity');
      function gravityToggle(e) {
         if (dC.gravity.checked) {
            c.g_ON = true;
         } else {
            c.g_ON = false;
         }
         setGravityRelatedParameters({'showMessage':true});
      }
      dC.gravity.addEventListener("click", gravityToggle, {capture: false});
      
      // COM (Center of Mass) selection toggle
      dC.comSelection = document.getElementById('chkCOM_Selection');
      function comSelection_Toggle(e) {
         if (dC.comSelection.checked) {
            // Change the attachment point of the cursor springs to be at the center of the selected body.
            cP.Client.applyToAll( client => {if (client.selectedBody) client.cursorSpring.spo2_ap_l_2d_m = new cP.Vec2D(0,0)});
         } else {
            // Change back to the actual selection points.
            cP.Client.applyToAll( client => {if (client.selectedBody) client.cursorSpring.spo2_ap_l_2d_m = client.selectionPoint_l_2d_m});
         }
      }
      dC.comSelection.addEventListener("click", comSelection_Toggle, {capture: false});
      
      // Multi-player toggle
      dC.multiplayer = document.getElementById('chkMultiplayer');
      dC.multiplayer.addEventListener("click", toggleMultiplayerStuff, {capture: false});
      
      // Stream choke
      dC.stream = document.getElementById('chkStream');
      dC.stream.addEventListener("click", toggleStream, {capture: false});
      function toggleStream() {
         // Turn the stream On/Off.
         if (dC.stream.checked) {
            hC.setCanvasStream('on');
         } else {
            hC.setCanvasStream('off');
         }
      }
      
      // Player option
      dC.player = document.getElementById('chkPlayer');
      dC.player.addEventListener("click", toggleLocalPlayer, {capture: false});
      function toggleLocalPlayer() {
         if (dC.player.checked) {
            clients['local'].player = true;
         } else {
            clients['local'].player = false;
         }
      }
      
      // Friendly-fire option
      dC.friendlyFire = document.getElementById('chkFriendlyFire');
      
      // Editor checkbox
      dC.editor = document.getElementById('chkEditor');
      function toggleEditorStuff() {
         if (dC.editor.checked) {
            messages['help'].newMessage('Wall and Pin editing is enabled', 1.0);
         } else {
            messages['help'].newMessage('Wall and Pin editing is now disabled', 1.0);
         }
      }
      dC.editor.addEventListener("click", toggleEditorStuff, {capture: false});
            
      // Pause toggle
      dC.pause = document.getElementById('chkPause');
      dC.pause.addEventListener("click", setPauseState, {capture: false});
      
      // Local cursor toggle
      dC.localCursor = document.getElementById('chkLocalCursor');
      dC.localCursor.checked = false;
      dC.localCursor.addEventListener("click", function() {
         if (dC.localCursor.checked) {
            canvas.style.cursor = 'default';
         } else {
            canvas.style.cursor = 'none';
         }
      }, {capture: false});   
      
      // Fullscreen button (on host)
      dC.fullScreen = document.getElementById('btnFullScreen');
      dC.fullScreen.addEventListener("click", function() {
         hC.changeFullScreenMode( canvas, 'on');
      }, {capture: false});
      
      // Fullcanvas button (on host)
      dC.fullCanvas = document.getElementById('btnFullCanvas');
      dC.fullCanvas.addEventListener("click", function() {
         
         hC.changeFullScreenMode( canvas, 'on');
         
         // A longer delay is needed with FireFox.
         var userAgent = window.navigator.userAgent;
         if (userAgent.includes("Firefox")) {
            var waitForFullScreen = 600;
            console.log("firefox detected");
         } else {
            var waitForFullScreen = 100;
         }
         
         window.setTimeout(function() {
            /*            
            Note that the 5-pixel edge here seems to resolve a system crashing 
            problem on my Intel NUC. The crash happens when exiting fullscreen mode 
            with the esc key when using a 0 or 1 pixel edge. The pattern is 
            full-canvas mode, then esc, then full-screen mode, then esc (crash). 
            */
            var edge_px = 5;           
            /*
            If one or both axes of the original canvas is larger than the window, 
            stretch the axis that has the lowest fractional value (relative to its 
            corresponding window axis). Stretch it in a way that the aspect ratio of 
            the stretched canvas is equal to the aspect ratio of the window. 
            That should yield a canvas that fills the window without cutting 
            off any territory or objects in the original canvas. 
            */
            var subwindow_px_w = window.innerWidth - edge_px;
            var subwindow_px_h = window.innerHeight - edge_px;
            
            var width_ratio = canvas.width / window.innerWidth;
            var height_ratio = canvas.height / window.innerHeight;
            
            if ((canvas.width > subwindow_px_w) || (canvas.height > subwindow_px_h)) {
               if (width_ratio < height_ratio) {
                  canvas.width = canvas.height * (window.innerWidth / window.innerHeight);
               } else {
                  canvas.height = canvas.width * (window.innerHeight / window.innerWidth);
               }
            } else {
               canvas.width  = subwindow_px_w;
               canvas.height = subwindow_px_h;               
            }
                    
            // This apparently needs to be reset after the canvas dimensional changes above.
            // (only needed for the color mixing demo #9)
            if (c.demoIndex == 9) ctx.globalCompositeOperation = 'screen';
            
            // Take down the old fence and put up a new one running along the edge of the canvas.
            // (leave the fence as is when playing pool) 
            if ( ! (c.demoVersion.slice(0,3) == "3.d")) {
               cP.Wall.deleteFence();
               if ( ['1.c','1.d','1.e'].includes( demoVersionBase( c.demoVersion)) ) {
                  // Have only a top wall for the piCalcEngine demos.
                  cP.Wall.makeFence({'bOn':false, 'rOn':false, 'lOn':false}, canvas); 
               } else {
                  cP.Wall.makeFence({}, canvas);
               }
            }
            
            // Let the clients know that the canvas dimensions have changed.
            setClientCanvasToMatchHost();
            // Capture the new layout so the demo can be restarted without having to run this again.
            saveState();
         }, waitForFullScreen); // delay needed for Firefox
         
      }, {capture: false});
      
      // For handling full-screen mode changes
      $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange msfullscreenchange', function(e) {
         // Check the state:
         // Starting fullscreen
         if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
            console.log('fullscreen state: TRUE');
            c.fullScreenState = true;
            canvas.style.borderWidth = '0px';
         
         // Exiting fullscreen
         } else {
            console.log('fullscreen state: FALSE');
            c.fullScreenState = false;
            canvas.style.borderWidth = '5px';
         }
      });
      
      // The running average.
      aT.dt_RA_ms = new cP.RunningAverage(60);
      dC.fps = document.getElementById("fps");
      
      dC.extraDemos = document.getElementById("extraDemos");
      
      // Add a local user to the clients dictionary.
      new cP.Client({'name':'local', 'color':'tomato'});
    
      // Start the blank demo for frame rate testing.
      demoStart( 0);
      var fpsTestDelay = 1800;
      var startupDelay =  2000;
      
      // Wait about 2 seconds for the blank demo (#0) to settle in, then set the physics time-step (frame rate) 
      // based on the observed display rate.
      messages['help'].newMessage('starting...', startupDelay/1000.0);
      window.setTimeout( function() { 
         setFrameRateBasedOnDisplayRate();
      }, fpsTestDelay);
      
      window.setTimeout( function() { 
         // Start the "ready" message about 0.5 seconds before the demo starts.
         messages['help'].newMessage('...ready.', 0.8);
      }, startupDelay - 500);
      
      // Now, about 0.2 seconds after the framerate measurement, start the demo.
      window.setTimeout( function() {
         if (demoFromURL.file) {
            demoStart_fromCapture( demoFromURL.index, {'fileName':demoFromURL.file});
         } else if (!demoFromURL.file && demoFromURL.index) {
            demoStart( demoFromURL.index);
         } else {
            // don't scroll the demo help when the page loads
            demoStart( 9, true, false);
            if (scrollTargetAtStart) scrollDemoHelp( scrollTargetAtStart); 
         }
      }, startupDelay);
      
   } // End of init()

   
   // It's alive. MuuuUUuuuAhhhh Haaaaaa Ha Ha Ha.
   function gameLoop( timeStamp_ms) {
      // Note: The time-stamp argument can have any name.
      
      dt_frame_ms = timeStamp_ms - time_previous;
      //dt_frame_ms = c.deltaT_s * 1000;
      //dt_frame_ms = 1000 * 1/60.0
      dt_frame_s = dt_frame_ms / 1000.0;
      
      if (resumingAfterPause || (dt_frame_s > 0.1)) {
         // Use the dt info saved in last frame before it was paused.
         dt_frame_ms = dt_frame_previous_ms;
         dt_frame_s = dt_frame_ms / 1000.0;
         time_previous = performance.now();
         resumingAfterPause = false;
      }
      
      if (c.dtFloating) c.deltaT_s = dt_frame_s;
      
      var dt_avg_ms = aT.dt_RA_ms.update( dt_frame_ms);
      
      // Report frame-rate every half second.
      if (aT.dt_RA_ms.totalSinceReport > 500.0) {
         dC.fps.innerHTML = (1/(dt_avg_ms/1000)).toFixed(0);
         aT.dt_RA_ms.totalSinceReport = 0.0;
      }
      
      // Draw the walls, step the engine, draw the pucks.
      updateAirTable();
 
      //console.log("timeStamp_ms = " + timeStamp_ms);
      //console.log("performance.now = " + performance.now());
      //console.log("dt_frame_ms = " + dt_frame_ms.toFixed(2) + " ms");
      
      time_previous = timeStamp_ms;
      dt_frame_previous_ms = dt_frame_ms
      
      myRequest = window.requestAnimationFrame( gameLoop);
      if (c.singleStep) stopit();
   
   }
   
   function clearCanvas() {
      // Clear the canvas (from one corner to the other)
      if (ctx.globalCompositeOperation == 'screen') {
         ctx.clearRect(0,0, canvas.width, canvas.height);
         
         ctx.fillStyle = 'black';
         ctx.fillRect(0,0, canvas.width, canvas.height);
         
      } else {
         ctx.fillStyle = c.canvasColor;
         ctx.fillRect(0,0, canvas.width, canvas.height);
      }
   }
   
   function updateAirTable() {
      
      if ( ! c.pauseErase) {
         clearCanvas();
      }
      
      // Calculate the state of the objects.
      if (c.piCalcs.usePiEngine) {
         piCalcEngine.step( c.deltaT_s);
      } else {
         aT.collisionInThisStep = false;
         world.Step( c.deltaT_s, 10, 10);  // dt, vel iterations, pos iterations: dt,10,10
         world.ClearForces();
      }
      
      // Draw the walls first (render these on the bottom).
      cP.Wall.applyToAll( wall => {
         wall.updateState();
         wall.draw( ctx);
      });
      
      cP.Puck.applyToAll( puck => {
         if ( ! c.piCalcs.usePiEngine) puck.updateState();
         puck.draw( ctx, c.deltaT_s);
      });
      
      /*
      Leaving this commented block here as an example of a technique for deleting elements
      from an array when looping over it.
      
      // Clean out old bullets and unhealthy pucks. Note this loops
      // in reverse order over the array to avoid indexing problems as the
      // array elements are deleted.
      for (var j = aT.pucks.length - 1; j >= 0; j--) {
         if (aT.pucks[j].bullet) {
            var age_ms = window.performance.now() - aT.pucks[j].createTime;
            if (age_ms > aT.pucks[j].ageLimit_ms) {  
               deletePuckAndParts( aT.pucks[j]);
               aT.pucks.splice(j, 1);
            }
         } else if (aT.pucks[j].poorHealthFraction >= 1.0) {
            deletePuckAndParts( aT.pucks[j]);
            aT.pucks.splice(j, 1);
         }
      }     
      */
      
      // Clean out old bullets and unhealthy pucks.
      if (c.demoIndex == 7 || c.demoIndex == 8) {
         cP.Puck.applyToAll( puck => {
            if (puck.bullet) {
               //var age_ms = window.performance.now() - puck.createTime;
               puck.age_ms += c.deltaT_s * 1000;
               if (puck.age_ms > puck.ageLimit_ms) { 
                  // First penalize the shooter if no hits by this bullet.
                  if ((!puck.atLeastOneHit) && (!cP.Client.winnerBonusGiven)) {
                     // Make sure the client is still there...
                     if (clients[ puck.clientNameOfShooter]) {
                        // Now the penalty.
                        clients[ puck.clientNameOfShooter].score -= 1;
                     }
                  }
                  
                  // Then remove it.
                  puck.deleteThisOne({});
               }
            } else if (puck.poorHealthFraction >= 1.0) {
               puck.deleteThisOne({});
            }
         });  
      }
      
      // Use timer to limit checks to pool game state...
      if (c.demoVersion.slice(0,3) == "3.d") {
         if (c.poolTimer_s < c.poolTimer_limit_s) {
            c.poolTimer_s += c.deltaT_s;
         } else {
            cP.Puck.applyToAll( puck => {
               puck.checkPoolBallState();
            });               
            c.poolTimer_s = 0;
         }
      }
      
      if (aT.collisionInThisStep) {
         // If not using the PiEngine but still doing some pi calculations (e.g. demo 1c), 
         // you'll need to do a few things like play the clack sound.
         if ( ! c.piCalcs.usePiEngine) {
            if (c.piCalcs.clacks) sounds['clack2'].play();
            if (c.piCalcs.enabled) {
               aT.puckMap['puck1'].vmax = Math.max( aT.puckMap['puck1'].vmax, aT.puckMap['puck1'].velocity_2d_mps.y);
               messages['help'].newMessage("count = " + aT.collisionCount + "\\v max = " + aT.puckMap['puck1'].vmax.toFixed(1));
            }
         } 
         // Pool game...
         //if ( demoVersionBase( c.demoVersion) == '1.f') {
         //   sounds['clack2'].play();
         //}
      }
      
      cP.Spring.applyToAll( spring => {
         // If either puck/pin has been deleted, remove the spring.
         if (spring.spo1.deleted || spring.spo2.deleted) {
            // Remove this spring from the spring map.
            spring.deleteThisOne({});
         } else {
            // Otherwise, business as usual.
            spring.force_on_pucks();
            spring.draw( ctx);
         }
      });
      
      cP.Pin.applyToAll( pin => pin.draw( ctx, pin.radius_px) );
      
      if ((c.demoIndex == 6) && (aT.jelloPucks.length > 0)) {
         checkForJelloTangle();
      }

      // Jets and Guns
      cP.Client.applyToAll( client => {
         if (client.puck) {
            // Tell the NPCs what to do.
            if (client.name.slice(0,3) == 'NPC') {
               if (!c.npcSleep) client.thinkForNPC( c.deltaT_s);
            }
            // Respond to client controls, calculate corresponding jet and gun recoil forces, and draw.
            client.puck.jet.updateAndDraw( ctx, c.deltaT_s);
            client.puck.gun.updateAndDraw( ctx, c.deltaT_s);
            
            // If sweeping the gun with the TwoThumbs scope control, send out the resulting gunAngle to the client.
            client.gunAngleFromHost( c.deltaT_s);
         }
      });
      
      // Draw a marking circle on each object in the multi-select map.
      if (hostMSelect.count() > 0) {
         hostMSelect.applyToAll( msObject => msObject.draw_MultiSelectPoint( ctx) );
      }
      
      // Dash the lines of the selected springs.
      cP.Spring.findAll_InMultiSelect( spring => spring.dashedLine = true);
      
      // Consider all client-mouse influences on a selected object.
      cP.Client.applyToAll( client => {
         // Check to see if the mouse button is down and if there's a body under the cursor.
         // Select it and/or add it to the multi-select group.
         client.checkForMouseSelection();
         
         // Note that network clients are NOT allowed to select walls and pins (see checkForMouseSelection).
         // So only the local client will get into the following block in those (wall and pin) cases.
         if (client.selectedBody) {
            // World position of selection points are needed for direct movements and for spring calculations.
            client.updateSelectionPoint();
               
            // direct movement
            // translation
            if ((client.key_ctrl == 'D') && (client.key_shift == 'U') && (client.key_alt == 'U')) {
               client.moveToCursorPosition();
            
            // rotation
            } else if ( ((client.key_ctrl == 'D') && (client.key_shift == 'D')) || ((client.ctrlShiftLock) && (client.selectedBody.constructor.name == 'Puck')) ) {
               client.rotateToCursorPosition();
               
            } else if ((client.key_ctrl == 'D') && (client.key_alt == 'D')) {
               client.rotateEachAboutItself();
            }
            client.drawSelectionPoint( ctx);
         }
         // Draw a cursor for the local and network clients.
         if ((client.name.slice(0,1) == 'u') || (client.name == 'local')) {
            if (client.deviceType != 'mobile') { 
               client.drawCursor( ctx);
            }
         } 
      });

      // Sum up all the forces and apply them to the pucks.
      cP.Puck.applyToAll( puck => puck.applyForces( c.deltaT_s) );
      
      if (c.demoIndex == 7 || c.demoIndex == 8) {
         checkForPuckPopperWinnerAndReport();
      }
      
      messages['help'].displayIt( c.deltaT_s, ctx);
      messages['gameTitle'].displayIt( c.deltaT_s, ctx);
      messages['win'].displayIt( c.deltaT_s, ctx);
      messages['lowHelp'].loc_px.y = canvas.height - 50; // adjust this bottom feeder as needed
      messages['lowHelp'].displayIt( c.deltaT_s, ctx);
      // See demo #0
      if (messages['videoTitle']) messages['videoTitle'].displayIt( c.deltaT_s, ctx);
      
      // Display the selection box.
      if (hostSelectBox.enabled) {
         hostSelectBox.update();
         hostSelectBox.draw( ctx);
      }
   } // End of updateAirTable
      
             
   // Reveal public pointers to private functions and properties.
   // Note: the clients object is revealed as expected, but had to make a getChatLayoutState
   //       function to reveal the c.chatLayoutState value. You can directly reveal the c object but not
   //       c.something. Must reveal either an object or function, not a simple variable.
   return {
      setDefault: setDefault,
            
      b2d_getPolygonVertices: b2d_getPolygonVertices,
      b2d_getBodyAt: b2d_getBodyAt,
      
      world: world,
      
      getCanvasDimensions: getCanvasDimensions,
      
      tableMap: tableMap,
      hostMSelect: hostMSelect,
      clients: clients,
      sounds: sounds,
      dC: dC,
      keyMap: keyMap,
      messages: messages,
      aT: aT,
      c: c,
      
      createNetworkClient: createNetworkClient,
      deleteNetworkClient: deleteNetworkClient,
      updateClientState: updateClientState,
      deleteRTC_onHost: deleteRTC_onHost,
      deleteRTC_onClientAndHost: deleteRTC_onClientAndHost,
      setClientCanvasToMatchHost: setClientCanvasToMatchHost,      
      getChatLayoutState: getChatLayoutState,
      adjustSizeOfChatDiv: adjustSizeOfChatDiv,
      
      init: init,
      startit: startit,
      stopit: stopit,
      setFrameRate: setFrameRate,
      freeze: freeze,
      stepAnimation: stepAnimation,
      stopRotation: stopRotation,
      reverseDirection: reverseDirection,
      toggleElementDisplay: toggleElementDisplay,
      toggleSpanValue: toggleSpanValue,
      scrollDemoHelp: scrollDemoHelp,
      openDemoHelp: openDemoHelp,
      fullScreenState: fullScreenState,
      demoStart: demoStart,
      
      saveState: saveState,
      clearState: clearState,
      cleanCapture: cleanCapture,
      demoStart_fromCapture: demoStart_fromCapture
      
   };
   
})();