// Game Window (gW) Module // Version 1.3.8 (9:28 PM Thu December 7, 2017) // Written by: James D. Miller // The demos, and their multi-player functionality, are dependent on two additional // JavaScript modules: hostAndClient.js (referenced here as hC) and server.js. // Discussion and links to these files are at http://www.timetocode.org/multiplayer.html var gW = (function() { // To insist on tighter code: e.g. globals, etc... "use strict"; // Short names for Box2D functions 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 , b2DebugDraw = Box2D.Dynamics.b2DebugDraw , b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef , b2MassData = Box2D.Collision.Shapes.b2MassData , b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape , b2CircleShape = Box2D.Collision.Shapes.b2CircleShape , b2AABB = Box2D.Collision.b2AABB ; // Common variables inside of gW (game window) ////////////////////////////// var g_2d_mps2, g_mps2 = 9.8; // The Air Table (aT): a place to call home for pucks, pins, springs, and walls. var aT = {}; aT.puckMap = {}; // keyed by puck name. aT.jelloPucks = []; // An array for use in testing for tangled jello. aT.pinMap = {}; // keyed by pin name. aT.springMap = {}; // keyed by spring name. aT.wallMap = {}; // keyed by wall name. // Make a separate container for constants (c) and control flags used by aT objects. This avoids // circular references (and associated problems with JSON capturing). var c = {}; c.restitution_default_gOn = 0.7; c.friction_default_gOn = 0.6; c.restitution_default_gOff = 1.0; c.friction_default_gOff = 0.1; c.g_ON = false; // This 60 corresponds with the selected (default) value on the index.html page. c.frameRate = 60.0; // Seconds per frame c.deltaT_s = 1.0/c.frameRate; c.dtFloating = false; c.demoIndex = null; //c.contactCounter = 0; c.tangleTimer_s = 0; c.pause_NPC_navigation = false; c.chatLayoutState = 'notSetYet'; c.singleStep = false; // Client map keyed by client name. var clients = {}; var tableMap = new Map(); // Special map where keys can be objects. var world, worldAABB; var myRequest, time_previous, dt_frame_ms, dt_frame_previous_ms, dt_frame_s, resumingAfterPause; var canvas, ctx, px_per_m; var helpMessage; var multiSelect; var selectBox; // Document Controls (dC). var dC = {}; dC.gravity = null; dC.pause = null; dC.comSelection = null; dC.multiplayer = null; dC.stream = null; dC.editor = null; dC.localCursor = null; // Key values. var keyMap = {'48':'key_0', '49':'key_1', '50':'key_2', '51':'key_3', '52':'key_4', '53':'key_5', '54':'key_6', '55':'key_7', '56':'key_8', '57':'key_9', '65':'key_a', '66':'key_b', '67':'key_c', '68':'key_d', '69':'key_e', '70':'key_f', '71':'key_g', '73':'key_i', '74':'key_j', '75':'key_k', '76':'key_l', '77':'key_m', '80':'key_p', '81':'key_q', '82':'key_r', '83':'key_s', '84':'key_t', '86':'key_v', '87':'key_w', '88':'key_x', '90':'key_z', '16':'key_shift', '17':'key_ctrl', '18':'key_alt', '32':'key_space', '37':'key_leftArrow', '38':'key_upArrow', '39':'key_rightArrow', '40':'key_downArrow'}; var fileName = "gwModule.js"; ///////////////////////////////////////////////////////////////////////////// //// //// Object Prototypes //// ///////////////////////////////////////////////////////////////////////////// function Vec2D(x, y) { this.x = x; this.y = y; } 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) { 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 { 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; } function HelpMessage( pars) { this.message = pars.message || ""; this.timeLimit_s = pars.timeLimit_s || 2.0; this.font = pars.font || "20px Arial"; this.color = pars.color || 'yellow'; this.loc_px = pars.loc_px || {x:30, y:40}; this.timeType = pars.timeType || 'system'; //'game' this.birthTime = window.performance.now(); this.time_s = 0.0; } HelpMessage.prototype.newMessage = function( message, timeLimit_s) { this.time_s = 0.0; this.birthTime = window.performance.now(); this.timeLimit_s = timeLimit_s; this.message = message; } HelpMessage.prototype.addToIt = function( moreText) { this.message = this.message + moreText; } HelpMessage.prototype.displayIt = function() { if (this.timeType == 'system') { this.time_s = (window.performance.now() - this.birthTime)/1000.0; } else { this.time_s += c.deltaT_s; } if ((this.message != "") && (this.time_s < this.timeLimit_s)) { ctx.font = this.font; ctx.fillStyle = this.color; ctx.fillText(this.message, this.loc_px.x, this.loc_px.y); } else { this.message = ""; this.time_s = 0; } } function MultiSelect() { this.map = null; this.center_2d_m = null; this.findCenterEnabled = null; this.resetAll(); } MultiSelect.prototype.resetAll = function() { 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( function( tableObj) { this.center_2d_m = this.center_2d_m.add( tableObj.position_2d_m); }.bind(this)); this.center_2d_m = this.center_2d_m.scaleBy( 1.0 / this.count()); //console.log('center='+ JSON.stringify(this.center_2d_m)); } MultiSelect.prototype.applyToAll = function( doThis) { for (var objName in this.map) { var tableObj = this.map[ objName]; doThis( tableObj); } } MultiSelect.prototype.removeOne = function( theBody) { // un-dash the springs Spring.findAll_InMultiSelect( function( spring) { spring.dashedLine = false; }); delete this.map[ theBody.name]; // re-dash the springs Spring.findAll_InMultiSelect( function( spring) { spring.dashedLine = true; }); } function SelectBox( pars) { this.clickPoint_2d_px = pars.clickPoint_2d_px || new Vec2D( 0, 0); this.currentMouse_2d_px = pars.currentMouse_2d_px || new Vec2D( 0, 0); this.enabled = false; this.limits = {}; } // Make this a global 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.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. world.QueryAABB( function( fixture) { var bd2_Body = fixture.GetBody(); var table_body = 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 && !(!dC.editor.checked && ((table_body.constructor.name == "Wall") || (table_body.constructor.name == "Pin")))) { objectCount += 1; // Add this body to the multiSelect map. multiSelect.map[ table_body.name] = table_body; } // Keep looking at all the fixtures found in the query. return true; }, aabb); /* Check each point in multiSelect 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 multiSelect object (setting "this") then passing it to applyToAll. The 3rd one is being used. for (var objName in multiSelect.map) { var tableObj = multiSelect.map[ objName]; if ( ! SelectBox.pointInside(tableObj.position_2d_m, this.limits)) { multiSelect.removeOne( tableObj); } } or multiSelect.applyToAll( function( tableObj) { if ( ! SelectBox.pointInside(tableObj.position_2d_m, limits)) { multiSelect.removeOne( tableObj); }; }); or Note "limits" is defined in the surrounding scope here. The "this" reference points to the multiSelect object as dictated in the call to bind method of the function that's being passed in. */ multiSelect.applyToAll( function( tableObj) { if ( ! SelectBox.pointInside(tableObj.position_2d_m, limits)) { this.removeOne( tableObj); }; }.bind( multiSelect)); } SelectBox.prototype.start = function() { this.enabled = true; this.clickPoint_2d_px = clients['local'].mouse_2d_px; } SelectBox.prototype.stop = function() { this.enabled = false; } SelectBox.prototype.update = function() { this.currentMouse_2d_px = clients['local'].mouse_2d_px; this.selectBodiesInBox(); } SelectBox.prototype.draw = function() { 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)]; drawPolygon( corners_2d_px, {'borderColor':'red', 'fillIt':false}); } function Client( pars) { this.color = pars.color || "red"; this.name = pars.name || "manWithNoName"; this.puck = null; this.isMouseDown = 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); 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; // Initialize all the key values to be Up. for (var key in keyMap) this[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 = 0.0; this.NPC_pin_timer_limit_s = 5.0; //this.NPC_aimStep_timer_s = 0.0; //this.NPC_aimStep_timer_limit_s = 0.02; this.NPC_aimStepCount = 0; this.NPC_aimStepCount_limit = 20; this.NPC_skipFrame = false; // rtc contains WebRTC peer connection and data channel objects. this.rtc = new hC.RTC({}); } // Variables common to all instances of Client... Client.mouse_strings = {'0':{'c_drag': 2.0, 'k_Npm': 60.0}, '1':{'c_drag': 0.1, 'k_Npm': 2.0}, '2':{'c_drag': 20.0, 'k_Npm': 1000.0}}; Client.colors = {'1':'yellow','2':'blue','3':'green','4':'pink','5':'orange', '6':'brown','7':'greenyellow','8':'cyan','9':'tan','0':'gray'}; Client.deleteNPCs = function() { for (var clientName in clients) { if (clientName.slice(0,3) == 'NPC') { delete clients[ clientName]; } } } 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 = b2d_getBodyAt( this.mouse_2d_m); if (selected_b2d_Body) { var selectedBody = 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. (!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")) { // Add this body to the multiple-select map (if not already there). if (!(selectedBody.name in multiSelect.map) && (this.button == 0)) { multiSelect.map[ selectedBody.name] = selectedBody; //console.log(multiSelect.count() + ', ' + selectedBody.name); // Remove this body from the map if doing a right-button (2) mouse click. } else if ((selectedBody.name in multiSelect.map) && (this.button == 2)) { multiSelect.removeOne( selectedBody); } // If using the box-selection feature... } else if ((this.name == 'local') && (this.key_alt == "D")) { if ((selectedBody.name in multiSelect.map) && (this.button == 2)) { multiSelect.removeOne( selectedBody); } // Normal single-body selection: // Allow single-body pin selection only if the wall/pin editor is on. } else if (!(!dC.editor.checked && (selectedBody.constructor.name == "Pin"))) { // Which body object has been selected? this.selectedBody = tableMap.get( selected_b2d_Body); // Mark it as selected and record the local point. this.selectionPoint_l_2d_m = selected_b2d_Body.GetLocalPoint( this.mouse_2d_m); } } } } } else { // Released the mouse button: if (!this.isMouseDown) { this.selectionPoint_l_2d_m = null; this.selectionPoint_w_2d_m = null; this.selectionPoint_w_2d_px = null; this.selectedBody = null; } } } Client.prototype.calc_string_forces_on_puck = function() { // Calculate the forces. var stretch_2d_m = this.mouse_2d_m.subtract( this.selectionPoint_w_2d_m); var spring_force_2d_N = stretch_2d_m.scaleBy( Client.mouse_strings[this.button]['k_Npm']); //this.selectedBody.cursorString_spring_force_2d_N.addTo( spring_force_2d_N); // Add this force and application point to the nonCOM force array. this.selectedBody.nonCOM_2d_N.push({force_2d_N: spring_force_2d_N, point_w_2d_m: this.selectionPoint_w_2d_m}); //console.log("v.x=" + this.selectedBody.velocity_2d_mps.x); var drag_force_2d_m = this.selectedBody.velocity_2d_mps.scaleBy( -1 * Client.mouse_strings[this.button]['c_drag']); //this.selectedBody.cursorString_puckDrag_force_2d_N.addTo( drag_force_2d_m); // Add a force,point object to the nonCOM array. this.selectedBody.nonCOM_2d_N.push({force_2d_N: drag_force_2d_m, point_w_2d_m: this.selectionPoint_w_2d_m}); } 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 (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 bodies by a similar amount. if (this.key_ctrl == "D") { // bind "this" to the function that is applied to all in multi-select group. multiSelect.applyToAll( function( tableObj) { if (tableObj !== this.selectedBody) { this.moveSBtoPosition( tableObj, tableObj.position_2d_m.add( movement_2d_m)); } }.bind(this)); //bind "this" to the client object here so you can use its moveSBtoPosition method. } } 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 (multiSelect.count() > 1) { // Find the center only at the beginning of the rotation action. if (multiSelect.findCenterEnabled) { multiSelect.findCenter(); // Don't do this again until one of the keys is released. multiSelect.findCenterEnabled = false; } // Measure the rotation relative to the center of the group. delta_r = multiSelect.center_2d_m.angleBetween_r(this.selectionPoint_w_2d_m, this.mouse_2d_m); multiSelect.applyToAll( function( tableObj) { // Rotate the vector that runs from the multiselect center out to the object center. var center_to_center_2d = tableObj.position_2d_m.subtract( multiSelect.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, multiSelect.center_2d_m.add( center_to_center_2d)); // Rotate the object about its center. this.rotateSB(tableObj, delta_r); }.bind(this)); // Bind "this" because using two client methods. // 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 ( ! 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.drawCursor = function() { // Draw a cursor the for the network clients. //this.drawCircle( this.mouse_2d_px); // 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); } drawPolygon( this.triangle_2d_px, {'borderColor':'white','borderWidth_px':1,'fillColor':this.color}); } Client.prototype.updateSelectionPoint = function() { // Calculate (update) the world location of the selection point (for use in force calculations) if (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)); } } Client.prototype.drawSelectionString = function() { this.selectionPoint_w_2d_px = screenFromWorld( this.selectionPoint_w_2d_m); // A nick name: var sP_2d_px = this.selectionPoint_w_2d_px; drawLine(sP_2d_px, this.mouse_2d_px, {'width_px':5,'color':'MediumSpringGreen'}); // Draw the small selection circle. drawCircle( sP_2d_px, {'borderColor':'white', 'borderWidth_px':2, 'fillColor':this.color, '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 client to 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 velocity as seen by the client (in the client reference frame). var target_inNPCrf_2d_mps = this.puck.gun.rayBody.velocity_2d_mps.subtract( this.puck.velocity_2d_mps); // Component of target velocity along the line between client and target. var NPC_parallel_2d_mps = target_inNPCrf_2d_mps.projection_onto( parallel_unit_vector); if (NPC_parallel_2d_mps) { // Component of target velocity perpendicular to line between client and target var NPC_perpendicular_2d_mps = target_inNPCrf_2d_mps.subtract( NPC_parallel_2d_mps); var perpendicular_unit_vector = NPC_perpendicular_2d_mps.normal(); // The bullet vector (relative to client) needed to intercept the target. var parallel_speed_mps = Math.sqrt( Math.pow( this.puck.gun.bulletSpeed_mps, 2) - Math.pow( NPC_perpendicular_2d_mps.length(), 2)); var bullet_2d_mps = NPC_perpendicular_2d_mps.add( parallel_unit_vector.scaleBy( parallel_speed_mps)); //console.log(NPC_perpendicular_2d_mps.x + ',' + NPC_perpendicular_2d_mps.y); var bullet_angle_deg = bullet_2d_mps.get_angle(); //var client_angle_deg = this.puck.velocity_2d_mps.get_angle(); //console.log(Math.round(bullet_angle_deg)); 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() { // 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 += c.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) { 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 += c.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 springOnly_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 += c.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.attachSpringToNextPin(); 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.rotateTubeAndJetToThis( this.puck.springOnly_force_2d_N.scaleBy(-1)); } } // For use in sound field, demo #2. function PuckTail( pars) { this.firstPoint_2d_m = pars.firstPoint_2d_m || new Vec2D( 1.0, 1.0); this.initial_radius_m = pars.initial_radius_m || 1.0; /* this.emitFrequency_hz = pars.emitFrequency_hz || 60; //60 c.frameRate this.emitInterval_s = 1.0/this.emitFrequency_hz; this.emitTimer_s = 0.0; */ this.propSpeed_mps = pars.propSpeed_mps || 3.0; this.length_limit = pars.length_limit || 25; this.color = pars.color || 'lightgrey'; // The wait (time in seconds) before making a pure white color ping. this.markerPingTimerLimit_s = pars.markerPingTimerLimit_s || 1.0; this.markerPingTimer_s = 0.0; this.values = []; this.update( this.firstPoint_2d_m); } PuckTail.prototype.update = function( newPoint_2d_m) { // Mark one ring specially so to see ring propagation better. this.markerPingTimer_s += c.deltaT_s; if (this.markerPingTimer_s < this.markerPingTimerLimit_s) { this.pingColor = this.color; } else { this.pingColor = 'white'; this.markerPingTimer_s = 0.0; } /* This commented block is here as a reminder the 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 += c.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 += px_from_meters( this.propSpeed_mps * c.deltaT_s); //this.values[t].r_px += 5; // Draw the sound circle (make the 'white' marker ring even more visible, using green, if single stepping). var lineColor = (c.singleStep && (this.values[t].color == 'white')) ? 'red' : this.values[t].color; //#008080 cyan yellow magenta orange drawCircle( 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) { this.parsAtBirth = pars; this.bullet = setDefault( pars.bullet, false); this.jello = setDefault( pars.jello, false); this.clientName = pars.clientName || null; if (this.clientName) Puck.playerCount += 1; if (pars.name) { this.name = pars.name; Puck.nameIndex = Math.max(Puck.nameIndex, Number(this.name.slice(4, this.name.length))); } else { Puck.nameIndex += 1; this.name = 'puck' + Puck.nameIndex; } //console.log("n-puck = " + Puck.nameIndex); 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 = pars.color || "DarkSlateGray"; this.shape = pars.shape || "circle"; this.colorSource = setDefault( pars.colorSource, false); this.density = pars.density || 1.5; this.linDamp = pars.linDamp || 0.0; this.hitLimit = pars.hitLimit || 10; // This identifies the owner of bullets (so you can't shoot yourself in the foot). this.createdByClient = pars.createdByClient || null; this.ageLimit_ms = pars.ageLimit_ms || null; this.createTail = setDefault( pars.createTail, false); this.tailPars = pars.tailPars || null; this.tail = null; this.groupIndex = pars.groupIndex || 0; this.categoryBits = pars.categoryBits || 0x0001; this.maskBits = pars.maskBits || 0xFFFF; // Rotational state this.angle_r = pars.angle_r || 0; this.angularSpeed_rps = pars.angularSpeed_rps || 0; this.borderWidth_px = pars.borderWidth_px || 3; // Put a reference to this puck in the client. if (this.clientName) { clients[this.clientName].puck = this; } this.age_ms = 0; //this.createTime = window.performance.now(); // These two parameters are fixed (not affected by the g toggle) if they // are specified in the pars object. if (pars.restitution === undefined) { if (c.g_ON) { this.restitution = c.restitution_gOn; } else { this.restitution = c.restitution_gOff; } this.restitution_fixed = false; } else { this.restitution = pars.restitution; this.restitution_fixed = true; } if (pars.friction === undefined) { if (c.g_ON) { this.friction = c.friction_gOn; } else { this.friction = c.friction_gOff; } this.friction_fixed = false; } else { this.friction = pars.friction; this.friction_fixed = true; } // Dimensions this.radius_m = pars.radius_m || 1.0; this.aspectR = pars.aspectR || 1.0; this.half_height_m = pars.half_height_m || null; this.half_width_m = 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); } this.tail = new PuckTail( tailPars); } 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! 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.springOnly_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 if (this.clientName) { // Add client controls and give each control a reference to this puck. this.jet = new Jet(this, {'initial_angle':10}); this.gun = new Gun(this, {'initial_angle':60, 'indicator':true, 'tube_color':'gray', 'rayCast_init_deg':pars.rayCast_init_deg}); } this.shield = new Shield(this, {'color':'yellow'}); this.hitCount = 0; this.poorHealthFraction = 0; this.flash = false; this.inComing = false; this.flashCount = 0; this.springName = null; this.pinName = pars.pinName || null; if (this.pinName) { this.attachSpring(); } this.deleted = false; } Puck.nameIndex = 0; Puck.playerCount = 0; Puck.deleteAll = function() { for (var clientName in clients) { clients[ clientName].puck = null; } for (var puckName in aT.puckMap) { tableMap.delete( aT.puckMap[ puckName].b2d); world.DestroyBody( aT.puckMap[ puckName].b2d); } aT.jelloPucks = []; aT.puckMap = {}; Puck.nameIndex = 0; Puck.playerCount = 0; } Puck.prototype.attachSpring = function() { var springName = "s" + (100 + Puck.nameIndex); // Note that instantiation adds the new spring to the spring map. var temp = new Spring(this, aT.pinMap[this.pinName], {strength_Npm:8.0, unstretched_width_m:0.1, color:'brown', damper_Ns2pm2:5.0, navigationForNPC:true, 'name':springName}); this.springName = temp.name; } Puck.prototype.stepTheJetAngle = function() { var spring = aT.springMap[this.springName]; // 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; } //console.log("nextPinName = " + 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( 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.rotateTubeAndJet( 0.15 * changeNeeded_deg); } } Puck.prototype.attachSpringToNextPin = function() { var spring = aT.springMap[this.springName]; // 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 (aT.pinMap[ nextPinName]) { // Move the end of the spring that's attached to the pin. if (aT.springMap[this.springName].spo1.constructor.name == "Pin") aT.springMap[this.springName].spo1 = aT.pinMap[ nextPinName]; else { aT.springMap[this.springName].spo2 = aT.pinMap[ nextPinName]; } this.pinName = nextPinName; } } else { console.log('no spring to use.'); } } Puck.prototype.deleteThisOne = function() { // 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; // 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. clients[this.clientName].puck = null; } else { deleteRTC_onClientAndHost( this.clientName); } Puck.playerCount -= 1; } // Delete the corresponding Box2d object. tableMap.delete( this.b2d); world.DestroyBody( this.b2d); // Remove this puck from our puck map. delete aT.puckMap[ this.name]; delete multiSelect.map[ this.name]; // Filter out this puck from the jelloPuck array. if (this.jello) { aT.jelloPucks = aT.jelloPucks.filter( function( eachPuck) { // Keep these (those NOT deleted) return (!eachPuck.deleted == true); }); } } Puck.prototype.copyThisOne = function() { // Make a copy of the mutable objects that are passed into the Puck constructor. var p_2d_m = Object.assign({}, this.position_2d_m); var v_2d_mps = Object.assign({}, this.velocity_2d_mps); var pars = Object.assign({}, this.parsAtBirth); // Make sure the name is nulled so the auto-naming feature is used in the constructor. pars.name = null; // Don't allow any network client or NPC features. pars.clientName = null; pars.pinName = null; // Update pars to reflect any edits that have done. if (this.shape == 'circle') { pars.radius_m = this.radius_m; } else { pars.half_height_m = this.half_height_m; pars.half_width_m = this.half_width_m; } new Puck( p_2d_m, v_2d_mps, pars); } 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 = 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(); //console.log("m=" + this.mass_kg); this.b2d.SetLinearDamping( this.linDamp); this.b2d.SetBullet( this.bullet); } Puck.prototype.define_fixture = function( pars) { this.width_scaling = pars.width_scaling || 1.0; this.height_scaling = pars.height_scaling || 1.0; this.radius_scaling = pars.radius_scaling || 1.0; // Create a circular or rectangular dynamic box2d object. var fixDef = new b2FixtureDef; fixDef.density = this.density; fixDef.friction = this.friction; fixDef.restitution = this.restitution; 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; 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); 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})); } else { this.b2d.CreateFixture( this.define_fixture({'width_scaling':width_factor,'height_scaling':height_factor})); } // Update the mass. this.mass_kg = this.b2d.GetMass(); // Update the puck tail 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() { 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'; } drawCircle( 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) { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'chocolate', 'radius_px':poorHealthRadius}); } // Update and draw the shield. if (clients[this.clientName]) this.shield.updateState(); // Show rotational orientation: draw a line segment along the line from the center out to a local point on the radius. if (!this.gun) { 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)) ) ); drawLine(pointAtHalfRadius_2d_px, pointOnEdge_2d_px, {'width_px':2,'color':'white'}); } // Draw the tail if we have one. if (this.tail) this.tail.update( this.position_2d_m); } else { // Draw the rectangle. drawPolygon( b2d_getPolygonVertices( this.b2d), {'borderColor':'white','borderWidth_px':2,'fillColor':this.color}); } } Puck.prototype.draw_MultiSelectPoint = function() { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5}); } Puck.prototype.applyForces = function() { // Net resulting force on the puck. // First consider all forces acting on the COM. // F = acc * mass var puck_forces_2d_N = 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/c.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) { // Make a (circular) reference to the host puck. this.puck = puck; // Optional parameters and defaults. this.color = pars.color || 'lime'; // Make a direct reference to the client. this.client = 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.updateState = function() { // 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]; } drawCircle( 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 += c.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 += c.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) { // Circular reference back to the puck. this.puck = puck; // Optional parameters and defaults. this.initial_angle = pars.initial_angle || 20; this.indicator = setDefault( pars.indicator, false); // Make a direct reference to the client. this.client = 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; //console.log('inside the Tube constructor.'); } 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() { this.AbsPositionOfEnds(); drawLine(this.base_2d_px, this.end_2d_px, {'width_px':this.width_px,'color':this.tube_color}); if (this.indicator) { this.AbsPositionOfIndicator(); drawLine(this.indicatorBase_2d_px, this.indicatorEnd_2d_px, {'width_px':this.indicatorWidth_px, 'color':this.puck.shield.color}); } } function Jet( puck, pars) { // Call the Tube constructor. Bind it to "this" jet. Pass the puck and pars to the constructor. 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 in the same direction as the tube. this.rotateJet( 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 this.jet_force_N = 1.3 * this.puck.mass_kg * Math.abs( g_mps2); } // Use the Tube prototype as starting point for the Jet (inheritance). This brings // in all the methods and attributes from Tube. Jet.prototype = Object.create( Tube.prototype, { // This object, passed as the second parameter (propertiesObject argument), is another way that you can // add in properties for Jet. 'example2': {value: 22, writable:true}, 'example3': {value:333, writable:true} }); // Set the constructor name to Jet, so it is not "Tube" (default). Jet.prototype.constructor = Jet; // Define any new methods for Jet. Jet.prototype.rotateJet = function( degrees) { for (var i = 0, len = this.triangle_2d_m.length; i < len; i++) { // Rotate each vertex. this.triangle_2d_m[i].rotated_by( degrees); } } Jet.prototype.rotateTubeAndJet = function( deg) { this.rotateTube( deg); this.rotateJet( deg); } Jet.prototype.rotateTubeAndJetToThis = function( v_2d_m) { // Rotate the 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.rotateTubeAndJet( -current_deg); // Scale the jet relative to the length of the supplied vector. var height_m = this.height_m * (v_2d_m.length() / this.jet_force_N); if (height_m < 0.10) height_m = 0.10; this.initializeFlame( height_m); var target_deg = v_2d_m.get_angle(); var change_deg = target_deg - 0; // Rotate, starting from 0, by this amount. this.rotateTubeAndJet( change_deg); } Jet.prototype.rotateJetByClient = function() { // The Rate, degrees per frame (dpf), gives the degrees of rotation in one frame. // Left/Right pointing control if (this.client.key_d == "D") { this.rotateTubeAndJet(-this.rotationRate_dps * c.deltaT_s); } if (this.client.key_a == "D") { this.rotateTubeAndJet(+this.rotationRate_dps * c.deltaT_s); } // For use in stopping the puck... if ((this.client.key_s == "D") && (this.client.key_s_enabled)) { if (this.client.key_shift == "D") { // This simply flips the jet (180 degrees). this.rotateTubeAndJet(+180); } else { // 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. this.rotateTubeAndJet(this.puck.velocity_2d_mps.get_angle() - this.rel_position_2d_m.get_angle()); } 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.triangle_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.drawJetFlame = function() { // Before you can draw it, you have to know where it is on the screen. this.triangle_2d_px = []; for (var i = 0, len = this.triangle_2d_m.length; i < len; i++) { // Put it on the end of the tube. var p_2d_m = this.end_2d_m.add(this.triangle_2d_m[i]); var p_2d_px = screenFromWorld( p_2d_m); // Put it in the triangle array. this.triangle_2d_px.push( p_2d_px); } drawPolygon( this.triangle_2d_px, {'borderColor':this.flameEdge_color,'borderWidth_px':3,'fillColor':this.flame_color}); } Jet.prototype.updateAndDraw = function() { // Only draw the jet for regular clients (not NPCs) if (!(this.client.name.slice(0,3) == 'NPC')) { // Respond to client controls to rotate the Tube and Jet. this.rotateJetByClient(); // Always draw the tube. this.drawTube(); // If the jet is on (w key down), draw it, and calculate jet forces. if (this.client.key_w == "D") { this.drawJetFlame(); this.puck.jet_force_2d_N = this.rel_position_2d_m.scaleBy( -this.jet_force_N/this.length_m); } else { this.puck.jet_force_2d_N = this.rel_position_2d_m.scaleBy( 0); } } else { if (!dC.editor.checked) { this.drawTube(); this.drawJetFlame(); } } } // (This ain't no BB gun). function Gun( puck, pars) { // Call the Tube constructor. Bind it to "this" gun. Pass the puck and pars to the constructor. Tube.call(this, puck, pars); this.tube_color = 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.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 || 3.5; this.rayCast_init_deg = 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 = pars.rayRotationRate_dps || 80; this.scanning = setDefault( pars.scanning, true); this.rayBody = null; this.angleToFoundPuck = 0; } // Use the Tube prototype as starting point for the Gun (inheritance). This brings // in all the methods and attributes from Tube. Gun.prototype = Object.create( Tube.prototype); // Set the constructor name to Gun, so it is not "Tube" (default). Gun.prototype.constructor = Gun; // Define any new methods for Gun. Gun.prototype.rotateGunByClient = function() { // The Rate, degrees per frame (dpf), gives the degrees of rotation in one frame. // Left/Right pointing control if (this.client.key_l == "D") { this.rotateTube(-this.rotationRate_dps * c.deltaT_s); } if (this.client.key_j == "D") { this.rotateTube(+this.rotationRate_dps * c.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); var bullet = new Puck( this.end_2d_m, absoluteVel_2D_mps, {'radius_m':0.04, 'bullet':true, 'color':this.client.color, 'borderWidth_px':1, 'createdByClient':this.client.name, 'ageLimit_ms':1500, 'restitution_fixed':true , 'restitution':1.0}); // Calculate the recoil impulse from firing the gun (opposite the direction of the bullet). 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.timeLastFired = window.performance.now() - this.timeBetweenBullets_ms; this.bulletWaitTimer_ms = 0; } Gun.prototype.stop_BulletStream = function() { this.bulletStream = 'off'; } Gun.prototype.update_BulletStream = function() { //var deltaTime_ms = window.performance.now() - this.timeLastFired; this.bulletWaitTimer_ms += c.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.timeLastFired = window.performance.now(); this.bulletWaitTimer_ms = 0; this.bulletCount += 1; } } Gun.prototype.drawRayCastLine = function() { // Update the angle of the ray. if (this.scanning) { this.rayCastLine_2d_m.rotated_by( +this.rayRotationRate_dps * c.deltaT_s); } 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; world.RayCast( function( fixture, point, outputNormal, fraction) { var fixtureBody = 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) { //console.log('in here'); this.scanning = true; this.rayBody = null; } // Draw it. var raycast_end_2d_px = screenFromWorld( raycast_end_2d_m); drawLine(this.puck.position_2d_px, raycast_end_2d_px, {'width_px':1,'color':'yellow','dashArray':[4]}); } Gun.prototype.updateAndDraw = function() { // Respond to client controls to rotate the Gun. this.rotateGunByClient(); // Always draw the tube. this.drawTube(); // Cast and draw ray based on gun orientation. if (this.client.name.slice(0,3) == 'NPC') { this.drawRayCastLine(); } // 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") { 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(); } else if ((this.client.key_i == "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) { this.parsAtBirth = pars; if (pars.name) { this.name = pars.name; Pin.nameIndex = Math.max(Pin.nameIndex, Number(this.name.slice(3, this.name.length))); } else { Pin.nameIndex += 1; this.name = 'pin' + Pin.nameIndex; } //console.log("n-pin = " + Pin.nameIndex); aT.pinMap[this.name] = this; this.position_2d_m = Vec2D_check( position_2d_m); this.position_2d_px = screenFromWorld( this.position_2d_m); this.radius_px = 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); // Masking parameters for b2d object (default is to prevent collisions with the pin) //this.groupIndex = pars.groupIndex || 0; this.categoryBits = pars.categoryBits || 0x0000; this.maskBits = pars.maskBits || 0x0000; this.velocity_2d_mps = new Vec2D(0.0,0.0); this.b2d = null; this.create_b2d_pin(); // Create a reference back to this pin from the b2d pin. 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 = pars.nextPinName || null; this.previousPinName = pars.previousPinName || null; this.fillColor = pars.fillColor || 'blue'; this.borderColor = pars.borderColor || 'gray'; /* console.log('inside pin constructor'); for (var pinMapKey in aT.pinMap) { console.log("key in pinMap = " + pinMapKey); } */ } Pin.nameIndex = 0; Pin.deleteAll = function () { for (var pinName in aT.pinMap) { tableMap.delete( aT.pinMap[ pinName].b2d); world.DestroyBody( aT.pinMap[ pinName].b2d); } aT.pinMap = {}; Pin.nameIndex = 0; } Pin.prototype.deleteThisOne = function() { // 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. aT.pinMap[this.nextPinName].previousPinName = aT.pinMap[this.previousPinName].name; // Point the previous pin forward to the next pin. aT.pinMap[this.previousPinName].nextPinName = aT.pinMap[this.nextPinName].name; } // Delete reference in the tableMap. tableMap.delete( this.b2d); // Delete the corresponding Box2d object. world.DestroyBody( this.b2d); // Mark this pin as deleted. this.deleted = true; // Remove this pin from the pin map. delete aT.pinMap[ this.name]; } Pin.prototype.copyThisOne = function() { var p_2d_m = Object.assign({}, this.position_2d_m); var pars = Object.assign({}, this.parsAtBirth); // Make sure the name is nulled so the auto-naming feature is used in the constructor. pars.name = null; var newPin = new Pin( p_2d_m, pars); // 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. aT.pinMap[this.nextPinName].previousPinName = newPin.name; // Update the forward link of the original pin. this.nextPinName = newPin.name; } } 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 = 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); } 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() { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5}); } Pin.prototype.draw = function() { this.getPosition(); drawCircle( this.position_2d_px, {'borderColor':this.borderColor, 'borderWidth_px':2, 'fillColor':this.fillColor, 'radius_px':this.radius_px}); // Draw lines to indicate the relationships in the NPC navigation map. if (this.NPC && dC.editor.checked) { if (aT.pinMap[this.nextPinName]) { drawLine(this.position_2d_px, aT.pinMap[this.nextPinName].position_2d_px, {'width_px':1, 'color':this.color, 'dashArray':[3]}); } } } function Spring(puckOrPin1, puckOrPin2, pars) { this.parsAtBirth = pars; if (pars.name) { this.name = pars.name; Spring.nameIndex = Math.max(Spring.nameIndex, Number(this.name.slice(1, this.name.length))); } else { Spring.nameIndex += 1; this.name = 's' + Spring.nameIndex; } //console.log("n-spring = " + Spring.nameIndex); aT.springMap[this.name] = this; this.color = pars.color || "red"; this.length_m = pars.length_m || 0.0; this.strength_Npm = pars.strength_Npm || 0.5; this.unstretched_width_m = pars.unstretched_width_m || 0.025; this.drag_c = pars.drag_c || 0.0; this.damper_Ns2pm2 = pars.damper_Ns2pm2 || 0.5; this.dashedLine = setDefault( pars.dashedLine, false); this.roundedEnds = setDefault( pars.roundedEnds, true); this.navigationForNPC = setDefault( pars.navigationForNPC, 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; } // 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; } this.p1p2_separation_2d_m = new Vec2D(0,0); this.p1p2_separation_m = 0; this.p1p2_normalized_2d = new Vec2D(0,0); } Spring.nameIndex = 0; Spring.deleteAll = function () { /* for (var springName in aT.springMap) { tableMap.delete( aT.springMap[ springName].b2d); world.DestroyBody( aT.springMap[ springName].b2d); } */ 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. for (var springName in aT.springMap) { var spring = aT.springMap[ springName]; if ((spring.spo1.name in multiSelect.map) && (spring.spo2.name in multiSelect.map)) { // For each spring you find. doThis( spring); } } } Spring.prototype.deleteThisOne = function() { // Remove this spring from the spring map. delete aT.springMap[ this.name]; } Spring.prototype.copyThisOne = function(p1, p2) { // 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; // Note that this instantiation adds this new spring to the spring map. var tempSpring = new Spring( p1, p2, pars); return tempSpring.name; } Spring.prototype.modify_fixture = function( mode) { var width_factor = 1.0; var length_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; } // First deal with 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 (this.length_m == 0.0) this.length_m = 0.1; this.length_m *= length_factor; // 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; } Spring.prototype.force_on_pucks = function() { this.p1p2_separation_2d_m = this.spo1.position_2d_m.subtract( this.spo2.position_2d_m); this.p1p2_separation_m = this.p1p2_separation_2d_m.length(); // 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. if ((this.p1p2_separation_m == 0.0) && (this.length_m == 0.0)) { 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); } // Damper force: acts along the separation vector and is proportional to the relative speed. var v_relative_2d_mps = this.spo1.velocity_2d_mps.subtract( this.spo2.velocity_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); // Net force by both spring and damper var sprDamp_force_2d_N = spring_force_on_1_2d_N.subtract( damper_force_on_1_2d_N); // This force acts in opposite directions for each of the two pucks. Notice the "addTo" here, this // is an aggregate across all the springs. This aggregate MUST be reset (zeroed) after the movements are // calculated. So by the time you've looped through all the springs, you get the NET force, on each ball, // applied by all the individual springs. if (this.spo1.constructor.name != "Pin") { this.spo1.sprDamp_force_2d_N.addTo( sprDamp_force_2d_N.scaleBy( +1)); } if (this.spo2.constructor.name != "Pin") { this.spo2.sprDamp_force_2d_N.addTo( sprDamp_force_2d_N.scaleBy( -1)); } // Add in some drag forces if a non-zero drag coefficient is specified. These are based on the // velocity of the pucks (not relative speed as is the case above for damper forces). if (this.spo1.constructor.name != "Pin") { this.spo1.sprDamp_force_2d_N.addTo( this.spo1.velocity_2d_mps.scaleBy( -1 * this.drag_c)); this.spo1.springOnly_force_2d_N = spring_force_on_1_2d_N.scaleBy( +1); } if (this.spo2.constructor.name != "Pin") { this.spo2.sprDamp_force_2d_N.addTo( this.spo2.velocity_2d_mps.scaleBy( -1 * this.drag_c)); this.spo2.springOnly_force_2d_N = spring_force_on_1_2d_N.scaleBy( -1); } } Spring.prototype.draw = function() { if (!this.navigationForNPC || dC.editor.checked) { // 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; // This version looks better for zero-length (pinned pucks) var width_m = this.unstretched_width_m * (1 + 0.40 * (this.length_m - this.p1p2_separation_m)); var width_px = px_from_meters( width_m); if (width_px < 2) {width_px = 2}; if (this.dashedLine) { var dashArray = [3]; //} else if (this.p1p2_separation_m > (1.01 * this.length_m)) { // var dashArray = [0]; // use 1 to show stretched springs with dashed lines (surprising CPU drain). } else { var dashArray = [0]; } drawLine( this.spo1.position_2d_px, this.spo2.position_2d_px, {'width_px':width_px, 'color':this.color, 'dashArray':dashArray} ); // Round the ends by drawing end-point circles. if (this.roundedEnds) { var radius_px = math.floor(width_px/2) - 1; if (radius_px < 1) radius_px = 1; if (radius_px > 15) radius_px = 15; var pars = {'radius_px':radius_px, 'borderWidth_px':1, 'borderColor':this.color, 'fillColor':this.color}; drawCircle( this.spo1.position_2d_px, pars); drawCircle( this.spo2.position_2d_px, pars); } } } function Wall( position_2d_m, pars) { this.parsAtBirth = pars; 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, this.name.length))); } else { Wall.nameIndex += 1; this.name = 'wall' + Wall.nameIndex; } //console.log("n-wall = " + Wall.nameIndex); 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.velocity_2d_mps = pars.velocity_2d_mps || new Vec2D(0.0, 0.0); this.angle_r = pars.angle_r || 0.0; this.angularSpeed_rps = pars.angularSpeed_rps || 0.0; // Dimensions (as specified in box2D) this.half_width_m = pars.half_width_m || 0.5; this.half_height_m = 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. tableMap.set(this.b2d, this); var color_default; Wall.color_default = "white"; this.color = Wall.color_default; this.deleted = false; } Wall.nameIndex = 0; Wall.deleteAll = function() { for (var wallName in aT.wallMap) { tableMap.delete( aT.wallMap[ wallName].b2d); world.DestroyBody( aT.wallMap[ wallName].b2d); } aT.wallMap = {}; Wall.nameIndex = 0; } Wall.prototype.deleteThisOne = function() { // Delete reference in the tableMap. tableMap.delete( this.b2d); // Delete the corresponding Box2d object. world.DestroyBody( this.b2d); // Mark this wall as deleted. this.deleted = true; // Remove this wall from the wall map. delete aT.wallMap[ this.name]; } Wall.prototype.copyThisOne = function() { new Wall( this.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 = pars.width_scaling || 1.0; this.height_scaling = 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); //console.log('hh_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 = 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() { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5}); } Wall.prototype.updateState = function() { this.angle_r = this.b2d.GetAngle(); } Wall.prototype.draw = function() { drawPolygon( 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) { 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; } ///////////////////////////////////////////////////////////////////////////// //// //// Functions //// ///////////////////////////////////////////////////////////////////////////// // Misc utility stuff function setDefault( theValue, theDefault) { // Return the default if the value is undefined. return (typeof theValue !== "undefined") ? theValue : theDefault; } // Support for the network client /////////////////////////////////////////// function createNetworkClient( clientName) { var n = clientName.slice(1); var colorIndex = n - Math.trunc(n/10)*10; var pars = {}; pars.color = Client.colors[ colorIndex]; pars.name = clientName; clients[ clientName] = new Client( pars); } function deleteNetworkClient( clientName) { // This function does not directly remove the client socket at the node server, but // that does happen at the server... //console.log('inside gW.deleteNetworkClient, clientName=' + clientName + ", fileName="+fileName); if (clients[clientName]) { // If it's driving a puck. First, delete that. if (clients[clientName].puck) { var thePuck = clients[clientName].puck // Remove this puck and do associated clean-up. thePuck.jet = null; thePuck.gun = null; thePuck.shield = null; tableMap.delete( thePuck.b2d); world.DestroyBody( thePuck.b2d); delete aT.puckMap[ thePuck.name]; } deleteRTC_onHost( clientName); } } function deleteRTC_onHost( clientName) { // Shutdown and nullify any references to the host side of this WebRTC p2p connection. if (clients[clientName].rtc) { clients[ clientName].rtc.shutdown(); //console.log('after rtc shutdown, clientName=' + clientName); } // Remove the client in the clients map. if (clients[clientName]) { delete clients[ clientName]; //console.log('after client deletion, clientName=' + clientName); } } function deleteRTC_onClientAndHost( clientName) { // Remove network clients on the node server. // (Note: this is one of the several places where hC is used inside of gW.) if (clientName.slice(0,1) == 'u') { // Send message to the server and then to the client to disconnect. hC.forceClientDisconnect( clientName); } // Remove the client in the clients map. deleteRTC_onHost( clientName); } function updateClientState( clientName, state) { /* This is mouse and keyboard input as generated from non-host-client events. Note that this can happen at anytime as triggered by events on the client. This is not fired each frame. Repetition can be an issue here as mouse movement will repeatedly send the state. If you want to avoid repeating actions, it may be appropriate here to compare the incoming state with the current client state (or make use of a key_?_enabled properties) to stop after the first act. This blocking of repetition does not necessarily need to happen here. For an example of this, search on key_i_enabled. It is handy to do the blocking here because you have access to the incoming state and don't need the key_?_enabled properties. But for actions that are repeating each frame, you need to use the key_?_enabled approach. */ if (clients[ clientName]) { clients[ clientName].mouseX_px = state.mX; clients[ clientName].mouseY_px = state.mY; clients[ clientName].isMouseDown = state.MD; // Immediately unselect the puck of the network client if the mouse is up. if (clients[ clientName].isMouseDown == false) clients[ clientName].selectedBody = null; clients[ clientName].button = state.bu; clients[ clientName].mouse_2d_px = new Vec2D(clients[ clientName].mouseX_px, clients[ clientName].mouseY_px); clients[ clientName].mouse_2d_m = worldFromScreen( clients[ clientName].mouse_2d_px); clients[ clientName].key_a = state.a; clients[ clientName].key_s = state.s; // uses key_s_enabled clients[ clientName].key_d = state.d; clients[ clientName].key_w = state.w; clients[ clientName].key_j = state.j; clients[ clientName].key_k = state.k; // uses key_k_enabled clients[ clientName].key_l = state.l; clients[ clientName].key_i = state.i; // uses key_i_enabled clients[ clientName].key_space = state.sp; clients[ clientName].key_shift = state.sh; // Compare incoming state with the current state. Only act if changing from U to D. if ((state['1'] == "D") && (clients[ clientName].key_1 == "U")) demoStart(1); clients[ clientName].key_1 = state['1']; if ((state['2'] == "D") && (clients[ clientName].key_2 == "U")) demoStart(2); clients[ clientName].key_2 = state['2']; if ((state['3'] == "D") && (clients[ clientName].key_3 == "U")) demoStart(3); clients[ clientName].key_3 = state['3']; if ((state['4'] == "D") && (clients[ clientName].key_4 == "U")) demoStart(4); clients[ clientName].key_4 = state['4']; if ((state['5'] == "D") && (clients[ clientName].key_5 == "U")) demoStart(5); clients[ clientName].key_5 = state['5']; if ((state['6'] == "D") && (clients[ clientName].key_6 == "U")) demoStart(6); clients[ clientName].key_6 = state['6']; if ((state['7'] == "D") && (clients[ clientName].key_7 == "U")) demoStart(7); clients[ clientName].key_7 = state['7']; if ((state['8'] == "D") && (clients[ clientName].key_8 == "U")) demoStart(8); clients[ clientName].key_8 = state['8']; if ((state['f'] == "D") && (clients[ clientName].key_f == "U")) freeze(); clients[ clientName].key_f = state['f']; /* var stateString = ""; for (var key in state) stateString += key + ":" + state[key] + ","; console.log("stateString=" + stateString); */ } } // box2d functions to interact with the engine ////////////////////////////// function b2d_getBodyAt( mousePVec_2d_m) { var x = mousePVec_2d_m.x; var y = mousePVec_2d_m.y; var aabb = new b2AABB(); var size_m = 0.001; aabb.lowerBound.Set(x - size_m, y - size_m); aabb.upperBound.Set(x + size_m, y + size_m); // Query the world for overlapping bodies. Where the body's bounding box overlaps // with the aabb box defined above. Run the function provided to QueryAABB for each // body found to overlap the aabb box. var selectedBody = null; world.QueryAABB( function( fixture) { // Don't consider any static bodies. if (fixture.GetBody().GetType() != b2Body.b2_staticBody) { // Take the first fixture where this point can be found locally on it. if (fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), mousePVec_2d_m)) { selectedBody = fixture.GetBody(); return false; // stop checking the query results } } // return true to continue checking at the rest of the fixtures returned by the query return true; }, aabb); return selectedBody; } function b2d_getPolygonVertices( b2d_body) { // Make an array that has the world vertices scaled to screen coordinates. var poly_px = []; for (var i = 0; i < b2d_body.m_fixtureList.m_shape.m_vertices.length; i++) { var p_2d_px = screenFromWorld( b2d_body.GetWorldPoint( b2d_body.m_fixtureList.m_shape.m_vertices[i])); poly_px.push( p_2d_px); } return poly_px; } // Relationships between the screen and the b2d world /////////////////////// // Scaler conversions function meters_from_px( length_px) { return length_px / px_per_m; } function px_from_meters( length_m) { return Math.round(length_m * px_per_m); } // Vector conversions. function screenFromWorld( position_2d_m) { var x_px = px_from_meters( position_2d_m.x); var y_px = px_from_meters( position_2d_m.y); return new Vec2D( x_px, canvas.height - y_px); } function worldFromScreen( position_2d_px) { var x_m = meters_from_px( position_2d_px.x); var y_m = meters_from_px( canvas.height - position_2d_px.y); return new Vec2D( x_m, y_m); } // Functions to convert between vector types function Vec2D_from_b2Vec2( b2Vector) { return new Vec2D( b2Vector.x, b2Vector.y); } function b2Vec2_from_Vec2D( vec2D) { return new b2Vec2( vec2D.x, vec2D.y); } // This check is useful to prevent problems (objects stripped of their methods) when reconstructing from a // JSON capture. function Vec2D_check( vector_2d) { if (vector_2d.constructor.name == "Vec2D") { return vector_2d; } else { return new Vec2D( vector_2d.x, vector_2d.y); } } // High-level functions for drawing to the Canvas function drawLine(p1_2d_px, p2_2d_px, pars) { ctx.strokeStyle = pars.color || 'white'; ctx.lineWidth = pars.width_px || 2; var dashArray = pars.dashArray || [0]; ctx.setLineDash( dashArray); ctx.beginPath(); ctx.moveTo(p1_2d_px.x, p1_2d_px.y); ctx.lineTo(p2_2d_px.x, p2_2d_px.y); ctx.stroke(); } function drawCircle( center_2d_px, pars) { var radius_px = pars.radius_px || 6; ctx.strokeStyle = pars.borderColor || 'white'; ctx.lineWidth = pars.borderWidth_px || 2; var fillColor = pars.fillColor || 'red'; var dashArray = pars.dashArray || [0]; ctx.setLineDash( dashArray); ctx.beginPath(); ctx.arc(center_2d_px.x, center_2d_px.y, radius_px, 0, 2 * Math.PI); if (fillColor != 'noFill') { ctx.fillStyle = fillColor; ctx.fill(); } ctx.stroke(); // Turn off the dashes. Remember, ctx is global... ctx.setLineDash([0]); } function drawPolygon( poly_px, pars) { ctx.strokeStyle = pars.borderColor || 'white'; ctx.lineWidth = pars.borderWidth_px || 2; ctx.fillStyle = pars.fillColor || 'red'; var fillIt = setDefault( pars.fillIt, true); ctx.setLineDash([0]); ctx.beginPath(); ctx.moveTo(poly_px[0].x, poly_px[0].y); for (var i = 1, len = poly_px.length; i < len; i++) { ctx.lineTo(poly_px[i].x, poly_px[i].y); } //ctx.lineTo(poly_px[0].x, poly_px[0].y); ctx.closePath(); if (fillIt) ctx.fill(); ctx.stroke(); } // Functions called by the buttons ////////////////////////////////////////// function toggleElementDisplay(id, displayStyle) { var e = document.getElementById( id); // Use ternary operator (?): condition ? expr1 : expr2 // If the current style isn't equal to the incoming displayStyle, set it to be displayStyle. // If it is equal, set it to 'none'. When the value is 'none', the element is hidden. // The effect of this function is that repeated calls to it, with the same displayStyle value, will // toggle the style between 'none' and the specified style value. e.style.display = (e.style.display != displayStyle) ? displayStyle : 'none'; } function setElementDisplay(id, displayStyle) { var e = document.getElementById( id); e.style.display = displayStyle; } function toggleSpanValue(id, value1, value2) { var e = document.getElementById(id); e.innerText = (e.innerText == value1) ? value2 : value1; } function resetFenceColor( newColor) { for (var wallName in aT.wallMap) { var theWall = aT.wallMap[ wallName]; if (theWall.fence) { theWall.color = newColor; theWall.draw(); } } } function fenceIsClientColor( clientName) { var theyMatch = true; for (var wallName in aT.wallMap) { var theWall = aT.wallMap[ wallName]; if (theWall.fence) { if (theWall.color != clients[clientName].color) { theyMatch = false; } } } return theyMatch; } function setPauseState( e) { // Make the pause state agree with the check box. if (dC.pause.checked) { stopit(); setElementDisplay("fps_wrapper", "none"); setElementDisplay("stepper_wrapper", "inline"); } else { startit(); c.singleStep = false; setElementDisplay("fps_wrapper", "inline"); setElementDisplay("stepper_wrapper", "none"); } } function startit() { // Only start a game loop if there is no game loop running. if (myRequest === null) { resetFenceColor( "white"); if (!c.singleStep) dC.pause.checked = false; // Start the game loop. myRequest = window.requestAnimationFrame( gameLoop); } } function stopit() { resetFenceColor( "red"); aT.dt_RA_ms.reset(); dC.fps.innerHTML = '0'; window.cancelAnimationFrame( myRequest); myRequest = null; resumingAfterPause = true; } function stepAnimation() { dC.pause.checked = true; // Set flag to allow only one step. c.singleStep = true; startit(); } function setFrameRateBasedOnDisplayRate() { console.log("fps=" + dC.fps.innerHTML); var current_fps = dC.fps.innerHTML; var fps_choices = [60,85,100,120,144]; var min_diff = 1000; var min_diff_index = null; var len = fps_choices.length; for (var i = 0; i < len; i++) { var diff = Math.abs( fps_choices[i] - current_fps); if (diff < min_diff) { min_diff = diff; min_diff_index = i; } } var bestMatch = fps_choices[ min_diff_index]; // Set the value in the pulldown control. $('#FrameRate').val( bestMatch); setFrameRate(); } function setFrameRate() { var frameRate = $('#FrameRate').val(); //console.log("Frame Rate = " + frameRate); if (frameRate != 'float') { c.frameRate = frameRate; c.deltaT_s = 1.0 / frameRate; c.dtFloating = false; // experiment with setting puck tail length for each frame rate var tail_length = {30:25, 60:25, 85:25 ,100:25 ,120:25, 144:25}; for (var puckName in aT.puckMap) { var puck = aT.puckMap[ puckName]; if (puck.tail) { //puck.tail.length_limit = Math.round(10 * (frameRate / 60.0)); puck.tail.length_limit = tail_length[ frameRate]; //console.log('puck.tail.length_limit=' + puck.tail.length_limit); } } } else { c.dtFloating = true; } } function freeze() { for (var puckName in aT.puckMap) { aT.puckMap[ puckName].b2d.SetLinearVelocity( new b2Vec2(0.0,0.0)); } } function stopRotation() { for (var puckName in aT.puckMap) { aT.puckMap[ puckName].b2d.SetAngularVelocity( 0.0); } } function saveState() { // Make a record of what pin each NPC is attached to at the time of the capture. var tableState = {'demoIndex':c.demoIndex, 'gravity':c.g_ON, 'wallMapData':aT.wallMap, 'puckMapData':aT.puckMap, 'pinMapData':aT.pinMap, 'springMapData':aT.springMap, 'clients':clients}; // Use this function to exclude the b2d objects in the stringify process. Apparently // the b2d and rtc objects have circular references that stringify doesn't like. So have // to regenerate the b2d objects in the demo area when the json capture is restored. // Also have to avoid the client related addons: jet, gun, and shield. These have references // back their pucks, this too causes circular issue for stringify. // Remove the spo1 and spo2 keys (from Springs object) mainly to keep the wordiness // down; don't need them in the reconstruction process. // || (key == 'spo1') || (key == 'spo2') // So be careful here: any key with a name in the OR list below will be excluded. var table_JSON = JSON.stringify( tableState, function(key, value) { if ((key == 'b2d') || (key == 'rtc') || (key == 'jet') || (key == 'gun') || (key == 'shield') || (key == 'spo1') || (key == 'spo2')) { return undefined; } else { return value; } }); // Write the json string to this visible input field. dC.json.value = table_JSON; // Wait 0.5 seconds, then scroll the input field to the top. window.setTimeout( function() { dC.json.scrollTop = 0; }, 500); // Select, copy to clipboard, and then remove focus from the input field. dC.json.select(); document.execCommand('copy'); window.getSelection().removeAllRanges(); // this is necessary for the blur method to work in MS Edge. dC.json.blur(); // jquery test for isolating the Edge issue above. //$( "#chkEditor" ).focus(); //$( "#chkEditor" ).blur(); } function clearState() { dC.json.value = ''; } function newBirth( captureObj) { // Update the birth object (based on the capture state) and use it for restoration. var newBirthState = {}; for (var birthParm in captureObj.parsAtBirth) { newBirthState[ birthParm] = captureObj[ birthParm]; } // To override the default naming process, specify a name in the birth parameters. This gives // the new object the name used in the capture object. This is critical in reconstructing // springs (that use the original puck name). This is all especially important if pucks are // deleted in a jello matrix. if (captureObj.name) { newBirthState.name = captureObj.name; } return newBirthState; } function restoreFromState( state_data) { // Environmental parameters... c.g_ON = state_data.gravity; dC.gravity.checked = c.g_ON; setGravityRelatedParameters(); // Rebuild the walls from the capture data. for (var wallName in state_data.wallMapData) { // wall is one wall (captured state) var wall = state_data.wallMapData[ wallName]; // Keep rotational state changes (put the capture in the birth object). if (wall.angle_r) wall.parsAtBirth.angle_r = wall.angle_r; if (wall.angularSpeed_rps) wall.parsAtBirth.angularSpeed_rps = wall.angularSpeed_rps; // Create the new Wall and add it to the wallMap (via its constructor). new Wall( wall.position_2d_m, newBirth( wall)); } // NPC clients... for (var clientName in state_data.clients) { if (clientName.slice(0,3) == 'NPC') { clients[ clientName] = new Client({'name':clientName, 'color':'purple'}); } } // Rebuild the pins. for (var pinName in state_data.pinMapData) { // "pin" is one pin (captured state) var pin = state_data.pinMapData[ pinName]; // Create the new Pin and add it to the pinMap (via its constructor). new Pin( pin.position_2d_m, newBirth( pin)); } // Rebuild the pucks (and the puck map). for (var p_key in state_data.puckMapData) { // puck is a single puck (captured state) var puck = state_data.puckMapData[ p_key]; // Keep rotational state changes (put the capture in the birth object). if (puck.angle_r) puck.parsAtBirth.angle_r = puck.angle_r; if (puck.angularSpeed_rps) puck.parsAtBirth.angularSpeed_rps = puck.angularSpeed_rps; // Keep edit changes to the dimensions (of a rectangular puck). if (puck.half_height_m) puck.parsAtBirth.half_height_m = puck.half_height_m; if (puck.half_width_m) puck.parsAtBirth.half_width_m = puck.half_width_m; // Now create the puck and give it the old name. // Note that vectors are recreated here because the state capture doesn't include any methods (functions). // Network-client pucks are prevented from recreation here (because it depends on active clients for assignment). if ((!puck.bullet) && ((puck.clientName == null) || (puck.clientName.slice(0,3) == 'NPC'))) { var newPuck = new Puck( puck.position_2d_m, puck.velocity_2d_mps, newBirth( puck)); if (puck.jello) { aT.jelloPucks.push( newPuck); } } } // Rebuild the spring. for (var springName in state_data.springMapData) { var theSpring = state_data.springMapData[ springName]; // Don't try and restore a navigation spring. Those are created // when the NPC pucks are restored. if (!theSpring.navigationForNPC) { var p1_type = theSpring.p1_name.slice(0,3); if (p1_type == "pin") { var p1 = aT.pinMap[ theSpring.p1_name]; } else { var p1 = aT.puckMap[ theSpring.p1_name]; } var p2_type = theSpring.p2_name.slice(0,3); if (p2_type == "pin") { var p2 = aT.pinMap[ theSpring.p2_name]; } else { var p2 = aT.puckMap[ theSpring.p2_name]; } new Spring(p1, p2, newBirth( theSpring)); } } } // Functions in support of the demos //////////////////////////////////////// function scrollDemoHelp( targetID) { var container = $('#helpScroller'); var scrollTo = $(targetID); var tweak_px = -6; /* console.log('--------'); console.log('scrollTo.offset().top: ' + scrollTo.offset().top); console.log('container.offset().top: ' + container.offset().top); console.log('container.scrollTop(): ' + container.scrollTop()); console.log('tweak_px: ' + tweak_px); */ container.animate( {scrollTop: scrollTo.offset().top - container.offset().top + container.scrollTop() + tweak_px}, 500 ); } // Editor help toggle function openDemoHelp() { // Not using this anymore. A bit confusing. Might bring it back. if (dC.multiplayer.checked) { dC.multiplayer.checked = !dC.multiplayer.checked; toggleMultiplayerStuff(); } toggleElementDisplay('outline1','block'); toggleSpanValue('moreOrLess','More','Less'); toggleSpanValue('moreOrLess2','More','Less'); scrollDemoHelp('#editorMark'); } function resetFrictionParameters() { c.restitution_gOn = c.restitution_default_gOn; c.friction_gOn = c.friction_default_gOn; c.restitution_gOff = c.restitution_default_gOff; c.friction_gOff = c.friction_default_gOff; } function setGravityRelatedParameters() { if (c.g_ON) { Box2D.Common.b2Settings.b2_velocityThreshold = 1.0; g_2d_mps2 = new Vec2D(0.0, -g_mps2); // Global var restitution = c.restitution_gOn; var friction = c.friction_gOn; } else { Box2D.Common.b2Settings.b2_velocityThreshold = 0.0; g_2d_mps2 = new Vec2D(0.0, 0.0); // Global var restitution = c.restitution_gOff; var friction = c.friction_gOff; } // If there are some existing pucks on the table: // If not fixed, set restitution and friction properties. for (var puckName in aT.puckMap) { if (!aT.puckMap[ puckName].restitution_fixed) { aT.puckMap[ puckName].b2d.m_fixtureList.m_restitution = restitution; } if (!aT.puckMap[ puckName].friction_fixed) { aT.puckMap[ puckName].b2d.m_fixtureList.m_friction = friction; } } } function make_fence() { // 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; 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 new Wall( new Vec2D( half_width_m, height_m - pull_in_m), short_wide_dimensions); // bottom new Wall( new Vec2D( half_width_m, 0.00 + pull_in_m), short_wide_dimensions); // left new Wall( new Vec2D( 0.00 + pull_in_m, half_height_m), tall_skinny_dimensions); // right new Wall( new Vec2D( width_m - pull_in_m, half_height_m), tall_skinny_dimensions); } function em( px) { // Convert to em units based on a font-size of 16px. return px/16.0; } function getChatLayoutState() { // This (exposed) function is needed to share this parameter with the other module. return c.chatLayoutState; } function adjustSizeOfChatDiv( mode) { // Input fields dC.nodeServer = document.getElementById('nodeServer'); dC.roomName = document.getElementById('roomName'); dC.inputField = document.getElementById('inputField'); dC.connectionCanvas = document.getElementById('connectionCanvas'); // The two divs that toggle dC.multiPlayer = document.getElementById('multiPlayer'); dC.ttcIntro = document.getElementById('ttcIntro'); var divW_Large = em(540); var divW_Small = em(540-118); var tweek = -8; var nodeServer_Large = em(332+tweek); var roomName_Large = em( 70+0); var inputField_Large = em(527+tweek); var connectionCanvas_Large_px = 518 + tweek; var shrink_px = 141; var shrink = em( shrink_px); var nodeServer_Small = nodeServer_Large - shrink; var roomName_Small = roomName_Large - em(0); var inputField_Small = inputField_Large - shrink; var connectionCanvas_Small_px = connectionCanvas_Large_px - 117; if (mode == 'small') { dC.nodeServer.style.width = (nodeServer_Small) + 'em'; dC.roomName.style.width = (roomName_Small ) + 'em'; dC.inputField.style.width = (inputField_Small) + 'em'; // The connectionCanvas is only on the client. So check here before the resize attempt. if (dC.connectionCanvas) { dC.connectionCanvas.width = connectionCanvas_Small_px; dC.connectionCanvas.height = 15; hC.refresh_P2P_indicator(); } dC.ttcIntro.style.maxWidth = divW_Small + 'em'; dC.ttcIntro.style.minWidth = divW_Small + 'em'; dC.multiPlayer.style.maxWidth = divW_Small + 'em'; dC.multiPlayer.style.minWidth = divW_Small + 'em'; } else { dC.nodeServer.style.width = (nodeServer_Large) + 'em'; dC.roomName.style.width = (roomName_Large ) + 'em'; dC.inputField.style.width = (inputField_Large) + 'em'; if (dC.connectionCanvas) { dC.connectionCanvas.width = connectionCanvas_Large_px; dC.connectionCanvas.height = 15; hC.refresh_P2P_indicator(); } dC.ttcIntro.style.maxWidth = divW_Large + 'em'; dC.ttcIntro.style.minWidth = divW_Large + 'em'; dC.multiPlayer.style.maxWidth = divW_Large + 'em'; dC.multiPlayer.style.minWidth = divW_Large + 'em'; } } function makeJello( pars) { var pinned = setDefault( pars.pinned, false); var gridsize = pars.gridsize || 4; var offset_2d_m = new Vec2D(2.0, 2.0); var spacing_factor_m = 0.9; var v_init_2d_mps = new Vec2D(0.0, 0.0); var puckParms = {'radius_m':0.20, 'density':5.0, 'jello':true}; var springParms = { 'unstretched_width_m': 0.07, 'strength_Npm': 350.0, 'length_m': spacing_factor_m * 1.0, 'damper_Ns2pm2': 5.0}; // Grid of pucks. for (var j = 0; j < gridsize; j++) { for (var k = 0; k < gridsize; k++) { if ((j==2) && (k==2)) { puckParms.color = "orange"; } else { puckParms.color = null; } var pos_2d_m = new Vec2D( spacing_factor_m * j, spacing_factor_m * k); pos_2d_m.addTo( offset_2d_m); aT.jelloPucks.push( new Puck( pos_2d_m, v_init_2d_mps, puckParms)); } } // Horizontal springs (between neighbors) for (var m = 0; m < gridsize*(gridsize-1); m++) { springParms.color = "blue"; // Note: Object.assign is used here to make a copy of the springParms object (mutable). This avoids the multiple reference to springParms // and any associated mutation side effects (from this and the following color changes) when the state is captured. new Spring(aT.jelloPucks[m], aT.jelloPucks[m+gridsize], Object.assign({}, springParms)); } // Vertical springs for (var m = 0; m < gridsize-1; m++) { for (var n = 0; n < gridsize; n++) { var o_index = m + (n * gridsize); springParms.color = "blue"; new Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index+1], Object.assign({}, springParms)); } } // Diagonal springs for (var m = 0; m < gridsize-1; m++) { for (var n = 1; n < gridsize; n++) { var o_index = m + (n * gridsize); springParms.color = "yellow"; springParms.length_m = spacing_factor_m * 1.41; // A diagonal new Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index-(gridsize-1)], Object.assign({}, springParms)); } } // Diagonal springs (perpendicular to the other diagonals) for (var m = 0; m < gridsize-1; m++) { for (var n = 0; n < gridsize-1; n++) { var o_index = m + (n * gridsize); springParms.color = "yellow"; springParms.length_m = spacing_factor_m * 1.41; // A diagonal new Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index+(gridsize+1)], Object.assign({}, springParms)); } } // Add two pinned springs. if (pinned) { var corner_puck = (gridsize * gridsize) - 1; new Spring(aT.jelloPucks[ 0], new Pin( new Vec2D( 0.5, 0.5), {radius_px:4}), {strength_Npm:800.0, unstretched_width_m:0.3, color:'brown',damper_Ns2pm2:5.0}); new Spring(aT.jelloPucks[ corner_puck], new Pin( new Vec2D( 9.0, 9.0), {radius_px:4}), {strength_Npm:800.0, unstretched_width_m:0.3, color:'brown',damper_Ns2pm2:5.0}); } } function checkForJelloTangle() { // Determine if tangled by looking for balls that are fairly close to // each other. This does not require puck contact to detect a tangle. var diameter = 2 * aT.jelloPucks[0].radius_m; // A little more than the square of the diameter. var separation_check = Math.pow(diameter, 2) * 1.5; // A looping structure that avoids self reference and repeated puck-otherpuck references. for (var j = 0, len = aT.jelloPucks.length; j < len; j++) { for (var k = j+1; k < len; k++) { // Check distance between j and k pucks. var diff_2d_m = aT.jelloPucks[j].position_2d_m.subtract( aT.jelloPucks[k].position_2d_m); // Square of the vector length. var lenSquared = diff_2d_m.length_squared(); if (lenSquared < separation_check) { // This one is too close to be in a non-tangled jello block. //console.log(j + "," + k); c.tangleTimer_s += c.deltaT_s; j = k = 10000; // get out of the loops. } } } ctx.font = "30px Arial"; ctx.fillStyle = 'yellow'; ctx.fillText(c.tangleTimer_s.toFixed(2),10,40); } /* Tried using the B2D contact listener to detect tangle. But this approach fails to deal with a tangled state where the balls are not quite touching... So the approach above is used. function checkForJelloTangle2() { if (c.contactCounter > 0) { c.tangleTimer_s += c.deltaT_s; } ctx.font = "30px Arial"; ctx.fillStyle = 'yellow'; ctx.fillText(c.tangleTimer_s.toFixed(2),10,50); } */ function demoStart_fromCapture( index, pars) { var fileName = pars.fileName || 'null'; $.getScript( 'http://timetocode.org/'+fileName, function() { console.log('fetching '+ fileName +' from main server'); // Note: demo_capture is a page level global and is referenced in the loading file. // Put the capture (from this file) into the capture input box on the page. dC.json.value = JSON.stringify( demo_capture); demoStart( index); }).fail( function() { // Try the local web server. Maybe the file hasn't been published yet. This will // only work for the developer (that's me). console.log('fetching '+ fileName +' from local server'); $.getScript( 'http://localhost/ttc-root/'+fileName, function() { dC.json.value = JSON.stringify( demo_capture); demoStart( index); }).fail( function() { console.log('capture file not found'); }); }); } function demoStart( index) { var v_init_2d_mps, wallColor; // So you can see the name of the capture if it's there. //$('jsonCapture').scrollTop(0); dC.json.scrollTop = 0; // Set this global to support the JSON capture. c.demoIndex = index; // Scaling factor between the Box2d world and the screen (pixels per meter) px_per_m = 100; // a global canvas.width = 600, canvas.height = 600; adjustSizeOfChatDiv('normal'); hC.resizeClients('normal'); // Set this global to help new connecting clients adjust their layout. c.chatLayoutState = 'normal'; // Change the color of the button that was clicked. for (var j = 1; j <= 8; j++) { if (j == index) wallColor = "yellow"; else wallColor = "lightgray"; // goldenrod yellow document.getElementById('b'+j).style.backgroundColor = wallColor; } // Delete pucks (and references to them) from the previous demo. Puck.deleteAll(); // Clean out the old springs. Spring.deleteAll(); c.springNameForPasting = null; // Clean out the non-player clients Client.deleteNPCs(); // Clean out the old pins and their representation in the b2d world. Pin.deleteAll(); // Clean out the old walls and their representation in the b2d world. Wall.deleteAll(); // De-select anything still selected. clients['local'].selectedBody = null; multiSelect.resetAll(); resetFenceColor( "white"); if (dC.pause.checked) { dC.pause.checked = false; } setPauseState(); // Turn gravity off by default. if (c.g_ON) { c.g_ON = false; dC.gravity.checked = false; } resetFrictionParameters(); setGravityRelatedParameters(); // Convert (parse) the json capture into a local object. if (dC.json.value != '') { try { var state_capture = JSON.parse( dC.json.value); } catch (err) { var state_capture = null; window.alert("There's a formatting error in the state capture. Try clicking the 'Clear' button."); } } else { var state_capture = null; } if (index == 1) { scrollDemoHelp('#d1234'); if ((state_capture) && (state_capture.demoIndex == 1)) { restoreFromState( state_capture); } else { make_fence(); var v_init_2d_mps = new Vec2D(0.0, -2.0); new Puck( new Vec2D(2.0, 3.99), v_init_2d_mps, {'radius_m':0.10, 'color':'GoldenRod', 'colorSource':true}); new Puck( new Vec2D(2.0, 3.00), v_init_2d_mps, {'radius_m':0.80}); var v_init_2d_mps = new Vec2D(0.0, 2.0); new Puck( new Vec2D(5.00, 1.60+1.5*2), v_init_2d_mps, {'radius_m':0.35}); new Puck( new Vec2D(5.00, 1.60+1.5), v_init_2d_mps, {'radius_m':0.35, 'color':'GoldenRod', 'colorSource':true}); new Puck( new Vec2D(5.00, 1.60), v_init_2d_mps, {'radius_m':0.35}); new Puck( new Vec2D(0.50, 5.60), new Vec2D(0.40, 0.00), {'radius_m':0.15}); } } else if (index == 2) { scrollDemoHelp('#d2'); if ((state_capture) && (state_capture.demoIndex == 2)) { restoreFromState( state_capture); } else { make_fence(); new Puck( new Vec2D(4.5, 4.5), new Vec2D( 0.0, 0.0), {'radius_m':0.20, 'createTail':true,'tailPars':{'propSpeed_mps':1.5, 'length_limit':25}}); new Puck( new Vec2D(3.0, 3.0), new Vec2D( 0.0, 0.0), {'radius_m':0.60, 'color':'GoldenRod', 'colorSource':true, 'angularSpeed_rps':0.0}); new Puck( new Vec2D(1.5, 1.5), new Vec2D( 0.0, 0.0), {'radius_m':0.20, 'createTail':true,'tailPars':{'propSpeed_mps':1.5, 'length_limit':25}}); } } else if (index == 3) { scrollDemoHelp('#d1234'); c.restitution_gOn = 0.7; c.friction_gOn = 0.6; c.restitution_gOff = 1.0; c.friction_gOff = 0.0; v_init_2d_mps = new Vec2D(0.0, 2.0); if ((state_capture) && (state_capture.demoIndex == 3)) { restoreFromState( state_capture); } else { make_fence(); var grid_order = 7; var grid_spacing_m = 0.45; var startPosition_2d_m = new Vec2D(0.0, 0.0); for (var i = 1; i <= grid_order; i++) { for (var j = 1; j <= grid_order; j++) { var delta_2d_m = new Vec2D( i * grid_spacing_m, j * grid_spacing_m); var position_2d_m = startPosition_2d_m.add( delta_2d_m); new Puck(position_2d_m, v_init_2d_mps, {'radius_m':0.10}); } } v_init_2d_mps = new Vec2D(0.2, 0.0); new Puck( new Vec2D(5.5, 3.5), v_init_2d_mps, {'radius_m':0.10, 'color':'GoldenRod', 'colorSource':true} ); } } else if (index == 4) { scrollDemoHelp('#d4'); c.restitution_gOn = 0.7; c.friction_gOn = 0.6; c.restitution_gOff = 1.0; c.friction_gOff = 0.0; if ((state_capture) && (state_capture.demoIndex == 4)) { restoreFromState( state_capture); } else { make_fence(); new Puck( new Vec2D(3.00, 3.00), new Vec2D( 0.0, 0.0), {'radius_m':0.40, 'color':'GoldenRod', 'colorSource':true , 'shape':'rect', 'angularSpeed_rps':25.0}); new Puck( new Vec2D(0.25, 3.00), new Vec2D( 2.0, 0.0), {'radius_m':0.15, 'shape':'rect', 'aspectR':4.0, 'angularSpeed_rps':0, 'angle_r': Math.PI/2}); new Puck( new Vec2D(5.75, 3.00), new Vec2D(-2.0, 0.0), {'radius_m':0.15, 'shape':'rect', 'aspectR':4.0, 'angularSpeed_rps':0, 'angle_r': Math.PI/2}); // Include two pins and a spring as a source for replicating. new Spring( new Pin( new Vec2D( 0.1, 0.2),{}), new Pin( new Vec2D( 0.1, 1.2),{}), {'length_m':1.5, 'strength_Npm':10.0, 'unstretched_width_m':0.1, 'color':'yellow', 'damper_Ns2pm2':1.0}); } } else if (index == 5) { scrollDemoHelp('#d5'); c.restitution_gOn = 0.7; c.friction_gOn = 0.6; c.restitution_gOff = 1.0; c.friction_gOff = 0.0; v_init_2d_mps = new Vec2D(0.0,0.0); if ((state_capture) && (state_capture.demoIndex == 5)) { restoreFromState( state_capture); } else { make_fence(); // Spring triangle. var tri_vel_mps = new Vec2D( 6.0, 0.0); new Puck( new Vec2D(1.00, 0.70 + Math.sin(60.0*Math.PI/180)), tri_vel_mps, {'radius_m':0.20, 'name': 'puck1', 'restitution':0.0}); tri_vel_mps.rotated_by(-240.0); new Puck( new Vec2D(0.50, 0.70 ), tri_vel_mps, {'radius_m':0.20, 'name': 'puck2', 'restitution':0.0}); tri_vel_mps.rotated_by(-240.0); new Puck( new Vec2D(1.50, 0.70 ), tri_vel_mps, {'radius_m':0.20, 'name': 'puck3', 'restitution':0.0}); new Spring(aT.puckMap['puck1'], aT.puckMap['puck2'], {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':'blue'}); new Spring(aT.puckMap['puck1'], aT.puckMap['puck3'], {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':'blue'}); new Spring(aT.puckMap['puck2'], aT.puckMap['puck3'], {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':'blue'}); // A spring with one puck and one pin. new Puck( new Vec2D(4.0, 5.0), new Vec2D(5.0, 0.0), {'radius_m':0.55, 'name':'puck4', 'restitution':0.0}); new Spring(aT.puckMap['puck4'], new Pin( new Vec2D( 4.0, 4.0),{borderColor:'yellow'}), {'strength_Npm':20.0, 'unstretched_width_m':0.2, 'color':'yellow', 'damper_Ns2pm2':1.0}); // Two pucks (one bigger than the other) on spring orbiting each other (upper left corner) new Puck( new Vec2D(0.75, 5.00), new Vec2D(0.0, -5.00 * 1.2), {'radius_m':0.15, 'name':'puck5'}); new Puck( new Vec2D(1.25, 5.00), new Vec2D(0.0, 1.80 * 1.2), {'radius_m':0.25, 'name':'puck6'}); new Spring(aT.puckMap['puck5'], aT.puckMap['puck6'], {'length_m':0.5, 'strength_Npm':5.0, 'unstretched_width_m':0.05, 'color':'yellow'}); // Same thing (lower right corner) new Puck( new Vec2D(5.00, 0.55), new Vec2D(+4.90, 0.0), {'radius_m':0.20, 'name':'puck7'}); new Puck( new Vec2D(5.00, 1.55), new Vec2D(-4.90, 0.0), {'radius_m':0.20, 'name':'puck8'}); new Spring(aT.puckMap['puck7'], aT.puckMap['puck8'], {'length_m':0.5, 'strength_Npm':5.0, 'unstretched_width_m':0.05, 'color':'yellow'}); } } else if (index == 6) { scrollDemoHelp('#d6'); c.g_ON = false; dC.gravity.checked = false; c.restitution_gOn = 0.0; c.friction_gOn = 0.6; c.restitution_gOff = 0.0; c.friction_gOff = 0.6; c.tangleTimer_s = 0.0; if ((state_capture) && (state_capture.demoIndex == 6)) { restoreFromState( state_capture); } else if ( demo_6_fromFile) { restoreFromState( demo_6_fromFile); } else { make_fence(); makeJello({}); } setGravityRelatedParameters(); // An extra puck to play with. //puckParms.restitution = 0.0; //new Puck( 3.8, 5.5, v_init_2d_mps, puck_radius_m * 2.8, puckParms); } else if (index == 7) { scrollDemoHelp('#d7'); if ((state_capture) && (state_capture.demoIndex == 7)) { restoreFromState( state_capture); } else { make_fence(); // Normal pucks new Puck( new Vec2D(0.35, 0.35), new Vec2D( 0.0, 4.0), {'radius_m':0.25}); // , 'categoryBits':'0x0000', 'maskBits':'0x0000', 'color':'pink' new Puck( new Vec2D(5.65, 0.35), new Vec2D( 0.0, 4.0), {'radius_m':0.25}); // , 'categoryBits':'0x0000', 'maskBits':'0x0000', 'color':'pink' new Puck( new Vec2D(2.75, 0.35), new Vec2D(+2.0, 0.0), {'radius_m':0.25}); new Puck( new Vec2D(3.25, 0.35), new Vec2D(-2.0, 0.0), {'radius_m':0.25}); new Puck( new Vec2D(0.35, 5.65), new Vec2D(+2.0, 0.0), {'radius_m':0.25}); new Puck( new Vec2D(5.65, 5.65), new Vec2D(-2.0, 0.0), {'radius_m':0.25}); // Shelter // Vertical part new Wall( new Vec2D( 3.0, 3.0), {'half_width_m':0.02, 'half_height_m':0.50}); // Horizontal part new Wall( new Vec2D( 3.0, 3.0), {'half_width_m':0.50, 'half_height_m':0.02}); // Add some pins for NPC client navigation. var pinRadius = 3; var e1 = 1.5, e2 = 4.5; var p1 = new Pin( new Vec2D( e1, e1), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin4', 'name':'pin1', 'nextPinName':'pin2'}); var p2 = new Pin( new Vec2D( e2, e1), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin1', 'name':'pin2', 'nextPinName':'pin3'}); var p3 = new Pin( new Vec2D( e2, e2), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin2', 'name':'pin3', 'nextPinName':'pin4'}); var p4 = new Pin( new Vec2D( e1, e2), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin3', 'name':'pin4', 'nextPinName':'pin1'}); // Add some local non-player clients (NPCs) clients['NPC1'] = new Client({'name':'NPC1', 'color':'purple'}); clients['NPC2'] = new Client({'name':'NPC2', 'color':'purple'}); // Controllable pucks for these NPC clients; assign a starting pin. new Puck( p1.position_2d_m, new Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC1', 'linDamp':1.0, 'hitLimit':20, 'pinName':'pin1', 'rayCast_init_deg':90} ); new Puck( p3.position_2d_m, new Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC2', 'linDamp':1.0, 'hitLimit':20, 'pinName':'pin3', 'rayCast_init_deg':-90} ); } // Make a controlled puck for local and network clients. for (var clientName in clients) { if (clientName.slice(0,3) != 'NPC') { new Puck( new Vec2D(3.0, 5.5), new Vec2D(0.0, -2.5), {'radius_m':0.30, 'color':'black', 'colorSource':true, 'clientName':clientName, 'linDamp':1.0, 'hitLimit':20} ); } } } else if (index == 8) { canvas.width = 1250, canvas.height = 950; adjustSizeOfChatDiv('small'); hC.resizeClients('small'); // Set this global to help new connecting clients adjust their layout. c.chatLayoutState = 'small'; // Must do this after the chat-div adjustment. scrollDemoHelp('#d8'); c.g_ON = false; dC.gravity.checked = false; c.restitution_gOn = 0.0; //0.7 c.friction_gOn = 0.6; c.restitution_gOff = 0.0; //1.0 c.friction_gOff = 0.6; setGravityRelatedParameters(); if ((state_capture) && (state_capture.demoIndex == 8)) { restoreFromState( state_capture); } else if (demo_8_fromFile) { // Don't need to parse here because read in from a file. restoreFromState( demo_8_fromFile); // Some little walls in the middle. /* new Wall( new Vec2D( 2.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14}); new Wall( new Vec2D( 3.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 4.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2}); new Wall( new Vec2D( 5.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 6.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14}); new Wall( new Vec2D( 7.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 8.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2}); */ /* var pinRadius = 3; var p1 = new Pin( new Vec2D( 1.0, 2.0), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin103', 'name':'pin101', 'nextPinName':'pin102'}); var p2 = new Pin( new Vec2D( 1.0, 4.0), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin101', 'name':'pin102', 'nextPinName':'pin103'}); var p3 = new Pin( new Vec2D( 1.0, 5.0), {'radius_px':pinRadius, 'NPC':true, 'previousPinName':'pin102', 'name':'pin103', 'nextPinName':'pin101'}); */ /* // Add some local non-player clients (NPCs) clients['NPC3'] = new Client({'name':'NPC3', 'color':'purple'}); clients['NPC4'] = new Client({'name':'NPC4', 'color':'purple'}); // Controllable pucks for these NPC clients; assign a starting pin. new Puck( new Vec2D( 1.0, 2.0), new Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC3', 'linDamp':1.0, 'hitLimit':20, 'pinName':'pin102'} ); new Puck( new Vec2D( 1.0, 2.0), new Vec2D(0.0, 0.0), {'radius_m':0.30, 'color':'darkblue', 'colorSource':false, 'clientName':'NPC4', 'linDamp':1.0, 'hitLimit':20, 'pinName':'pin103'} ); */ } else { makeJello({'pinned':true, 'gridsize':4}); make_fence(); // Some little walls in the middle. new Wall( new Vec2D( 2.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2}); new Wall( new Vec2D( 3.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 4.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 5.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 6.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 7.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 8.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); } // Make a controlled puck for each non-NPC client. Randomize the position and initial velocity. for (var clientName in clients) { if (clientName.slice(0,3) != 'NPC') { new Puck( new Vec2D( (meters_from_px( canvas.width)-0.3) * Math.random(), (meters_from_px( canvas.height)-0.3) * Math.random() ), new Vec2D( 15.0 * (Math.random()-0.5), 15.0 * (Math.random()-0.5)), {'radius_m':0.30, 'color':'black', 'colorSource':true, 'clientName':clientName, 'linDamp':1.0, 'hitLimit':20} ); } } } else if (index == 9) { } else if (index == 0) { } } /////////////////////////////////////////////////////// // Initialize almost everything /////////////////////// /////////////////////////////////////////////////////// // init() is called from the index.html page. function init() { // Make a world in Box2D // Constraint on space in world worldAABB = new b2AABB(); worldAABB.lowerBound.Set(-20.0, -20.0); worldAABB.upperBound.Set( 20.0, 20.0); // b2d world: set gravity vector to 0, allow sleep. world = new b2World( new b2Vec2(0, -0.0), true); // Event handlers for Box2D. Get collision information. var listener = new Box2D.Dynamics.b2ContactListener; listener.BeginContact = function (contact) { // Use the table map to get a reference back to a gW object. var body_A = tableMap.get(contact.GetFixtureA().GetBody()); var body_B = tableMap.get(contact.GetFixtureB().GetBody()); //console.log(""); //console.log("A=" + body_A.constructor.name); //console.log("B=" + body_B.constructor.name); // Set the wall color to that of the puck hitting it. if (body_A.constructor.name == "Wall" || body_B.constructor.name == "Wall") { if (body_B.constructor.name == "Puck") { var body_Puck = body_B, body_Wall = body_A; } else { var body_Puck = body_A, body_Wall = body_B; } // If it's a puck designated as a color source, use its client color for the wall. if (body_Puck.colorSource) { if (body_Puck.clientName && body_Wall.fence) { body_Wall.color = clients[body_Puck.clientName].color; } else if (body_Wall.fence) { body_Wall.color = body_Puck.color; } } else { // Reset the wall color to it's default. body_Wall.color = Wall.color_default; } } else if (body_A.constructor.name == "Puck" && body_B.constructor.name == "Puck") { //c.contactCounter++; if (body_A.bullet && !body_B.bullet) { // Can't shoot yourself in the foot. if (body_A.createdByClient != body_B.clientName) { if (!body_B.shield.ON || (body_B.shield.ON && !body_B.shield.STRONG)) { body_B.hitCount += 1; body_B.inComing = true; body_B.flash = true; } } } else if (body_B.bullet && !body_A.bullet) { if (body_B.createdByClient != body_A.clientName) { if (!body_A.shield.ON || (body_A.shield.ON && !body_A.shield.STRONG)) { body_A.hitCount += 1; body_A.inComing = true; body_A.flash = true; } } } } } /* listener.EndContact = function (contact) { // Use the table map to get a reference back to a gW object. var body_A = tableMap.get(contact.GetFixtureA().GetBody()); var body_B = tableMap.get(contact.GetFixtureB().GetBody()); if (body_A.constructor.name == "Puck" && body_B.constructor.name == "Puck") { c.contactCounter--; } } */ world.SetContactListener(listener); // Initialize the canvas display window. myRequest = null; resumingAfterPause = false; time_previous = performance.now(); // Initialize the previous time variable to now. canvas = document.getElementById('hostCanvas'); ctx = canvas.getContext('2d'); helpMessage = new HelpMessage({'message':"...", 'timeLimit_s':1.0, 'loc_px':{'x':15,'y':70} }); selectBox = new SelectBox({}); multiSelect = new MultiSelect(); // Miscellaneous pointers to DOM elements dC.json = document.getElementById('jsonCapture'); ///////////////////////////////////////////////////// // Event handlers for local client (user input) ///////////////////////////////////////////////////// // Inhibit the context menu that pops up when right clicking (third button). // Do this on mainDiv to prevent the menu from appearing when you drag the // mouse off the canvas. var mainDiv = document.getElementById('mainDiv'); mainDiv.addEventListener("contextmenu", function(e) { //console.log('contextmenu event'); e.preventDefault(); return false; }, {capture: false}); // Start a listener for the mousemove event. // // Note: This call to addEventListener could be put (and was for a while) inside the mousedown handler. // Then, if there is a corresponding removeEventListen for this in the mouseup handler, effectively the // the mousemove listener would only run while a mouse button is down. That works out // nicely if you are using the native Windows cursor. But if you are drawing a cursor into // the canvas, you need to keep track of it even if the mouse isn't clicked down. I've commented // out the corresponding removeEventListen (in the mouseup handler) that is no longer in use. document.addEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); canvas.addEventListener("mousedown", function(e) { clients['local'].isMouseDown = true; clients['local'].button = e.button; // Pass this first mouse position to the move handler. This will establish // the world position of the mouse. handleMouseOrTouchMove(e); // (Note: also see the checkForMouseSelection method in the Client prototype.) // Check for body at the mouse position. If nothing there, and shift (and alt) keys are UP, reset the // multi-select map. So, user needs to release the shift (and alt) key and click on open area to // flush out the multi-select. var selected_b2d_Body = b2d_getBodyAt( clients['local'].mouse_2d_m); var selectedBody = tableMap.get( selected_b2d_Body); if ((clients['local'].key_shift == "U") && (clients['local'].key_alt == "U") && (clients['local'].key_ctrl == "U")) { // Un-dash all the springs. Spring.findAll_InMultiSelect( function( spring) { spring.dashedLine = false; }); // Clicked on blank space on air table (un-selecting everything) if (!selected_b2d_Body) { // Un-select everything in the multi-select map. multiSelect.resetAll(); } } // start a cursor-based selection box. if ((clients['local'].key_alt == 'D') && (clients['local'].button == 0) && (!selectBox.enabled)) { selectBox.start(); selectBox.update(); } // This prevents the middle mouse button from doing scrolling operations. e.preventDefault(); }, {capture: false}); canvas.addEventListener("touchstart", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; clients['local'].isMouseDown = true; clients['local'].button = 0; // Start a listener for the touchmove event. document.addEventListener("touchmove", handleMouseOrTouchMove, {passive: true, capture: false}); //Pass this first mouse position to the move handler. handleMouseOrTouchMove(e); }, {passive: true, capture: false}); function handleMouseOrTouchMove(e) { // Determine if mouse or touch. if (e.clientX) { // Mouse var raw_x_px = e.clientX; var raw_y_px = e.clientY; } else if (e.touches) { // Touch var raw_x_px = e.touches[0].clientX; var raw_y_px = e.touches[0].clientY; } clients['local'].mouseX_px = raw_x_px - canvas.getBoundingClientRect().left; clients['local'].mouseY_px = raw_y_px - canvas.getBoundingClientRect().top; clients['local'].mouse_2d_px = new Vec2D(clients['local'].mouseX_px, clients['local'].mouseY_px); // A tweak to make the cursor location be at the very tip of the arrow icon. // Note the offsets (tweaks) here are the same as in the handleMouseOrTouchMove handler of hostAndClient.js var cursorCorrection_2d_px = new Vec2D(-5,-4); clients['local'].mouse_2d_px.addTo( cursorCorrection_2d_px); clients['local'].mouse_2d_m = worldFromScreen( clients['local'].mouse_2d_px); //console.log("x,y=" + clients['local'].mouse_2d_m.x + "," + clients['local'].mouse_2d_m.y); }; document.addEventListener("mouseup", function(e) { // Remove focus from checkboxes after use (release mouse button). This is needed for // the canvas to get immediate attention when using the control and shift keys. dC.gravity.blur(); dC.pause.blur(); dC.comSelection.blur(); dC.multiplayer.blur(); dC.stream.blur(); dC.editor.blur(); dC.localCursor.blur(); if (!clients['local'].isMouseDown) return; // Stop (using cpu) watching the mouse position. // Note the following code line is necessarily commented now that the mousemove listener is created // outside of the mousedown event. //document.removeEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); resetMouseOrFingerState(e); // Close the selection box. selectBox.stop(); }, {capture: false}); canvas.addEventListener("touchend", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; if (!clients['local'].isMouseDown) return; // Stop (using cpu) watching the position. document.removeEventListener("touchmove", handleMouseOrTouchMove, {passive: true, capture: false}); resetMouseOrFingerState(e); }, {passive: true, capture: false}); function resetMouseOrFingerState(e) { clients['local'].isMouseDown = false; clients['local'].button = null; clients['local'].mouseX_m = null; clients['local'].mouseY_m = null; } var arrowKeysMap = {'key_leftArrow':'thinner', 'key_rightArrow':'wider', 'key_upArrow':'taller', 'key_downArrow':'shorter'}; document.addEventListener("keydown", function(e) { //console.log(e.keyCode + "(down/repeated)=" + String.fromCharCode(e.keyCode)); // The following is necessary in Firefox to avoid the spacebar from re-clicking // page controls (like the demo buttons) if they have focus. // This also prevents some unwanted spacebar-related button behavior in Chrome. if ((document.activeElement.tagName != 'BODY') && (document.activeElement.tagName != 'INPUT')) { document.activeElement.blur(); } //console.log("activeElement tagName = " + document.activeElement.tagName); /* Anything at this outer level will repeat if the key is held down for a while. Holding it down will fire the keydown event repeatedly. Of course this area only affects the local client. Note there is another area in this code where repetition is avoided though use of the key_?_enabled attributes; search on key_s_enabled for example. That repetition is of a different nature in that it comes from action triggered by observing the key state (up/down) each frame. */ // Note: added the activeElement clause to avoid acting on keystrokes while typing in the input cells in MS Edge. if ((e.keyCode in keyMap) && (document.activeElement.tagName != 'INPUT')) { // If you want down keys to repeat, put them here. // This allows the spacebar to be used for the puck shields. if (keyMap[e.keyCode] == 'key_space') { // Inhibit page scrolling that results from using the spacebar. // Also inhibit repeat presses of the demo keys. e.preventDefault(); } else if (keyMap[e.keyCode] in arrowKeysMap) { // Note: if arrowKeys is an array, instead of an object map, // the check can be done this way: (arrowKeys.indexOf( keyMap[e.keyCode]) != -1). // Prevent page scrolling when using the arrow keys in the editor. e.preventDefault(); } // Control body angle when editing. if (keyMap[e.keyCode] == 'key_z') { if (clients['local'].key_shift == 'D') { // Rotate counterclockwise var angle_change_d = +2; // degrees } else { // Rotate clockwise var angle_change_d = -2; // degrees } if (clients['local'].selectedBody) { var current_angle_r = clients['local'].selectedBody.b2d.GetAngle(); var new_angle_r = current_angle_r + angle_change_d*(Math.PI/180); clients['local'].selectedBody.angle_r = new_angle_r; clients['local'].selectedBody.b2d.SetAngle( new_angle_r); } } // Change body rotation when editing. if (keyMap[e.keyCode] == 'key_t') { if (clients['local'].key_shift == 'D') { // Increase rate counterclockwise var rotRate_change_dps = +5; // degrees per second } else { // Increase rate clockwise var rotRate_change_dps = -5; // degrees per second } if (clients['local'].selectedBody) { var current_rotRate_rps = clients['local'].selectedBody.b2d.GetAngularVelocity(); var new_rotRate_rps = current_rotRate_rps + rotRate_change_dps*(Math.PI/180); clients['local'].selectedBody.angularSpeed_rps = new_rotRate_rps; clients['local'].selectedBody.b2d.SetAngularVelocity( new_rotRate_rps); if (clients['local'].selectedBody.constructor.name == "Wall") { // If not currently rotating, will need to delete and recreate the body. This is // an oddity of b2d in that you can't change the rotation rate on an existing kinematic body that currently // is NOT rotating. if (current_rotRate_rps == 0.0) { // Make a temporary reference to the selected body. var oldWall = clients['local'].selectedBody; // Delete the selected wall. clients['local'].selectedBody.deleteThisOne(); // Point the client reference to a new wall. Rebuild the wall at the new rotational rate (all other parameters are equal to those of the old wall). clients['local'].selectedBody = new Wall( oldWall.position_2d_m, {'half_width_m':oldWall.half_width_m, 'half_height_m':oldWall.half_height_m, 'angle_r':oldWall.angle_r, 'angularSpeed_rps':new_rotRate_rps}); } } } } // Use the arrow keys to change the dimensions of the selected body. if (keyMap[e.keyCode] in arrowKeysMap) { // Multi-select if (multiSelect.count() > 0) { // Springs (s key is down) if (clients['local'].key_s == 'D') { Spring.findAll_InMultiSelect( function( spring) { spring.modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]); }); // All other object types } else { for (var objName in multiSelect.map) { if (multiSelect.map[ objName].constructor.name != "Pin") { multiSelect.map[ objName].modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]) } } } } // Single-body selection (client string) if (clients['local'].selectedBody) { clients['local'].selectedBody.modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]); } } /* Keys that are held down will NOT repeat in this next block. Current key state must be up before it will change the state to down and perform the action. This is for cases where you are toggling the state of the client's key parameter. Also see comment paragraph on repetition above. */ // If the current key state is UP... if (clients['local'][keyMap[e.keyCode]] == 'U') { // Set the key state to be DOWN. clients['local'][keyMap[e.keyCode]] = 'D'; //console.log(e.keyCode + "(down)=" + keyMap[e.keyCode]); // Immediate execution on keydown (that's the event that got you in here.): if ((keyMap[e.keyCode] == 'key_c') && (clients['local'].key_ctrl != 'D')) { dC.comSelection.checked = !dC.comSelection.checked; comSelection_Toggle(); } else if (keyMap[e.keyCode] == 'key_f') { freeze(); } else if (keyMap[e.keyCode] == 'key_r') { stopRotation(); } else if (keyMap[e.keyCode] == 'key_g') { c.g_ON = !c.g_ON; if (c.g_ON) { dC.gravity.checked = true; } else { dC.gravity.checked = false; } setGravityRelatedParameters(); /* // If there is only one fixture, m_fixtureList (a linked list) is a reference to that single fixture. console.log(' '); console.log("fixture count=" + aT.wallMap['wall1'].b2d.m_fixtureCount); // also might want to look here: m_fixtureList, m_fixtureList.m_shape, m_fixtureList.m_shape.m_vertices for (var x in aT.wallMap['wall1'].b2d.m_fixtureList) { console.log("name=" + x); } */ } else if (keyMap[e.keyCode] == 'key_m') { dC.multiplayer.checked = !dC.multiplayer.checked; toggleMultiplayerStuff(); } else if (keyMap[e.keyCode] == 'key_e') { dC.editor.checked = !dC.editor.checked; toggleEditorStuff(); } else if (keyMap[e.keyCode] == 'key_p') { dC.pause.checked = !dC.pause.checked; setPauseState(); // Pause NPC navigation. } else if ((keyMap[e.keyCode] == 'key_q') && (clients['local'].key_ctrl == 'D')) { c.pause_NPC_navigation = !c.pause_NPC_navigation; // Delete stuff } else if ((keyMap[e.keyCode] == 'key_x') && (clients['local'].key_ctrl == 'D')) { // First process multi-select var foundSpring = false; if (multiSelect.count() > 0) { // Delete each spring that has both it's pucks (or pins) in the multi-select. Spring.findAll_InMultiSelect( function( spring) { spring.deleteThisOne(); // This function includes the scope of the function in which is being defined. // So foundSpring, defined in the surrounding function, is accessible (and changeable) here. foundSpring = true; }); // If springs have been cleared during first delete, now remove pucks, pins and walls that are still selected. //console.log('foundSpring='+foundSpring); if (!foundSpring) { for (var name_multiSelect in multiSelect.map) { multiSelect.map[ name_multiSelect].deleteThisOne(); delete multiSelect.map[ name_multiSelect]; } } } else if (clients['local'].selectedBody) { // A single-object selection. clients['local'].selectedBody.deleteThisOne(); // Pucks, pins, and walls all have there own version of this method. clients['local'].selectedBody = null; } // Copy stuff } else if ((keyMap[e.keyCode] == 'key_c') && (clients['local'].key_ctrl == 'D')) { if (multiSelect.count() > 0) { helpMessage.newMessage( multiSelect.count() + " selected", 1.0); } // Copy a Spring for pasting. // First deal with multi-select case (a length of 2 indicates trying to copy a spring) if (multiSelect.count() == 2) { // Make a copy of the spring (if there is one connected to these two objects). Spring.findAll_InMultiSelect( function( spring) { // Make a reference to this existing spring. c.springNameForPasting = spring.name; helpMessage.addToIt(", spring = " + spring.name); }); // Normal copying of an object that is identified by single-object selection } else if (clients['local'].selectedBody) { var cn = clients['local'].selectedBody.constructor.name; if ((cn == "Wall") || (cn == "Pin") || (cn == "Puck")) { clients['local'].selectedBody.copyThisOne(); } } // Paste a spring onto a pair of pucks. } else if ((keyMap[e.keyCode] == 'key_v') && (clients['local'].key_ctrl == 'D')) { var p = []; for (var p_name in multiSelect.map) { // Unselect the walls (don't allow the user to attach springs to the walls). if (multiSelect.map[ p_name].constructor.name == 'Wall') { delete multiSelect.map[ p_name]; //return; } else { // Populate the p array so you can pass the pucks as parameters (see call to copyThisOne). p.push( multiSelect.map[ p_name]); } } // Only consider the case where there are two pucks selected. if (multiSelect.count() == 2) { if (c.springNameForPasting in aT.springMap) { // Check if there's already a spring between these two. Delete it // before pasting. Spring.findAll_InMultiSelect( function( spring) { if (spring.name == c.springNameForPasting) { helpMessage.newMessage("Pasting spring on itself is not allowed", 1.0); return; } else { spring.deleteThisOne(); } }); // Paste a copy of the spring on these two pucks (or pins). var newSpringName = aT.springMap[ c.springNameForPasting].copyThisOne( p[0], p[1]); // If one of these is a NPC puck and the other a NPC navigation pin, supply the puck attributes needed for navigation. if ((p[0].clientName) && (p[0].constructor.name == 'Puck') && (p[0].clientName.slice(0,3) == 'NPC') && (p[1].NPC)) { p[0].springName = newSpringName; p[0].pinName = p[1].name; } else if ((p[1].clientName) && (p[1].constructor.name == 'Puck') && (p[1].clientName.slice(0,3) == 'NPC') && (p[0].NPC)) { p[1].springName = newSpringName; p[1].pinName = p[0].name; } } } else { helpMessage.newMessage("Need 2 pucks to paste a spring; "+multiSelect.count()+" selected", 1.0); } // numbers 0 to 9 } else if ((e.keyCode >= 48) && (e.keyCode <= 57)) { if (document.activeElement.tagName == 'BODY') { demoStart(e.keyCode - 48); } } } } }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase). document.addEventListener("keyup", function(e) { if (e.keyCode in keyMap) { // Set the key state to be UP. clients['local'][keyMap[e.keyCode]] = 'U'; //console.log(e.keyCode + "(up)=" + keyMap[e.keyCode]); // numbers 0 to 9 } else if ((e.keyCode >= 48) && (e.keyCode <= 57)) { } // Some specific actions. // Done with box-based selection. if (keyMap[e.keyCode] == 'key_alt') { selectBox.stop(); // Done with the rotation action. Get ready for the next one. } else if ((keyMap[e.keyCode] == 'key_ctrl') || (keyMap[e.keyCode] == 'key_shift')) { multiSelect.resetCenter(); } }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase). // Gravity toggle dC.gravity = document.getElementById('chkGravity'); function gravityToggle(e) { if (dC.gravity.checked) { c.g_ON = true; } else { c.g_ON = false; } setGravityRelatedParameters(); } dC.gravity.addEventListener("click", gravityToggle, {capture: false}); // COM (Center of Mass) selection toggle dC.comSelection = document.getElementById('chkCOM_Selection'); function comSelection_Toggle(e) { if (dC.comSelection.checked) { } else { } } dC.comSelection.addEventListener("click", comSelection_Toggle, {capture: false}); // Multi-player toggle dC.multiplayer = document.getElementById('chkMultiplayer'); dC.multiplayer.addEventListener("click", toggleMultiplayerStuff, {capture: false}); function toggleMultiplayerStuff() { // This double toggle has the effect of switching between the following two divs. toggleElementDisplay("multiPlayer", "table-cell"); toggleElementDisplay("ttcIntro", "table-cell"); // This toggles (displays/hides) the client links. toggleElementDisplay("clientLinks", "inline"); } // Stream choke dC.stream = document.getElementById('chkStream'); dC.stream.addEventListener("click", toggleStream, {capture: false}); function toggleStream() { // Turn the stream On/Off. if (dC.stream.checked) { hC.setCanvasStream('on'); } else { hC.setCanvasStream('off'); } } // Editor toggle dC.editor = document.getElementById('chkEditor'); function toggleEditorStuff() { // Have disabled this for now. There use to be some help text for the editor // that this would make visible when checked. //toggleElementDisplay("editControls", "inline"); } dC.editor.addEventListener("click", toggleEditorStuff, {capture: false}); // Pause toggle dC.pause = document.getElementById('chkPause'); dC.pause.addEventListener("click", setPauseState, {capture: false}); // Local cursor toggle dC.localCursor = document.getElementById('chkLocalCursor'); dC.localCursor.checked = false; dC.localCursor.addEventListener("click", function() { if (dC.localCursor.checked) { canvas.style.cursor = 'default'; } else { canvas.style.cursor = 'none'; } }, {capture: false}); // The running average. aT.dt_RA_ms = new RunningAverage(60); dC.fps = document.getElementById("fps"); // Add a local user to the clients dictionary. clients['local'] = new Client({'name':'local'}); // Start the first demo. demoStart( 2); // Wait a bit and then set the physics time-step (frame rate) based on the observed display rate. helpMessage.newMessage('starting...', 3.0); // Right after the message above, do this one. window.setTimeout( function() { helpMessage.newMessage('...ready.', 0.8); }, 3000); window.setTimeout( function() { setFrameRateBasedOnDisplayRate(); }, 1000); // Wait even longer to run another check. window.setTimeout( function() { setFrameRateBasedOnDisplayRate(); }, 2000); // Wait even longer yet to run a final check. window.setTimeout( function() { setFrameRateBasedOnDisplayRate(); }, 3000); } // End of init() // It's alive. MuuuUUuuuAhhhh Haaaaaa Ha Ha Ha. function gameLoop( timeStamp_ms) { // Note: The time-stamp argument can have any name. dt_frame_ms = timeStamp_ms - time_previous; //dt_frame_ms = c.deltaT_s * 1000; //dt_frame_ms = 1000 * 1/60.0 dt_frame_s = dt_frame_ms / 1000.0; if (resumingAfterPause || (dt_frame_s > 0.1)) { // Use the dt info saved in last frame before it was paused. dt_frame_ms = dt_frame_previous_ms; dt_frame_s = dt_frame_ms / 1000.0; time_previous = performance.now(); resumingAfterPause = false; } if (c.dtFloating) c.deltaT_s = dt_frame_s; var dt_avg_ms = aT.dt_RA_ms.update( dt_frame_ms); // Report frame-rate every half second. if (aT.dt_RA_ms.