// Game Window (gW) Module // Version 1.21 (5:57 PM Sat August 12, 2017) // Written by: James D. Miller // The demos, and their multi-player functionality, are dependent on two additional // JavaScript modules: hostAndClient.js 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.multiSelectMap = {}; 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) 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; // Seconds per frame (at page load) c.deltaT_s = 1.0/60.0; c.dtFloating = false; c.demoIndex = null; //c.contactCounter = 0; c.tangleTimer_s = 0; c.pause_NPC_navigation = 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; // Document Controls (dC). var dC = {}; dC.gravity = null; dC.pause = null; dC.comSelection = null; dC.multiplayer = null; dC.editor = null; // Key values. var keyMap = {'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', '32':'key_space', '37':'key_leftArrow', '38':'key_upArrow', '39':'key_rightArrow', '40':'key_downArrow'}; ///////////////////////////////////////////////////////////////////////////// //// //// 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 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); } function Client( pars) { this.color = pars.color || "red"; this.name = pars.name || "manWithNoName"; this.puck = null; this.isMouseDown = false; this.button = null; 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. 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. this.key_f_enabled = true; // Freeze all the pucks. 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; } // 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 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 selection if the wall/pin editor is off. (!dC.editor.checked && (selectedBody.constructor.name == "Wall")) ) { selected_b2d_Body = null; } else { // Consider the case where local client is trying to edit multiple objects (shift key is down). if ((this.name == 'local') && (this.key_shift == "D")) { // Add this body to the multiple-select map (if not already there). if (!(selectedBody.name in aT.multiSelectMap) && (this.button == 0)) { aT.multiSelectMap[ selectedBody.name] = selectedBody; console.log(Object.keys(aT.multiSelectMap).length + ', ' + selectedBody.name); // Remove this body from the map. } else if ((selectedBody.name in aT.multiSelectMap) && (this.button == 2)) { // un-dash the springs Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = false; }); delete aT.multiSelectMap[ selectedBody.name]; // re-dash the springs Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = true; }); } // 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.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.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.moveToCursorPosition = function() { // For manipulating kinematic objects (walls and pins) if (dC.comSelection.checked) { // If COM selection, simply put the object at the mouse position. var newPosition = this.mouse_2d_m; } else { // If not COM selection, calculate the world delta between the current mouse position and the original selection point. // This delta is useful for positioning (dragging) a kinematic body (like a wall) so that it's selection point // follows the moving mouse location. var deltaForKinematicBodies_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 = this.selectedBody.position_2d_m.add( deltaForKinematicBodies_w_2d_m); } this.selectedBody.position_2d_m = newPosition; this.selectedBody.position_2d_px = screenFromWorld( this.selectedBody.position_2d_m); this.selectedBody.b2d.SetPosition( newPosition); } 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(2,2); //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.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 += dt_frame_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 += dt_frame_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 += dt_frame_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(); //aT.springs[ this.puck.springName].force_on_pucks(); //console.log("----" + this.NPC_aimStepCount); 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.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) { if (this.markerPingTimer_s < this.markerPingTimerLimit_s) { this.pingColor = this.color; this.markerPingTimer_s += dt_frame_s; } else { this.pingColor = 'white'; this.markerPingTimer_s = 0; } // Ping out a new ring (once each 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); // Draw the sound circle. var lineColor = this.values[t].color; 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 = pars.bullet || false; this.jello = pars.jello || false; this.clientName = pars.clientName || null; 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 = 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.tailSwitch = pars.tailSwitch || false; 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.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.tailSwitch) { this.tail = new PuckTail({'firstPoint_2d_m':this.position_2d_m, 'initial_radius_m':this.radius_m}); } 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.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.prototype.attachSpring = function() { var springName = "s" + (100 + Puck.nameIndex); 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; //console.log( this.springName); } Puck.prototype.stepTheJetAngle = function() { var spring = aT.springMap[this.springName]; // If this spring still exists. if (spring) { var nextPinName = spring.spo2.nextPinName; // Gradually rotate jet to be in the direction of the next pin. // Vector between this puck and the next pin. var toNextPin_2d_m = this.position_2d_m.subtract( 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. //console.log(Math.round(angleOfJet_deg) + "," + Math.round(angleOfNextPin_deg) + "," + Math.round(changeNeeded_deg)); this.jet.rotateTubeAndJet( 0.15 * changeNeeded_deg); } } Puck.prototype.attachSpringToNextPin = function() { var spring = aT.springMap[this.springName]; // If this spring still exists. if (spring) { var nextPinName = spring.spo2.nextPinName; //console.log('nextPinName = ' + nextPinName); // If there's a pin in the map by that name, attach to it. if (aT.pinMap[ nextPinName]) { aT.springMap[this.springName].spo2 = aT.pinMap[ nextPinName]; this.pinName = nextPinName; } } } 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; 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 if (this.clientName) { // Remove network clients on the node server. // (Note: this is the one place the hC is used inside of gW.) if (this.clientName.slice(0,1) == 'u') { hC.forceClientDisconnect( this.clientName); } // Remove the client in the clients map. delete clients[this.clientName]; } // 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 aT.multiSelectMap[ 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.tailSwitch) 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 += dt_frame_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 += dt_frame_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 = 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. 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 * dt_frame_s); } if (this.client.key_a == "D") { this.rotateTubeAndJet(+this.rotationRate_dps * dt_frame_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. 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.timeBetweenBullets_ms = 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 = 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 * dt_frame_s); } if (this.client.key_j == "D") { this.rotateTube(+this.rotationRate_dps * dt_frame_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; } Gun.prototype.stop_BulletStream = function() { this.bulletStream = 'off'; } Gun.prototype.update_BulletStream = function() { var deltaTime_ms = window.performance.now() - this.timeLastFired; // If ok to fire, do so. if ((this.bulletStream == 'on') && (deltaTime_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.bulletCount += 1; } } Gun.prototype.drawRayCastLine = function() { // Update the angle of the ray. if (this.scanning) { this.rayCastLine_2d_m.rotated_by( +this.rayRotationRate_dps * dt_frame_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 = 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 = pars.dashedLine || false; this.roundedEnds = pars.roundedEnds || true; this.navigationForNPC = 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.findInMultiSelect = function ( doThis) { // Find all the springs that have both ends (puck or pin) in the multi-select map. for (var springName in aT.springMap) { if ((aT.springMap[ springName].spo1.name in aT.multiSelectMap) && (aT.springMap[ springName].spo2.name in aT.multiSelectMap)) { // For each spring you find. doThis( springName); } } } 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; var tempSpring = new Spring( p2, p1, 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) { 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 { 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 = 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() { /* ctx.fillStyle = this.color; // Canvas draws a rectangle based on an upper-left-corner position. ctx.fillRect(this.position_2d_px.x - this.half_width_px, this.position_2d_px.y - this.half_height_px, this.half_width_px * 2, this.half_height_px * 2); */ 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 //// ///////////////////////////////////////////////////////////////////////////// // 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... if (clients[clientName]) { 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]; } delete clients[ 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 of the client. 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].checkForMouseSelection(); 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; clients[ clientName].key_d = state.d; clients[ clientName].key_w = state.w; clients[ clientName].key_j = state.j; clients[ clientName].key_k = state.k; clients[ clientName].key_l = state.l; clients[ clientName].key_i = state.i; clients[ clientName].key_space = state.sp; clients[ clientName].key_f = state.f; if ((state.f == "D") && (clients[ clientName].key_f_enabled)) { freeze(); // inhibit the freeze until the f key goes up again. clients[ clientName].key_f_enabled = false; } if ((state.f == "U") && (!clients[ clientName].key_f_enabled)) { clients[ clientName].key_f_enabled = true; } /* var stateString = ""; for (var key in state) stateString += key + ":" + state[key] + ","; console.log(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(); aabb.lowerBound.Set(x - 0.001, y - 0.001); aabb.upperBound.Set(x + 0.001, y + 0.001); // Query the world for overlapping shapes. var selectedBody = null; world.QueryAABB( function( fixture) { if(fixture.GetBody().GetType() != b2Body.b2_staticBody) { // b2_kinematicBody if(fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), mousePVec_2d_m)) { selectedBody = fixture.GetBody(); return false; } } 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'; 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(); 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 displayStyle, set it to be displayStyle. If it is equal, set it to 'none'. // When the value is 'none', the element is hidden. e.style.display = (e.style.display != displayStyle) ? displayStyle : 'none'; } 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 startit() { // Only start a game loop if there is no game loop running. if (myRequest === null) { resetFenceColor( "white"); 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 setFrameRate() { var frameRate = $('#FrameRate').val(); console.log("Frame Rate = " + frameRate); if (frameRate != 'float') { c.deltaT_s = 1.0 / frameRate; c.dtFloating = false; } 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 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 == '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; // 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 ); } function toggleMultiplayerStuff() { //This has the effect of switching between the following two divs. toggleElementDisplay("multiPlayer", "table-cell"); toggleElementDisplay("ttcIntro", "table-cell"); toggleElementDisplay("clientLinks", "inline"); } // 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 adjustSizeOfChatDiv( mode) { // Input fields dC.nodeServer = document.getElementById('nodeServer'); dC.roomName = document.getElementById('roomName'); dC.inputField = document.getElementById('inputField'); // The two divs that toggle dC.multiPlayer = document.getElementById('multiPlayer'); dC.ttcIntro = document.getElementById('ttcIntro'); 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 shrink = em(141); var nodeServer_Small = nodeServer_Large - shrink; var roomName_Small = roomName_Large - em(0); var inputField_Small = inputField_Large - shrink; if (mode == 'small') { dC.nodeServer.style.width = (nodeServer_Small) + 'em'; dC.roomName.style.width = (roomName_Small ) + 'em'; dC.inputField.style.width = (inputField_Small) + 'em'; 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'; 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 = 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 += dt_frame_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 += dt_frame_s; } ctx.font = "30px Arial"; ctx.fillStyle = 'yellow'; ctx.fillText(c.tangleTimer_s.toFixed(2),10,50); } */ function demoStart( index) { var v_init_2d_mps, wallColor; // 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 adjustSizeOfChatDiv('normal'); canvas.width = 600, canvas.height = 600; // 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; aT.multiSelectMap = {}; resetFenceColor( "white"); startit(); // if paused // 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, 'tailSwitch':true}); 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, 'tailSwitch':true}); } } 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(+5.00, 0.0), {'radius_m':0.20, 'name':'puck7'}); new Puck( new Vec2D(5.00, 1.55), new Vec2D(-5.00, 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 new Wall( new Vec2D( 3.0, 3.0), {'half_width_m':0.50, 'half_height_m':0.02}); new Wall( new Vec2D( 3.0, 3.0), {'half_width_m':0.02, 'half_height_m':0.50}); // 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'); // 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 ///////////////////////////////////////////// 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('canvas'); ctx = canvas.getContext('2d'); // Miscellaneous pointers to DOM elements dC.json = document.getElementById('jsonCapture'); // Event handlers for local client (user input) // Inhibit the context menu that pops up when right clicking (third button). // Do this on mainDiv to prevent the menu from appearing when you drag the // mouse off the canvas. var mainDiv = document.getElementById('mainDiv'); mainDiv.addEventListener("contextmenu", function(e) { //console.log('contextmenu event'); e.preventDefault(); return false; }, {capture: false}); canvas.addEventListener("mousedown", function(e) { clients['local'].isMouseDown = true; clients['local'].button = e.button; // Start a listener for the mousemove event. document.addEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); // 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 key is UP, reset the // multi-select map. So, user needs to release the shift 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") { // Un-dash all the springs. Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = false; }); // Clicked on blank space on air table (un-selecting everything) if (!selected_b2d_Body) { // Un-select each spring that is within the multi-select map. aT.multiSelectMap = {}; } } // 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. 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) { if (!clients['local'].isMouseDown) return; // Stop (using cpu) watching the mouse position. document.removeEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); resetMouseOrFingerState(e); }, {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. if (document.activeElement != document.body) document.activeElement.blur(); // Anything at this outer level will repeat if the key is held down. if (e.keyCode in keyMap) { // 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. 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 (Object.keys(aT.multiSelectMap).length > 0) { // Springs (s key is down) if (clients['local'].key_s == 'D') { Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]); }); // All other object types } else { for (var objName in aT.multiSelectMap) { if (aT.multiSelectMap[ objName].constructor.name != "Pin") { aT.multiSelectMap[ 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. // This is for cases where you are toggling the state of the client's // key parameter. // 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; pause_Toggle(); // 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 (Object.keys(aT.multiSelectMap).length > 0) { // Delete each spring that has both it's pucks (or pins) in the multi-select. Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].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 aT.multiSelectMap) { aT.multiSelectMap[ name_multiSelect].deleteThisOne(); delete aT.multiSelectMap[ 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')) { // Copy a Spring for pasting. // First deal with multi-select case (a length of 2 indicates trying to copy a spring) if (Object.keys(aT.multiSelectMap).length == 2) { // Make a copy of the spring (if there is one connected to these two objects). Spring.findInMultiSelect( function( springName) { // Make a reference to this existing spring. c.springNameForPasting = springName; }); // 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')) { if (Object.keys(aT.multiSelectMap).length == 2) { var p = []; // Populate this little array so you can pass the pucks as parameters. for (var p_name in aT.multiSelectMap) { p.push( aT.multiSelectMap[ p_name]); } if (c.springNameForPasting in aT.springMap) { // Check if there's already a spring between these two. Delete it // before pasting. Spring.findInMultiSelect( function( springName) { if (aT.springMap[ springName]) aT.springMap[ springName].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; } } } } } /* Could have a second block for keys that repeat while held down. In this case you wouldn't require the UP state to execute. */ // numbers 0 to 9 } else if ((e.keyCode >= 48) && (e.keyCode <= 57)) { 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)) { } }, {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}); // Editor toggle dC.editor = document.getElementById('chkEditor'); function toggleEditorStuff() { toggleElementDisplay("editControls", "inline"); } dC.editor.addEventListener("click", toggleEditorStuff, {capture: false}); // Pause toggle dC.pause = document.getElementById('chkPause'); function pause_Toggle(e) { if (dC.pause.checked) { stopit(); } else { startit(); } } dC.pause.addEventListener("click", pause_Toggle, {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); } // It's alive. MuuuUUuuuAhhhh Ha Ha Ha. function gameLoop( timeStamp_ms) { // Note: The time-stamp argument can have any name. dt_frame_ms = timeStamp_ms - time_previous; //dt_frame_ms = c.deltaT_s * 1000; //dt_frame_ms = 1000 * 1/60.0 dt_frame_s = dt_frame_ms / 1000.0; if (resumingAfterPause || (dt_frame_s > 0.1)) { // Use the dt info saved in last frame before it was paused. dt_frame_ms = dt_frame_previous_ms; dt_frame_s = dt_frame_ms / 1000.0; time_previous = performance.now(); resumingAfterPause = false; } if (c.dtFloating) c.deltaT_s = dt_frame_s; var dt_avg_ms = aT.dt_RA_ms.update( dt_frame_ms); // Report frame-rate every half second. if (aT.dt_RA_ms.totalSinceReport > 500.0) { dC.fps.innerHTML = (1/(dt_avg_ms/1000)).toFixed(0); aT.dt_RA_ms.totalSinceReport = 0.0; } // Draw the walls, step the engine, draw the pucks. updateAirTable(); //console.log("timeStamp_ms = " + timeStamp_ms); //console.log("performance.now = " + performance.now()); //console.log("dt_frame_ms = " + dt_frame_ms.toFixed(2) + " ms"); time_previous = timeStamp_ms; dt_frame_previous_ms = dt_frame_ms //console.log("5"); myRequest = window.requestAnimationFrame( gameLoop); } function updateAirTable() { // Clear the canvas (from one corner to the other) ctx.fillStyle = "black"; ctx.fillRect(0,0, canvas.width, canvas.height); //ctx.clearRect(0,0, canvas.width, canvas.height); // Calculate the state of the objects. world.Step( c.deltaT_s, 10, 10); // dt_frame_s c.deltaT_s world.ClearForces(); // Draw the walls first (render these on the bottom). for (var wallName in aT.wallMap) { aT.wallMap[ wallName].updateState(); aT.wallMap[ wallName].draw(); } /* Leaving this commented block here as an example of a technique for deleting elements from an array when looping over it. // Clean out old bullets and unhealthy pucks. Note this loops // in reverse order over the array to avoid indexing problems as the // array elements are deleted. for (var j = aT.pucks.length - 1; j >= 0; j--) { if (aT.pucks[j].bullet) { var age_ms = window.performance.now() - aT.pucks[j].createTime; if (age_ms > aT.pucks[j].ageLimit_ms) { deletePuckAndParts( aT.pucks[j]); aT.pucks.splice(j, 1); } } else if (aT.pucks[j].poorHealthFraction >= 1.0) { deletePuckAndParts( aT.pucks[j]); aT.pucks.splice(j, 1); } } */ // Clean out old bullets and unhealthy pucks. for (var puckName in aT.puckMap) { if (aT.puckMap[ puckName].bullet) { var age_ms = window.performance.now() - aT.puckMap[ puckName].createTime; if (age_ms > aT.puckMap[ puckName].ageLimit_ms) { aT.puckMap[ puckName].deleteThisOne(); } } else if (aT.puckMap[ puckName].poorHealthFraction >= 1.0) { aT.puckMap[ puckName].deleteThisOne(); } } for (var puckName in aT.puckMap) { aT.puckMap[ puckName].updateState(); aT.puckMap[ puckName].draw(); } for (var springName in aT.springMap) { var theSpring = aT.springMap[ springName]; // If either puck/pin has been deleted, remove the spring. if (theSpring.spo1.deleted || theSpring.spo2.deleted) { // Remove this spring from the spring map. aT.springMap[ springName].deleteThisOne(); } else { // Otherwise, business as usual. theSpring.force_on_pucks(); theSpring.draw(); } } for (var pinName in aT.pinMap) { aT.pinMap[ pinName].draw(); } // Check for jello tangle if (c.demoIndex == 6) { if (aT.jelloPucks.length > 0) checkForJelloTangle(); //checkForJelloTangle2(); } // Jets and Guns for (var clientName in clients) { if (clients[clientName].puck) { // Tell the NPCs what to do. if (clientName.slice(0,3) == 'NPC') { if (!c.pause_NPC_navigation) clients[clientName].thinkForNPC(); } // Respond to client controls, calculate corresponding jet and gun recoil forces, and draw. clients[clientName].puck.jet.updateAndDraw(); clients[clientName].puck.gun.updateAndDraw(); } } // Draw the client strings last (so they render on top). for (var clientName in clients) { clients[clientName].checkForMouseSelection(); if (clients[clientName].selectedBody) { clients[clientName].updateSelectionPoint(); clients[clientName].drawSelectionString(); } // Draw a cursor for the network clients. if (clientName.slice(0,1) == 'u') { clients[clientName].drawCursor(); } } // Draw a marking circle on each object in the multi-select map. if (Object.keys(aT.multiSelectMap).length > 0) { for (var multiSelect_name in aT.multiSelectMap) { aT.multiSelectMap[ multiSelect_name].draw_MultiSelectPoint(); } } Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = true; }); // Consider all client-mouse influences on a selected object. for (var clientName in clients) { if (clients[clientName].selectedBody) { var bodyType = clients[clientName].selectedBody.b2d.GetType(); // Pucks if (bodyType == b2Body.b2_dynamicBody) { clients[clientName].calc_string_forces_on_puck(); // Walls } else if (bodyType == b2Body.b2_kinematicBody) { // Move the selected wall to the cursor location. clients[clientName].moveToCursorPosition(); } } } // Sum up all the forces and apply them to the pucks. for (var puckName in aT.puckMap) { aT.puckMap[ puckName].applyForces(); } } // Reveal public pointers to private functions and properties /////////////// return { startit: startit, stopit: stopit, setFrameRate: setFrameRate, init: init, freeze: freeze, stopRotation: stopRotation, createNetworkClient: createNetworkClient, deleteNetworkClient: deleteNetworkClient, updateClientState: updateClientState, toggleElementDisplay: toggleElementDisplay, toggleSpanValue: toggleSpanValue, saveState: saveState, clearState: clearState, scrollDemoHelp: scrollDemoHelp, adjustSizeOfChatDiv: adjustSizeOfChatDiv, openDemoHelp: openDemoHelp, demoStart: demoStart }; })();