// Constructors and Prototypes (cP) module
// constructorsAndPrototypes.js 
   console.log('CP version 2.3.0');
// 9:00 PM Thu February 6, 2020 
// Written by: James D. Miller 

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

var cP = (function() {
   
   // For tighter code: e.g. globals, etc...
   "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,   
      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;
      
   /////////////////////////////////////////////////////////////////////////////
   ////
   ////  Object Constructors and Prototypes
   ////
   /////////////////////////////////////////////////////////////////////////////

   function Vec2D(x, y) {
      this.x = x;
      this.y = y;
   }
   Vec2D.areEqual = function( p1_2d, p2_2d) {
      if ((p1_2d.x == p2_2d.x) && (p1_2d.y == p2_2d.y)) {
         return true;
      } else {
         return false;
      }
   }
   Vec2D.prototype.addTo = function( vectorToAdd) {
      // Modify the base vector.
      this.x += vectorToAdd.x;
      this.y += vectorToAdd.y;
   }
   Vec2D.prototype.add = function( vectorToAdd) {
      // Return a new vector.
      var x_sum = this.x + vectorToAdd.x;      
      var y_sum = this.y + vectorToAdd.y;
      return new Vec2D( x_sum, y_sum);
   }
   Vec2D.prototype.subtract = function( vectorToSubtract) {
      // Return a new vector.
      var x_diff = this.x - vectorToSubtract.x;      
      var y_diff = this.y - vectorToSubtract.y;
      return new Vec2D( x_diff, y_diff);
   }   
   Vec2D.prototype.scaleBy = function( scalingFactor) {
      var x_prod = this.x * scalingFactor;
      var y_prod = this.y * scalingFactor;
      return new Vec2D( x_prod, y_prod);
   }
   Vec2D.prototype.length = function() {
      return Math.sqrt(this.x*this.x + this.y*this.y);
   }
   Vec2D.prototype.normal = function() {
      var length = this.length();
      var x = this.x / length;
      var y = this.y / length;
      return new Vec2D(x, y);
   }
   Vec2D.prototype.dot = function( vector) {
      return (this.x * vector.x) + (this.y * vector.y);
   }
   Vec2D.prototype.projection_onto = function( vec_B) {
      var vB_dot_vB = vec_B.dot( vec_B);
      if (vB_dot_vB > 0) {
         return vec_B.scaleBy( this.dot( vec_B) / vB_dot_vB );
      } else {
         // Must catch this null when dealing with pinned springs (can have 
         // zero separation)
         return null;
      }
   }
   Vec2D.prototype.rotate90 = function() {
      return new Vec2D(-this.y, this.x);
   }
   Vec2D.prototype.rotated_by = function( angle_degrees) {
      // Rotate relative to the current orientation
      // angle_degrees is the change in the angle, from current to new.
      var angle_radians = (Math.PI/180) * angle_degrees;
      var cos = Math.cos( angle_radians);
      var sin = Math.sin( angle_radians);
      // The rotation transformation.
      var x = this.x * cos - this.y * sin;
      var y = this.x * sin + this.y * cos;
      // Modify the original vector.
      this.x = x;
      this.y = y;
   }
   Vec2D.prototype.length_squared = function() {
      return (this.x*this.x + this.y*this.y);
   }
   Vec2D.prototype.get_angle = function() {
      // Determine the angle (in degrees) that this vector makes with the x axis. Measure
      // counterclockwise from the x axis.
      if (this.length_squared() == 0) {
         return 0;
      } else {
         // Yes, this is correct, y is the first parameter.
         return Math.atan2(this.y, this.x) * (180/Math.PI);
      }
   }
   Vec2D.prototype.set_angle = function( angle_degrees) {
      // Set the direction of the vector to a specific angle.
      this.x = this.length();
      this.y = 0;
      this.rotated_by( angle_degrees);
   }
   Vec2D.prototype.angleBetween_r = function( p1_2d, p2_2d) {
      // Find the angle formed by the two vectors that originate at this vector, with end points at
      // p1 and p2.
      
      // Angle (degrees relative to x axis) of the differential vector between this vector p1_2d.
      var angle_1_d = p1_2d.subtract(this).get_angle();
      
      // Angle (degrees relative to x axis) of the differential vector between this vector p2_2d.
      var angle_2_d = p2_2d.subtract(this).get_angle();
      
      // Change in angle (radians) from p1 to p2.
      var delta_r = (angle_2_d - angle_1_d) * (Math.PI/180.0);
      
      return delta_r;
   }
   Vec2D.prototype.matchAngle = function( p_2d) {
      var newAngle_d = p_2d.get_angle();
      this.set_angle( newAngle_d);
      return newAngle_d;
   }
   
   
   function HelpMessage( pars) {
      this.message = setDefault( pars.message, "");
      this.timeLimit_s = setDefault( pars.timeLimit_s, 2.0);
      this.font = setDefault( pars.font, "20px Arial");
      this.color = setDefault( pars.color, 'yellow');
      this.loc_px = setDefault( pars.loc_px, {x:30, y:40});
      
      this.messageSeries = null;
      this.index = 0;
      
      this.timeType = setDefault( pars.timeType, 'system'); //'game'
      
      this.birthTime = window.performance.now();
      this.time_s = 0.0;
      
      this.popAtEnd = setDefault( pars.popAtEnd, false);
   }
   HelpMessage.prototype.resetMessage = function( message, timeLimit_s) {
      this.message = "";
      this.messageSeries = null;
   }
   HelpMessage.prototype.newMessage = function( message, timeLimit_s) {
      this.time_s = 0.0;
      this.birthTime = window.performance.now();
      this.timeLimit_s = setDefault( timeLimit_s, this.timeLimit_s)
      this.message = message;
   }
   HelpMessage.prototype.newMessageSeries = function( message) {
      this.messageSeries = message;
      // Initialize the first message.
      this.time_s = 0.0;
      this.birthTime = window.performance.now();
      this.index = 1;
      this.message = this.messageSeries[this.index].message;
      this.timeLimit_s = this.messageSeries[this.index].tL_s;
   }
   HelpMessage.prototype.addToIt = function( moreText) {
      this.message = this.message + moreText;
   }
   HelpMessage.prototype.displayIt = function( deltaT_s, drawingContext) {
      if (this.timeType == 'system') {
         this.time_s = (window.performance.now() - this.birthTime)/1000.0;
      } else {
         this.time_s += deltaT_s;
      }
      
      if ((this.message != "") && (this.time_s < this.timeLimit_s)) {         
         var lines = this.message.split("\\");
         for (var line_index in lines) {
            drawingContext.font = this.font;
            drawingContext.fillStyle = this.color;
            var y_px = this.loc_px.y + (line_index * parseInt(this.font.substring(0,3)) * 1.20);
            drawingContext.fillText(lines[ line_index], this.loc_px.x, y_px);
         }
         
      } else {
         // Before ending the message, make an optional pop sound.
         if (this.popAtEnd && (this.message != "")) gW.sounds['lowPop'].play();
         
         this.message = "";
         this.time_s = 0;
         
         // If it's a series, check to see if there's another message...
         if (this.messageSeries) {
            this.index += 1;
            if (this.messageSeries[this.index]) {
               // Update the characteristics of the text if changes have been supplied in the series.
               if (this.messageSeries[this.index].loc_px) this.loc_px = this.messageSeries[this.index].loc_px;
               if (this.messageSeries[this.index].font) this.font = this.messageSeries[this.index].font;
               this.popAtEnd = setDefault( this.messageSeries[this.index].popAtEnd, false);
               
               this.message = this.messageSeries[this.index].message;
               this.timeLimit_s = this.messageSeries[this.index].tL_s;
               this.time_s = 0;
               this.birthTime = window.performance.now();
            }
         }
      }
   }
   
   
   function MultiSelect() {
      this.map = null;
      this.center_2d_m = null;
      this.findCenterEnabled = null;
      this.resetAll();
   }
   // A method that loops over the selected objects (this.map) of this instance of MultiSelect
   MultiSelect.prototype.applyToAll = function( doThis) {
      for (var objName in this.map) {
         var tableObj = this.map[ objName];
         doThis( tableObj);
      }
   }
   MultiSelect.prototype.resetAll = function() {
      this.applyToAll( msObject => msObject.selectionPoint_l_2d_m = new Vec2D(0,0) );
      this.map = {};
      this.resetCenter();
   }
   MultiSelect.prototype.resetCenter = function() {
      this.center_2d_m = new Vec2D(0,0);
      this.findCenterEnabled = true;
   }
   MultiSelect.prototype.count = function() {
      return Object.keys(this.map).length;
   }
   MultiSelect.prototype.findCenter = function() {
      this.center_2d_m = new Vec2D(0,0);
      this.applyToAll( tableObj => {
         this.center_2d_m = this.center_2d_m.add( tableObj.position_2d_m);
      });
      this.center_2d_m = this.center_2d_m.scaleBy( 1.0 / this.count());
   }
   MultiSelect.prototype.removeOne = function( theBody) {
      // un-dash the springs
      Spring.findAll_InMultiSelect( spring => spring.dashedLine = false);
      delete this.map[ theBody.name];
      // re-dash the springs
      Spring.findAll_InMultiSelect( spring => spring.dashedLine = true);
   }
   MultiSelect.prototype.pasteCopyAtCursor = function() {
      if (this.count() < 1) {
         gW.messages['help'].newMessage("Nothing in multi-select. Use shift (or alt) key to multi-select.", 1.0);
         return;
      }
      this.findCenter();
      // Offset between the center of the group and the cursor position.
      var changeInPosition_2d_m = gW.clients['local'].mouse_2d_m.subtract( this.center_2d_m);
      // A temporary map to associated the original pucks to the copies.
      var copyMap = {};
      // Copy pucks, pins, and walls to the cursor position.
      this.applyToAll( tableObj => {
         // Exclude navigation pins and client pucks.
         if ( ! (tableObj.nextPinName || tableObj.clientName) ) {
            var newPosition_2d_m = tableObj.position_2d_m.add( changeInPosition_2d_m);
            var newTableObj = tableObj.copyThisOne({'position_2d_m':newPosition_2d_m});
            copyMap[tableObj.name] = newTableObj;
         } else {
            gW.messages['help'].newMessage("Note: client pucks and navigation pins are excluded\\   from multi-select replication.", 2.0);
         }
      });
      // Copy all the springs onto the newly created pucks. Use the copyMap to determine
      // correspondence.
      Spring.findAll_InMultiSelect( spring => {
         // Exclude navigation springs
         if ( ! (spring.navigationForNPC)) {
            // Copy this spring onto these two pucks.
            var targetPuck1 = copyMap[ spring.spo1.name];
            var targetPuck2 = copyMap[ spring.spo2.name];
            spring.copyThisOne( targetPuck1, targetPuck2);
         }
      });
   }
   
   
   function SelectBox( pars) {
      DrawingFunctions.call(this); // Inherit
      this.clickPoint_2d_px   = setDefault( pars.clickPoint_2d_px, new Vec2D(0,0));
      this.currentMouse_2d_px = setDefault( pars.currentMouse_2d_px, new Vec2D(0,0));
      this.enabled = false;
      this.limits = {};
   }
   // Make this a module-level function, not part of the prototype, so it can be used in the callback of the QueryAABB.
   // Check if this point is inside the bounding limits of the box.
   SelectBox.pointInside = function( p_2d_m, limits) {
      if (( p_2d_m.x > limits.min_x ) && ( p_2d_m.x < limits.max_x ) && ( p_2d_m.y > limits.min_y ) && ( p_2d_m.y < limits.max_y )) {
         return true;
      } else {
         return false;
      }
   }
   SelectBox.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   SelectBox.prototype.constructor = SelectBox; // Rename the constructor (after inheriting)
   SelectBox.prototype.selectBodiesInBox = function() {
      var aabb = new b2AABB();
      
      // The two corners of the box, 1 and 2, in world coordinates.
      var c1_2d_m = worldFromScreen( this.clickPoint_2d_px);
      var c2_2d_m = worldFromScreen( this.currentMouse_2d_px);
      
      this.limits.min_x = Math.min(c1_2d_m.x, c2_2d_m.x);
      this.limits.max_x = Math.max(c1_2d_m.x, c2_2d_m.x);
      this.limits.min_y = Math.min(c1_2d_m.y, c2_2d_m.y);
      this.limits.max_y = Math.max(c1_2d_m.y, c2_2d_m.y);
      
      // Provide the corners with the lowest values (lower left) and the highest values (upper right)
      aabb.lowerBound.Set( this.limits.min_x, this.limits.min_y);
      aabb.upperBound.Set( this.limits.max_x, this.limits.max_y);
      
      // Query the world for overlapping shapes.
      var objectCount = 0;
      
      // The callback function can't use "this" so make a reference in the local scope.
      var limits = this.limits;
      
      // This runs the box query. The function gets called once for each fixture found
      // to be overlapping the box.
      gW.world.QueryAABB( function( fixture) {
         
         var bd2_Body = fixture.GetBody();
         var table_body = gW.tableMap.get( bd2_Body);
         
         // COM of this body.
         var p_2d_m = table_body.position_2d_m;
         
         // Check if Center-Of-Mass of this object is within the selection box. This is needed because the
         // query returns all bodies for which their bounding box is overlapping the selection box. So this 
         // give more selection control to avoid nearby objects.
         var itsInside = SelectBox.pointInside( p_2d_m, limits);
         
         // Don't select walls or pins if the editor is off.
         if (itsInside && !(!gW.dC.editor.checked && ((table_body.constructor.name == "Wall") || (table_body.constructor.name == "Pin")))) {
            objectCount += 1;
            // Add this body to the hostMSelect map.
            gW.hostMSelect.map[ table_body.name] = table_body;
         }
         
         // Keep looking at all the fixtures found in the query.
         return true;
      }, aabb);
      /*
      Check each point in hostMSelect map. Remove any that are no longer in the box.
      
      Wrote this in three different ways below: (1) with a loop over the map, (2) passing
      a function to the applyToAll method, and (3) binding the function to the hostMSelect
      object (setting "this") then passing it to applyToAll. The 3rd one is being used.
      
      for (var objName in gW.hostMSelect.map) {
         var tableObj = gW.hostMSelect.map[ objName];
         if ( ! SelectBox.pointInside(tableObj.position_2d_m, this.limits)) {
            gW.hostMSelect.removeOne( tableObj);
         }
      }
      
      or
      
      gW.hostMSelect.applyToAll( function( tableObj) {
         if ( ! SelectBox.pointInside(tableObj.position_2d_m, limits)) {
            gW.hostMSelect.removeOne( tableObj);
         };
      });
      
      or
      
      Note "limits" is defined in the surrounding scope here. The "this"
      reference points to the gW.hostMSelect object as dictated in the call
      to bind method of the function that's being passed in.
      
      gW.hostMSelect.applyToAll( function( tableObj) {
         if ( ! SelectBox.pointInside(tableObj.position_2d_m, limits)) {
            this.removeOne( tableObj);
         };
      }.bind( gW.hostMSelect));
      
      or 
      
      Using arrow-function notation. And without using bind and the "this" to get at the removeone method.
      Note you can't (and shouldn't want to) bind to an arrow function. Must use a regular function (see above).
      Generally the arrow functions are nice for passing in a function so that the "this" in the function
      refers to the surrounding context. Of course, can't use "this", and the surrounding context here, to 
      get at removeOne, since it is part of the MultiSelect class.
      */
      gW.hostMSelect.applyToAll( tableObj => {
         if (!SelectBox.pointInside( tableObj.position_2d_m, limits)) gW.hostMSelect.removeOne( tableObj);
      });
   }
   SelectBox.prototype.start = function() {
      Puck.applyToAll( puck => puck.selectionPoint_l_2d_m = new Vec2D(0,0) );
      this.enabled = true;
      this.clickPoint_2d_px = gW.clients['local'].mouse_2d_px;
   }
   SelectBox.prototype.stop = function() {
      this.enabled = false;
   }
   SelectBox.prototype.update = function() {
      this.currentMouse_2d_px = gW.clients['local'].mouse_2d_px;
      this.selectBodiesInBox();
   }
   SelectBox.prototype.draw = function( drawingContext) {
      var corners_2d_px = [this.clickPoint_2d_px,   new Vec2D(this.currentMouse_2d_px.x, this.clickPoint_2d_px.y), 
                           this.currentMouse_2d_px, new Vec2D(this.clickPoint_2d_px.x,   this.currentMouse_2d_px.y)];
      this.drawPolygon( drawingContext, corners_2d_px, {'borderColor':'red', 'fillIt':false});
   }
   
   
   function Client( pars) {
      DrawingFunctions.call(this); // inherit
      this.parsAtBirth = pars;
      //this.alsoThese = [];
      this.color = setDefault( pars.color, "red");

      // Incrementing the network client name is done in server.js.
      this.name = setDefault( pars.name, "manWithNoName");
            
      // Increment the NPC index, but use the higher value.
      if (this.name.slice(0,3) == 'NPC') {
         Client.npcIndex += 1;
         Client.npcIndex = Math.max(Client.npcIndex, Number(this.name.slice(3)));
         this.name = 'NPC' + Client.npcIndex;
      }
      // Add this client to the map.
      gW.clients[this.name] = this;
      
      this.puck = null;
      this.player = setDefault( pars.player, true);
      this.nickName = setDefault( pars.nickName, null);
      this.twoThumbs = false;
      this.deviceType = 'desktop';
      
      this.isMouseDown = false;
      this.mouseUsage = false;
      this.button = null;
      
      // Initially put the drawn cursor (for the local user) out of range of the canvas. That way the cursor doesn't
      // render there initially if the page is refreshed, looks cleaner when first coming to the page.
      if (this.name == 'local') {
         this.mouseX_px = -20;
         this.mouseY_px = -20;
      } else {
         this.mouseX_px = +10;
         this.mouseY_px = +10;
      }
      
      this.mouse_2d_px = new Vec2D(this.mouseX_px, this.mouseY_px);
      this.mouse_2d_m = worldFromScreen( this.mouse_2d_px);
      
      // Make a cursor pin for all human clients.
      if (this.name.slice(0,3) != 'NPC') {
         this.pin = new Pin( Object.assign({}, this.mouse_2d_m), {'name':this.name, 'cursorPin':true, 'borderColor':'white', 'fillColor':this.color});
      } else {
         this.pin = null;
      }
      
      this.selectedBody = null;
      
      // Selection point (in the local coordinate system of the selected object).
      this.selectionPoint_l_2d_m = null;
      // Selection point (in the world coordinates). 
      this.selectionPoint_w_2d_m = null;
      this.selectionPoint_w_2d_px = null;
      
      this.cursorSpring = null;
      
      // Initialize all the key values to be Up.
      for (var key in gW.keyMap) this[gW.keyMap[key]] = 'U';
      
      /*      
      The following enable/disable feature is needed for keys that do 
      something that should only be done once while the key is down (not each 
      frame). This technique is needed in cases where action is potentially 
      triggered each frame and it is not possible to compare the new key state 
      (coming from a client or the local keyboard) with the current key state. 
      
      Examples where this is NOT needed are the tube rotation keys. In 
      those cases, something must be done in each frame while the key is down. 
      The action repeats as the key state is inspected each frame (and seen to 
      be down). 
      
      Note there is an area in this code where pure-local-client key events 
      are handled to avoid repetition; see the keydown area in this file. 
      There, repetition is caused by holding the key down and the associated 
      repeated firing of the keydown event. There, new and current states can 
      be compared to avoid repetition.
      
      See also the updateClientState function and how it suppressed 
      unwanted repetition by comparing new and current states.
      */
      this.key_s_enabled = true;  // Flip the jet.
      this.key_k_enabled = true;  // Change the gun orientation by 1 large increment.
      this.key_i_enabled = true;  // Start a bullet stream.
      
      // This client-cursor triangle is oriented like an arrow pointing to 10 o'clock.
      //this.triangle_raw_2d_px = [new Vec2D(0,0), new Vec2D(14,8), new Vec2D(8,14)];
      this.triangle_raw_2d_px = [new Vec2D(0,0), new Vec2D(11,12), new Vec2D(3,16)];
      
      this.NPC_guncooling_timer_s = 0.0;
      this.NPC_guncooling_timer_limit_s = 2.0;
      this.NPC_shield_timer_s = 0.0;
      this.NPC_shield_timer_limit_s = 0.5;
      this.NPC_pin_timer_s = setDefault( pars.NPC_pin_timer_s, 0.0);
      this.NPC_pin_timer_limit_s = setDefault( pars.NPC_pin_timer_limit_s, 5.0);
      
      this.NPC_aimStepCount = 0;
      this.NPC_aimStepCount_limit = 20;
      this.NPC_skipFrame = false;
      
      this.gunAngle_timer_s = 0.0;
      this.gunAngle_timer_limit_s = 0.03;
      
      // rtc contains WebRTC peer connection and data channel objects.
      this.rtc = new hC.RTC({});
      
      // Score for the leaderboard
      this.score = 0;
   }
   // Variables common to all instances of Client...
   Client.npcIndex = 0;
   Client.countAtGameStart = 0;
   /* The drag_c parameter affects a drag force that depends on the 
   absolute motion of the COM of the puck (not relative to cursor). This is 
   needed for providing the user with a controlled selection (and 
   positioning) of pucks. If you set these to zero you'll see it's just too 
   bouncy. Unfortunately, this also gives a somewhat counterintuitive feel 
   when selecting long rectangular pucks, near an end edge, with gravity on 
   (the expected swing is strongly damped). Refer to 
   Spring.prototype.force_on_pucks to see where these drag forces are 
   applied. There is also a relative-motion drag parameter, damper_Ns2pm2, 
   that is set to a default value when the cursor springs are instantiated. 
   */ 
   Client.mouse_springs = {'0':{'drag_c':   2.0, 'strength_Npm':   60.0, 'unstretched_width_m':0.060},   // 'drag_c':2.0
                           '1':{'drag_c':   0.1, 'strength_Npm':    2.0, 'unstretched_width_m':0.002},
                           '2':{'drag_c':  20.0, 'strength_Npm': 1000.0, 'unstretched_width_m':0.200}};
   Client.colors = {'1':'yellow','2':'blue','3':'green','4':'pink','5':'orange',
                    '6':'brown','7':'greenyellow','8':'cyan','9':'tan','0':'purple'};

   Client.applyToAll = function ( doThis) {
      for (var clientName in gW.clients) {
         var client = gW.clients[ clientName];
         doThis( client);
      }
   }
   Client.deleteNPCs = function() {
      Client.applyToAll( client => {if (client.name.slice(0,3) == 'NPC') delete gW.clients[ client.name]});
      Client.npcIndex = 0;
   }
   Client.scoreSummary = [];
   Client.winnerBonusGiven = false;
   Client.resetScores = function() {
      Client.applyToAll( client => {
         client.mouseUsage = false;
         client.score = 0;
         client.twoThumbs = false;
      });
      // If the npc are still paused, indicate pause usage.
      // (Note that two globals are being changed here: gW.c.npcSleepUsage and gW.c.puckPopperTimer_s.)
      if (gW.c.npcSleep) {
         gW.c.npcSleepUsage = true;
      } else {
         gW.c.npcSleepUsage = false;
      }
      gW.c.puckPopperTimer_s = 0;
      Client.winnerBonusGiven = false;
      Client.countAtGameStart = Object.keys(gW.clients).length;
      Client.scoreSummary = [];
   }
   // Sometimes it's just better to see 'host' displayed instead of 'local'.
   Client.translateIfLocal = function( clientName) {
      var nameString;
      if (clientName == 'local') {
         nameString = 'host';
      } else {
         nameString = clientName;
      }
      return nameString;
   }
   Client.makeNPCtracks = function( nTracks, pinIndexStart, npcIndexStart, initialLocation_2d_m) {
      // Make multiple NPC clients, each on its own navigation track (single pin). Use editor to add
      // more pins as wanted.
      var pinIndex, pinName, npcIndex, npcName, x_m;
      for (var i = 0, len = nTracks; i < len; i++) {
         pinIndex = pinIndexStart + i;
         npcIndex = npcIndexStart + i;
         pinName = 'pin' + pinIndex;
         npcName = 'NPC' + npcIndex;
         console.log(pinName + ',' + npcName);
         // Pin is referenced by the NPC puck (so do this before instantiating the puck)
         new Pin( initialLocation_2d_m, {'radius_px':3, 'NPC':true, 'previousPinName':pinName, 'name':pinName, 'nextPinName':pinName});
         // NPC client is referenced by the NPC puck (so do this before instantiating the puck)
         new Client({'name':npcName, 'color':'purple'});
         new Puck( initialLocation_2d_m, new Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':npcName, 'linDamp':1.0, 'hitLimit':20, 'pinName':pinName} );
         // Put the next one a little more to the right
         initialLocation_2d_m.x += 1.0;
      }
   }
   Client.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   Client.prototype.constructor = Client; // Rename the constructor (after inheriting)
   Client.prototype.addScoreToSummary = function( winnerTimeString, demoIndex, npcSleepUsage) {
      var nameString, finalNameString, mouseString, npcSleepString, virtualGamePadString;
      nameString = Client.translateIfLocal(this.name);
      if (this.nickName) {
         finalNameString = this.nickName + ' (' + nameString + ')';
      } else {
         finalNameString = nameString;
      }
      // Clear the mouseString warning for Jello Madness. Mouse is always used.
      if (demoIndex == 6) {
         mouseString = '';
      } else {
         mouseString = (this.mouseUsage) ? 'x':'';
      }
      npcSleepString = (npcSleepUsage) ? 'x':'';
      virtualGamePadString = (this.twoThumbs) ? 'x':'';
      // The randomIndex provides a way to nearly uniquely associate records in the leaderboard report with the local game summary.
      Client.scoreSummary.push( {'score':this.score, 'rawName':this.name, 'name':finalNameString, 'virtualGamePad':virtualGamePadString, 
                                 'winner':winnerTimeString, 'mouse':mouseString, 'npcSleep':npcSleepString, 'randomIndex':Math.floor((Math.random() * 100000))} );
   }
   Client.prototype.checkForMouseSelection = function() {
      // Deal with selection.
      if (this.selectedBody === null) {
         if (this.isMouseDown) {

            // Check for a body at the mouse position.
            var selected_b2d_Body = gW.b2d_getBodyAt( this.mouse_2d_m);
            
            if (selected_b2d_Body) {
               var selectedBody = gW.tableMap.get( selected_b2d_Body);
               //console.log("body=" + selectedBody.name + ", pin=" + selectedBody.pinName);
                        
               // Block the selection on kinematic bodies (like walls and pins) by a network client.
               if ( ((selected_b2d_Body.GetType() == b2Body.b2_kinematicBody) && (this.name != 'local')) || 
                    // Block wall and pin selection if the wall/pin editor is off.
                    (!gW.dC.editor.checked && ((selectedBody.constructor.name == "Wall") || (selectedBody.constructor.name == "Pin"))) ) {
                  
                  selected_b2d_Body = null;
                  
               } else {
                  // Consider the case where local client is trying to edit multiple objects (only shift key is down).
                  if ((this.name == 'local') && (this.key_shift == "D") && (this.key_ctrl == "U") && (this.key_alt == "U")) {
                     
                     // Add this body to the multiple-select map (if not already there).
                     if (!(selectedBody.name in gW.hostMSelect.map) && (this.button == 0)) {
                        // Record the local selection point on the body.
                        if (gW.dC.comSelection.checked) {
                           selectedBody.selectionPoint_l_2d_m = new Vec2D(0,0);
                        } else {
                           selectedBody.selectionPoint_l_2d_m = selected_b2d_Body.GetLocalPoint( this.mouse_2d_m);
                        }

                        gW.hostMSelect.map[ selectedBody.name] = selectedBody;
                     
                     // Remove this body from the map if doing a right-button (2) mouse click.
                     } else if ((selectedBody.name in gW.hostMSelect.map) && (this.button == 2)) {
                        gW.hostMSelect.removeOne( selectedBody);
                     }
                  
                  // If using the box-selection feature...
                  } else if ((this.name == 'local') && (this.key_alt == "D") && (this.key_ctrl == "U")) {
                     if ((selectedBody.name in gW.hostMSelect.map) && (this.button == 2)) {
                        gW.hostMSelect.removeOne( selectedBody);
                     }
                  
                  // Normal single-body selection:
                  // Allow single-body pin selection only if the wall/pin editor is on.
                  } else if (!(!gW.dC.editor.checked && (selectedBody.constructor.name == "Pin"))) {
                     // Which body object has been selected?
                     this.selectedBody = gW.tableMap.get( selected_b2d_Body);
                     console.log('\nbody,gI=' + this.selectedBody.name + ',' + this.selectedBody.groupIndex);
                     if (this.selectedBody.clientName) console.log('client,gI = ' + this.selectedBody.clientName +','+ this.selectedBody.groupIndex);
                     
                     // Mark it as selected and record the local point.
                     this.selectionPoint_l_2d_m = selected_b2d_Body.GetLocalPoint( this.mouse_2d_m);
                     this.modifyCursorSpring('attach');
                     
                     // If selecting a small puck with right-button on mouse, warn user about stability:
                     if ((this.selectedBody.mass_kg < 0.15) && (this.button == 2)) {
                        gW.messages['help'].newMessage("For a small puck, use the middle or left mouse button.", 3.0);
                     }
                     
                     // If using the control key (deterministic drag or rotation) and there already are
                     // some bodies in the multi-select, add this body to the multi-select group. This
                     // insures normal group-rotation behaviors.
                     if ((this.key_ctrl == "D") && (gW.hostMSelect.count() > 0)) {
                        gW.hostMSelect.map[ selectedBody.name] = selectedBody;
                     }
                  }
               }
            }
         }
      } else {
         // Released the mouse button:
         if (!this.isMouseDown) {
            this.modifyCursorSpring('dettach');
            this.selectionPoint_l_2d_m = null;
            this.selectionPoint_w_2d_m = null;
            this.selectionPoint_w_2d_px = null;
            this.selectedBody = null;
         }
      }
   }
   Client.prototype.modifyCursorSpring = function( mode) {
      // If there isn't already a cursor spring, add one. 
      if ((mode == 'attach') && (!this.cursorSpring)) {
         
         // Local selection point on puck.
         if (gW.dC.comSelection.checked) {
            var selectionPoint_l_2d_m = new Vec2D(0.0,0.0);
         } else {
            var selectionPoint_l_2d_m = this.selectionPoint_l_2d_m;
         }
         // Always use a normal spring for the cursor ('softContraints':false). I have played around with using the distance joints but they
         // seem to have similar instability problems with small masses and strong springs.
         this.cursorSpring = new Spring(this.pin, this.selectedBody, Object.assign({}, Client.mouse_springs[this.button], 
            {'spo2_ap_l_2d_m':selectionPoint_l_2d_m, 'color':this.color, 'forCursor':true, 'softContraints':false, 'name':this.name}));
         
      } else if ((mode == 'dettach') && (this.cursorSpring)) {
         this.cursorSpring.deleteThisOne({});
         this.cursorSpring = null;
      }
   }
   Client.prototype.moveSBtoPosition = function(theBody, pos_2d_m) {
      theBody.position_2d_m = pos_2d_m;
      theBody.position_2d_px = screenFromWorld( theBody.position_2d_m);
      theBody.b2d.SetPosition( pos_2d_m);
      // If it's a puck, freeze it, for more predictable put-it-here behavior.
      if (theBody.constructor.name == "Puck") {
         theBody.velocity_2d_mps = new Vec2D(0.0,0.0);
         theBody.b2d.SetLinearVelocity( new Vec2D(0.0,0.0));
         theBody.angularSpeed_rps = 0.0;
         theBody.b2d.SetAngularVelocity( theBody.angularSpeed_rps);
      }
   }
   Client.prototype.moveToCursorPosition = function() {
      // For manipulating kinematic objects (walls and pins)
      if (gW.dC.comSelection.checked) {
         // If COM selection, simply position the object (its center) at the mouse position.
         var newPosition_2d_m = this.mouse_2d_m;
      } else {
         // If not COM selection, calculate the world (w) delta between the current mouse position and the original selection point.
         // This delta is especially useful for positioning (dragging) a kinematic body (like a wall) so that it's selection point
         // follows the moving mouse location.
         var delta_w_2d_m = this.mouse_2d_m.subtract( this.selectionPoint_w_2d_m);
         // Adding the delta to the body position, moves the body so that the original selection point is at the mouse position.
         var newPosition_2d_m = this.selectedBody.position_2d_m.add( delta_w_2d_m);
      }
      // Before actually moving it, keep track of the calculated amount of movement.
      var movement_2d_m = newPosition_2d_m.subtract( this.selectedBody.position_2d_m);
      
      // Move the single selected body (SB) to the mouse position.
      this.moveSBtoPosition( this.selectedBody, newPosition_2d_m);
      
      // If control key is down, move all the other selected bodies by a similar amount.
      if (this.key_ctrl == "D") {
         // Note: the arrow function, used here, will take "this" from the surrounding context.
         gW.hostMSelect.applyToAll( tableObj => {
            if (tableObj !== this.selectedBody) this.moveSBtoPosition( tableObj, tableObj.position_2d_m.add( movement_2d_m));
         });
         
         // If just one object selected (nothing in multi-select), output its position and elasticity characteristics (if a puck);
         if (gW.hostMSelect.count() == 0) {
            var objReport = this.selectedBody.name + " @ x:" + this.selectedBody.position_2d_m.x.toFixed(2) + ", " + "y:" + this.selectedBody.position_2d_m.y.toFixed(2);
            if (this.selectedBody.constructor.name == "Puck") {
               objReport += "\\  restitution = " + this.selectedBody.restitution.toFixed(3) +
                            "\\  surface friction = " + this.selectedBody.friction.toFixed(3) +
                            "\\  translational drag = " + this.selectedBody.linDamp.toFixed(3) +
                            "\\  rotational drag = " + this.selectedBody.angDamp.toFixed(3);
            }
            gW.messages['help'].newMessage(objReport, 0.05);
         }
      }
   }
   Client.prototype.rotateSB = function(theBody, delta_angle_r) {
      if (theBody.constructor.name == "Puck") {
         theBody.velocity_2d_mps = new Vec2D(0.0,0.0);
         theBody.b2d.SetLinearVelocity( new Vec2D(0.0,0.0));
         theBody.angularSpeed_rps = 0.0;
         theBody.b2d.SetAngularVelocity( theBody.angularSpeed_rps);
      }
      // Everything but pins... If you don't exclude pins here, they become un-selectable after
      // a rotation with the editor.
      if (theBody.constructor.name != "Pin") {
         theBody.angle_r += delta_angle_r;
         theBody.b2d.SetAngle( theBody.angle_r);
      }
   }
   Client.prototype.rotateToCursorPosition = function() {
      var delta_r;
      
      // Rotate about the center of the group.
      if (gW.hostMSelect.count() > 1) {
         // Find the center only at the beginning of the rotation action.
         if (gW.hostMSelect.findCenterEnabled) {
            gW.hostMSelect.findCenter();
            // Don't do this again until one of the keys is released.
            gW.hostMSelect.findCenterEnabled = false;
         }
         // Measure the rotation relative to the center of the group.
         delta_r = gW.hostMSelect.center_2d_m.angleBetween_r(this.selectionPoint_w_2d_m, this.mouse_2d_m);
      
         gW.hostMSelect.applyToAll( tableObj => {
            // Rotate the vector that runs from the hostMSelect center out to the object center. 
            var center_to_center_2d = tableObj.position_2d_m.subtract( gW.hostMSelect.center_2d_m);
            center_to_center_2d.rotated_by( delta_r * 180.0/ Math.PI );
            
            // Then reassemble the object vector and put the object there.
            this.moveSBtoPosition( tableObj, gW.hostMSelect.center_2d_m.add( center_to_center_2d));
            
            // Rotate the object about its center.
            this.rotateSB( tableObj, delta_r);
         });
      
      // Rotate about the center of the single object.
      } else {
         // Things get weird if you do single body rotation about the body's center. So avoid that here
         // by insisting that the COM feature is unchecked. If COM-selection is checked, just translate the body.
         if ( ! gW.dC.comSelection.checked) {
            // Find the angle formed by these three points (angle based at the center of this selected body). This is the angle formed
            // as the mouse moves from the old selection point.
            delta_r = this.selectedBody.position_2d_m.angleBetween_r(this.selectionPoint_w_2d_m, this.mouse_2d_m);
            this.rotateSB(this.selectedBody, delta_r);
         } else {
            this.moveToCursorPosition();
         }
      }
   }
   Client.prototype.rotateEachAboutItself = function() {
      var delta_r = this.selectedBody.position_2d_m.angleBetween_r(this.selectionPoint_w_2d_m, this.mouse_2d_m);
      if (gW.hostMSelect.count() > 0) {
         gW.hostMSelect.applyToAll( tableObj => {
            if ( ! gW.dC.comSelection.checked) {
               this.rotateSB(tableObj, delta_r);
            } else {
               this.moveToCursorPosition();
            }
         });
      } else {
         this.rotateSB(this.selectedBody, delta_r);
      }
   }
   Client.prototype.drawCursor = function( drawingContext) {
      // Draw a triangle for the network client's cursor.
      // Before you can draw it, you have to know where it is on the screen.
      this.triangle_2d_px = [];
      var offset_2d_px = new Vec2D(0,1); //tweak the positioning of the cursor.
      for (var i = 0, len = this.triangle_raw_2d_px.length; i < len; i++) {
         // Put it at the mouse position: mouse + triangle-vertex + offset. 
         var p_2d_px = this.mouse_2d_px.add(this.triangle_raw_2d_px[i]).add(offset_2d_px);
         // Put it in the triangle array.
         this.triangle_2d_px.push( p_2d_px);
      }
      
      // Move the client pin (part of the cursor) to that position also.
      this.mouse_2d_m = worldFromScreen( this.mouse_2d_px);
      
      if (this.pin) this.pin.b2d.SetPosition( Object.assign({}, this.mouse_2d_m));
      
      if (this.isMouseDown) {
         if ((this.key_shift == "D") || (this.key_alt == "D")) {
            this.pin.draw_MultiSelectPoint( drawingContext);
         } else {
            this.pin.draw( drawingContext, 4); //radius of 4px
         }
      }
      
      var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? 'white' : this.color;
      this.drawPolygon( drawingContext, this.triangle_2d_px, {'borderColor':'white', 'borderWidth_px':1, 'fillColor':fillColor});
   }  
   Client.prototype.updateAndDrawSelectionPoint = function( drawingContext) {
      // Calculate (update) the world location of the selection point (for use in force calculations)
      if (gW.dC.comSelection.checked) {
         this.selectionPoint_w_2d_m = this.selectedBody.position_2d_m;
      } else {
         // Convert the local selection-point vector to a world vector.
         this.selectionPoint_w_2d_m = Vec2D_from_b2Vec2( this.selectedBody.b2d.GetWorldPoint( this.selectionPoint_l_2d_m));
      }      
      this.selectionPoint_w_2d_px = screenFromWorld( this.selectionPoint_w_2d_m);
      var sP_2d_px = this.selectionPoint_w_2d_px;
      
      // Draw small selection circle.
      var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? this.selectedBody.color : this.color;
      this.drawCircle( drawingContext, sP_2d_px, {'borderColor':'white', 'borderWidth_px':2, 'fillColor':fillColor, 'radius_px':6});
   }
   Client.prototype.AimToLead = function() {
      // Point gun along the ray-cast line and lead for target movement. 
      if (this.puck.gun.rayBody) {
         // Vector from the shooting NPC to the target
         var NPC_to_TargetPuck_2d_m = this.puck.gun.rayBody.position_2d_m.subtract( this.puck.position_2d_m);
         var parallel_unit_vector = NPC_to_TargetPuck_2d_m.normal();
         
         // Target's velocity as seen by the shooting NPC (in the NPC reference frame).
         var target_inNPCrf_2d_mps = this.puck.gun.rayBody.velocity_2d_mps.subtract( this.puck.velocity_2d_mps);
         
         // Component of the target's velocity along (parallel to) the line between the shooting NPC and the target.
         var target_parallel_2d_mps = target_inNPCrf_2d_mps.projection_onto( parallel_unit_vector);
         
         if (target_parallel_2d_mps) {
            // Component of target's relative (to shooter) velocity that is perpendicular to the line between the shooting NPC and the target
            var target_perpendicular_2d_mps = target_inNPCrf_2d_mps.subtract( target_parallel_2d_mps);
            var perpendicular_unit_vector = target_perpendicular_2d_mps.normal();
            /*          
            This next line of code is the clever part. Find the angle, at which the 
            bullet needs to fire, such that its perpendicular component matches the 
            perpendicular component of the target. Then, in cases where the parallel 
            component of the bullet is larger than that of the target, the bullet 
            will overtake and hit the target. These words can be represented with a 
            right triangle where the bullet speed is the hypotenuse and the target's 
            perpendicular speed is one of the legs. Use Pythagorean equation to find 
            the parallel component of the bullet velocity vector (the third leg of 
            the triangle). This defines the orientation of the bullet vector. 
            */           
            var bullet_parallelSpeed_mps = Math.sqrt( Math.pow( this.puck.gun.bulletSpeed_mps, 2) - Math.pow( target_perpendicular_2d_mps.length(), 2));
            // The bullet vector (relative to the shooting NPC) needed to intercept the target.
            var bullet_2d_mps = target_perpendicular_2d_mps.add( parallel_unit_vector.scaleBy( bullet_parallelSpeed_mps));
            var bullet_angle_deg = bullet_2d_mps.get_angle();
            
            if (bullet_angle_deg) {
               if (this.puck.gun.rayBody.linDamp > 0.0) {
                  // If the body has some linear damping drag, simply (easiest to) take the average of the leading angle 
                  // and the line-of-sight angle. Otherwise, the leading aim will overshoot the target as it slows
                  // down under drag forces.
                  var gun_angle_deg = (bullet_angle_deg + this.puck.gun.angleToFoundPuck)/2.0
               } else {
                  var gun_angle_deg = bullet_angle_deg;
               }
               this.puck.gun.setTubeAngle(gun_angle_deg);
            }
         }
      } else {
         this.puck.gun.setTubeAngle( this.puck.gun.angleToFoundPuck);
      }
   }
   Client.prototype.thinkForNPC = function( deltaT_s) {
      
      // If a hit is detected, turn the shield on for a while.
      if ((this.puck.inComing) && (this.NPC_shield_timer_s < this.NPC_shield_timer_limit_s)) {
         this.key_space = "D";
         this.NPC_shield_timer_s += deltaT_s;
      } else {
         this.key_space = "U";
         this.NPC_shield_timer_s = 0.0;
         this.puck.inComing = false;
      }
      
      // If found a target.
      if (!this.puck.gun.scanning) {
         // Note that thinkForNPC runs every frame, so this aiming adjustment continuously updates
         // during a bullet-firing sequence, adjusting the aim for each bullet that fires. This will
         // give a curved look to the bullet group unless both shooter and target have the same velocity.
         // The gun tube updates its orientation even when not shooting.
         this.AimToLead();
         if (this.NPC_guncooling_timer_s < this.NPC_guncooling_timer_limit_s) {
            // Keep shooting
            this.key_i = "D";
            this.NPC_guncooling_timer_s += deltaT_s;
         } else {
            // Release the i key.
            this.key_i = "U";
            this.NPC_guncooling_timer_s = 0.0;
         }
      } else {
         this.key_i = "U";
      }
      
      // This flag forces a needed update to navSpringOnly_force_2d_N before drawing
      // the jet along the direction of the spring force.
      this.NPC_skipFrame = false;
      
      // Move NPC to the next pin
      if (this.NPC_pin_timer_s < this.NPC_pin_timer_limit_s) {
         this.NPC_pin_timer_s += deltaT_s;
      } else {
         // First aim the jet gradually toward the next pin.
         if (this.NPC_aimStepCount < this.NPC_aimStepCount_limit) {
            this.puck.stepTheJetAngle();
            this.NPC_aimStepCount += 1;
         } else {
            this.puck.attachNavSpringToNextPin();
            this.NPC_aimStepCount = 0;
            this.NPC_pin_timer_s = 0.0;
            this.NPC_skipFrame = true;
         }
      }
      
      // Aim the jet in the direction opposite to the spring force.
      if (this.NPC_aimStepCount == 0 && !this.NPC_skipFrame) {
         this.puck.jet.rotateJetAndScaleFlameToThis( this.puck.navSpringOnly_force_2d_N.scaleBy(-1));
      }
   }
   Client.prototype.gunAngleFromHost = function( deltaT_s, bypassLimits = false) {
      if (this.puck) {
         if ((this.puck.gun.scopeRotRateFrac != 0) || (bypassLimits)) {
            // No need to send this every frame, so use timer to limit this.
            if ((this.gunAngle_timer_s >= this.gunAngle_timer_limit_s) || (bypassLimits)) {
               var gunAngle = this.puck.gun.rel_position_2d_m.get_angle();
               // If RTC data channel available:
               if (this.rtc.dataChannel && (this.rtc.dataChannel.readyState == 'open') && ( true )) {
                  this.rtc.dataChannel.send( JSON.stringify( {'data':{'gunAngle':gunAngle}} ));
               // Otherwise, send via socket.io
               } else {
                  var control_message = {'from':'host', 'to':this.name, 'data':{'gunAngle':gunAngle} };
                  hC.sendCommandToClients( control_message);
               }
               this.gunAngle_timer_s = 0;
            } else {
               this.gunAngle_timer_s += deltaT_s;
            }
         }
      }
   }
   Client.prototype.jetAngleFromHost = function() {
      if (this.puck) {
         var jetAngle = this.puck.jet.rel_position_2d_m.get_angle();
         var control_message = {'from':'host', 'to':this.name, 'data':{'jetAngle':jetAngle} };
         hC.sendCommandToClients( control_message);
      }
   }

   // For use in sound field, demo #2.
   function PuckTail( pars) {
      DrawingFunctions.call(this); // inherit
      this.firstPoint_2d_m = setDefault( pars.firstPoint_2d_m, new Vec2D(1.0, 1.0));
      this.initial_radius_m = setDefault( pars.initial_radius_m, 1.0);
      
      /*
      this.emitFrequency_hz = setDefault( pars.emitFrequency_hz, 60); //60 c.frameRate
      this.emitInterval_s = 1.0/this.emitFrequency_hz;
      this.emitTimer_s = 0.0;
      */
      
      // ppf: pixels per frame
      this.propSpeed_ppf_px = setDefault( pars.propSpeed_ppf_px, PuckTail.defaults.propSpeed_ppf_px);
      this.length_limit = setDefault( pars.length_limit,         PuckTail.defaults.length_limit);
      
      this.color = setDefault( pars.color,                       PuckTail.defaults.color);
      this.rainbow = setDefault( pars.rainbow,                   PuckTail.defaults.rainbow);
      this.rbSaturation = setDefault( pars.rbSaturation,         PuckTail.defaults.rbSaturation);
      this.rbLightness = setDefault( pars.rbLightness,           PuckTail.defaults.rbLightness);
      this.machSwitch = setDefault( pars.machSwitch,             PuckTail.defaults.machSwitch);
      this.machValue = setDefault( pars.machValue,               PuckTail.defaults.machValue);
      
      this.hueIndex = 0;
            
      // The wait (time in seconds) before making a pure white color ping.
      this.markerPingTimerLimit_s = setDefault( pars.markerPingTimerLimit_s, 1.0);
      this.markerPingTimer_s = 0.0;
      
      this.values = [];
      
      //this.update( this.firstPoint_2d_m);
   }
   PuckTail.defaults = {
      'propSpeed_ppf_px':1, 'length_limit':25, 'color':'lightgray',
      'rainbow':false, 'rbSaturation': 75, 'rbLightness': 40,
      'machSwitch':false, 'machValue':0
   }
   PuckTail.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   PuckTail.prototype.constructor = PuckTail; // Rename the constructor (after inheriting)
   PuckTail.prototype.machCalc = function( puckSpeed_mps) {
      var waveSpeed_mps = meters_from_px(this.propSpeed_ppf_px) * gW.c.frameRate;
      var mach =  puckSpeed_mps / waveSpeed_mps;
      console.log('mach=' + mach.toFixed(2));
   }
   PuckTail.prototype.speedFromMach = function() {
      var waveSpeed_mps = meters_from_px(this.propSpeed_ppf_px) * gW.c.frameRate;
      var puckSpeed_mps = waveSpeed_mps * this.machValue;
      console.log('speed from mach=' + puckSpeed_mps.toFixed(2));
      return puckSpeed_mps;
   }
   PuckTail.prototype.update = function( drawingContext, newPoint_2d_m, deltaT_s) {
      var lineColor;
      
      if (this.rainbow) {
         // hue,   saturation, lightness
         // 0-360, 0-100%,     0-100%
         if (this.hueIndex > 360) this.hueIndex = 0;
         // hsl( hue, saturation%, lightness%)
         this.pingColor = 'hsl(' + this.hueIndex + ', ' + this.rbSaturation + '%, ' + this.rbLightness + '%)';
         this.hueIndex += 3;
      } else {
         this.pingColor = this.color;
      }
         
      // Color one ring specially so to see it propagation better.
      this.markerPingTimer_s += deltaT_s;
      if ((this.markerPingTimer_s > this.markerPingTimerLimit_s) && !this.rainbow) {
         this.pingColor = 'white';
         this.markerPingTimer_s = 0.0;
      }
      
      /*
      This commented block is here as a reminder that an adjustable emit frequency doesn't render
      well. Can play with this to be convinced. Best to emit once per frame as is done in the single
      line that follows. Also commented out the corresponding three attribute lines (above) in the 
      constructor for this.
      
      // Ping out a new ring (at the rate of the emit frequency). Each value is a position vector and radius.
      this.emitTimer_s += deltaT_s;
      if (this.emitTimer_s >= this.emitInterval_s) {
         this.values.push({'p_2d_px':screenFromWorld( newPoint_2d_m), 'r_px':px_from_meters(this.initial_radius_m), 'color':this.pingColor});
         this.emitTimer_s = 0.0;
      }
      */
      
      // Ping out a new ring (once per frame). Each value is a position vector and radius.
      this.values.push({'p_2d_px':screenFromWorld( newPoint_2d_m), 'r_px':px_from_meters(this.initial_radius_m), 'color':this.pingColor});
      
      // Remove the oldest value if needed.
      if (this.values.length > this.length_limit) {
         this.values.shift();
      }
      
      // Loop through the tail.
      for (var t = 0, len = this.values.length; t < len; t++) {
         
         // Expand the radius of the ping (like a sound wave propagating). Note: doing this addition in pixels (not meters)
         // to yield a more consistent and pleasing rendering.
         this.values[t].r_px += this.propSpeed_ppf_px;
         
         // Draw the sound circle (make the 'white' marker ring even more visible, using red, if single stepping).
         if (gW.c.singleStep && (this.values[t].color == 'white')) {
            lineColor = 'red'; //#008080 cyan yellow magenta orange
         } else {
            lineColor = this.values[t].color;
         } 
         this.drawCircle( drawingContext, this.values[t].p_2d_px, {'radius_px':this.values[t].r_px, 'borderColor':lineColor, 'borderWidth_px':2, 'fillColor':'noFill'});
      }
   }
 

 
   function Puck( position_2d_m, velocity_2d_mps, pars) {
      DrawingFunctions.call(this); // Inherit attributes
      this.parsAtBirth = pars;
      //this.alsoThese = [];
      
      this.bullet = setDefault( pars.bullet, false);
      // If a bullet puck never hits another puck, this stays false.
      this.atLeastOneHit = false;
      
      this.jello = setDefault( pars.jello, false);
      
      this.clientName = setDefault( pars.clientName, null);
      if (this.clientName) {
         // Don't allow a client puck if there is not already a client. Client first, then puck.
         // Throwing an error forces an exit from this constructor.
         if (!(gW.clients[this.clientName])) {
            var errorObj = new Error('Constructor declines to create a puck for a non-existent client.');
            errorObj.name = 'from Puck constructor';
            throw errorObj;
         }
         Puck.playerCount += 1;
         if (this.clientName.includes('NPC')) Puck.npcCount += 1;
      }
      if (pars.name) {
         this.name = pars.name;
         Puck.nameIndex = Math.max(Puck.nameIndex, Number(this.name.slice(4)));
      } else {
         Puck.nameIndex += 1;
         this.name = 'puck' + Puck.nameIndex;
      }
      //console.log("n-puck = " + Puck.nameIndex);
      gW.aT.puckMap[this.name] = this;
      
      // Position of Center of Mass (COM)
      this.position_2d_m = Vec2D_check( position_2d_m);
      // Position (in pixels).
      this.position_2d_px = screenFromWorld( this.position_2d_m);
      
      // Velocity of COM
      this.velocity_2d_mps = Vec2D_check( velocity_2d_mps);
      
      // Parse out the parameters in the pars object. The values on the right
      // are the defaults (used if pars value is undefined).
      this.color = setDefault( pars.color, "DarkSlateGray");
      this.shape = setDefault( pars.shape, "circle");
      this.colorSource = setDefault( pars.colorSource, false);
      this.density = setDefault( pars.density, 1.5);
      // Linear damping is like a drag force from translational movement through a surrounding fluid.
      // Note that springs have the attribute drag_c, with an effect similar to linDamp.
      this.linDamp = setDefault( pars.linDamp, 0.0);
      // Rotational drag
      this.angDamp = setDefault( pars.angDamp, 0.0);
      this.hitLimit = setDefault( pars.hitLimit, 10);
      // This is used if the puck is a bullet. It identifies the owner of this bullet 
      // (so you can't shoot yourself in the foot).
      this.clientNameOfShooter = setDefault( pars.clientNameOfShooter, null);
      this.ageLimit_ms = setDefault( pars.ageLimit_ms, null);
      
      this.createTail = setDefault( pars.createTail, false);
      this.tailPars = setDefault( pars.tailPars, null);
      this.tail = null;
      
      // www.iforce2d.net/b2dtut/collision-filtering
      // For client pucks, assign a negative group index that is based on the puck's name
      // This group index can be used to prevent collisions with bullets (having the same negative group index) 
      // coming from a gun hosted by this puck.
      if (this.clientName) {
         this.groupIndex = -this.name.slice(4)-1000;
      } else {
         this.groupIndex = setDefault( pars.groupIndex, 0);
      }
      // The following are defaults for Box2D.
      this.categoryBits = setDefault( pars.categoryBits, 0x0001);
      this.maskBits = setDefault( pars.maskBits, 0xFFFF);
      
      // Rotational state
      this.angle_r = setDefault( pars.angle_r, 0);
      this.angularSpeed_rps = setDefault( pars.angularSpeed_rps, 0);
      this.angleLine = setDefault( pars.angleLine, true);
      
      this.borderWidth_px = setDefault( pars.borderWidth_px, 3);
      
      // Put a reference to this puck in the client.
      if (this.clientName) {
         gW.clients[this.clientName].puck = this;
      }
      
      this.age_ms = 0;
      //this.createTime = window.performance.now();
      
      // Note that a call to setGravityRelatedParameters() may override the restitution and friction settings
      // in what follows unless they have been "fixed" (set to be constant).
      
      // Restitution (elasticity) of the object in collisions
      if (typeof pars.restitution === 'undefined') {
         if (gW.c.g_ON) {
            this.restitution = Puck.restitution_gOn;                
         } else {
            this.restitution = Puck.restitution_gOff;
         }
      } else {
         this.restitution = pars.restitution;
      }
      // Option to fix restitution to be independent of the g toggle.
      this.restitution_fixed = setDefault( pars.restitution_fixed, false);
      
      // Friction (tangential tackiness) of the object in collisions
      if (typeof pars.friction === 'undefined') {
         if (gW.c.g_ON) {
            this.friction = Puck.friction_gOn;
         } else {
            this.friction = Puck.friction_gOff;
         }
      } else {
         this.friction = pars.friction;
      }
      // Option to fix friction to be independent of the g toggle.
      this.friction_fixed = setDefault( pars.friction_fixed, false);
      
      // Dimensions
      this.radius_m = setDefault( pars.radius_m, 1.0);
      this.aspectR = setDefault( pars.aspectR, 1.0);
      this.half_height_m = setDefault( pars.half_height_m, null);
      this.half_width_m = setDefault( pars.half_width_m, null);
      
      if (this.shape == 'circle') {
         this.radius_px = px_from_meters( this.radius_m);
      
      // Rectangular
      } else {
         // Height and width given explicitly.
         if (this.half_height_m) {
            this.half_width_px = px_from_meters( this.half_width_m);
            this.half_height_px = px_from_meters( this.half_height_m);
         
         // Aspect ratio given.
         } else {
            this.half_width_m = this.radius_m * this.aspectR;
            this.half_width_px = px_from_meters( this.half_width_m);
            
            this.half_height_m = this.radius_m;
            this.half_height_px = px_from_meters( this.half_height_m);
         }
      }
      
      // Tail
      if (this.createTail) {
         var tailPars = {'firstPoint_2d_m':this.position_2d_m, 'initial_radius_m':this.radius_m};
         // Add any specified characteristics to the tail parameters.
         if (this.tailPars) {
            tailPars = Object.assign({}, tailPars, this.tailPars);
         } else {
            /*
            If a tail is requested, but this puck has no tailPars, this next line 
            of code adds these defaults to the puck. These will be available for 
            editing after capturing. 
            
            Yes, a little confusing. Here is a little more detail on how this works: 
            (First, please note that this is being done to avoid verbose puck attributes.)
            
            For pucks that don't have tails, set the creatTail flag to true in a 
            capture. Then run the capture (click the corresponding yellow number 
            button). That will instantiate the puck with a tail having default 
            parameters and also add those default tail parameters 
            (PuckTail.defaults) to the puck. Now take a new capture and those tail 
            parameters will be visible and available for editing (and runs).
            */
            this.tailPars = PuckTail.defaults;
         }
         this.tail = new PuckTail( tailPars);
         if (this.tail.machSwitch) {
            // Calculate the puck velocity based on the specified Mach number.
            var temp_v_2d_mps = new Vec2D(0, this.tail.speedFromMach());
            temp_v_2d_mps.matchAngle( this.velocity_2d_mps);
            this.velocity_2d_mps = temp_v_2d_mps;
         }
         this.tail.machCalc( this.velocity_2d_mps.length() );
      }
            
      this.b2d = null;
      this.create_Box2d_Puck();
      // Create a reference back to this puck from the b2d puck.
      // Note that a Map allows any type of object for the key!
      gW.tableMap.set(this.b2d, this);
      
      this.cursorString_spring_force_2d_N = new Vec2D(0.0,0.0);
      this.cursorString_puckDrag_force_2d_N = new Vec2D(0.0,0.0);
      this.nonCOM_2d_N = [];
      
      this.sprDamp_force_2d_N = new Vec2D(0.0,0.0);
      // This vector is needed for aiming the NPC's navigation jets.
      this.navSpringOnly_force_2d_N = new Vec2D(0.0,0.0);
      this.jet_force_2d_N = new Vec2D(0.0,0.0);
      this.impulse_2d_Ns = new Vec2D(0.0,0.0);
      
      // Puck-popper features
      this.gun = null, this.jet = null;
      this.rayCastLineLength_m = setDefault( pars.rayCastLineLength_m, 3.5);
      this.rayCast_init_deg = setDefault( pars.rayCast_init_deg, 0);
      this.rayRotationRate_dps = setDefault( pars.rayRotationRate_dps, 80);
      // Disables and hides the jet
      this.disableJet = setDefault( pars.disableJet, false);
      this.noRecoil = setDefault( pars.noRecoil, false);
      this.bullet_restitution = setDefault( pars.bullet_restitution, 0.92);
      this.bulletAgeLimit_ms = setDefault( pars.bulletAgeLimit_ms, Puck.bulletAgeLimit_ms);
      if (this.clientName) {
         // Add client controls and give each control a reference to this puck.
         this.jet = new Jet(this, {'initial_angle':-20});
         this.gun = new Gun(this, {'initial_angle':200, 'indicator':true, 'tube_color':'gray', 
             'rayCast_init_deg':this.rayCast_init_deg, 'rayRotationRate_dps':this.rayRotationRate_dps, 'rayCastLineLength_m':this.rayCastLineLength_m});
      }
      this.shield = new Shield(this, {'color':'yellow'});
      
      this.hitCount =  0;
      this.poorHealthFraction = 0;
      // Keep track of the owner of the last bullet that hit.
      this.whoShotBullet = null;
      this.flash = false;
      this.inComing = false;
      this.flashCount = 0;
      
      // Navigation spring (not generally the name of any attached spring). There can be only
      // one navigation spring.
      if (this.clientName && this.clientName.includes('NPC')) {
         this.navSpringName = null;
         this.pinName = setDefault( pars.pinName, null);
         // If there's named pin and it still exists...
         if (this.pinName && (gW.aT.pinMap[ this.pinName])) {
            this.disableJet = false;
            this.attachNavSpring();
         } else {
            this.disableJet = true;
         }
      }
      
      // Local selection point where candidate springs are to be attached.
      this.selectionPoint_l_2d_m = new Vec2D(0,0);
      
      this.deleted = false;
   }
   Puck.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   Puck.prototype.constructor = Puck; // Rename the constructor (after inheriting)
   Puck.nameIndex = 0;
   Puck.playerCount = 0;
   Puck.npcCount = 0;
   Puck.restitution_default_gOn = null, Puck.restitution_default_gOff = null;
   Puck.restitution_gOn = null,         Puck.restitution_gOff = null;
   Puck.friction_default_gOn = null,    Puck.friction_default_gOff = null;
   Puck.friction_gOn = null,            Puck.friction_gOff = null;
   Puck.bulletAgeLimit_ms = null;
   Puck.g_2d_mps2 = null;
   Puck.applyToAll = function ( doThis) {
      for (var puckName in gW.aT.puckMap) {
         var puck = gW.aT.puckMap[ puckName];
         doThis( puck);
      }
   }
   Puck.deleteAll = function() {
      Client.applyToAll( client => client.puck = null);
      Puck.applyToAll( puck => {
         gW.tableMap.delete( puck.b2d);
         if (puck.b2d) gW.world.DestroyBody( puck.b2d);
         // Tell clients that their puck is gone.
         if ((puck.clientName) && (puck.clientName != 'local')) {
            var control_message = {'from':'host', 'to':puck.clientName, 'data':{'puckPopped':{'value':true}} };
            hC.sendCommandToClients( control_message);
         }
      });
      gW.aT.jelloPucks = [];
      gW.aT.puckMap = {};
      Puck.nameIndex = 0;
      Puck.playerCount = 0;
      Puck.npcCount = 0;
   }
   Puck.prototype.attachNavSpring = function() {
      var navSpringName = "s" + (100 + Puck.nameIndex);
      // Note that instantiation adds the new spring to the spring map.
      var temp = new Spring(this, gW.aT.pinMap[this.pinName], {strength_Npm:8.0, unstretched_width_m:0.1, color:'brown', damper_Ns2pm2:5.0, navigationForNPC:true, 'name':navSpringName});
      this.navSpringName = temp.name;
   }
   Puck.prototype.stepTheJetAngle = function() {
      var spring = gW.aT.springMap[this.navSpringName];
      // If this spring still exists.
      if (spring) {
         // Use the end of the spring that's attached to the pin.
         if (spring.spo1.nextPinName) {
            var nextPinName = spring.spo1.nextPinName;
         } else {
            var nextPinName = spring.spo2.nextPinName;
         }
         // Gradually rotate jet to be in the direction of the next pin.
         // Vector between this puck and the next pin.
         var toNextPin_2d_m = this.position_2d_m.subtract( gW.aT.pinMap[ nextPinName].position_2d_m);
         var angleOfNextPin_deg = toNextPin_2d_m.get_angle();
         var angleOfJet_deg = this.jet.rel_position_2d_m.get_angle();
         var changeNeeded_deg = angleOfNextPin_deg - angleOfJet_deg;
         
         // Take the short way around.
         if (changeNeeded_deg >  180.0) changeNeeded_deg = changeNeeded_deg - 360;
         if (changeNeeded_deg < -180.0) changeNeeded_deg = changeNeeded_deg + 360;
         
         // Rotate by a percentage in this single step. This will yield a gradual sweep-to-target effect.
         this.jet.rotateTubeAndFlame( 0.15 * changeNeeded_deg);
      }
   }
   Puck.prototype.attachNavSpringToNextPin = function() {
      var spring = gW.aT.springMap[this.navSpringName];
      // If this spring still exists.
      if (spring) {
         // Use the end of the spring that's attached to the pin.
         if (spring.spo1.nextPinName) {
            var nextPinName = spring.spo1.nextPinName;
         } else {
            var nextPinName = spring.spo2.nextPinName;
         }
         
         // If there's a pin in the map by that name, attach to it.
         if (gW.aT.pinMap[ nextPinName]) {
            // Move the end of the spring that's attached to the pin.
            if (gW.aT.springMap[this.navSpringName].spo1.constructor.name == "Pin")
               gW.aT.springMap[this.navSpringName].spo1 = gW.aT.pinMap[ nextPinName];
            else {
               gW.aT.springMap[this.navSpringName].spo2 = gW.aT.pinMap[ nextPinName];
            }
            this.pinName = nextPinName;
         }
      } else {
         //console.log('no spring to use.');
      }
   }
   Puck.prototype.deleteThisOne = function( pars) {
      var deleteMode = setDefault( pars.deleteMode, 'fromBullet');
      
      // Add this player's score to the summary.
      if (this.clientName) gW.clients[this.clientName].addScoreToSummary('', gW.c.demoIndex, gW.c.npcSleepUsage);
      
      // But first, give credit to the owner of the bullet that last hit you.
      // Ignore old bullets that are being removed. Don't give any credit for
      // deletion by the editor. Make sure the client is still there before
      // changing its score.
      if ((! this.bullet) && (deleteMode != 'fromEditor')) {
         if ((!Client.winnerBonusGiven) && (gW.clients[this.whoShotBullet])) {
            // Give 100 for client and drone pucks, 50 for regular pucks.
            if (this.clientName) {
               gW.clients[this.whoShotBullet].score += 100;
            } else {
               gW.clients[this.whoShotBullet].score += 50;
            }
         }
      }
      
      // JavaScript uses garbage collection. Deleting a puck involves
      // mainly nullifying all references to the puck. (Also removing references
      // from the puck.)
      
      // Note that springs are checked, in the updateAirTable function, to
      // see if either of the two objects it is attached to has been deleted.
      // If so, the spring is deleted. So that's not needed here.
      
      this.deleted = true;
      this.jet = null;
      this.gun = null;
      this.shield = null;
      
      // Sound effect
      if (! this.bullet) gW.sounds['highPop'].play();
      
      // For pucks that are driven by clients (users or NPC)
      if (this.clientName) {
         if (this.clientName == 'local') {
            // Must keep the local client. Just null out the puck reference in the local client.
            gW.clients[this.clientName].puck = null;
         } else {
            // Recently decided to turn off (for now) the client disconnect when the client puck gets
            // destroyed in a game of Puck Popper. So the following line is commented and then added
            // the next line where the puck on the client is nulled.
            //deleteRTC_onClientAndHost( this.clientName);
            gW.clients[this.clientName].puck = null;
            
            // Tell the client that his puck has been popped.
            var control_message = {'from':'host', 'to':this.clientName, 'data':{'puckPopped':{'value':true}} };
            hC.sendCommandToClients( control_message);
            
            // Remove the client if it's an NPC.
            if (this.clientName.slice(0,3) == 'NPC') {
               delete gW.clients[ this.clientName];
               Puck.npcCount -= 1;
            }
         }
         Puck.playerCount -= 1;
      }
      
      // Delete the corresponding Box2d object.
      gW.tableMap.delete( this.b2d);
      gW.world.DestroyBody( this.b2d);
      
      // Remove this puck from our puck map.
      delete gW.aT.puckMap[ this.name];
      // ...and from the multi-select map.
      gW.hostMSelect.removeOne( this);
      
      // Filter out this puck from the jelloPuck array.
      if (this.jello) {
         gW.aT.jelloPucks = gW.aT.jelloPucks.filter( function( eachPuck) {
            // Keep these (those NOT deleted)
            return (!eachPuck.deleted == true);
         });
      }
   }
   Puck.prototype.copyThisOne = function( pars) {
      // If the position is not specified in pars, put the copy at the same position as the original.
      var position_2d_m = setDefault( pars.position_2d_m, this.position_2d_m);
      
      // Make a copy of the mutable objects that are passed into the Puck constructor.
      var p_2d_m =          Object.assign({}, position_2d_m);
      var v_2d_mps =        Object.assign({}, this.velocity_2d_mps);
      var parsForNewBirth = Object.assign({}, this.parsAtBirth);
      
      // Make sure the name is nulled so the auto-naming feature is used in the constructor.
      parsForNewBirth.name = null;
      // Don't allow any network client or NPC features.
      parsForNewBirth.clientName = null;
      parsForNewBirth.pinName = null;
      
      /*
      Update pars to reflect any edits or state changes. For example,
      this loop, for the first element in the array, does the following:
      parsForNewBirth.angle_r = this.angle_r;
      */
      var parsToCopy = ['angle_r','angularSpeed_rps','friction','restitution','linDamp','angDamp','bullet_restitution','jello'];
      for (var i = 0, len = parsToCopy.length; i < len; i++) {
         parsForNewBirth[ parsToCopy[i]] = this[ parsToCopy[i]];
      }
      
      if (this.shape == 'circle') {
         parsForNewBirth.radius_m = this.radius_m;
      } else {
         parsForNewBirth.half_height_m = this.half_height_m;
         parsForNewBirth.half_width_m = this.half_width_m;
      }
      
      // If this is a drone puck, make a new NPC client for the copy.
      if (this.clientName && (this.clientName.slice(0,3) == 'NPC')) {
         // Sync the navigation timer of the copy to that of the original.
         // Note: instantiating with the current NPC name will increment the NPC counter (and the name).
         var theClientForTheCopy = new Client({'name':this.clientName, 'color':'purple', 
                                                  'NPC_pin_timer_s':gW.clients[this.clientName].NPC_pin_timer_s,
                                                  'NPC_pin_timer_limit_s':gW.clients[this.clientName].NPC_pin_timer_limit_s});
         // Add the client name to the birth parameters for the puck.
         parsForNewBirth.clientName = theClientForTheCopy.name;
      }
      
      var newPuck = new Puck( p_2d_m, v_2d_mps, parsForNewBirth);
      if (newPuck.jello) gW.aT.jelloPucks.push( newPuck);
      
      return newPuck;
   }
   Puck.prototype.updateState = function() {
      this.getPosition();
      this.getVelocity();
      
      this.getAngle();
      this.getAngularSpeed();
   }   
   Puck.prototype.create_Box2d_Puck = function() {
      var bodyDef = new b2BodyDef;
      bodyDef.type = b2Body.b2_dynamicBody;
      
      // Make it be.
      this.b2d = gW.world.CreateBody(bodyDef);
      this.b2d.CreateFixture( this.define_fixture( {}) );      
      
      // Set the state: position and velocity (angle and angular speed).
      this.b2d.SetPosition( this.position_2d_m);
      this.b2d.SetLinearVelocity( this.velocity_2d_mps);
      this.b2d.SetAngle( this.angle_r);
      this.b2d.SetAngularVelocity( this.angularSpeed_rps);
      
      // Use the mass calculated by box2d.
      this.mass_kg = this.b2d.GetMass();
      
      this.b2d.SetLinearDamping( this.linDamp);
      this.b2d.SetAngularDamping( this.angDamp);
      this.b2d.SetBullet( this.bullet);
   }
   Puck.prototype.define_fixture = function( pars) {
      this.width_scaling = setDefault( pars.width_scaling, 1.0);
      this.height_scaling = setDefault( pars.height_scaling, 1.0);
      this.radius_scaling = setDefault( pars.radius_scaling, 1.0);
      this.restitution_scaling = setDefault( pars.restitution_scaling, 1.0);
      this.friction_scaling = setDefault( pars.friction_scaling, 1.0);
      this.linDamp_scaling = setDefault( pars.linDamp_scaling, 1.0);
      
      // Create a circular or rectangular dynamic box2d object.
      
      var fixDef = new b2FixtureDef;
      fixDef.density = this.density;
      
      // Adjust elasticity (bounciness).
      if (this.restitution_scaling != 1.0) {
         // If restitution is zero, bump it up a little so the scaling factor has something to work with.
         if (this.restitution == 0.0) this.restitution = 0.01;
         // Apply the scaling factor.
         this.restitution *= this.restitution_scaling;
         // Keep it between 0.0 and 1.0.
         if (this.restitution > 1.00) this.restitution = 1.0;
         if (this.restitution < 0.01) this.restitution = 0.0;
         
         // Keep this new restitution value independent of the gravity toggle.
         this.restitution_fixed = true;
         
         gW.messages['help'].newMessage("puck restitution = " + this.restitution.toFixed(4), 0.5);
      }
      fixDef.restitution = this.restitution;
      
      // Adjust friction (surface tackiness).
      if (this.friction_scaling != 1.0) {
         // If friction is zero, bump it up a little so the scaling factor has something to work with.
         if (this.friction == 0.0) this.friction = 0.01;
         // Apply the scaling factor.
         this.friction *= this.friction_scaling;
         // Stop at zero.
         if (this.friction < 0.01) this.friction = 0.0;
         
         // Keep this new friction value independent of the gravity toggle.
         this.friction_fixed = true;
         
         gW.messages['help'].newMessage("puck friction = " + this.friction.toFixed(4), 0.5);
      }
      fixDef.friction = this.friction;
      
      // Adjust linear damping (damping from fluid drag).
      if (this.linDamp_scaling != 1.0) {
         // If linear damping is zero, bump it up a little so the scaling factor has something to work with.
         if (this.linDamp == 0.0) this.linDamp = 0.01;
         // Apply the scaling factor.
         this.linDamp *= this.linDamp_scaling;
         // Stop at zero.
         if (this.linDamp < 0.01) this.linDamp = 0.0;
         gW.messages['help'].newMessage("puck drag coefficient = " + this.linDamp.toFixed(4), 0.5);
      }
      // Note: linearDamping is a body property (not fixture property)
      this.b2d.SetLinearDamping( this.linDamp);
      
      fixDef.filter.groupIndex = this.groupIndex;
      fixDef.filter.categoryBits = this.categoryBits;
      fixDef.filter.maskBits = this.maskBits;
      
      if (this.shape == 'circle') {
         // Apply the radius scaling factor.
         this.radius_m *= this.radius_scaling;
         this.radius_px = px_from_meters( this.radius_m);
         
         // Don't let it get too small.
         if ((this.radius_px < 9) && (!this.bullet)) {
            this.radius_px = 9;
            this.radius_m = meters_from_px( this.radius_px);
         }
         
         // Don't let client pucks get so big that their bullets can collide with the body of their ship.
         if (this.clientName) {
            if (this.radius_m > this.parsAtBirth.radius_m) {
               this.radius_m = this.parsAtBirth.radius_m;
               this.radius_px = px_from_meters( this.radius_m);
            }
         }
         
         fixDef.shape = new b2CircleShape( this.radius_m);
      
      // Rectangular shapes
      } else {
         // Apply the scaling factors to the current width and height.
         this.half_width_m *= this.width_scaling;
         this.half_height_m *= this.height_scaling;
         
         this.half_width_px = px_from_meters( this.half_width_m);
         // Don't let it get too skinny because it becomes hard to select.
         if (this.half_width_px < 3) {
            this.half_width_px = 3;
            this.half_width_m = meters_from_px( this.half_width_px);
         }
         
         this.half_height_px = px_from_meters( this.half_height_m);
         if (this.half_height_px < 3) {
            this.half_height_px = 3;
            this.half_height_m = meters_from_px( this.half_height_px);
         }
         
         fixDef.shape = new b2PolygonShape;
         fixDef.shape.SetAsBox(this.half_width_m, this.half_height_m);
      }
      
      return fixDef;
   }
   Puck.prototype.modify_fixture = function( mode) {
      // For shape editing...
      
      // If you are going to modify the fixture dimensions you have to delete
      // the old one and make a new one. The m_fixtureList linked list always
      // points to the most recent addition to the linked list. If there's only
      // one fixture, then m_fixtureList is a reference to that single fixture.
      
      var width_factor = 1.0;
      var height_factor = 1.0;
      var restitution_factor = 1.0;
      var friction_factor = 1.0;
      var drag_factor = 1.0;
      
      if (mode == 'wider') {
         width_factor = 1.1;
      } else if (mode == 'thinner') {
         width_factor = 0.9;         
         
      } else if (mode == 'taller') {
         height_factor = 1.1;
      } else if (mode == 'shorter') {
         height_factor = 0.9;       
         
      } else if (mode == 'moreDamping') {
         restitution_factor = 0.95;
      } else if (mode == 'lessDamping') {
         restitution_factor = 1.05;
      
      } else if (mode == 'moreFriction') {
         friction_factor = 1.05;
      } else if (mode == 'lessFriction') {
         friction_factor = 0.95;
      
      } else if (mode == 'moreDrag') {
         drag_factor = 1.05;
      } else if (mode == 'lessDrag') {
         drag_factor = 0.95;
      
      } else if (mode == 'noChange') {
         // don't change anything.
      }
      
      this.b2d.DestroyFixture( this.b2d.m_fixtureList);
      if (this.shape == 'circle') {
         // Use either left/right or up/down to change the circle radius.
         if (width_factor == 1.0) width_factor = height_factor;
         this.b2d.CreateFixture( this.define_fixture({'radius_scaling':width_factor,                                'restitution_scaling':restitution_factor, 
                                                      'friction_scaling':friction_factor, 'linDamp_scaling':drag_factor}));
      } else {
         this.b2d.CreateFixture( this.define_fixture({'width_scaling':width_factor, 'height_scaling':height_factor, 'restitution_scaling':restitution_factor, 
                                                      'friction_scaling':friction_factor, 'linDamp_scaling':drag_factor}));
      }
      
      // Update the mass.
      this.mass_kg = this.b2d.GetMass();
      if ((height_factor != 1.0) || (width_factor != 1.0)) {
         gW.messages['help'].newMessage('puck mass = ' + this.mass_kg.toFixed(3) + ' kg', 1.0);
      }
      
      // If there's a spring that has one (or both) of its ends attached to THIS puck, 
      // and it's a b2d spring, update that spring.
      Spring.applyToAll( spring => {
         if (((this == spring.spo1) || (this == spring.spo2)) && (spring.softContraints)) {
            //console.log('puck=' + this.name + ',' + spring.spo1.name + ',' + spring.spo2.name + ':' + spring.name);
            spring.updateB2D_spring();
         }
      });
            
      // Update the puck tail radius.
      if (this.tail) {
         this.tail.initial_radius_m = this.radius_m;
      }
   }
   Puck.prototype.getPosition = function() {
      this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition());
   }
   Puck.prototype.getVelocity = function() {
      // COM velocity
      this.velocity_2d_mps = Vec2D_from_b2Vec2( this.b2d.GetLinearVelocity());
   }
   Puck.prototype.getAngle = function() {
      // COM angle (radians)
      this.angle_r = this.b2d.GetAngle();
   }   
   Puck.prototype.getAngularSpeed = function() {
      // COM angular speed (radians per second)
      this.angularSpeed_rps = this.b2d.GetAngularVelocity();
   }   
   Puck.prototype.draw = function( drawingContext, deltaT_s) {
      this.position_2d_px = screenFromWorld( this.position_2d_m);
      var borderColor;
      
      if (this.shape == 'circle') {
      
         // Draw the main circle.
         // If hit, color the border red for a few frames.
         if (this.flash) {
            borderColor = 'red';
            this.flashCount += 1;
            if (this.flashCount >= 3) {
               this.flash = false;
               this.flashCount = 0;
            }
         } else {
            borderColor = 'white';            
         }
         this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':borderColor, 'borderWidth_px':this.borderWidth_px, 'fillColor':this.color, 'radius_px':this.radius_px});
         
         // Draw the health circle.
         this.poorHealthFraction = this.hitCount / this.hitLimit;
         var poorHealthRadius = this.radius_px * this.poorHealthFraction;
         if (poorHealthRadius > 0) {
            this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'chocolate', 'radius_px':poorHealthRadius});
         }
         
         // Update and draw the shield.
         if (gW.clients[this.clientName]) {
            this.shield.updateState( drawingContext, deltaT_s);
            // Draw the client finder circle. Big fat one. Easy to see. So to find your puck.
            if (gW.clients[this.clientName].key_questionMark == "D") {
               this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':gW.clients[this.clientName].color, 'borderWidth_px':10, 'fillColor':'noFill', 'radius_px':this.radius_px+15});
            }
         }
         
         // Show rotational orientation: draw a line segment along the line from the center out to a local point on the radius.
         if (!this.gun && this.angleLine) {
            var pointOnEdge_2d_px = screenFromWorld( this.b2d.GetWorldPoint( new b2Vec2(0.0, this.radius_m) ) );
            var pointAtHalfRadius_2d_px = screenFromWorld( this.b2d.GetWorldPoint( new b2Vec2(0.0, this.radius_m * (1.0/2.0)) ) );
            
            this.drawLine( drawingContext, pointAtHalfRadius_2d_px, pointOnEdge_2d_px, {'width_px':2, 'color':'white'});
         }
         
         // Draw the tail if we have one.
         if (this.tail) this.tail.update( drawingContext, this.position_2d_m, deltaT_s);
      
      } else {
         // Draw the rectangle.         
         this.drawPolygon( drawingContext, gW.b2d_getPolygonVertices( this.b2d), {'borderColor':'white','borderWidth_px':2,'fillColor':this.color});
      }
   }
   Puck.prototype.draw_MultiSelectPoint = function( drawingContext) {
      var selectionPoint_2d_px;
      if (!gW.dC.comSelection.checked) {
         selectionPoint_2d_px = screenFromWorld( this.b2d.GetWorldPoint( this.selectionPoint_l_2d_m));
      } else {
         selectionPoint_2d_px = this.position_2d_px;
      }
      this.drawCircle( drawingContext, selectionPoint_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5});
   }
   Puck.prototype.applyForces = function( deltaT_s) {
      // Net resulting force on the puck.
      
      // First consider all forces acting on the COM.
      
      // F = acc * mass
      var puck_forces_2d_N = Puck.g_2d_mps2.scaleBy( this.mass_kg);
          puck_forces_2d_N.addTo( this.cursorString_spring_force_2d_N);
          puck_forces_2d_N.addTo( this.cursorString_puckDrag_force_2d_N);
         
          puck_forces_2d_N.addTo( this.sprDamp_force_2d_N); 
          puck_forces_2d_N.addTo( this.jet_force_2d_N);
          puck_forces_2d_N.addTo( this.impulse_2d_Ns.scaleBy(1.0/deltaT_s));
      
      
      // Apply this force to the puck's center of mass (COM) in the Box2d world
      this.b2d.ApplyForce( puck_forces_2d_N, this.position_2d_m);

      
      // Apply any non-COM forces in the array.
      for (var j = 0, len = this.nonCOM_2d_N.length; j < len; j++) {
         //console.log("force.force_2d_N.x = " + nonCOM_2d_N[j].force_2d_N.x);
         this.b2d.ApplyForce( this.nonCOM_2d_N[j].force_2d_N, this.nonCOM_2d_N[j].point_w_2d_m);
      }

      /*         
      // Apply torques.   #b2d
      //this.b2d.ApplyTorque( this.cursorString_torque_force_Nm, wake=True)
      */
      
      // Now reset the aggregate forces.
      this.cursorString_spring_force_2d_N = new Vec2D(0.0,0.0);
      this.cursorString_puckDrag_force_2d_N = new Vec2D(0.0,0.0); 
      this.nonCOM_2d_N = [];
      
      this.sprDamp_force_2d_N = new Vec2D(0.0,0.0);
      this.impulse_2d_Ns = new Vec2D(0.0,0.0);
      
      /*
      this.cursorString_torque_force_Nm = 0.0;
      */
   }

   
   
   function Shield( puck, pars) {
      DrawingFunctions.call(this); // Inherit attributes
      
      // Make a (circular) reference to the host puck.
      this.puck = puck;

      // Optional parameters and defaults.
      this.color = setDefault( pars.color, 'lime');

      // Make a direct reference to the client.
      this.client = gW.clients[this.puck.clientName];
            
      this.radius_px = px_from_meters( this.puck.radius_m * 1.15);
      
      this.ON = false;
      this.STRONG = true;
      this.STRONG_timer_s = 0;
      this.STRONG_time_limit_s = 3.0;
      this.CHARGING_timer_s = 0;
      this.CHARGING_time_limit_s = 2.0;
      this.charge_level = 1.0;
   }
   Shield.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   Shield.prototype.constructor = Shield; // Rename the constructor (after inheriting)
   Shield.prototype.updateState = function( drawingContext, deltaT_s) {
      // Let the client control the state and draw if ON.
      if (this.client.key_space == "D") {
         this.ON = true;
         if (this.STRONG) {
            var dashArray = [ 0];
         } else {
            // Shields are weak.
            var dashArray = [10];
         }
         this.drawCircle( drawingContext, this.puck.position_2d_px, {'borderColor':this.color, 'borderWidth_px':2, 'fillColor':'noFill', 'radius_px':this.radius_px, 'dashArray':dashArray});
      } else {
         this.ON = false;
      }

      // Drain the shield
      if (this.ON && this.STRONG) {
         this.STRONG_timer_s += deltaT_s;  
         this.charge_level = 1.00 - (this.STRONG_timer_s / this.STRONG_time_limit_s);
         if (this.STRONG_timer_s > this.STRONG_time_limit_s) {
            this.STRONG = false;
            this.STRONG_timer_s = 0.0;
         }  
      }
      
      // Recharge the shield only if completely drained.
      if (!this.STRONG) {
         this.CHARGING_timer_s += deltaT_s;
         this.charge_level = this.CHARGING_timer_s / this.CHARGING_time_limit_s;
         if (this.CHARGING_timer_s > this.CHARGING_time_limit_s) {
            this.STRONG = true;
            this.CHARGING_timer_s = 0.0;
         }
      }
      
      // Display the shield timer on the gun tube.
      this.puck.gun.indicatorFraction = this.charge_level;
   }



   function Tube( puck, pars) {
      DrawingFunctions.call(this); // Inherit attributes
      
      // Circular reference back to the puck.
      this.puck = puck;
      
      // Optional parameters and defaults.
      this.initial_angle = setDefault( pars.initial_angle, 20);
      this.indicator = setDefault( pars.indicator, false);

      // Make a direct reference to the client.
      this.client = gW.clients[this.puck.clientName];
      
      // 360 degrees/second  /  60 frames/second = 6 degrees/frame
      this.rotationRate_dps = 240.0; //4.0dpf;
      
      this.tube_color = 'blue';
      this.length_m = 1.05 * this.puck.radius_m;
      this.width_m =  0.30 * this.puck.radius_m;
      this.width_px = px_from_meters( this.width_m);
      
      // Establish the relative-position vector (for the end of the tube) using the length of the tube.
      this.rel_position_2d_m = new Vec2D(0.0, this.length_m);
      this.rel_position_2d_m.set_angle( this.initial_angle);
      this.AbsPositionOfEnds();
      
      this.indicatorWidth_px = px_from_meters( this.width_m * 0.40);
      this.indicatorFraction = 0.00;
   }
   Tube.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   Tube.prototype.constructor = Tube; // Rename the constructor (after inheriting)
   Tube.prototype.AbsPositionOfEnds = function() {
      // Determine the absolute positions of the base and the end of the tube.
      this.base_2d_px = screenFromWorld( this.puck.position_2d_m);
      this.end_2d_m = this.puck.position_2d_m.add( this.rel_position_2d_m);
      this.end_2d_px = screenFromWorld( this.end_2d_m);
   }
   Tube.prototype.AbsPositionOfIndicator = function() {
      // The starting point will indicate the "amount" of the indicator.
      this.indicatorBase_2d_m = this.puck.position_2d_m.add( this.rel_position_2d_m.scaleBy(1 - this.indicatorFraction));
      this.indicatorBase_2d_px = screenFromWorld( this.indicatorBase_2d_m);
      
      // Draw to the end of the tube.
      this.indicatorEnd_2d_m = this.puck.position_2d_m.add( this.rel_position_2d_m.scaleBy( 1.00));
      this.indicatorEnd_2d_px = screenFromWorld( this.indicatorEnd_2d_m);
   }
   Tube.prototype.rotateTube = function( deg) {
      this.rel_position_2d_m.rotated_by( deg);
   }
   Tube.prototype.setTubeAngle = function( deg) {
      this.rel_position_2d_m.set_angle( deg);
   }
   Tube.prototype.drawTube = function( drawingContext) {      
      this.AbsPositionOfEnds();
      this.drawLine( drawingContext, this.base_2d_px, this.end_2d_px, {'width_px':this.width_px, 'color':this.tube_color});
      
      if (this.indicator) {
         this.AbsPositionOfIndicator();
         this.drawLine( drawingContext, this.indicatorBase_2d_px, this.indicatorEnd_2d_px, {'width_px':this.indicatorWidth_px, 'color':this.puck.shield.color});
      }
   }
   
   
   
   function Jet( puck, pars) {
      // The following link has a good explanation of the inheritance techniques used in this Jet (and the Gun) prototype.
      // https://tylermcginnis.com/javascript-inheritance-and-the-prototype-chain/
      //
      // Call the Tube constructor. Bind it to "this" jet. Pass the puck and pars to the constructor and run it. This brings in
      // all the attributes of Tube and makes them accessible in Jet. Note that Tube inherits from DrawingFunctions.
      Tube.call(this, puck, pars);
            
      // Add properties specific to Jet.
      this.width_m  = 0.17 * this.puck.radius_m;
      this.height_m = 1.00 * this.puck.radius_m;
      
      // This jet-flame triangle is oriented like an arrow pointing the positive x direction.
      this.initializeFlame( this.height_m);
      
      // Point the jet flame in the same direction as the tube.
      this.rotateFlame( this.initial_angle);
      
      // Set the tube color to match the client color.
      this.tube_color = this.client.color;
      
      this.flame_color = 'red';
      this.flameEdge_color = 'blue';
      
      // Scaler magnitude of jet thrust force
      this.jet_force_N = 1.3 * this.puck.mass_kg * Math.abs( gW.c.g_mps2);
      // Controlled by the Two Thumbs interface.
      this.throttle = 1.0;
      
      this.rotationCounter = 0;
      
      this.noseCone_2d_m = [new Vec2D(0,0),                    new Vec2D(0,-this.width_m),
                            new Vec2D(this.width_m * 2.0, 0),  new Vec2D(0, this.width_m)];
   }
   // Use the Tube prototype as starting point for the Jet (inheritance). This brings
   // in all the methods from Tube.
   Jet.prototype = Object.create( Tube.prototype);
   // Reset the constructor name back to Jet, so it is not left as "Tube" from the inheritance.
   Jet.prototype.constructor = Jet;
   // Define any new methods for Jet.
   Jet.prototype.rotateShape = function( shapeArray_2d_m, degrees) {
      // degrees is the change from the current orientation.
      for (var i = 0, len = shapeArray_2d_m.length; i < len; i++) {
         // Rotate each vertex.
         shapeArray_2d_m[i].rotated_by( degrees);
      }
   }
   Jet.prototype.rotateFlame = function( degrees) {
      this.rotateShape( this.flameTriangle_2d_m, degrees);
   }
   Jet.prototype.rotateTubeAndFlame = function( degrees) {
      // deg is the change from the current orientation.
      this.rotateTube( degrees);
      this.rotateFlame( degrees);
   }
   Jet.prototype.rotateJetToAngle = function( targetAngle_deg) {
      var currentAngle_deg = this.rel_position_2d_m.get_angle();
      this.rotateTubeAndFlame( -currentAngle_deg + targetAngle_deg);
   }
   Jet.prototype.rotateJetAndScaleFlameToThis = function( f_2d_N) {
      // Rotate the tube and jet to be in the same direction as the supplied vector
      var current_deg = this.rel_position_2d_m.get_angle();
      
      // Orient the jet along the x axis so it is simple to scale it. The
      // angle will be 0 after this.
      this.rotateTubeAndFlame( -current_deg);
      
      // Scale the jet flame relative to the length of the supplied vector.
      var height_m = this.height_m * (f_2d_N.length() / this.jet_force_N);
      if (height_m < 0.10) height_m = 0.10;
      this.initializeFlame( height_m);
      
      var target_deg  = f_2d_N.get_angle();
      var change_deg = target_deg - 0;
      
      // Rotate, starting from 0, by this amount.
      this.rotateTubeAndFlame( change_deg);
   }
   Jet.prototype.rotateJetByClient = function( deltaT_s) {
      // The rate, expressed as degrees per second (dps), instead of degrees per frame, is useful for 
      // accommodating the various physics-engine timestep options. 

      // Left/Right pointing control
      if ((this.client.key_d == "D") && (this.client.key_shift != 'D')) {
         this.rotateTubeAndFlame(-this.rotationRate_dps * deltaT_s);
      }
      if (this.client.key_a == "D") {
         this.rotateTubeAndFlame(+this.rotationRate_dps * deltaT_s);
      }
      
      // For use in stopping the puck...
      if ((this.client.key_s == "D") && (this.client.key_shift != "D") && (this.client.key_s_enabled)) {
         // This rotates the rel_position vector (the tube pointer) by the amount that it differs from the direction of motion.
         // The result being that it flips the tube to be in a direction opposite of the motion.
         // After the first flip, the subsequent s pressed rotate by -90 degrees. The rotationCounter is reset
         // when the jet is used.
         if (this.rotationCounter == 0) {
            this.rotateTubeAndFlame(this.puck.velocity_2d_mps.get_angle() - this.rel_position_2d_m.get_angle());
         } else {
            this.rotateTubeAndFlame(-90);
         }
         this.rotationCounter += 1;
         this.client.key_s_enabled = false;
      }
      if ((this.client.key_s == "U") && (!this.client.key_s_enabled)) {
         this.client.key_s_enabled = true;
      }
   }
   Jet.prototype.initializeFlame = function( height_m) {
      // This jet-flame triangle is oriented like an arrow pointing the positive x direction.
      this.flameTriangle_2d_m = [new Vec2D(0,0),        new Vec2D(0,-this.width_m),
                                 new Vec2D(height_m,0), new Vec2D(0, this.width_m)];
   }
   Jet.prototype.displaceShapeForRendering = function( shapeArray_2d_m, offSetVector_2d_m) {
      // Before you draw a shape, you have to know where it should appear on the screen. Return a
      // rendering array (in pixels) that has those vector addition results.
      var shapeArray_2d_px = [];
      for (var i = 0, len = shapeArray_2d_m.length; i < len; i++) {
         // Calculate where the vertices would need to be so as to appear (reander) on the end of the tube.
         var p_2d_m = offSetVector_2d_m.add( shapeArray_2d_m[i]);
         var p_2d_px = screenFromWorld( p_2d_m);
         // Put it in the triangle array.
         shapeArray_2d_px.push( p_2d_px);
      }
      return shapeArray_2d_px;
   }
   Jet.prototype.drawFlame = function( drawingContext) {
      // Draw flame at the end of the tube.
      var flameShape_2d_px = this.displaceShapeForRendering( this.flameTriangle_2d_m, this.end_2d_m);
      this.drawPolygon( drawingContext, flameShape_2d_px, {'borderColor':this.flameEdge_color,'borderWidth_px':3,'fillColor':this.flame_color});
   }
   Jet.prototype.drawNoseCone = function( drawingContext) {
      // The nose cone is processed with only rotation and displacement, no shape
      // elongation like with the jet flame. So there is no call to a function like
      // initializeFlame, that facilitates the flame shape change.
      
      // rotate nose cone to match tube
      var currentAngle_deg = this.noseCone_2d_m[2].get_angle();
      var targetAngle_deg = this.rel_position_2d_m.get_angle() + 180;
      this.rotateShape( this.noseCone_2d_m, (targetAngle_deg - currentAngle_deg));
      
      // Draw nose cone at the opposite side of the puck from the jet tube.
      var offset_2d_m = this.puck.position_2d_m.subtract( this.rel_position_2d_m);
      var noseConeShape_2d_px = this.displaceShapeForRendering( this.noseCone_2d_m, offset_2d_m);
      
      this.drawPolygon( drawingContext, noseConeShape_2d_px, {'borderColor':this.tube_color,'borderWidth_px':2,'fillColor':'black'});
   }
   Jet.prototype.updateAndDraw = function( drawingContext, deltaT_s) {
      // Note, for NPC clients, orientation is established in the clients thinkForNPC method.
      // rotateJetAndScaleFlameToThis is called there to point the jet in the direction opposite to the navigation-spring force.
      // Jet flame is always on for NPCs (unless floating free from navigational track).
      if (this.client.name.slice(0,3) == 'NPC') {
         if (!gW.dC.editor.checked && !this.puck.disableJet) {
            this.drawTube( drawingContext);
            this.drawFlame( drawingContext);
         }
      // Jet flame is controlled to be on/off for human (non NPC) users
      } else {
         // Respond to client controls to rotate the Tube and Jet.
         this.rotateJetByClient( deltaT_s);
         
         // Always draw the tube and nose cone.
         this.drawTube( drawingContext);
         this.drawNoseCone( drawingContext);
         
         // Fire the jet flame: if on (w key down), draw it, and calculate jet forces.
         if (this.client.key_w == "D") {
            this.puck.jet_force_2d_N = this.rel_position_2d_m.scaleBy( -this.jet_force_N * this.throttle / this.length_m);
            // Set the length of the jet flame to be proportional to the strength of the jet.
            this.rotateJetAndScaleFlameToThis( this.puck.jet_force_2d_N.scaleBy(-1));
            this.drawFlame( drawingContext);
            // If the jet is used, reset the rotation event counter.
            this.rotationCounter = 0;
         } else {
            this.puck.jet_force_2d_N = this.rel_position_2d_m.scaleBy( 0);
         }
      }
   }
   
   
   
   function Gun( puck, pars) {
      Tube.call(this, puck, pars); // Inherit attributes from Tube (see details in Jet)
      
      this.tube_color = setDefault( pars.tube_color, 'white');
      
      // Add properties specific to Gun.
      this.width_m  = 0.17 * this.puck.radius_m;
      this.height_m = 1.00 * this.puck.radius_m;

      // This overrides the rotationRate_dps inherited from the tube.
      this.rotationRate_dps = 90.0; //1.5dpf
      this.bulletSpeed_mps = 7.0;
      this.bulletCountLimit = 5;
      
      this.bulletWaitTimer_ms;
      this.timeBetweenBullets_ms = 70; //70
      
      this.rayCastLineLength_m = pars.rayCastLineLength_m; //always provided when a new gun is made for the host's puck.
      this.rayCast_init_deg = setDefault( pars.rayCast_init_deg, 0.0);
      // Orient this along the x-axis, zero degrees.
      this.rayCastLine_2d_m = new Vec2D(this.rayCastLineLength_m, 0);
      this.rayCastLine_2d_m.rotated_by( this.rayCast_init_deg);
      
      this.rayRotationRate_dps = setDefault( pars.rayRotationRate_dps, 80);
      this.scanning = setDefault( pars.scanning, true);
      this.rayBody = null;
      this.angleToFoundPuck = 0;
      
      // Attributes controlled only by the Two Thumbs interface (not the keyboard).
      this.scopeRotRateFrac = 0.0;
      this.scopeTrigger = 'U';
      this.scopeBreak = false;
      this.breakTimer_ms = 0;
      this.breakTimer_limit_ms = 100;
   }
   Gun.prototype = Object.create( Tube.prototype); // Inherit methods from Tube.
   Gun.prototype.constructor = Gun; // Reset the constructor name back to Gun
   // Define any new methods for Gun.
   Gun.prototype.rotateGunByClient = function( deltaT_s) {
      // The Rate, degrees per frame (dpf), gives the degrees of rotation in one frame.
      
      // Left/Right pointing control using the keyboard
      if (this.client.key_l == "D") {
         this.rotateTube(-this.rotationRate_dps * deltaT_s);
      }
      if (this.client.key_j == "D") {
         this.rotateTube(+this.rotationRate_dps * deltaT_s);
      }
      
      // Similar, but using the Two-Thumbs interface.
      //if (this.scopeTrigger == "D") {
      if (this.scopeRotRateFrac != 0.0) {
         //console.log('rF='+this.scopeRotRateFrac);
         this.rotateTube((-1) * this.rotationRate_dps * this.scopeRotRateFrac * deltaT_s);
      }
      
      if ((this.client.key_k == "D") && (this.client.key_k_enabled)) {
         if (this.client.key_shift == "D") {
            this.rotateTube(+90.0);
         } else {
            this.rotateTube(-90.0);
         }
         
         this.client.key_k_enabled = false;
      }
      if ((this.client.key_k == "U") && (!this.client.key_k_enabled)) {
         this.client.key_k_enabled = true;
      }
   }
   Gun.prototype.fireBullet = function() {      
      // The bullet velocity as seen from the puck (dividing by length produces a normalized vector)
      var relativeVel_2D_mps = this.rel_position_2d_m.scaleBy( this.bulletSpeed_mps/this.length_m);
      
      // Absolute velocity of bullet as seen from the world.
      var absoluteVel_2D_mps = relativeVel_2D_mps.add( this.puck.velocity_2d_mps);
      
      // Setting bullet friction, to be near 0.0, and bullet restitution, to be near 1.0, 
      // gives simple and symmetric collision behavior when the bullets hit the walls.
      
      // Set the group index of the bullets to equal the negative value assigned by default to the
      // host puck. That will prevent bullets (from this gun) from colliding with each other and the host puck.
      
      // Note that the target-leading algorithm for the NPCs is more accurate if you use puck.position_2d_m as compared to end_2d_m (tube end).
      // So, for NPC clients, this will fire the bullet (to fly free) starting from the base of the tube not starting from the end of the tube.
      if (this.client.name.slice(0,3) == 'NPC') {
         var bulletStartPosition_2d_m = this.puck.position_2d_m;
      } else {
         var bulletStartPosition_2d_m = this.end_2d_m;
      }
      // Make this bullet with the same groupIndex as the host puck (so no collisions with the host).
      var bullet = new Puck( bulletStartPosition_2d_m, absoluteVel_2D_mps, 
         {'radius_m':0.04, 'bullet':true, 'color':this.client.color, 'borderWidth_px':1, 'clientNameOfShooter':this.client.name, 
          'ageLimit_ms':this.puck.bulletAgeLimit_ms, 'restitution_fixed':true , 'restitution':this.puck.bullet_restitution, 'friction_fixed':true, 'friction':0.0,
          'groupIndex':this.puck.groupIndex});
      
      // Calculate the recoil impulse from firing the gun (opposite the direction of the bullet).
      if ((this.scopeTrigger == 'U') && (!this.puck.noRecoil)) {
         this.puck.impulse_2d_Ns.addTo( relativeVel_2D_mps.scaleBy(-1 * bullet.mass_kg));
      }
   }
   Gun.prototype.start_BulletStream = function() {
      this.bulletCount = 1;
      this.bulletStream = 'on';
      // This allows the gun to immediately fire the first bullet.
      this.bulletWaitTimer_ms = 0;
   }
   Gun.prototype.stop_BulletStream = function() {
      this.bulletStream = 'off';
   }
   Gun.prototype.update_BulletStream = function( deltaT_s) {
      this.bulletWaitTimer_ms += deltaT_s * 1000;
      
      // If ok to fire, do so.
      if ((this.bulletStream == 'on') && (this.bulletWaitTimer_ms >= this.timeBetweenBullets_ms) && (this.bulletCount <= this.bulletCountLimit)) {
         // If the shields are down.
         if (!this.puck.shield.ON) {
            this.fireBullet();
         }
         this.bulletWaitTimer_ms = 0;
         this.bulletCount += 1;
      }
   }
   Gun.prototype.drawRayCastLine = function( drawingContext, deltaT_s) {
      // Update the angle of the ray.
      if (this.client.name.slice(0,3) == 'NPC') {
         if (this.scanning && !gW.c.npcSleep) {
            this.rayCastLine_2d_m.rotated_by( +this.rayRotationRate_dps * deltaT_s);
         }
      } else {
         this.rayCastLine_2d_m.matchAngle(this.rel_position_2d_m); 
      }
      
      var ray_end_2d_m = this.puck.position_2d_m.add( this.rayCastLine_2d_m);

      var rayBody = null;
      // Set an endpoint in case nothing is hit in the raycast.
      var raycast_end_2d_m = ray_end_2d_m;
      var minFraction = 1.0;

      gW.world.RayCast( function( fixture, point, outputNormal, fraction) {
         
         var fixtureBody = gW.tableMap.get( fixture.GetBody());
         /*
         This "if" block updates the ray cast results only if it finds something closer.
         I didn't expect to have to do this when returning "fraction". But without
         this block, the callback will run multiple times and the last fixture to run it
         will determine the point vector. Last object always wins. So this block makes
         the closest object (along the ray) win out in identifying the fixture and point.
         */
         if ((fraction < minFraction) && (!((fixtureBody.bullet) || (fixtureBody.constructor.name == "Pin")))) {
            minFraction = Math.min(fraction, minFraction);
            rayBody = fixtureBody;
            raycast_end_2d_m = Vec2D_from_b2Vec2( point);
         }
         return fraction;
      }, b2Vec2_from_Vec2D( this.puck.position_2d_m), b2Vec2_from_Vec2D( ray_end_2d_m) );
      
      //console.log(this.client.NPC_guncooling_timer_s);
      if (rayBody && (rayBody.constructor.name == "Puck")) {
         // Make a reference to this rayBody on the gun
         this.rayBody = rayBody;
         //this.rayBody.color = 'green';
         this.scanning = false;
         
         // Point the ray at the center of the found puck.
         this.angleToFoundPuck = this.rayBody.position_2d_m.subtract( this.puck.position_2d_m).get_angle();
         this.rayCastLine_2d_m.set_angle( this.angleToFoundPuck);
         
         //console.log( "this.rayBody.name = " + this.rayBody.name);
      
      // This time check keeps you from sweeping during the bullet stream. Something about the small bullets
      // and their speed that yields occasional errors from the raycast.
      } else if (this.client.NPC_guncooling_timer_s >= this.client.NPC_guncooling_timer_limit_s) {
         this.scanning = true;
         this.rayBody = null;
      }
      
      // Draw it.
      var raycast_end_2d_px = screenFromWorld( raycast_end_2d_m);
      this.drawLine( drawingContext, this.puck.position_2d_px, raycast_end_2d_px, {'width_px':1, 'color':'yellow', 'dashArray':[4]});
   }
   Gun.prototype.updateAndDraw = function( drawingContext, deltaT_s) {
      // Respond to client controls to rotate the Gun.
      this.rotateGunByClient( deltaT_s);
      
      // Always draw the tube.
      this.drawTube( drawingContext);
      // Cast and draw ray based on gun orientation.
      var scopeRayOn = (this.scopeTrigger == "D") || (this.scopeRotRateFrac != 0);
      if ((this.client.name.slice(0,3) == 'NPC') || scopeRayOn) {
         this.drawRayCastLine( drawingContext, deltaT_s);
      }
      
      if (this.scopeBreak) {
         if (this.breakTimer_ms > this.breakTimer_limit_ms) {
            this.breakTimer_ms = 0;
            this.scopeBreak = false;
         } else {
            this.drawCircle( drawingContext, this.puck.position_2d_px, {'borderColor':'red', 'borderWidth_px':10, 'fillColor':'noFill', 'radius_px':40});
            this.breakTimer_ms += deltaT_s * 1000;
         }
      }
      
      // Fire the gun:
      // This draw method gets called every frame. If the i key is down, you
      // don't want it to fire a bullet every frame. The following logic allows one
      // call to fireBullet and then disables the i key. To enable, must release 
      // the key to the up position.
      if ((this.client.key_i == "D") || (this.scopeTrigger == "D")) {
         if (this.client.key_i_enabled) {
            this.start_BulletStream();
            this.client.key_i_enabled = false;
            //console.log("gun draw: i down and disabled.");
         }
         this.update_BulletStream( deltaT_s);
         
         // Reseting this counter here allows you to compensate for recoil with the s key (align opposite the motion) 
         // then w (some jet).
         this.puck.jet.rotationCounter = 0;
         
      } else if (((this.client.key_i == "U") && (this.scopeTrigger == "U")) && (!this.client.key_i_enabled)) {
         this.stop_BulletStream();
         this.client.key_i_enabled = true;
         //console.log("gun draw: i up and enabled.");
      }
   }
   

   
   // Static spring anchors (no collisions)
   function Pin( position_2d_m, pars) {
      DrawingFunctions.call(this) // inherit
      this.parsAtBirth = pars;
      //this.alsoThese = [];
      this.cursorPin = setDefault( pars.cursorPin, false);
      
      if (pars.name) {
         this.name = pars.name;
         // Get the number part of the name
         var numberInName = this.name.slice(3);
         // Don't change the index if no number in name.
         if (isNaN( numberInName)) {
            numberInName = 0;
         } else {
            numberInName = Number( numberInName);
         }
         Pin.nameIndex = Math.max( Pin.nameIndex, numberInName);
      } else {
         Pin.nameIndex += 1;
         this.name = 'pin' + Pin.nameIndex;
      }
      //console.log("pin name, index = " + this.name + ',' + Pin.nameIndex);
      
      // Don't put cursor pins in the map.
      if (!this.cursorPin) gW.aT.pinMap[this.name] = this;
      
      this.position_2d_m = Vec2D_check( position_2d_m);
      this.position_2d_px = screenFromWorld( this.position_2d_m);
      
      // Local selection point for a pin is always at its center.
      this.selectionPoint_l_2d_m = new Vec2D(0.0, 0.0);
       
      this.velocity_2d_mps =  setDefault( pars.velocity_2d_mps, new Vec2D(0.0, 0.0));
     
      this.radius_px = setDefault( pars.radius_px, 6);
      // Make the radius in box2d a little larger so can select it easier.
      this.radius_m = meters_from_px( this.radius_px + 2);
      
      // www.iforce2d.net/b2dtut/collision-filtering
      this.groupIndex = setDefault( pars.groupIndex, 0);
      this.categoryBits = setDefault( pars.categoryBits, 0x0001);
      // Masking parameters for b2d object for the pin:
      // The default Box2D values are 0xFFFF for maskBits (collide with everything).
      // Default here, 0x0000, will prevent collisions with the pin (collide with nothing).
      this.maskBits = setDefault( pars.maskBits, 0x0000);
      
      this.b2d = null;
      this.create_b2d_pin();
      // Create a reference back to this pin from the b2d pin.
      gW.tableMap.set(this.b2d, this);
      
      this.deleted = false;
      
      // For creating a circular linked-list of pins to guide the NPC movement.
      this.NPC = setDefault( pars.NPC, false);
      this.nextPinName = setDefault( pars.nextPinName, null);
      this.previousPinName = setDefault( pars.previousPinName, null);
      
      this.visible = setDefault( pars.visible, true);
      this.fillColor = setDefault( pars.fillColor, 'blue');
      this.borderColor = setDefault( pars.borderColor, 'gray');
   }
   Pin.nameIndex = 0;
   Pin.applyToAll = function ( doThis) {
      for (var pinName in gW.aT.pinMap) {
         var pin = gW.aT.pinMap[ pinName];
         doThis( pin);
      }
   }
   Pin.deleteAll = function () {
      Pin.applyToAll( pin => {
         gW.tableMap.delete( pin.b2d);
         if (pin.b2d) gW.world.DestroyBody( pin.b2d);
      });
      gW.aT.pinMap = {};
      Pin.nameIndex = 0;      
   }
   Pin.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   Pin.prototype.constructor = Pin; // Rename the constructor (after inheriting)
   Pin.prototype.deleteThisOne = function( pars) {
      var deleteMode = setDefault( pars.deleteMode, null);
      
      // Note that springs are checked, in the updateAirTable function, to
      // see if either of the two objects it is attached to has been deleted.
      // If so, the spring is deleted. So that's not needed here.
      
      // Reassign the surrounding pins (if they are part of an NPC path)
      if (this.NPC) {
         // Point the next pin back at the previous pin.
         gW.aT.pinMap[this.nextPinName].previousPinName = gW.aT.pinMap[this.previousPinName].name;
         // Point the previous pin forward to the next pin.
         gW.aT.pinMap[this.previousPinName].nextPinName = gW.aT.pinMap[this.nextPinName].name;
      }
      
      // Delete reference in the tableMap.
      gW.tableMap.delete( this.b2d);
      
      // Delete the corresponding Box2d object.
      gW.world.DestroyBody( this.b2d);
      
      // Mark this pin as deleted.
      this.deleted = true;
      
      // Remove this pin from the pin map.
      delete gW.aT.pinMap[ this.name];
      // ...and from the multi-select map.
      gW.hostMSelect.removeOne( this);
   }
   Pin.prototype.copyThisOne = function( pars) {
      var position_2d_m = setDefault( pars.position_2d_m, this.position_2d_m);
      
      var p_2d_m = Object.assign({}, position_2d_m);
      var parsForNewBirth =   Object.assign({}, this.parsAtBirth);
      // Make sure the name is nulled so the auto-naming feature is used in the constructor.
      parsForNewBirth.name = null;
      
      var newPin = new Pin( p_2d_m, parsForNewBirth);
      
      // Slide the new pin in front of the old one if it's in a NPC.
      if (this.NPC) {
         // Set the two links for the new pin.
         newPin.nextPinName = this.nextPinName;
         newPin.previousPinName = this.name;
         
         // Update the backward link of the original next pin.
         gW.aT.pinMap[this.nextPinName].previousPinName = newPin.name;
         
         // Update the forward link of the original pin.
         this.nextPinName = newPin.name;
      }
      return newPin;
   }
   Pin.prototype.define_fixture = function() {
      
      var fixDef = new b2FixtureDef;   
      
      fixDef.filter.groupIndex = this.groupIndex;
      fixDef.filter.categoryBits = this.categoryBits;
      fixDef.filter.maskBits = this.maskBits;
      
      fixDef.shape = new b2CircleShape( this.radius_m);
      
      return fixDef;
   }
   Pin.prototype.create_b2d_pin = function() {
      // Create a rectangular and static box2d object.
      
      var bodyDef = new b2BodyDef;
      bodyDef.type = b2Body.b2_kinematicBody; // b2_kinematicBody b2_staticBody
      
      this.b2d = gW.world.CreateBody(bodyDef);
      this.b2d.CreateFixture( this.define_fixture());
      
      // Set the state: position.
      this.b2d.SetPosition( this.position_2d_m);
      this.b2d.SetLinearVelocity( this.velocity_2d_mps);
   }
   Pin.prototype.getPosition = function() {
      this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition());
      this.position_2d_px = screenFromWorld( this.position_2d_m);
   }
   Pin.prototype.draw_MultiSelectPoint = function( drawingContext) {
      this.getPosition();
      this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5});
   }
   Pin.prototype.draw = function( drawingContext, radius_px) {
      radius_px = setDefault( radius_px, this.radius_px);
      if (gW.dC.editor.checked || this.visible) {
         this.getPosition();
         var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? 'white' : this.fillColor;
         this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':this.borderColor, 'borderWidth_px':2, 'fillColor':fillColor, 'radius_px':radius_px});
      }
      
      // Draw lines to indicate the relationships in the NPC navigation map.
      if (this.NPC && gW.dC.editor.checked) {
         if (gW.aT.pinMap[this.nextPinName]) {
            this.drawLine( drawingContext, this.position_2d_px, gW.aT.pinMap[this.nextPinName].position_2d_px, {'width_px':1, 'color':this.color, 'dashArray':[3]});
         }
      }
   }
   
   

   function Spring(puckOrPin1, puckOrPin2, pars) {
      DrawingFunctions.call(this); // inherit
      
      // Must have both objects to attach the spring.
      // Throwing an error forces an exit from this constructor.
      if ( !((puckOrPin1) && (puckOrPin2)) ) {
         var errorObj = new Error('Attempting to construct a spring with one or both connected objects missing.');
         errorObj.name = 'from Spring constructor';
         throw errorObj;
      }
      this.parsAtBirth = pars;
      //this.alsoThese = [];
      
      if (pars.name) {
         this.name = pars.name;
         // Get the number part of the name
         var numberInName = this.name.slice(1);
         // Don't change the index if no number in name.
         if (isNaN( numberInName)) {
            numberInName = 0;
         } else {
            numberInName = Number( numberInName);
         }
         Spring.nameIndex = Math.max( Spring.nameIndex, numberInName);
      } else {
         Spring.nameIndex += 1;
         this.name = 's' + Spring.nameIndex;
      }
      //console.log("n-spring = " + Spring.nameIndex + ',' + this.name);
      
      gW.aT.springMap[this.name] = this;
      
      this.color = setDefault( pars.color, "red");
      this.visible = setDefault( pars.visible, true);
      this.length_m = setDefault( pars.length_m, 0.0);
      this.strength_Npm = setDefault( pars.strength_Npm, 0.5);
      this.unstretched_width_m = setDefault( pars.unstretched_width_m, 0.025);
      
      // Note that pucks have an attribute linDamp, with an effect similar to drag_c. Both can be
      // used to model a drag force on the pucks at the end of the spring.
      this.drag_c = setDefault( pars.drag_c, 0.0);
      this.damper_Ns2pm2 = setDefault( pars.damper_Ns2pm2, 0.5);
      
      this.dashedLine = setDefault( pars.dashedLine, false);
      this.roundedEnds = setDefault( pars.roundedEnds, true);
      this.navigationForNPC = setDefault( pars.navigationForNPC, false);
      this.forCursor = setDefault( pars.forCursor, false);
      
      // Spring-puck/pin Object (spo1, not p1). Giving this a distinctive name so that it can be filtered
      // out in the JSON capture. This filtering avoids some wordiness in the capture.
      this.spo1 = puckOrPin1;
      this.p1_name = puckOrPin1.name;
      // Pin one end of the spring to a fixed location.
      if (this.spo1.constructor.name == "Pin") {
         this.pinned = true;
      } else {
         this.pinned = false;
      }
      // local point where spring is attached on spo1
      this.spo1_ap_l_2d_m = setDefault( pars.spo1_ap_l_2d_m, new Vec2D(0,0));
      
      // Same reasoning here for the distinctive name (spo2, not p2).
      this.spo2 = puckOrPin2;
      this.p2_name = puckOrPin2.name;
      // Pin one end of the spring to a fixed location.
      if (this.spo2.constructor.name == "Pin") {
         this.pinned = true;
      } else {
         this.pinned = false;
      }
      // local point where spring is attached on spo2
      this.spo2_ap_l_2d_m = setDefault( pars.spo2_ap_l_2d_m, new Vec2D(0,0));
      
      this.p1p2_separation_2d_m = new Vec2D(0,0);
      this.p1p2_separation_m = 0;
      this.p1p2_normalized_2d = new Vec2D(0,0);
      
      // To model the spring as a distance joint in b2d. Don't allow this
      // for the navigation springs.
      this.b2d = null;
      this.softContraints = setDefault( pars.softContraints, gW.c.softContraints_default);
      this.collideConnected = setDefault( pars.collideConnected, true);
      
      if (this.softContraints && ( ! this.navigationForNPC)) {
         this.createDistanceJoint();
      } else if (this.navigationForNPC) {
         this.softContraints = false;
      }
   }
   Spring.nameIndex = 0;
   Spring.applyToAll = function ( doThis) {
      // Run the doThis code on each spring.
      for (var springName in gW.aT.springMap) {
         var spring = gW.aT.springMap[ springName];
         doThis( spring);
      }
   }
   Spring.deleteAll = function () {
      // If any of these springs are b2d distance joints, remove these from the b2d world.
      Spring.applyToAll( spring => {
         if (spring.softContraints) {
            gW.world.DestroyJoint( spring.b2d);
            spring.b2d = null;
         }
      });
      gW.aT.springMap = {};
      Spring.nameIndex = 0;
   }
   Spring.findAll_InMultiSelect = function ( doThis) {
      // Find all the springs that have both ends (puck or pin) in the multi-select map.
      // Then run the doThis function that has been passed in here.
      Spring.applyToAll( spring => {
         if ((spring.spo1.name in gW.hostMSelect.map) && (spring.spo2.name in gW.hostMSelect.map)) {
            // For each spring you find.
            doThis( spring);
         }
      });
   }
   Spring.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   Spring.prototype.constructor = Spring; // Rename the constructor (after inheriting)
   Spring.prototype.createDistanceJoint = function() {
      var distance_joint = new b2DistanceJointDef;
      
      // Identify the connected bodies.
      distance_joint.bodyA = this.spo1.b2d;
      distance_joint.bodyB = this.spo2.b2d;
      
      // Connect to the attachment point on each body.
      distance_joint.localAnchorA = b2Vec2_from_Vec2D( this.spo1_ap_l_2d_m);
      distance_joint.localAnchorB = b2Vec2_from_Vec2D( this.spo2_ap_l_2d_m);
      
      // Initialize the soft constraints.
      distance_joint.length = this.length_m;
      distance_joint.frequencyHz  = 1.0;
      distance_joint.dampingRatio = 0.0;
      
      // Will the connected bodies collide?
      distance_joint.collideConnected = this.collideConnected;

      // Add the joint to the world. And keep a reference to it here (this spring) as b2d.
      this.b2d = gW.world.CreateJoint( distance_joint);
      
      // Update it to reflect the traditional spring parameters and the effective mass.
      this.updateB2D_spring();
   }
   Spring.prototype.updateB2D_spring = function() {
      // Use the smaller of the two pucks in the frequency calculation.
      var smallerMass_kg = 10000;
      if (this.spo1.constructor.name == 'Puck') smallerMass_kg = Math.min(this.spo1.mass_kg, smallerMass_kg);
      if (this.spo2.constructor.name == 'Puck') smallerMass_kg = Math.min(this.spo2.mass_kg, smallerMass_kg);
      
      this.b2d.SetLength( this.length_m);
      
      // The frequency and damping ratio expressions are based on the equations on page 45 of this
      // presentation by Erin Catto.
      // http://box2d.org/files/GDC2011/GDC2011_Catto_Erin_Soft_Constraints.pdf
      
      // omega = (k/m)^0.5
      // f = omega / 2Pi = (k/m)^0.5 / 2Pi
      var freq_hz = Math.sqrt( this.strength_Npm/ smallerMass_kg)/(2.0 * Math.PI);
      this.b2d.SetFrequency( freq_hz);
      
      // dampingRatio = c / (2 * m * omega)
      var dampingRatio = this.damper_Ns2pm2 / (2.0 * smallerMass_kg * (2.0 * Math.PI * this.b2d.GetFrequency()));
      var dampingRatio_tweaked = dampingRatio /1.0 ;
      this.b2d.SetDampingRatio( dampingRatio_tweaked);
   }
   Spring.prototype.deleteThisOne = function( pars) {
      var deleteMode = setDefault( pars.deleteMode, null);
      
      if (this.softContraints) {
         gW.world.DestroyJoint( this.b2d);
         this.b2d = null;
      }
      
      if (this.navigationForNPC) {
         // Dissociate the NPC puck from the navigation pin. Do this to prevent the 
         // navigation spring from regenerating when the capture is restored.
         // Also disable the jet, since the NPC puck won't be motoring until attached to navigation again.
         if (this.spo1.constructor.name == "Puck") {
            this.spo1.pinName = null;
            this.spo1.parsAtBirth.pinName = null;
            this.spo1.disableJet = true;
         }
         if (this.spo2.constructor.name == "Puck") {
            this.spo2.pinName = null;
            this.spo2.parsAtBirth.pinName = null;
            this.spo2.disableJet = true;
         }
      }
      
      // Remove this spring from the spring map.
      delete gW.aT.springMap[ this.name];
   }
   Spring.prototype.copyThisOne = function(p1, p2, copyMode) {
      copyMode = setDefault( copyMode, "regular");
      
      // Make a copy of the mutable objects that are passed into the Spring constructor.
      var pars = Object.assign({}, this.parsAtBirth);
      // Null the name so the auto-naming feature is used in the constructor.
      pars.name = null;
      pars.length_m = this.length_m;
      pars.unstretched_width_m = this.unstretched_width_m
      pars.strength_Npm = this.strength_Npm;
      pars.damper_Ns2pm2 = this.damper_Ns2pm2;
      // factor for drag force on attached pucks (proportional to velocity)
      pars.drag_c = this.drag_c;
      
      // Set local attachment points when pasting a spring.
      if (copyMode == "pasteSingle") {
         if (gW.dC.comSelection.checked) {
            pars.spo1_ap_l_2d_m = new Vec2D(0,0);
            pars.spo2_ap_l_2d_m = new Vec2D(0,0);
         } else {
            // Always paste onto the center of a pin.
            pars.spo1_ap_l_2d_m = (p1.constructor.name == "Pin") ? new Vec2D(0,0) : p1.selectionPoint_l_2d_m;
            pars.spo2_ap_l_2d_m = (p2.constructor.name == "Pin") ? new Vec2D(0,0) : p2.selectionPoint_l_2d_m;
         }
      }
      // Note that this instantiation adds this new spring to the spring map. 
      var tempSpring = new Spring( p1, p2, pars);
      
      // Also enable the jet for NPC pucks, since the NPC puck will be motoring now that it is attached to navigation again.
      if (tempSpring.navigationForNPC) {
         if (tempSpring.spo1.constructor.name == "Puck") {
            tempSpring.spo1.disableJet = false;
         }
         if (tempSpring.spo2.constructor.name == "Puck") {
            tempSpring.spo2.disableJet = false;
         }
      }
      
      return tempSpring.name;
   }
   Spring.prototype.modify_fixture = function( mode) {
      var width_factor = 1.0;
      var length_factor = 1.0;
      var damping_factor = 1.0;
      
      if (mode == 'wider') {
         width_factor = 1.1;
      } else if (mode == 'thinner') {
         width_factor = 0.9;         
      } else if (mode == 'taller') {
         length_factor = 1.1;
      } else if (mode == 'shorter') {
         length_factor = 0.9;         
      } else if (mode == 'moreDamping') {
         damping_factor = 1.1;
      } else if (mode == 'lessDamping') {
         damping_factor = 0.9;
      } else if (mode == 'noChange') {
         // don't change anything.
      }
      
      // First, the special case of the pinned puck that is using a zero length spring. Give
      // it a little length to start with, otherwise the zero will always scale to zero (it will never
      // get longer). 
      if (mode=='shorter' || mode=='taller') {
         if (this.length_m == 0.0) this.length_m = 0.1;
         this.length_m *= length_factor;
         if (this.length_m < 0.1) this.length_m = 0.0;
         gW.messages['help'].newMessage('length = ' + this.length_m.toFixed(4), 0.5);
         
      } else if (mode=='thinner' || mode=='wider') {
         // Use the wider/thinner width_factor to affect both the visual width and strength of the spring.
         this.unstretched_width_m *= width_factor;
         this.strength_Npm *= width_factor;
         gW.messages['help'].newMessage('k = ' + this.strength_Npm.toFixed(4), 0.5);
         
      } else if (mode=='lessDamping' || mode=='moreDamping') {
         // If at zero, give the scaling factor something to work with.
         if (this.damper_Ns2pm2 == 0.0) this.damper_Ns2pm2 = 0.1;
         // Apply the scaling factor.
         this.damper_Ns2pm2 *= damping_factor;
         // A lower limit.
         if (this.damper_Ns2pm2 < 0.1) this.damper_Ns2pm2 = 0.0;
         gW.messages['help'].newMessage('spring damping = ' + this.damper_Ns2pm2.toFixed(4), 0.5);
      }
      
      // If you're using a distance joint in Box2D...
      if (this.softContraints) {
         this.updateB2D_spring();
      }
   }
   Spring.prototype.force_on_pucks = function() {
      /*
      If springs are modeled with Hooke's law, determine all the forces 
      (related to the spring) that act on the two attached bodies. This 
      includes forces acting at the attachment points and those acting at the 
      COMs. Calculate:
      -- separation distance (length) and vector between the two attachment points for calculating the spring forces
      -- relative speed of the attachment points for use in calculating the damping forces
      -- absolute speed of each attachment point for use in calculating drag forces
      
      Some of this is also needed for drawing the springs modeled as distance 
      joints (in Box2D engine).
      */
      // Find the world position of the attachment points.
      if ((this.spo1_ap_l_2d_m.x != 0) || (this.spo1_ap_l_2d_m.y != 0)) {
         this.spo1_ap_w_2d_m = Vec2D_from_b2Vec2( this.spo1.b2d.GetWorldPoint( this.spo1_ap_l_2d_m));
      } else {
         this.spo1_ap_w_2d_m = this.spo1.position_2d_m;
      }
      this.spo1_ap_w_2d_px = screenFromWorld( this.spo1_ap_w_2d_m);
      if ((this.spo2_ap_l_2d_m.x != 0) || (this.spo2_ap_l_2d_m.y != 0)) {
         this.spo2_ap_w_2d_m = Vec2D_from_b2Vec2( this.spo2.b2d.GetWorldPoint( this.spo2_ap_l_2d_m));
      } else {
         this.spo2_ap_w_2d_m = this.spo2.position_2d_m;
      }
      this.spo2_ap_w_2d_px = screenFromWorld( this.spo2_ap_w_2d_m);
      
      // Separation vector and its length:
      // Need these two results for both distance joints and regular springs: 
      this.p1p2_separation_2d_m = this.spo1_ap_w_2d_m.subtract( this.spo2_ap_w_2d_m);
      this.p1p2_separation_m = this.p1p2_separation_2d_m.length();
      
      //  If not using the native spring modeling (distance joints) in b2d, calculate the spring and damping forces.
      if ( ! this.softContraints) {
         /*
         First, calculate the forces that don't necessarily act on the center of the body, non COM.
         
         The pinned case needs to be able to handle the zero length spring. The 
         separation distance will be zero when the pinned spring is at rest.
         This will cause a divide by zero error if not handled here.
         
         The second clause in this if statement checks for use of the editor, 
         the control key. Block cursor-spring forces when doing deterministic 
         movements. This only blocks traditional springs. If in distance-joint 
         mode, the cursor movement will drag the selected puck some (a little) 
         even when control key is down (and using shift or alt keys for 
         rotation). 
         */
         if (((this.p1p2_separation_m == 0.0) && (this.length_m == 0.0)) || 
             ((gW.clients['local'].key_ctrl == "D") && this.forCursor) ) {
            var spring_force_on_1_2d_N = new Vec2D(0.0,0.0);
         } else {
            this.p1p2_normalized_2d = this.p1p2_separation_2d_m.scaleBy( 1/this.p1p2_separation_m);
            // Spring force:  acts along the separation vector and is proportional to the separation distance.
            var spring_force_on_1_2d_N = this.p1p2_normalized_2d.scaleBy( (this.length_m - this.p1p2_separation_m) * this.strength_Npm);
         }
         
         /*
         These non-COM spring forces must be applied individually, at the
         attachment points. That's why these are appended to the puck's 
         nonCOM_2d_N force array. This array is reset (emptied) after the 
         movements are calculated in the physics engine.
         */
         
         if (this.spo1.constructor.name == "Puck") {
            this.spo1.nonCOM_2d_N.push({'force_2d_N': spring_force_on_1_2d_N.scaleBy( +1), 'point_w_2d_m': this.spo1_ap_w_2d_m});   
            /*
            The following vector is used for aiming the NPC's navigation jets. (Note 
            navigation springs are always conventional springs.) Check to see that 
            this is on a navigation pin before updating navSpringOnly_force_2d_N. We 
            only want the navigation spring force to be affecting the drawing of the 
            navigation jet. This will exclude other springs, like cursor springs, 
            from affecting the jet representation.
            */
            if ((this.spo2.constructor.name == "Pin") && (this.spo2.NPC)) this.spo1.navSpringOnly_force_2d_N = spring_force_on_1_2d_N.scaleBy( +1);
         }
         if (this.spo2.constructor.name == "Puck") {
            this.spo2.nonCOM_2d_N.push({'force_2d_N': spring_force_on_1_2d_N.scaleBy( -1), 'point_w_2d_m': this.spo2_ap_w_2d_m});   
            // (see explanation in spo1 block above)
            if ((this.spo1.constructor.name == "Pin") && (this.spo1.NPC)) this.spo2.navSpringOnly_force_2d_N = spring_force_on_1_2d_N.scaleBy( -1);
         }
         
         // Damper force: acts along the separation vector and is proportional to the relative speed.
         // First, get the velocity at each attachment point.
         var v_spo1_ap_2d_mps = Vec2D_from_b2Vec2( this.spo1.b2d.GetLinearVelocityFromWorldPoint( this.spo1_ap_w_2d_m));
         var v_spo2_ap_2d_mps = Vec2D_from_b2Vec2( this.spo2.b2d.GetLinearVelocityFromWorldPoint( this.spo2_ap_w_2d_m));
         
         var v_relative_2d_mps = v_spo1_ap_2d_mps.subtract( v_spo2_ap_2d_mps);
         var v_relative_alongNormal_2d_mps = v_relative_2d_mps.projection_onto( this.p1p2_separation_2d_m);
         if (v_relative_alongNormal_2d_mps == null) v_relative_alongNormal_2d_mps = v_relative_2d_mps.scaleBy(0.0);
         
         var damper_force_on_1_2d_N = v_relative_alongNormal_2d_mps.scaleBy( this.damper_Ns2pm2);
         // This damper force acts in opposite directions for each of the two pucks. 
         if (this.spo1.constructor.name == "Puck") {
            // Again, notice the negative sign here, opposite to the spring force.
            this.spo1.nonCOM_2d_N.push({'force_2d_N': damper_force_on_1_2d_N.scaleBy( -1), 'point_w_2d_m': this.spo1_ap_w_2d_m});
         }
         if (this.spo2.constructor.name == "Puck") {
            this.spo2.nonCOM_2d_N.push({'force_2d_N': damper_force_on_1_2d_N.scaleBy( +1), 'point_w_2d_m': this.spo2_ap_w_2d_m});   
         }
      }
      /* 
      The following drag forces act at the puck's COM.

      These forces are not calculated for the b2d distance joints. So, 
      need these in order to reproduce the behavior of the old cursor strings 
      (now springs). These are based on the velocity of the pucks (not 
      relative speed as is the case above for damper forces). 
      
      This adds to (vector add using addTo) the puck's sprDamp_force_2d_N 
      vector. By the time you've looped through all the springs, you get the 
      NET damping force, on each puck COM, applied by all the individual springs. 
      This aggregate is reset (zeroed) after the movements are calculated. 
      */
      if (this.spo1.constructor.name == "Puck") {
         this.spo1.sprDamp_force_2d_N.addTo( this.spo1.velocity_2d_mps.scaleBy( -1 * this.drag_c));
      }
      if (this.spo2.constructor.name == "Puck") {
         this.spo2.sprDamp_force_2d_N.addTo( this.spo2.velocity_2d_mps.scaleBy( -1 * this.drag_c));
      }
   }
   Spring.prototype.draw = function( drawingContext) {
      if ((this.navigationForNPC && gW.dC.editor.checked) || (!this.visible && gW.dC.editor.checked) || (this.visible && !this.navigationForNPC)) {
         // These two width calculations will cause some discontinuity in how the springs look if they are being
         // length adjusted between zero and non-zero, especially for a puck in gravity on a zero-length spring. It's a compromise.
         if (this.length_m == 0) {
            // This version looks better for zero-length (pinned pucks)
            var width_m = this.unstretched_width_m * (1 - (0.40 * this.p1p2_separation_m));
         } else {
            // This version of the width calculation conserves the area of the spring.
            var width_m = (this.unstretched_width_m * this.length_m) / this.p1p2_separation_m;
         }
         // Prevent the width value from getting too large.
         if (width_m > (3 * this.unstretched_width_m)) width_m = 3 * this.unstretched_width_m;
         
         var width_px = px_from_meters( width_m);
         if (width_px < 2) width_px = 2;
         
         if (this.dashedLine) {
            var dashArray = [3];
            // Must use the default 'butt' ends if the lines are dashed.
            // Note: dashed lines require surprising CPU drain.
            var lineCap = 'butt';
         } else {
            var dashArray = [0];
            // If not dashed, you can use the fancy 'round' ends. Nice.
            var lineCap = 'round';
         }
         
         var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? 'white' : this.color;
         this.drawLine(drawingContext, this.spo1_ap_w_2d_px, this.spo2_ap_w_2d_px,
            {'width_px':width_px, 'color':fillColor, 'dashArray':dashArray, 'alpha':0.7, 'lineCap':lineCap} );
      }
   }
   
   
   
   function Wall( position_2d_m, pars) {
      DrawingFunctions.call(this); // inherit
      
      this.parsAtBirth = pars;
      //this.alsoThese = [];
      
      if (pars.name) {
         this.name = pars.name;
         // Set nameIndex to the max of the two indexes. Do this to avoid issues related to holes
         // in the name sequence caused by state captures after object deletions. This insures a
         // unique new name for any new wall.
         Wall.nameIndex = Math.max(Wall.nameIndex, Number(this.name.slice(4)));
      } else {
         Wall.nameIndex += 1;
         this.name = 'wall' + Wall.nameIndex; 
      }
      //console.log("n-wall = " + Wall.nameIndex);
      gW.aT.wallMap[this.name] = this;
      
      // Position of Center of Mass (COM)
      this.position_2d_m = Vec2D_check( position_2d_m);
      this.position_2d_px = screenFromWorld( this.position_2d_m);
      
      this.fence = setDefault( pars.fence, false);
      this.fenceLeg = setDefault( pars.fenceLeg, null);
      if (this.fenceLeg == 'top') Wall.topFenceLegName = this.name; // For use in piCalcEngine
      
      this.velocity_2d_mps = setDefault( pars.velocity_2d_mps, new Vec2D(0.0, 0.0));
      this.angle_r = setDefault( pars.angle_r, 0.0);
      this.angularSpeed_rps = setDefault( pars.angularSpeed_rps, 0.0);
      
      // Dimensions (as specified in box2D)
      this.half_width_m  = setDefault( pars.half_width_m , 0.5);
      this.half_height_m = setDefault( pars.half_height_m, 2.5);
      
      // Calculate these characteristics in screen units (pixels).
      this.half_width_px = px_from_meters( this.half_width_m);
      this.half_height_px = px_from_meters( this.half_height_m);
      
      this.b2d = null;
      this.create_b2d_wall();
      // Create a reference back to this wall from the b2d wall.
      gW.tableMap.set(this.b2d, this);

      Wall.color_default = "white";
      this.color = setDefault( pars.color, Wall.color_default);
      
      this.deleted = false;
   }
   Wall.nameIndex = 0;
   Wall.topFenceLegName = null; // For use in piCalcEngine
   Wall.applyToAll = function( doThis) {
      for (var wallName in gW.aT.wallMap) {
         var wall = gW.aT.wallMap[ wallName];
         doThis( wall);
      }
   }
   Wall.deleteAll = function() {
      Wall.applyToAll( wall => {
         gW.tableMap.delete( wall.b2d);
         if (wall.b2d) gW.world.DestroyBody( wall.b2d);
      });
      gW.aT.wallMap = {};
      Wall.nameIndex = 0;
   }
   Wall.makeFence = function( pars = {}, canvas) {
      // Build perimeter fence (4 walls) using the canvas dimensions.
      
      var width_m  = meters_from_px( canvas.width );
      var half_width_m = width_m / 2.0;
      
      var height_m = meters_from_px( canvas.height);
      var half_height_m = height_m / 2.0;
      
      var wall_thickness_m = 0.10;
      
      var pull_in_m = 0.0;
      
      // By default, all four walls of the fence are generated.
      var tOn = setDefault( pars.tOn, true);
      var bOn = setDefault( pars.bOn, true);
      var lOn = setDefault( pars.lOn, true);
      var rOn = setDefault( pars.rOn, true);
      
      var short_wide_dimensions  = {'fence':true, 'half_width_m':half_width_m,         'half_height_m':wall_thickness_m/2.0};
      var tall_skinny_dimensions = {'fence':true, 'half_width_m':wall_thickness_m/2.0, 'half_height_m':half_height_m};
            
      // Add four bumper walls to the table.
      // top
      if (tOn) new Wall( new Vec2D( half_width_m, height_m - pull_in_m), Object.assign({'fenceLeg':'top'}, short_wide_dimensions) );
      // bottom
      if (bOn) new Wall( new Vec2D( half_width_m,     0.00 + pull_in_m), short_wide_dimensions);
      // left
      if (lOn) new Wall( new Vec2D(    0.00 + pull_in_m, half_height_m), tall_skinny_dimensions);
      // right
      if (rOn) new Wall( new Vec2D( width_m - pull_in_m, half_height_m), tall_skinny_dimensions);
   }
   Wall.deleteFence = function() {
      Wall.applyToAll( wall => {
         if (wall.fence) {
            delete gW.aT.wallMap[ wall.name];
            gW.tableMap.delete( wall.b2d);
            if (wall.b2d) gW.world.DestroyBody( wall.b2d);
         }
      });
   }
   Wall.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
   Wall.prototype.constructor = Wall; // Rename the constructor (after inheriting)
   Wall.prototype.deleteThisOne = function( pars) {
      var deleteMode = setDefault( pars.deleteMode, null);
      
      // Delete reference in the tableMap.
      gW.tableMap.delete( this.b2d);
      
      // Delete the corresponding Box2d object.
      gW.world.DestroyBody( this.b2d);
      
      // Mark this wall as deleted.
      this.deleted = true;
      
      // Remove this wall from the wall map.
      delete gW.aT.wallMap[ this.name];
      // ...and from the multi-select map.
      gW.hostMSelect.removeOne( this);
   }
   Wall.prototype.copyThisOne = function( pars) {
      var position_2d_m = setDefault( pars.position_2d_m, this.position_2d_m);
      
      return new Wall( position_2d_m, 
                        {'half_width_m':this.half_width_m, 
                         'half_height_m':this.half_height_m, 
                         'angle_r':this.angle_r, 
                         'angularSpeed_rps':this.angularSpeed_rps});
   }
   Wall.prototype.define_fixture = function( pars) {
      this.width_scaling = setDefault( pars.width_scaling, 1.0);
      this.height_scaling = setDefault( pars.height_scaling, 1.0);
      
      var fixDef = new b2FixtureDef;      
      fixDef.shape = new b2PolygonShape;
      
      // Apply the scaling factors to the current width and height.
      this.half_width_m *= this.width_scaling;
      this.half_height_m *= this.height_scaling;
      
      this.half_width_px = px_from_meters( this.half_width_m);
      // Don't let it get too skinny because it becomes hard to select.
      if (this.half_width_px < 1) {
         this.half_width_px = 1;
         this.half_width_m = meters_from_px( this.half_width_px);
      }
      
      this.half_height_px = px_from_meters( this.half_height_m);
      if (this.half_height_px < 1) {
         this.half_height_px = 1;
         this.half_height_m = meters_from_px( this.half_height_px);
      }
      
      fixDef.shape.SetAsBox(this.half_width_m, this.half_height_m);
      
      return fixDef;
   }
   Wall.prototype.create_b2d_wall = function() {
      // Create a rectangular and static box2d object.
      
      var bodyDef = new b2BodyDef;
      bodyDef.type = b2Body.b2_kinematicBody; // b2_kinematicBody b2_staticBody
      
      this.b2d = gW.world.CreateBody( bodyDef);
      this.b2d.CreateFixture( this.define_fixture({}));
      
      // Set the state: position and velocity (angle and angular speed).
      this.b2d.SetPosition( this.position_2d_m);
      this.b2d.SetLinearVelocity( this.velocity_2d_mps);
      this.b2d.SetAngle( this.angle_r);
      this.b2d.SetAngularVelocity( this.angularSpeed_rps);
   }
   Wall.prototype.modify_fixture = function( mode) {
      // If you are going to modify the fixture dimensions you have to delete
      // the old one and make a new one. The m_fixtureList linked list always
      // points to the most recent addition to the linked list. If there's only
      // one fixture, then m_fixtureList is a reference to that single fixture.
      
      var width_factor = 1.0;
      var height_factor = 1.0;
      
      if (mode == 'wider') {
         width_factor = 1.1;
      } else if (mode == 'thinner') {
         width_factor = 0.9;         
      } else if (mode == 'taller') {
         height_factor = 1.1;
      } else if (mode == 'shorter') {
         height_factor = 0.9;         
      }
      
      this.b2d.DestroyFixture( this.b2d.m_fixtureList);
      this.b2d.CreateFixture( this.define_fixture({'width_scaling':width_factor,'height_scaling':height_factor}));
   }
   Wall.prototype.draw_MultiSelectPoint = function( drawingContext) {
      this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5});
   }
   Wall.prototype.getPosition = function() {
      this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition());
      this.position_2d_px = screenFromWorld( this.position_2d_m);
   }
   Wall.prototype.updateState = function() {   
      this.getPosition();
      this.angle_r = this.b2d.GetAngle();
   }   
   Wall.prototype.draw = function( drawingContext) {
      this.drawPolygon( drawingContext, gW.b2d_getPolygonVertices( this.b2d), {'borderColor':this.color, 'borderWidth_px':0, 'fillColor':this.color});
   }
   

   
   function RunningAverage( n_target) {
      this.n_target = n_target;
      this.reset();
   }
   RunningAverage.prototype.reset = function() {
      this.n_in_avg = 0;
      this.result = 0.0;
      this.values = [];
      this.total = 0.0;
      this.totalSinceReport = 0.0;
   }
   RunningAverage.prototype.update = function( new_value) {
      // Only process good stuff.
      if (new_value) {
         if (this.n_in_avg < this.n_target) {
            this.total += new_value;
            this.n_in_avg += 1;
         } else {
            // Add the new value and subtract the oldest.
            this.total += new_value - this.values[0];
            // Discard the oldest value.
            this.values.shift();
         }   
         this.values.push( new_value);

         this.totalSinceReport += new_value;
         
         this.result = this.total / this.n_in_avg;
         return this.result;
         
      } else {
         return new_value;
      }
   }
   
   
   
   function SoundEffect( filePath, nCopies) {      
      this.copies = [];
      this.nCopies = nCopies;
      this.index = 0;

      for (var i = 0; i < nCopies; i++) {
         this.copies.push(new Audio( filePath));
      }      
   }
   SoundEffect.prototype.play = function( soundType) {
      // The play method returns a promise
      var playPromise = this.copies[this.index].play();
      playPromise.then( function() {
         //console.log('sound worked');
      }).catch( function(error) {
         //console.log('sound failed');
         console.log(error.name + ': ' + error.message);
         if (error.name == 'NotAllowedError') {
            gW.messages['help'].newMessage('Sound effects are disabled until you type or click.\\  Go ahead; interact.', 2.0);
         }
      });
      if (this.index < this.nCopies - 1) {
         this.index++;
      } else {
         this.index = 0;
      }  
   }
   
   
   
   function PiEngine( puck1, puck2, clackSound, pars = {} ) {
      // A 1D engine for calculating the digits of pi by counting the collisions of
      // of two pucks. Pucks must have a mass ratio of 100^(d-1), where d is the number
      // of digits of pi to be determined. Smaller puck is between the wall and the larger puck.
      this.p1 = puck1;
      this.p2 = puck2;
      this.clackSound = clackSound;
      // 1000 works well for up to 5 digits of pi.
      this.nFinerTimeStepFactor = 1000;
      
      this.lastCollidedWithWall = setDefault( pars.lastCollidedWithWall, true);
      this.atLeastOneCollisionInFrame = setDefault( pars.atLeastOneCollisionInFrame, false);
      this.p1_v_max = setDefault( pars.p1_v_max, 0);
      this.collisionCount = setDefault( pars.collisionCount, 0);
   }
   // This state object is used in the capture and restore process...
   PiEngine.state = {};
   PiEngine.prototype.step = function( dt_oneFrame_s) {      
      this.dt_s = dt_oneFrame_s / this.nFinerTimeStepFactor;
      
      this.atLeastOneCollisionInFrame = false;
      for (var i=0; i < this.nFinerTimeStepFactor; i++) {
         this.update( this.p1);
         this.update( this.p2);
         
         this.checkForCollisions();
      }
      if (this.atLeastOneCollisionInFrame) {
         this.clackSound.play();
         this.report();
      } 
   }
   PiEngine.prototype.update = function( puck) {
      puck.position_2d_m.y += puck.velocity_2d_mps.y * this.dt_s;
   }
   PiEngine.prototype.checkForCollisions = function() {
      // Note that the lastCollidedWithWall logical mandates alternation between wall and puck collisions.
      // Check pucks for puck-puck collisions
      if (((this.p1.position_2d_m.y - this.p1.radius_m) < (this.p2.position_2d_m.y + this.p2.radius_m)) && (this.lastCollidedWithWall)) {
         this.lastCollidedWithWall = false;
         this.puckCollisionResult();
         this.countit();
      }
      // Check puck1 for collisions with the top leg of the fence.
      var botEdgeTopWall_y_m = gW.aT.wallMap[ Wall.topFenceLegName].position_2d_m.y - gW.aT.wallMap[ Wall.topFenceLegName].half_height_m;
      if ( ((this.p1.position_2d_m.y + this.p1.radius_m) > botEdgeTopWall_y_m) && (!this.lastCollidedWithWall) ) {
         this.lastCollidedWithWall = true;
         this.p1.velocity_2d_mps.y *= -1.0;
         this.countit();
      }
   }
   PiEngine.prototype.countit = function() {
      this.atLeastOneCollisionInFrame = true;
      this.collisionCount += 1;
   }
   PiEngine.prototype.report = function() {
      gW.messages['help'].newMessage("count = " + this.collisionCount + "\\v max = " + this.p1_v_max.toFixed(1));
   }
   PiEngine.prototype.puckCollisionResult = function() {
      var CR = 1.0; 
      var p1_v_y =  ( (CR * this.p2.mass_kg * (this.p2.velocity_2d_mps.y - this.p1.velocity_2d_mps.y) +  
                            this.p1.mass_kg *  this.p1.velocity_2d_mps.y +  
                            this.p2.mass_kg *  this.p2.velocity_2d_mps.y) / (this.p1.mass_kg + this.p2.mass_kg) );
   
      var p2_v_y =  ( (CR * this.p1.mass_kg * (this.p1.velocity_2d_mps.y - this.p2.velocity_2d_mps.y) +  
                            this.p1.mass_kg *  this.p1.velocity_2d_mps.y +  
                            this.p2.mass_kg *  this.p2.velocity_2d_mps.y) / (this.p1.mass_kg + this.p2.mass_kg) );
      
      this.p1_v_max = Math.max( p1_v_y, this.p1_v_max);
      
      this.p1.velocity_2d_mps.y = p1_v_y;
      this.p2.velocity_2d_mps.y = p2_v_y;
   }
      
      
   
   function DrawingFunctions(){
      // High-level functions for drawing to the canvas element
   }
   DrawingFunctions.prototype.drawLine = function( drawingContext, p1_2d_px, p2_2d_px, pars) {
      drawingContext.strokeStyle = setDefault( pars.color, 'white');
      drawingContext.lineWidth = setDefault( pars.width_px, 2);
      var dashArray = setDefault( pars.dashArray, [0]);
      var alpha = setDefault( pars.alpha, 1.0);
      var lineCap = setDefault( pars.lineCap, 'butt');
      
      drawingContext.globalAlpha = alpha;
      drawingContext.setLineDash( dashArray);
      drawingContext.lineCap = lineCap;
      
      drawingContext.beginPath();
      
      drawingContext.moveTo(p1_2d_px.x, p1_2d_px.y);
      drawingContext.lineTo(p2_2d_px.x, p2_2d_px.y);
      
      drawingContext.stroke();
      drawingContext.globalAlpha = 1.0;
      drawingContext.lineCap = 'butt';
   }
   DrawingFunctions.prototype.drawCircle = function( drawingContext, center_2d_px, pars) {
      drawingContext.strokeStyle = setDefault( pars.borderColor, 'white');
      drawingContext.lineWidth = setDefault( pars.borderWidth_px, 2);
      var radius_px = setDefault( pars.radius_px, 6);
      var fillColor = setDefault( pars.fillColor, 'red');
      var dashArray = setDefault( pars.dashArray, [0]);
      
      drawingContext.setLineDash( dashArray);
      
      drawingContext.beginPath();
      
      drawingContext.arc(center_2d_px.x, center_2d_px.y, radius_px, 0, 2 * Math.PI);
      
      if (fillColor != 'noFill') {
         drawingContext.fillStyle = fillColor;
         drawingContext.fill();
      }
      if (pars.borderWidth_px > 0) {
         drawingContext.stroke();
      }
      
      // Turn off the dashes
      drawingContext.setLineDash([0]);
   }
   DrawingFunctions.prototype.drawPolygon = function( drawingContext, poly_px, pars) {
      drawingContext.strokeStyle = setDefault( pars.borderColor, 'white');
      drawingContext.lineWidth = setDefault( pars.borderWidth_px, 2);
      drawingContext.fillStyle = setDefault( pars.fillColor,  'red');
      var fillIt = setDefault( pars.fillIt, true);
      
      drawingContext.setLineDash([0]);
      
      drawingContext.beginPath();
      
      drawingContext.moveTo(poly_px[0].x, poly_px[0].y);
      for (var i = 1, len = poly_px.length; i < len; i++) {
         drawingContext.lineTo(poly_px[i].x, poly_px[i].y);
      }
      
      drawingContext.closePath();
      if (fillIt) drawingContext.fill();
      drawingContext.stroke();
   }
   DrawingFunctions.prototype.fillRectangle = function( drawingContext, upperLeft_2d_px, pars) {
      // Draw solid rectangle.
      var width_px  = setDefault( pars.width_px, 6);
      var height_px = setDefault( pars.height_px, width_px); // default is square
      drawingContext.fillStyle = setDefault( pars.fillColor, 'red');
      //                       -----------upper left corner--------
      drawingContext.fillRect( upperLeft_2d_px.x, upperLeft_2d_px.y, width_px, height_px);
   }
   
   
   return {
      Vec2D: Vec2D,
      HelpMessage: HelpMessage,
      MultiSelect: MultiSelect,
      SelectBox: SelectBox,
      
      Client: Client,
      Puck: Puck,
      Shield: Shield,
      Spring: Spring,
      Wall: Wall,
      Pin: Pin,
      
      RunningAverage: RunningAverage,
      SoundEffect: SoundEffect,
      PiEngine: PiEngine,
      
      DrawingFunctions: DrawingFunctions
      
   };
   
})();