diff --git a/package.json b/package.json index 68119883e..13d9923ee 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "Open Source Arras", + "name": "open-source-arras", "version": "1.0.0", "main": "server/index.js", "scripts": { - "startOptimized": "node --optimize-for-size --no-lazy --gc_interval=120 server/index", - "restartOnSaveOptimized": "node --optimize-for-size --no-lazy --gc_interval=120 --watch server/index", + "startOptimized": "node --no-lazy server/index", + "restartOnSaveOptimized": "node --no-lazy --watch server/index", "start": "node server/index", "restartOnSave": "node --watch server/index", "host": "node standaloneClient/index", diff --git a/public/app.js b/public/app.js index f11b59eac..7a0002d8f 100644 --- a/public/app.js +++ b/public/app.js @@ -688,7 +688,7 @@ function startGame() { } // initialize canvas. window.canvas.socket = global.socket; - setInterval(() => moveCompensation.iterate(global.motion), 1000 / 30); + setInterval(() => moveCompensation.iterate(global.motion), 1000 / 40); canvas.init(); document.getElementById("gameCanvas").focus(); window.onbeforeunload = () => true; diff --git a/public/lib/gameDraw.js b/public/lib/gameDraw.js index d2c3df250..f7efa4f62 100644 --- a/public/lib/gameDraw.js +++ b/public/lib/gameDraw.js @@ -11,23 +11,33 @@ var gameDraw = { hex2decimal: (h) => { return parseInt(h, 16); }, // convert a hex value to decimal - mixColors: (color_2, color_1, weight = 0.5) => { - if (weight === 1) return color_1; - if (weight === 0) return color_2; - var col = "#"; - for (var i = 1; i <= 6; i += 2) { - // loop through each of the 3 hex pairs—red, green, and blue, skip the '#' - var v1 = gameDraw.hex2decimal(color_1.substr(i, 2)), // extract the current pairs - v2 = gameDraw.hex2decimal(color_2.substr(i, 2)), - // combine the current pairs from each source color, according to the specified weight - val = gameDraw.decimal2hex(Math.floor(v2 + (v1 - v2) * weight)); - while (val.length < 2) { - val = "0" + val; - } // prepend a '0' if val results in a single digit - col += val; // concatenate val to our new color string + mixColors: (function () { + const mixCache = new Map(); + const intCache = new Map(); + return function mixColor(hex1, hex2, mix = 0.5) { + let c1 = intCache.get(hex1); + if (c1 === undefined) { + c1 = parseInt(hex1.slice(1), 16); + intCache.set(hex1, c1); + } + let c2 = intCache.get(hex2); + if (c2 === undefined) { + c2 = parseInt(hex2.slice(1), 16); + intCache.set(hex2, c2); + } + const key = c1 * (1 << 29) + c2 * (1 << 5) + Math.round(mix * 31); + if (mixCache.has(key)) { + return mixCache.get(key); + } + const r1 = (c1 >> 16) & 0xFF; + const g1 = (c1 >> 8) & 0xFF; + const b1 = c1 & 0xFF; + const result = ((1 << 24) | (((r1 + (((c2 >> 16) & 0xFF) - r1) * mix) | 0) << 16) | (((g1 + (((c2 >> 8) & 0xFF) - g1) * mix) | 0) << 8) | ((b1 + ((c2 & 0xFF) - b1) * mix) | 0)); + const hex = '#' + (result & 0xFFFFFF).toString(16).padStart(6, '0'); + mixCache.set(key, hex); + return hex; } - return col; // PROFIT! - }, + })(), hslToRgb: (h, s, l) => { let r, g, b; if (s === 0) { diff --git a/public/lib/socketInit.js b/public/lib/socketInit.js index 627376939..a25d05d6e 100644 --- a/public/lib/socketInit.js +++ b/public/lib/socketInit.js @@ -517,8 +517,8 @@ const process = (z = {}) => { lastRender: global.player.time, x: z.x, y: z.y, - lastx: z.x - global.metrics.rendergap * settings.roomSpeed * (1000 / 30) * z.vx, - lasty: z.y - global.metrics.rendergap * settings.roomSpeed * (1000 / 30) * z.vy, + lastx: z.x - global.metrics.rendergap * settings.roomSpeed * (1000 / 40) * z.vx, + lasty: z.y - global.metrics.rendergap * settings.roomSpeed * (1000 / 40) * z.vy, lastvx: z.vx, lastvy: z.vy, lastf: z.facing, diff --git a/server/index.js b/server/index.js index e8c5a3c1d..a94a8eccf 100644 --- a/server/index.js +++ b/server/index.js @@ -26,10 +26,7 @@ util.log(room.width + " x " + room.height + " room initalized."); // Collision stuff const auraCollideTypes = ["miniboss", "tank", "food", "crasher"] -function collide(collision) { - // Pull the two objects from the collision grid - let instance = collision[0], - other = collision[1]; +function collide(instance, other) { if (instance.noclip || other.noclip) { return 0; } @@ -43,10 +40,9 @@ function collide(collision) { util.error("x: " + other.x + " y: " + other.y); util.error(other.collisionArray); util.error("health: " + other.health.amount); - if (grid.checkIfInHSHG(other)) { + if (other.isInGrid) { other.kill(); util.warn("Ghost removed."); - grid.removeObject(other); } return 0; } @@ -56,10 +52,9 @@ function collide(collision) { util.error("x: " + instance.x + " y: " + instance.y); util.error(instance.collisionArray); util.error("health: " + instance.health.amount); - if (grid.checkIfInHSHG(instance)) { + if (instance.isInGrid) { other.kill(); util.warn("Ghost removed."); - grid.removeObject(instance); } return 0; } @@ -106,10 +101,10 @@ function collide(collision) { break; case instance.team !== other.team || (instance.team === other.team && - ( - instance.healer || - other.healer - )): + ( + instance.healer || + other.healer + )): // Exits if the aura is not hitting a boss, tank, food, or crasher if (instance.type === "aura") { if (!(auraCollideTypes.includes(other.type))) return; @@ -193,63 +188,61 @@ function collide(collision) { // The most important loop. Lots of looping. let ticks = 0; +const regenTickRate = Math.max(1, Math.round(room.regenerateTick / room.cycleSpeed)); const gameloop = () => { logs.loops.tally(); logs.master.startTracking(); logs.activation.startTracking(); + const isRegenTick = (ticks % regenTickRate === 0); logs.activation.endTracking(); // Do collisions - logs.collide.startTracking(); - if (entities.length > 1) { - // Load the grid - grid.update(); - // Run collisions in each grid - const pairs = grid.queryForCollisionPairs(); - for (let i = 0; i < pairs.length; i++) { - collide(pairs[i]); + logs.entities.startTracking(); + grid.clear(); + for (const instance of entities.values()) { + if (instance.contemplationOfMortality() === 1) { + instance.destroy(); + continue; + } + if (isRegenTick) { + if (instance.shield.max) { + instance.shield.regenerate(); + } + if (instance.health.max) { + instance.health.regenerate((instance.shield.max && instance.shield.max === instance.shield.amount) ? 1 : 0); + } + } + if (instance.activation.active || instance.isPlayer) { + if (instance.bond == null) { + instance.physics(); + } + instance.friction(); + instance.confinementToTheseEarthlyShackles(); + } + instance.updateAABB(instance.activation.active); + if (instance.activation.active) { + grid.insert(instance, instance.minX, instance.minY, instance.maxX, instance.maxY); } } - logs.collide.endTracking(); - // Do entities life - logs.entities.startTracking(); - for (let my of entities) { - // Consider death. - if (my.contemplationOfMortality()) { - my.destroy(); - } else { - if (my.activation.active || my.isPlayer) { - if (my.bond == null) { - // Resolve the physical behavior from the last collision cycle. - logs.physics.startTracking(); - my.physics(); - logs.physics.endTracking(); + for (const instance of entities.values()) { + if (!instance.isDead() && (instance.activation.active || instance.isPlayer)) { + instance.life(); + instance.collisionArray = []; + for (const other of grid.query(instance.minX, instance.minY, instance.maxX, instance.maxY).values()) { + if (instance.id !== other.id) { + collide(instance, other); } - logs.entities.tally(); - // Think about my actions. - logs.life.startTracking(); - my.life(); - logs.life.endTracking(); - // Apply friction. - my.friction(); - my.confinementToTheseEarthlyShackles(); - logs.selfie.startTracking(); - my.takeSelfie(); - logs.selfie.endTracking(); } - // Update collisions. - my.collisionArray = []; - // Activation - my.activation.update(); - my.updateAABB(my.activation.active); } - // Update collisions. - my.collisionArray = []; - my.emit('tick', { body: my }); + instance.activation.update(); + instance.takeSelfie(); + instance.emit('tick', { body: instance }); + const tile = room.getAt(instance); + if (tile) { + tile.entities.push(instance); + } } logs.entities.endTracking(); logs.master.endTracking(); - // Remove dead entities - purgeEntities(); room.lastCycle = performance.now(); ticks++; if (ticks & 1) { @@ -265,17 +258,6 @@ setTimeout(closeArena, 24 * 60 * 60 * 1000); // Restart every 2 hours global.naturallySpawnedBosses = []; global.bots = []; let bossTimer = 0; -let regenerateHealthAndShield = () => { - for (let i = 0; i < entities.length; i++) { - let instance = entities[i]; - if (instance.shield.max) { - instance.shield.regenerate(); - } - if (instance.health.max) { - instance.health.regenerate(instance.shield.max && instance.shield.max === instance.shield.amount); - } - } -} const maintainloop = () => { // Update the grid if (!naturallySpawnedBosses.length && bossTimer++ > Config.BOSS_SPAWN_COOLDOWN) { @@ -315,7 +297,7 @@ const maintainloop = () => { o.skill.score += Config.BOT_XP; } o.skill.maintain(); - o.skillUp([ "atk", "hlt", "spd", "str", "pen", "dam", "rld", "mob", "rgn", "shi" ][ran.chooseChance(...Config.BOT_SKILL_UPGRADE_CHANCES)]); + o.skillUp(["atk", "hlt", "spd", "str", "pen", "dam", "rld", "mob", "rgn", "shi"][ran.chooseChance(...Config.BOT_SKILL_UPGRADE_CHANCES)]); if (o.leftoverUpgrades && o.upgrade(ran.irandomRange(0, o.upgrades.length))) { o.leftoverUpgrades--; } @@ -367,13 +349,29 @@ if (Config.REPL_WINDOW) { // Bring it to life let counter = 0; -setInterval(() => { - regenerateHealthAndShield(); -}, room.regenerateTick); setInterval(() => { gameloop(); gamemodeLoop(); - roomLoop(); + for (let y = 0; y < room.setup.length; y++) { + for (let x = 0; x < room.setup[y].length; x++) { + let tile = room.setup[y][x]; + tile.tick(tile); + tile.entities = []; + } + } + + for (let y = 0; y < room.setup.length; y++) { + for (let x = 0; x < room.setup[y].length; x++) { + let tile = room.setup[y][x]; + tile.tick(tile); + tile.entities = []; + } + } + + if (room.sendColorsToClient) { + room.sendColorsToClient = false; + sockets.broadcastRoom(); + } if (counter++ / Config.runSpeed > 30) { chatLoop(); diff --git a/server/lib/hshg.js b/server/lib/hshg.js deleted file mode 100644 index bcd357ce3..000000000 --- a/server/lib/hshg.js +++ /dev/null @@ -1,618 +0,0 @@ -// Hierarchical Spatial Hash Grid: HSHG -// https://gist.github.com/kirbysayshi/1760774 -(function (root, undefined) { - //--------------------------------------------------------------------- - // GLOBAL FUNCTIONS - //--------------------------------------------------------------------- - - /** - * Updates every object's position in the grid, but only if - * the hash value for that object has changed. - * This method DOES NOT take into account object expansion or - * contraction, just position, and does not attempt to change - * the grid the object is currently in; it only (possibly) changes - * the cell. - * - * If the object has significantly changed in size, the best bet is to - * call removeObject() and addObject() sequentially, outside of the - * normal update cycle of HSHG. - * - * @return void desc - */ - function update_RECOMPUTE() { - var i, obj, grid, meta, objAABB, newObjHash; - - // for each object - for (i = 0; i < this._globalObjects.length; i++) { - obj = this._globalObjects[i]; - meta = obj.HSHG; - grid = meta.grid; - - // recompute hash - objAABB = obj.getAABB(); - newObjHash = grid.toHash(objAABB.min[0], objAABB.min[1]); - - if (newObjHash !== meta.hash) { - // grid position has changed, update! - grid.removeObject(obj); - grid.addObject(obj, newObjHash); - } - } - } - - // not implemented yet :) - function update_REMOVEALL() { } - - function testAABBOverlap(objA, objB) { - var a = objA.getAABB(), - b = objB.getAABB(); - - //if(a.min[0] > b.max[0] || a.min[1] > b.max[1] || a.min[2] > b.max[2] - //|| a.max[0] < b.min[0] || a.max[1] < b.min[1] || a.max[2] < b.min[2]){ - if (!a.active && !b.active) return false; - - if ( - a.min[0] > b.max[0] || - a.min[1] > b.max[1] || - a.max[0] < b.min[0] || - a.max[1] < b.min[1] - ) { - return false; - } else { - return true; - } - } - - function getLongestAABBEdge(min, max) { - return Math.max( - Math.abs(max[0] - min[0]), - Math.abs(max[1] - min[1]) - //,Math.abs(max[2] - min[2]) - ); - } - - //--------------------------------------------------------------------- - // ENTITIES - //--------------------------------------------------------------------- - - function HSHG() { - this.MAX_OBJECT_CELL_DENSITY = 1 / 8; // objects / cells - this.INITIAL_GRID_LENGTH = 256; // 16x16 - this.HIERARCHY_FACTOR = 2; - this.HIERARCHY_FACTOR_SQRT = Math.SQRT2; - this.UPDATE_METHOD = update_RECOMPUTE; // or update_REMOVEALL - - this._grids = []; - this._globalObjects = []; - } - - //HSHG.prototype.init = function(){ - // this._grids = []; - // this._globalObjects = []; - //} - - HSHG.prototype.addObject = function (obj) { - var x, - i, - cellSize, - objAABB = obj.getAABB(), - objSize = getLongestAABBEdge(objAABB.min, objAABB.max), - oneGrid, - newGrid; - - // for HSHG metadata - obj.HSHG = { - globalObjectsIndex: this._globalObjects.length, - }; - - // add to global object array - this._globalObjects.push(obj); - - if (this._grids.length == 0) { - // no grids exist yet - cellSize = objSize * this.HIERARCHY_FACTOR_SQRT; - newGrid = new Grid(cellSize, this.INITIAL_GRID_LENGTH, this); - newGrid.initCells(); - newGrid.addObject(obj); - - this._grids.push(newGrid); - } else { - x = 0; - - // grids are sorted by cellSize, smallest to largest - for (i = 0; i < this._grids.length; i++) { - oneGrid = this._grids[i]; - x = oneGrid.cellSize; - if (objSize < x) { - x = x / this.HIERARCHY_FACTOR; - if (objSize < x) { - // find appropriate size - while (objSize < x) { - x = x / this.HIERARCHY_FACTOR; - } - newGrid = new Grid( - x * this.HIERARCHY_FACTOR, - this.INITIAL_GRID_LENGTH, - this - ); - newGrid.initCells(); - // assign obj to grid - newGrid.addObject(obj); - // insert grid into list of grids directly before oneGrid - this._grids.splice(i, 0, newGrid); - } else { - // insert obj into grid oneGrid - oneGrid.addObject(obj); - } - return; - } - } - - while (objSize >= x) { - x = x * this.HIERARCHY_FACTOR; - } - - newGrid = new Grid(x, this.INITIAL_GRID_LENGTH, this); - newGrid.initCells(); - // insert obj into grid - newGrid.addObject(obj); - // add newGrid as last element in grid list - this._grids.push(newGrid); - } - }; - - HSHG.prototype.checkIfInHSHG = function (obj) { - var meta = obj.HSHG, - globalObjectsIndex, - replacementObj; - - if (meta === undefined) return false; - return true; - }; - - HSHG.prototype.removeObject = function (obj) { - var meta = obj.HSHG, - globalObjectsIndex, - replacementObj; - - if (meta === undefined) { - throw Error(obj + " was not in the HSHG."); - return; - } - - // remove object from global object list - globalObjectsIndex = meta.globalObjectsIndex; - if (globalObjectsIndex === this._globalObjects.length - 1) { - this._globalObjects.pop(); - } else { - replacementObj = this._globalObjects.pop(); - replacementObj.HSHG.globalObjectsIndex = globalObjectsIndex; - this._globalObjects[globalObjectsIndex] = replacementObj; - } - - meta.grid.removeObject(obj); - - // remove meta data - delete obj.HSHG; - }; - - HSHG.prototype.update = function () { - this.UPDATE_METHOD.call(this); - }; - - HSHG.prototype.queryForCollisionPairs = function (broadOverlapTestCallback) { - var i, - j, - k, - l, - c, - grid, - cell, - objA, - objB, - offset, - adjacentCell, - biggerGrid, - objAAABB, - objAHashInBiggerGrid, - possibleCollisions = [], - broadOverlapTest; - - // default broad test to internal aabb overlap test - broadOverlapTest = broadOverlapTestCallback || testAABBOverlap; - - // for all grids ordered by cell size ASC - for (i = 0; i < this._grids.length; i++) { - grid = this._grids[i]; - - // for each cell of the grid that is occupied - for (j = 0; j < grid.occupiedCells.length; j++) { - cell = grid.occupiedCells[j]; - - // collide all objects within the occupied cell - for (k = 0; k < cell.objectContainer.length; k++) { - objA = cell.objectContainer[k]; - if (!objA.getAABB().active) continue; - for (l = k + 1; l < cell.objectContainer.length; l++) { - objB = cell.objectContainer[l]; - if (!objB.getAABB().active) continue; - if (broadOverlapTest(objA, objB) === true) { - possibleCollisions.push([objA, objB]); - } - } - } - - // for the first half of all adjacent cells (offset 4 is the current cell) - for (c = 0; c < 4; c++) { - offset = cell.neighborOffsetArray[c]; - - //if(offset === null) { continue; } - - adjacentCell = grid.allCells[cell.allCellsIndex + offset]; - - // collide all objects in cell with adjacent cell - for (k = 0; k < cell.objectContainer.length; k++) { - objA = cell.objectContainer[k]; - if (!objA.getAABB().active) continue; - for (l = 0; l < adjacentCell.objectContainer.length; l++) { - objB = adjacentCell.objectContainer[l]; - if (!objB.getAABB().active) continue; - if (broadOverlapTest(objA, objB) === true) { - possibleCollisions.push([objA, objB]); - } - } - } - } - } - - // forall objects that are stored in this grid - for (j = 0; j < grid.allObjects.length; j++) { - objA = grid.allObjects[j]; - objAAABB = objA.getAABB(); - if (!objAAABB.active) continue; - // for all grids with cellsize larger than grid - for (k = i + 1; k < this._grids.length; k++) { - biggerGrid = this._grids[k]; - objAHashInBiggerGrid = biggerGrid.toHash( - objAAABB.min[0], - objAAABB.min[1] - ); - cell = biggerGrid.allCells[objAHashInBiggerGrid]; - - // check objA against every object in all cells in offset array of cell - // for all adjacent cells... - for (c = 0; c < cell.neighborOffsetArray.length; c++) { - offset = cell.neighborOffsetArray[c]; - - //if(offset === null) { continue; } - - adjacentCell = biggerGrid.allCells[cell.allCellsIndex + offset]; - - // for all objects in the adjacent cell... - for (l = 0; l < adjacentCell.objectContainer.length; l++) { - objB = adjacentCell.objectContainer[l]; - if (!objB.getAABB().active) continue; - // test against object A - if (broadOverlapTest(objA, objB) === true) { - possibleCollisions.push([objA, objB]); - } - } - } - } - } - } - - // return list of object pairs - return possibleCollisions; - }; - - HSHG.update_RECOMPUTE = update_RECOMPUTE; - HSHG.update_REMOVEALL = update_REMOVEALL; - - /** - * Grid - * - * @constructor - * @param int cellSize the pixel size of each cell of the grid - * @param int cellCount the total number of cells for the grid (width x height) - * @param HSHG parentHierarchy the HSHG to which this grid belongs - * @return void - */ - function Grid(cellSize, cellCount, parentHierarchy) { - this.cellSize = cellSize; - this.inverseCellSize = 1 / cellSize; - this.rowColumnCount = ~~Math.sqrt(cellCount); - this.xyHashMask = this.rowColumnCount - 1; - this.occupiedCells = []; - this.allCells = Array(this.rowColumnCount * this.rowColumnCount); - this.allObjects = []; - this.sharedInnerOffsets = []; - - this._parentHierarchy = parentHierarchy || null; - } - - Grid.prototype.initCells = function () { - // TODO: inner/unique offset rows 0 and 2 may need to be - // swapped due to +y being "down" vs "up" - - var i, - gridLength = this.allCells.length, - x, - y, - wh = this.rowColumnCount, - isOnRightEdge, - isOnLeftEdge, - isOnTopEdge, - isOnBottomEdge, - innerOffsets = [ - // y+ down offsets - //-1 + -wh, -wh, -wh + 1, - //-1, 0, 1, - //wh - 1, wh, wh + 1 - - // y+ up offsets - wh - 1, - wh, - wh + 1, - -1, - 0, - 1, - -1 + -wh, - -wh, - -wh + 1, - ], - leftOffset, - rightOffset, - topOffset, - bottomOffset, - uniqueOffsets = [], - cell; - - this.sharedInnerOffsets = innerOffsets; - - // init all cells, creating offset arrays as needed - - for (i = 0; i < gridLength; i++) { - cell = new Cell(); - // compute row (y) and column (x) for an index - y = ~~(i / this.rowColumnCount); - x = ~~(i - y * this.rowColumnCount); - - // reset / init - isOnRightEdge = false; - isOnLeftEdge = false; - isOnTopEdge = false; - isOnBottomEdge = false; - - // right or left edge cell - if ((x + 1) % this.rowColumnCount == 0) { - isOnRightEdge = true; - } else if (x % this.rowColumnCount == 0) { - isOnLeftEdge = true; - } - - // top or bottom edge cell - if ((y + 1) % this.rowColumnCount == 0) { - isOnTopEdge = true; - } else if (y % this.rowColumnCount == 0) { - isOnBottomEdge = true; - } - - // if cell is edge cell, use unique offsets, otherwise use inner offsets - if (isOnRightEdge || isOnLeftEdge || isOnTopEdge || isOnBottomEdge) { - // figure out cardinal offsets first - rightOffset = isOnRightEdge === true ? -wh + 1 : 1; - leftOffset = isOnLeftEdge === true ? wh - 1 : -1; - topOffset = isOnTopEdge === true ? -gridLength + wh : wh; - bottomOffset = isOnBottomEdge === true ? gridLength - wh : -wh; - - // diagonals are composites of the cardinals - uniqueOffsets = [ - // y+ down offset - //leftOffset + bottomOffset, bottomOffset, rightOffset + bottomOffset, - //leftOffset, 0, rightOffset, - //leftOffset + topOffset, topOffset, rightOffset + topOffset - - // y+ up offset - leftOffset + topOffset, - topOffset, - rightOffset + topOffset, - leftOffset, - 0, - rightOffset, - leftOffset + bottomOffset, - bottomOffset, - rightOffset + bottomOffset, - ]; - - cell.neighborOffsetArray = uniqueOffsets; - } else { - cell.neighborOffsetArray = this.sharedInnerOffsets; - } - - cell.allCellsIndex = i; - this.allCells[i] = cell; - } - }; - - Grid.prototype.toHash = function (x, y, z) { - var i, xHash, yHash, zHash; - - if (x < 0) { - i = -x * this.inverseCellSize; - xHash = this.rowColumnCount - 1 - (~~i & this.xyHashMask); - } else { - i = x * this.inverseCellSize; - xHash = ~~i & this.xyHashMask; - } - - if (y < 0) { - i = -y * this.inverseCellSize; - yHash = this.rowColumnCount - 1 - (~~i & this.xyHashMask); - } else { - i = y * this.inverseCellSize; - yHash = ~~i & this.xyHashMask; - } - - //if(z < 0){ - // i = (-z) * this.inverseCellSize; - // zHash = this.rowColumnCount - 1 - ( ~~i & this.xyHashMask ); - //} else { - // i = z * this.inverseCellSize; - // zHash = ~~i & this.xyHashMask; - //} - - return xHash + yHash * this.rowColumnCount; - //+ zHash * this.rowColumnCount * this.rowColumnCount; - }; - - Grid.prototype.addObject = function (obj, hash) { - var objAABB, objHash, targetCell; - - // technically, passing this in this should save some computational effort when updating objects - if (hash !== undefined) { - objHash = hash; - } else { - objAABB = obj.getAABB(); - objHash = this.toHash(objAABB.min[0], objAABB.min[1]); - } - targetCell = this.allCells[objHash]; - - if (targetCell.objectContainer.length === 0) { - // insert this cell into occupied cells list - targetCell.occupiedCellsIndex = this.occupiedCells.length; - this.occupiedCells.push(targetCell); - } - - // add meta data to obj, for fast update/removal - obj.HSHG.objectContainerIndex = targetCell.objectContainer.length; - obj.HSHG.hash = objHash; - obj.HSHG.grid = this; - obj.HSHG.allGridObjectsIndex = this.allObjects.length; - // add obj to cell - targetCell.objectContainer.push(obj); - - // we can assume that the targetCell is already a member of the occupied list - - // add to grid-global object list - this.allObjects.push(obj); - - // do test for grid density - if ( - this.allObjects.length / this.allCells.length > - this._parentHierarchy.MAX_OBJECT_CELL_DENSITY - ) { - // grid must be increased in size - this.expandGrid(); - } - }; - - Grid.prototype.removeObject = function (obj) { - var meta = obj.HSHG, - hash, - containerIndex, - allGridObjectsIndex, - cell, - replacementCell, - replacementObj; - - hash = meta.hash; - containerIndex = meta.objectContainerIndex; - allGridObjectsIndex = meta.allGridObjectsIndex; - cell = this.allCells[hash]; - - // remove object from cell object container - if (cell.objectContainer.length === 1) { - // this is the last object in the cell, so reset it - cell.objectContainer.length = 0; - - // remove cell from occupied list - if (cell.occupiedCellsIndex === this.occupiedCells.length - 1) { - // special case if the cell is the newest in the list - this.occupiedCells.pop(); - } else { - replacementCell = this.occupiedCells.pop(); - replacementCell.occupiedCellsIndex = cell.occupiedCellsIndex; - this.occupiedCells[cell.occupiedCellsIndex] = replacementCell; - } - - cell.occupiedCellsIndex = null; - } else { - // there is more than one object in the container - if (containerIndex === cell.objectContainer.length - 1) { - // special case if the obj is the newest in the container - cell.objectContainer.pop(); - } else { - replacementObj = cell.objectContainer.pop(); - replacementObj.HSHG.objectContainerIndex = containerIndex; - cell.objectContainer[containerIndex] = replacementObj; - } - } - - // remove object from grid object list - if (allGridObjectsIndex === this.allObjects.length - 1) { - this.allObjects.pop(); - } else { - replacementObj = this.allObjects.pop(); - replacementObj.HSHG.allGridObjectsIndex = allGridObjectsIndex; - this.allObjects[allGridObjectsIndex] = replacementObj; - } - }; - - Grid.prototype.expandGrid = function () { - var i, - j, - currentCellCount = this.allCells.length, - currentRowColumnCount = this.rowColumnCount, - currentXYHashMask = this.xyHashMask, - newCellCount = currentCellCount * 4, // double each dimension - newRowColumnCount = ~~Math.sqrt(newCellCount), - newXYHashMask = newRowColumnCount - 1, - allObjects = this.allObjects.slice(0), // duplicate array, not objects contained - aCell, - push = Array.prototype.push; - - // remove all objects - for (i = 0; i < allObjects.length; i++) { - this.removeObject(allObjects[i]); - } - - // reset grid values, set new grid to be 4x larger than last - this.rowColumnCount = newRowColumnCount; - this.allCells = Array(this.rowColumnCount * this.rowColumnCount); - this.xyHashMask = newXYHashMask; - - // initialize new cells - this.initCells(); - - // re-add all objects to grid - for (i = 0; i < allObjects.length; i++) { - this.addObject(allObjects[i]); - } - }; - - /** - * A cell of the grid - * - * @constructor - * @return void desc - */ - function Cell() { - this.objectContainer = []; - this.neighborOffsetArray; - this.occupiedCellsIndex = null; - this.allCellsIndex = null; - } - - //--------------------------------------------------------------------- - // EXPORTS - //--------------------------------------------------------------------- - - root["HSHG"] = HSHG; - HSHG._private = { - Grid: Grid, - Cell: Cell, - testAABBOverlap: testAABBOverlap, - getLongestAABBEdge: getLongestAABBEdge, - }; -})(this); diff --git a/server/modules/debug/speedLoop.js b/server/modules/debug/speedLoop.js index 03fc68046..fd3141aff 100644 --- a/server/modules/debug/speedLoop.js +++ b/server/modules/debug/speedLoop.js @@ -12,16 +12,16 @@ const speedcheckloop = () => { let loops = logs.loops.getTallyCount(), active = logs.entities.getTallyCount(); global.fps = (1000 / sum).toFixed(2); - for (let e of entities) { + for (let e of entities.values()) { if (e.isPlayer && e.socket) { // give the debug info i guess. e.socket.talk("svInfo", Config.gameModeName, (sum).toFixed(1)); } } - if (sum > 1000 / Config.runSpeed / 30) { + if (sum > 1000 / Config.runSpeed / 40) { //fails++; if (Config.LOGS) { util.warn('~~ LAST SERVER TICK TOOK TOO LONG TO CALCULATE ~~'); - util.warn('~~ LOOPS: ' + loops + '. ENTITIES: ' + entities.length + '//' + Math.round(active / loops) + '. VIEWS: ' + views.length + '. BACKLOGGED :: ' + (sum * Config.runSpeed * 3).toFixed(3) + '%! ~~'); + util.warn('~~ LOOPS: ' + loops + '. ENTITIES: ' + entities.size + '//' + Math.round(active / loops) + '. VIEWS: ' + views.length + '. BACKLOGGED :: ' + (sum * Config.runSpeed * 3).toFixed(3) + '%! ~~'); util.warn('Total activation time: ' + activationtime); util.warn('Total collision time: ' + collidetime); util.warn('Total cycle time: ' + movetime); diff --git a/server/modules/definitions/addons/defsReloadCommand.js b/server/modules/definitions/addons/defsReloadCommand.js index fe4a2fc67..58479965e 100644 --- a/server/modules/definitions/addons/defsReloadCommand.js +++ b/server/modules/definitions/addons/defsReloadCommand.js @@ -47,7 +47,7 @@ Events.on('chatMessage', ({ message, socket, preventDefault }) => { }; // Redefine all tanks and bosses - for (let entity of entities) { + for (let entity of entities.values()) { // If it's a valid type and it's not a turret if (!['tank', 'miniboss', 'food'].includes(entity.type)) continue; if (entity.bond) continue; diff --git a/server/modules/definitions/groups/bosses.js b/server/modules/definitions/groups/bosses.js index 48f13e54b..5ab8de6b8 100644 --- a/server/modules/definitions/groups/bosses.js +++ b/server/modules/definitions/groups/bosses.js @@ -2648,141 +2648,131 @@ for (let i = 0; i < arraySize; i++) { colorArray.push('#' + ((1 << 24) + (rgb << 16) + (rgb << 8) + rgb).toString(16).slice(1)); } class io_nearestDifferentMaster2 extends ioTypes.nearestDifferentMaster { - constructor(body, opts = {}) { - super(body); - this.lookAtDanger = opts.lookAtDanger ?? true; - this.firingAtMe = opts.firingAtMe ?? false; - this.timeout = opts.timeout || 90; - } - buildList(range) { - // Establish whom we judge in reference to - let mostDangerous = 0, - keepTarget = false; - // Filter through everybody... - let out = entities.filter(e => - // Only look at those within our view, and our parent's view, not dead, not invisible, not our kind, not a bullet/trap/block etc - this.validate(e, this.body, this.body.master.master, range * range, range * range * 4 / 3) - ).filter((e) => { - // Only look at those within range and arc (more expensive, so we only do it on the few) - if (this.body.firingArc == null || this.body.aiSettings.view360 || Math.abs(util.angleDifference(util.getDirection(this.body, e), this.body.firingArc[0])) < this.body.firingArc[1]) { - mostDangerous = Math.max(e.dangerValue, mostDangerous); - return true; - } - }).filter((e) => { - // Even more expensive - return !this.wouldHitWall(this.body, e); - }).filter((e) => { - // Only return the highest tier of danger - if (!this.lookAtDanger) return true; - if (this.body.aiSettings.farm || e.dangerValue === mostDangerous) { - if (this.targetLock && e.id === this.targetLock.id) keepTarget = true; - return true; - } - }); - // Reset target if it's not in there - if (!keepTarget) this.targetLock = undefined; - return out; - } - think(input) { - // Override target lock upon other commands - if (input.main || input.alt || this.body.master.autoOverride) { - this.targetLock = undefined; - return {}; - } - // Otherwise, consider how fast we can either move to ram it or shoot at a potiential target. - let tracking = this.body.topSpeed, - damageRef = (this.body.bond == null) ? this.body : this.body.bond, - range = this.body.fov; - // Use whether we have functional guns to decide - for (let i = 0; i < this.body.guns.length; i++) { - if (this.body.guns[i].canShoot && !this.body.aiSettings.SKYNET) { - let v = this.body.guns[i].getTracking(); - if (v.speed == 0 || v.range == 0) continue; - tracking = v.speed; - range = Math.min(range, (v.speed || 1.5) * (v.range < (this.body.size * 2) ? this.body.fov : v.range)); - break; - } - } - if (!Number.isFinite(tracking)) { - tracking = this.body.topSpeed + .01; - } - if (!Number.isFinite(range)) { - range = 640 * this.body.FOV; - } - // Check if my target's alive - if (this.targetLock && ( - !this.validate(this.targetLock, this.body, this.body.master.master, range * range, range * range * 4 / 3) || - this.wouldHitWall(this.body, this.targetLock) // Very expensive - )) { - this.targetLock = undefined; - this.tick = 100; - } - // Think damn hard - if (this.tick++ > 15 * Config.runSpeed) { - this.tick = 0; - this.validTargets = this.buildList(range); - // Ditch our old target if it's invalid - if (this.targetLock && this.validTargets.indexOf(this.targetLock) === -1) { - this.targetLock = undefined; - } - // Lock new target if we still don't have one. - if (this.targetLock == null && this.validTargets.length) { - this.targetLock = (this.validTargets.length === 1) ? this.validTargets[0] : nearest(this.validTargets, { - x: this.body.x, - y: this.body.y - }); - this.tick = -this.timeout; - } - } - // Lock onto whoever's shooting me. - if (this.firingAtMe && damageRef.collisionArray.length && damageRef.health.display() < this.oldHealth) { - this.oldHealth = damageRef.health.display(); - if (this.validTargets.indexOf(damageRef.collisionArray[0]) === -1) { - let a = (damageRef.collisionArray[0].master.id === -1) - ? damageRef.collisionArray[0].source - : damageRef.collisionArray[0].master; - if ( - this.body.firingArc == null || - this.body.aiSettings.view360 || - Math.abs(util.angleDifference(util.getDirection(this.body, a), this.body.firingArc[0])) < this.body.firingArc[1] - ) { - this.targetLock = a; - this.tick = -(this.timeout * 5); - } - } - } - // Consider how fast it's moving and shoot at it - if (this.targetLock != null) { - let radial = this.targetLock.velocity; - let diff = { - x: this.targetLock.x - this.body.x, - y: this.targetLock.y - this.body.y, - } - /// Refresh lead time - if (this.tick % 4 === 0) { - this.lead = 0 - // Find lead time (or don't) - if (!this.body.aiSettings.chase) { - let toi = timeOfImpact(diff, radial, tracking) - this.lead = toi - } - } - if (!Number.isFinite(this.lead)) { - this.lead = 0; - } - if (!this.accountForMovement) this.lead = 0; - // And return our aim - return { - target: { - x: diff.x + this.lead * radial.x, - y: diff.y + this.lead * radial.y, - }, - fire: true, - main: true - }; - } - return {}; - } + constructor(body, opts = {}) { + super(body); + this.lookAtDanger = opts.lookAtDanger ?? true; + this.firingAtMe = opts.firingAtMe ?? false; + this.timeout = opts.timeout || 90; + } + buildList(range) { + const { + x, + y + } = this.body; + const potentialTargets = global.grid.query(x - range, y - range, x + range, y + range); + const sqrRange = range * range; + const sqrRangeMaster = sqrRange * 4 / 3; + const validCandidates = []; + for (const e of potentialTargets) { + if (this.validate(e, this.body, this.body.master.master, sqrRange, sqrRangeMaster) && (this.body.aiSettings.view360 || Math.abs(util.angleDifference(util.getDirection(this.body, e), this.body.firingArc[0])) < this.body.firingArc[1]) && !this.wouldHitWall(this.body, e)) { + validCandidates.push(e); + } + } + if (!validCandidates.length) { + this.targetLock = undefined; + return []; + } + let mostDangerous = 0; + if (this.lookAtDanger) { + for (const e of validCandidates) { + mostDangerous = Math.max(e.dangerValue, mostDangerous); + } + } + let keepTarget = false; + const finalTargets = validCandidates.filter(e => { + const isViable = !this.lookAtDanger || this.body.aiSettings.farm || e.dangerValue === mostDangerous; + if (isViable) { + if (this.targetLock && e.id === this.targetLock.id) { + keepTarget = true; + } + return true; + } + return false; + }); + if (!keepTarget) { + this.targetLock = undefined; + } + return finalTargets; + } + think(input) { + if (input.main || input.alt || this.body.master.autoOverride) { + this.targetLock = undefined; + return {}; + } + let tracking = this.body.topSpeed, + damageRef = (this.body.bond == null) ? this.body : this.body.bond, + range = this.body.fov; + for (let i = 0; i < this.body.guns.length; i++) { + if (this.body.guns[i].canShoot && !this.body.aiSettings.SKYNET) { + let v = this.body.guns[i].getTracking(); + if (v.speed == 0 || v.range == 0) continue; + tracking = v.speed; + range = Math.min(range, (v.speed || 1.5) * (v.range < (this.body.size * 2) ? this.body.fov : v.range)); + break; + } + } + if (!Number.isFinite(tracking)) { + tracking = this.body.topSpeed + .01; + } + if (!Number.isFinite(range)) { + range = 640 * this.body.FOV; + } + if (this.targetLock && (!this.validate(this.targetLock, this.body, this.body.master.master, range * range, range * range * 4 / 3) || this.wouldHitWall(this.body, this.targetLock))) { + this.targetLock = undefined; + this.tick = 100; + } + if (this.tick++ > 15 * Config.runSpeed) { + this.tick = 0; + this.validTargets = this.buildList(range); + if (this.targetLock && this.validTargets.indexOf(this.targetLock) === -1) { + this.targetLock = undefined; + } + if (this.targetLock == null && this.validTargets.length) { + this.targetLock = (this.validTargets.length === 1) ? this.validTargets[0] : nearest(this.validTargets, { + x: this.body.x, + y: this.body.y + }); + this.tick = -this.timeout; + } + } + if (this.firingAtMe && damageRef.collisionArray.length && damageRef.health.display() < this.oldHealth) { + this.oldHealth = damageRef.health.display(); + if (this.validTargets.indexOf(damageRef.collisionArray[0]) === -1) { + let a = (damageRef.collisionArray[0].master.id === -1) ? damageRef.collisionArray[0].source : damageRef.collisionArray[0].master; + if (this.body.firingArc == null || this.body.aiSettings.view360 || Math.abs(util.angleDifference(util.getDirection(this.body, a), this.body.firingArc[0])) < this.body.firingArc[1]) { + this.targetLock = a; + this.tick = -(this.timeout * 5); + } + } + } + if (this.targetLock != null) { + let radial = this.targetLock.velocity; + let diff = { + x: this.targetLock.x - this.body.x, + y: this.targetLock.y - this.body.y, + } + if (this.tick % 4 === 0) { + this.lead = 0 + if (!this.body.aiSettings.chase) { + let toi = timeOfImpact(diff, radial, tracking) + this.lead = toi + } + } + if (!Number.isFinite(this.lead)) { + this.lead = 0; + } + if (!this.accountForMovement) this.lead = 0; + return { + target: { + x: diff.x + this.lead * radial.x, + y: diff.y + this.lead * radial.y, + }, + fire: true, + main: true + }; + } + return {}; + } } ioTypes.nearestDifferentMaster2 = io_nearestDifferentMaster2; Class.toothlessBase = { diff --git a/server/modules/definitions/groups/tanks.js b/server/modules/definitions/groups/tanks.js index 814162d28..a2d8b5763 100644 --- a/server/modules/definitions/groups/tanks.js +++ b/server/modules/definitions/groups/tanks.js @@ -3128,7 +3128,7 @@ Class.undertowEffect = { { event: "tick", handler: ({ body }) => { - for (let instance of entities) { + for (let instance of entities.values()) { let diffX = instance.x - body.x, diffY = instance.y - body.y, dist2 = diffX ** 2 + diffY ** 2; diff --git a/server/modules/gamemodes/closeArena.js b/server/modules/gamemodes/closeArena.js index 531fcc493..69471b9d6 100644 --- a/server/modules/gamemodes/closeArena.js +++ b/server/modules/gamemodes/closeArena.js @@ -11,9 +11,9 @@ function closeArena() { sockets.broadcast("Arena closed: No players may join!"); util.log('Arena Closing initiated'); global.arenaClosed = true; - for (let i = 0; i < entities.length; i++) { - if (entities[i].isBot) { - entities[i].kill(); + for (const entity of entitites.values()) { + if (entity.isBot) { + entity.kill(); } } for (let i = 0; i < 15; i++) { @@ -47,8 +47,7 @@ function closeArena() { ticks++; if (ticks >= 20) return close(); let alive = false; - for (let i = 0; i < entities.length; i++) { - let instance = entities[i]; + for (const instance of entities.values()) { if ( instance.isPlayer || instance.isMothership || (instance.isDominator && instance.team !== TEAM_ROOM) diff --git a/server/modules/gamemodes/manhunt.js b/server/modules/gamemodes/manhunt.js index 4c5e3580e..b39bc675b 100644 --- a/server/modules/gamemodes/manhunt.js +++ b/server/modules/gamemodes/manhunt.js @@ -7,7 +7,7 @@ class ManHunt { getLeader() { let highestScore = -Infinity, leader = { id: null }; - for (let entity of entities) { + for (let entity of entities.values()) { if (!entity.isPlayer && !entity.isBot) continue; if (entity.skill.score <= highestScore) continue; highestScore = entity.skill.score; diff --git a/server/modules/gamemodes/moon.js b/server/modules/gamemodes/moon.js index f13b27255..8fb2237af 100644 --- a/server/modules/gamemodes/moon.js +++ b/server/modules/gamemodes/moon.js @@ -9,9 +9,8 @@ class Moon { this.moon = o; } loop () { - let players = entities.filter(r => r.isPlayer || r.isBot); - for (let entity of players) { - if (entity.id != this.moon.id && !entity.isArenaCloser && entity.alpha) { + for (const entity of entities.values()) { + if ((entity.isPlayer || entity.isBot) || (entity.id != this.moon.id && !entity.isArenaCloser && entity.alpha)) { entity.velocity.x += util.clamp(this.moon.x - entity.x, -90, 90) * entity.damp * 0.02; entity.velocity.y += util.clamp(this.moon.y - entity.y, -90, 90) * entity.damp * 0.02; } diff --git a/server/modules/gamemodes/mothership.js b/server/modules/gamemodes/mothership.js index 2185b40cd..c49e17cf0 100644 --- a/server/modules/gamemodes/mothership.js +++ b/server/modules/gamemodes/mothership.js @@ -47,8 +47,7 @@ function spawn() { function death(entry) { sockets.broadcast(getTeamName(entry[1]) + "'s mothership has been killed!"); global.defeatedTeams.push(-entry[1] - 1); - for (let i = 0; i < entities.length; i++) { - let o = entities[i]; + for (const o of entities.values()) { if (o.team === -entry[1] - 1) { o.sendMessage("Your team has been eliminated."); o.kill(); @@ -64,7 +63,10 @@ function winner(teamId) { function loop() { if (teamWon) return; - let aliveNow = motherships.map(entry => [...entry, entities.find(entity => entity.id === entry[0])]); + const aliveNow = motherships.map(([id, data]) => { + const entity = entities.get(id); + return [id, data, entity]; + }); aliveNow = aliveNow.filter(entry => { if (!entry[2] || entry[2].isDead()) return death(entry); return true; diff --git a/server/modules/gamemodes/oldDreadnoughts.js b/server/modules/gamemodes/oldDreadnoughts.js index 4f05734f4..960495f1d 100644 --- a/server/modules/gamemodes/oldDreadnoughts.js +++ b/server/modules/gamemodes/oldDreadnoughts.js @@ -105,7 +105,7 @@ let generateLabyrinth = (size) => { y: y * mazeWallScale + mazeWallScale / 2, }; - if (!room.getAt(d).data.allowMazeWallSpawn) continue; + if (room.getAt(d).data.allowMazeWallSpawn === null) continue; let o = new Entity({ x: d.x, diff --git a/server/modules/gamemodes/tag.js b/server/modules/gamemodes/tag.js index 1a1b2dc43..506c452a2 100644 --- a/server/modules/gamemodes/tag.js +++ b/server/modules/gamemodes/tag.js @@ -7,8 +7,7 @@ function checkWin() { for (let i = 1; i <= Config.TEAMS; i++) { teams[-i] = 0; } - for (let i = 0; i < entities.length; i++) { - let o = entities[i]; + for (const o of entities.values()) { if (o.team < 0 && (o.isPlayer || o.isBot) && isPlayerTeam(o.team)) { teams[o.team]++; all++; diff --git a/server/modules/gamemodes/trainwars.js b/server/modules/gamemodes/trainwars.js index 3f33e1500..493807311 100644 --- a/server/modules/gamemodes/trainwars.js +++ b/server/modules/gamemodes/trainwars.js @@ -2,11 +2,15 @@ // https://discord.com/channels/366661839620407297/508125275675164673/1114907447195349074 class Train { - constructor () {} - loop () { - let train_able = entities.filter(r => r.isPlayer || r.isBot), - teams = new Set(train_able.map(r => r.team)); - for (let team of teams) { + constructor() { } + loop() { + const teams = new Set(); + for (const entity of entities.values()) { + if (entity.isPlayer || entity.isBot) { + teams.add(entity.team); + } + } + for (const team of teams) { let train = train_able.filter(r => r.team === team && !r.invuln).sort((a, b) => b.skill.score - a.skill.score); for (let [i, player] of train.entries()) { diff --git a/server/modules/global.js b/server/modules/global.js index da4b0f24d..884540e8a 100644 --- a/server/modules/global.js +++ b/server/modules/global.js @@ -3,21 +3,21 @@ Math.TAU = Math.PI * 2; // Global Utilities Requires let EventEmitter = require('events'); +const HashGrid = require('./physics/hashgrid.js'); global.Events = new EventEmitter(); global.ran = require(".././lib/random.js"); global.util = require(".././lib/util.js"); -global.hshg = require(".././lib/hshg.js"); global.protocol = require(".././lib/fasttalk.js"); // Global Variables (These must come before we import from the modules folder.) global.fps = "Unknown"; global.minimap = []; -global.entities = []; +global.entities = new Map(); global.walls = []; global.views = []; global.chats = {}; global.entitiesToAvoid = []; -global.grid = new hshg.HSHG(); +global.grid = new HashGrid(7); global.arenaClosed = false; global.mockupsLoaded = false; @@ -42,7 +42,7 @@ global.getWeakestTeam = () => { for (let i = -Config.TEAMS; i < 0; i++) { teamcounts[i] = 0; } - for (let o of entities) { + for (let o of entities.values()) { if ((o.isBot || o.isPlayer) && o.team in teamcounts && o.team < 0 && isPlayerTeam(o.team)) { if (!(o.team in teamcounts)) { teamcounts[o.team] = 0; @@ -60,7 +60,7 @@ global.getWeakestTeam = () => { }; global.Tile = class Tile { - constructor (args) { + constructor(args) { this.args = args; if ("object" !== typeof this.args) { throw new Error("First argument has to be an object!"); @@ -71,11 +71,11 @@ global.Tile = class Tile { if ("object" !== typeof this.data) { throw new Error("'data' property must be an object!"); } - this.init = args.init || (()=>{}); + this.init = args.init || (() => { }); if ("function" !== typeof this.init) { throw new Error("'init' property must be a function!"); } - this.tick = args.tick || (()=>{}); + this.tick = args.tick || (() => { }); if ("function" !== typeof this.tick) { throw new Error("'tick' property must be a function!"); } @@ -87,35 +87,60 @@ global.tickEvents = new EventEmitter(); global.syncedDelaysLoop = () => tickEvents.emit(tickIndex++); global.setSyncedTimeout = (callback, ticks = 0, ...args) => tickEvents.once(tickIndex + Math.round(ticks), () => callback(...args)); -const lowercaseRegex = /[a-z]/, - uppercaseRegexG = /[A-Z]/g; -function TO_SCREAMING_SNAKE_CASE(TEXT) { - if (lowercaseRegex.test(TEXT)) { - return TEXT.replace(uppercaseRegexG, _ => '_' + _).toUpperCase(); +const snakeCache = new Map(); +function TO_SCREAMING_SNAKE_CASE(text) { + const cache = snakeCache.get(text); + if (cache === undefined) { + let out = ""; + for (let i = 0, len = text.length; i < len; i++) { + const code = text.charCodeAt(i); + if (code >= 65 && code <= 90) { + if (i > 0) out += '_'; + out += text[i]; + } else if (code >= 97 && code <= 122) { + out += String.fromCharCode(code - 32); + } else { + out += text[i]; + } + } + snakeCache.set(text, out); + return out; + } else { + return cache; } - return TEXT; + let out = ""; + return out; } -global.Config = new Proxy(new EventEmitter(), { - get (obj, prop) { - return obj[TO_SCREAMING_SNAKE_CASE(prop)]; +const emitter = new EventEmitter(); +const emit = emitter.emit.bind(emitter); +const handler = { + get(target, prop, receiver) { + if (typeof prop === "string") { + return target[TO_SCREAMING_SNAKE_CASE(prop)]; + } + return Reflect.get(target, prop, receiver); }, - set (obj, prop, value) { - let abort; - prop = TO_SCREAMING_SNAKE_CASE(prop); - - obj.emit('change', { - setting: prop, - newValue: value, - oldValue: obj[prop], - preventDefault: () => abort = true - }); - - if (!abort) { - obj[prop] = value; + set(target, prop, value, receiver) { + if (typeof prop === 'string') { + const key = TO_SCREAMING_SNAKE_CASE(prop); + let aborted = false; + emit('change', { + setting: key, + newValue: value, + oldValue: target[key], + preventDefault: () => { aborted = true; } + }); + if (!aborted) { + target[key] = value; + } + return true; } + return Reflect.set(target, prop, value, receiver); } -}); +}; +global.Config = new Proxy(emitter, handler); + global.Config.port = process.env.PORT; for (let [key, value] of Object.entries(require('./setup/config.js'))) { @@ -142,11 +167,11 @@ global.makeHitbox = wall => { const _size = wall.size + 4; //calculate the relative corners let relativeCorners = [ - Math.atan2( _size, _size) + wall.angle, - Math.atan2(0 - _size, _size) + wall.angle, - Math.atan2(0 - _size, 0 - _size) + wall.angle, - Math.atan2( _size, 0 - _size) + wall.angle - ], + Math.atan2(_size, _size) + wall.angle, + Math.atan2(0 - _size, _size) + wall.angle, + Math.atan2(0 - _size, 0 - _size) + wall.angle, + Math.atan2(_size, 0 - _size) + wall.angle + ], distance = Math.sqrt(_size ** 2 + _size ** 2); //convert 4 corners into 4 lines diff --git a/server/modules/live/controllers.js b/server/modules/live/controllers.js index 9149eca0a..d6c568895 100644 --- a/server/modules/live/controllers.js +++ b/server/modules/live/controllers.js @@ -1,16 +1,16 @@ let compressMovementOffsets = [ - { x: 1, y: 0}, - { x: 1, y: 1}, - { x: 0, y: 1}, - { x:-1, y: 1}, - { x:-1, y: 0}, - { x:-1, y:-1}, - { x: 0, y:-1}, - { x: 1, y:-1} - ], + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 0, y: 1 }, + { x: -1, y: 1 }, + { x: -1, y: 0 }, + { x: -1, y: -1 }, + { x: 0, y: -1 }, + { x: 1, y: -1 } +], compressMovement = (current, goal) => { let offset = compressMovementOffsets[ - Math.round(( Math.atan2(current.y - goal.y, current.x - goal.x) / (Math.PI * 2) ) * 8 + 4) % 8 + Math.round((Math.atan2(current.y - goal.y, current.x - goal.x) / (Math.PI * 2)) * 8 + 4) % 8 ]; return { x: current.x + offset.x, @@ -43,9 +43,9 @@ let compressMovementOffsets = [ calcHowMuchAheadToShoot = (me, enemy) => { // Calculate relative position and velocity of enemy let relativeVelocity = { - x: enemy.velocity.x - me.velocity.x, - y: enemy.velocity.y - me.velocity.y - }, + x: enemy.velocity.x - me.velocity.x, + y: enemy.velocity.y - me.velocity.y + }, timeToIntercept = null, projectileSpeed = null; @@ -118,7 +118,7 @@ class io_bossRushAI extends IO { this.goalDefault = room.center; } think(input) { - if (new Vector( this.body.x - this.goalDefault.x, this.body.y - this.goalDefault.y ).isShorterThan(50)) { + if (new Vector(this.body.x - this.goalDefault.x, this.body.y - this.goalDefault.y).isShorterThan(50)) { this.enabled = false; } if (this.enabled) { @@ -198,7 +198,7 @@ class io_listenToPlayer extends IO { target.y *= this.body.reverseTank; } this.body.facingLocked = this.player.command.spinlock; - + // Autospin logic this.isAutospinning = this.player.command.autospin; if (this.isAutospinning && !this.wasAutospinning) { @@ -216,7 +216,7 @@ class io_listenToPlayer extends IO { // Define autospin facingType if (this.isAutospinning) { let speed = 0.05 * (alt ? -1 : 1) * this.body.autospinBoost; - this.body.facingTypeArgs = {speed}; + this.body.facingTypeArgs = { speed }; } this.body.autoOverride = this.player.command.override; if (this.body.invuln && (fire || alt)) this.body.invuln = false; @@ -394,7 +394,7 @@ class io_stackGuns extends IO { super(body); this.stackAtTime = opts.stackAtTime || 0.2; } - think ({ target }) { + think({ target }) { //why even bother? if (!target) { return; @@ -406,7 +406,7 @@ class io_stackGuns extends IO { for (let i = 0; i < this.body.guns.length; i++) { let gun = this.body.guns[i]; if (!gun.canShoot || !gun.stack) continue; - + let timeToFire = (1 - gun.cycleTimer) / (gun.shootSettings.reload * gun.reloadRateFactor * Config.runSpeed); if (lowestTimeToFire > timeToFire) { lowestTimeToFire = timeToFire; @@ -431,6 +431,7 @@ class io_stackGuns extends IO { } } class io_nearestDifferentMaster extends IO { + static validEntityTypes = new Set(["tank", "miniboss", "crasher", "ally"]); constructor(body, opts = {}) { super(body); this.accountForMovement = opts.accountForMovement ?? true; @@ -441,59 +442,86 @@ class io_nearestDifferentMaster extends IO { this.oldHealth = body.health.display(); } validate(e, m, mm, sqrRange, sqrRangeMaster) { - return (e.health.amount > 0) && - (!e.master.master.ignoredByAi) && - (e.master.master.team !== this.body.master.master.team) && - (e.master.master.team !== TEAM_ROOM) && - (!isNaN(e.dangerValue)) && - (!e.invuln && !e.master.master.passive && !this.body.master.master.passive) && - (this.body.aiSettings.seeInvisible || this.body.isArenaCloser || e.alpha > 0.5) && - (!e.bond) && - (e.type === "miniboss" || e.type === "tank" || e.type === "crasher" || (!this.body.aiSettings.IGNORE_SHAPES && e.type === 'food')) && - (this.body.aiSettings.BLIND || ((e.x - m.x) * (e.x - m.x) < sqrRange && (e.y - m.y) * (e.y - m.y) < sqrRange)) && - (this.body.aiSettings.SKYNET || ((e.x - mm.x) * (e.x - mm.x) < sqrRangeMaster && (e.y - mm.y) * (e.y - mm.y) < sqrRangeMaster)); - } - wouldHitWall (me, enemy) { - wouldHitWall(me, enemy); // Override + const myMaster = this.body.master.master; + const aiSettings = this.body.aiSettings; + const theirMaster = e.master.master; + if (e.health.amount <= 0) return false; + if (theirMaster.team === myMaster.team || theirMaster.team === TEAM_ROOM) return false; + if (theirMaster.ignoredByAi) return false; + if (e.bond) return false; + if (aiSettings.IGNORE_SHAPES && e.type === "food") return false; + if (e.invuln || theirMaster.passive || myMaster.passive) return false; + if (isNaN(e.dangerValue)) return false; + if (!(aiSettings.seeInvisible || this.body.isArenaCloser || e.alpha > 0.5)) return false; + if (!io_nearestDifferentMaster.validEntityTypes.has(e.type)) { + if (e.type !== "food") return false; + } + if (!aiSettings.BLIND) { + if ((e.x - m.x) * (e.x - m.x) >= sqrRange) return false; + if ((e.y - m.y) * (e.y - m.y) >= sqrRange) return false; + } + if (!aiSettings.SKYNET) { + if ((e.x - mm.x) * (e.x - mm.x) >= sqrRangeMaster) return false; + if ((e.y - mm.y) * (e.y - m.y) >= sqrRangeMaster) return false; + } + return true; } buildList(range) { - // Establish whom we judge in reference to - let mostDangerous = 0, - keepTarget = false; - // Filter through everybody... - let out = entities.filter(e => - // Only look at those within our view, and our parent's view, not dead, not invisible, not our kind, not a bullet/trap/block etc - this.validate(e, this.body, this.body.master.master, range * range, range * range * 4 / 3) - ).filter((e) => { - // Only look at those within range and arc (more expensive, so we only do it on the few) - if (this.body.firingArc == null || this.body.aiSettings.view360 || Math.abs(util.angleDifference(util.getDirection(this.body, e), this.body.firingArc[0])) < this.body.firingArc[1]) { - mostDangerous = Math.max(e.dangerValue, mostDangerous); - return true; + const body = this.body; + const { x, y, master, aiSettings, firingArc } = body; + const targetLock = this.targetLock; + const isFarm = aiSettings.farm; + const view360 = aiSettings.view360; + const sqrRange = range * range; + const halfrange = range / 2; + const fourThirdsSqrRange = sqrRange * 4 / 3; + let mostDangerous = -1; + let finalTargets = []; + for (const e of grid.query(x - halfrange, y - halfrange, x + halfrange, y + halfrange).values()) { + if (!view360) { + const angleDiff = util.angleDifference(util.getDirection(body, e), firingArc[0]); + if (Math.abs(angleDiff) >= firingArc[1]) { + continue; + } } - }).filter((e) => { - // Even more expensive - return !this.wouldHitWall(this.body, e); - }).filter((e) => { - // Only return the highest tier of danger - if (this.body.aiSettings.farm || e.dangerValue === mostDangerous) { - if (this.targetLock && e.id === this.targetLock.id) keepTarget = true; - return true; + if (this.validate(e, body, master.master, sqrRange, fourThirdsSqrRange) && !wouldHitWall(body, e)) { + if (isFarm) { + finalTargets.push(e); + } else { + if (e.dangerValue > mostDangerous) { + mostDangerous = e.dangerValue; + finalTargets = [e]; + } else if (e.dangerValue === mostDangerous) { + finalTargets.push(e); + } + } } - }); - // Reset target if it's not in there - if (!keepTarget) this.targetLock = undefined; - return out; + } + if (finalTargets.length === 0) { + this.targetLock = undefined; + return []; + } + let keepTarget = false; + if (targetLock) { + for (const e of finalTargets) { + if (e.id === targetLock.id) { + keepTarget = true; + break; + } + } + } + if (!keepTarget) { + this.targetLock = undefined; + } + return finalTargets; } think(input) { - // Override target lock upon other commands if (input.main || input.alt || this.body.master.autoOverride) { this.targetLock = undefined; return {}; } - // Otherwise, consider how fast we can either move to ram it or shoot at a potiential target. let tracking = this.body.topSpeed, range = this.body.fov; - // Use whether we have functional guns to decide for (let i = 0; i < this.body.guns.length; i++) { if (this.body.guns[i].canShoot && !this.body.aiSettings.SKYNET) { let v = this.body.guns[i].getTracking(); @@ -509,23 +537,16 @@ class io_nearestDifferentMaster extends IO { if (!Number.isFinite(range)) { range = 640 * this.body.FOV; } - // Check if my target's alive - if (this.targetLock && ( - !this.validate(this.targetLock, this.body, this.body.master.master, range * range, range * range * 4 / 3) || - this.wouldHitWall(this.body, this.targetLock) // Very expensive - )) { + if (this.targetLock && (!this.validate(this.targetLock, this.body, this.body.master.master, range * range, range * range * 4 / 3) || wouldHitWall(this.body, this.targetLock))) { this.targetLock = undefined; this.tick = 100; } - // Think damn hard if (this.tick++ > 15 * Config.runSpeed) { this.tick = 0; this.validTargets = this.buildList(range); - // Ditch our old target if it's invalid if (this.targetLock && this.validTargets.indexOf(this.targetLock) === -1) { this.targetLock = undefined; } - // Lock new target if we still don't have one. if (this.targetLock == null && this.validTargets.length) { this.targetLock = (this.validTargets.length === 1) ? this.validTargets[0] : nearest(this.validTargets, { x: this.body.x, @@ -534,27 +555,14 @@ class io_nearestDifferentMaster extends IO { this.tick = -90; } } - // Lock onto whoever's shooting me. - // let damageRef = (this.body.bond == null) ? this.body : this.body.bond; - // if (damageRef.collisionArray.length && damageRef.health.display() < this.oldHealth) { - // this.oldHealth = damageRef.health.display(); - // if (this.validTargets.indexOf(damageRef.collisionArray[0]) === -1) { - // let a = (damageRef.collisionArray[0].master.id === -1) - // ? damageRef.collisionArray[0].source - // : damageRef.collisionArray[0].master; - // } - // } - // Consider how fast it's moving and shoot at it if (this.targetLock != null) { let radial = this.targetLock.velocity; let diff = { x: this.targetLock.x - this.body.x, y: this.targetLock.y - this.body.y, } - /// Refresh lead time if (this.tick % 4 === 0) { this.lead = 0 - // Find lead time (or don't) if (!this.body.aiSettings.chase) { let toi = timeOfImpact(diff, radial, tracking) this.lead = toi @@ -564,7 +572,6 @@ class io_nearestDifferentMaster extends IO { this.lead = 0; } if (!this.accountForMovement) this.lead = 0; - // And return our aim return { target: { x: diff.x + this.lead * radial.x, @@ -748,7 +755,7 @@ class io_spin2 extends IO { let alt = this.body.master.control.alt; let reverse = (this.reverseOnAlt && alt) ? -1 : 1; this.body.facingType = "spin"; - this.body.facingTypeArgs = {speed: this.speed * reverse}; + this.body.facingTypeArgs = { speed: this.speed * reverse }; } think(input) { if (!this.reverseOnTheFly || !this.reverseOnAlt) return; @@ -758,7 +765,7 @@ class io_spin2 extends IO { if (this.lastAlt != alt) { let reverse = alt ? -1 : 1; this.body.facingType = "spin"; - this.body.facingTypeArgs = {speed: this.speed * reverse}; + this.body.facingTypeArgs = { speed: this.speed * reverse }; this.lastAlt = alt; } } @@ -810,7 +817,7 @@ class io_wanderAroundMap extends IO { } think(input) { if ( - new Vector( this.body.x - this.spot.x, this.body.y - this.spot.y ).isShorterThan(50) || + new Vector(this.body.x - this.spot.x, this.body.y - this.spot.y).isShorterThan(50) || wouldHitWall(this.body, this.spot) ) { this.spot = ran.choose(room.spawnableDefault).loc; @@ -834,7 +841,7 @@ class io_wanderAroundMap extends IO { // returns deviation from origin angle in radians let io_formulaTarget_sineDefault = (frame, body) => Math.sin(frame / 30); class io_formulaTarget extends IO { - constructor (b, opts = {}) { + constructor(b, opts = {}) { super(b); this.masterAngle = opts.masterAngle; this.formula = opts.formula || io_formulaTarget_sineDefault; @@ -842,7 +849,7 @@ class io_formulaTarget extends IO { this.originAngle = this.masterAngle ? b.master.facing : b.facing; this.frame = 0; } - think () { + think() { // if (this.updateOriginAngle) { // this.originAngle = this.masterAngle ? b.master.facing : getTheGunThatSpawnedMe("how do i do that????").angle; // } @@ -866,19 +873,19 @@ class io_whirlwind extends IO { this.body.inverseDist = this.maxDistance * this.body.size - this.body.dist + this.minDistance * this.body.size; this.radiusScalingSpeed = opts.radiusScalingSpeed || 10; } - + think(input) { this.body.angle += (this.body.skill.spd * 2 + this.body.aiSettings.SPEED) * Math.PI / 180; let trueMaxDistance = this.maxDistance * this.body.size; let trueMinDistance = this.minDistance * this.body.size; - if(input.fire){ - if(this.body.dist <= trueMaxDistance) { + if (input.fire) { + if (this.body.dist <= trueMaxDistance) { this.body.dist += this.radiusScalingSpeed; this.body.inverseDist -= this.radiusScalingSpeed; } } - else if(input.alt){ - if(this.body.dist >= trueMinDistance) { + else if (input.alt) { + if (this.body.dist >= trueMinDistance) { this.body.dist -= this.radiusScalingSpeed; this.body.inverseDist += this.radiusScalingSpeed; } @@ -893,22 +900,22 @@ class io_orbit extends IO { this.realDist = 0; this.invert = opts.invert ?? false; } - + think(input) { let invertFactor = this.invert ? -1 : 1, master = this.body.master.master, dist = this.invert ? master.inverseDist : master.dist, angle = (this.body.angle * Math.PI / 180 + master.angle) * invertFactor; - - if(this.realDist > dist){ + + if (this.realDist > dist) { this.realDist -= Math.min(10, Math.abs(this.realDist - dist)); } - else if(this.realDist < dist){ + else if (this.realDist < dist) { this.realDist += Math.min(10, Math.abs(dist - this.realDist)); } this.body.x = master.x + Math.cos(angle) * this.realDist; this.body.y = master.y + Math.sin(angle) * this.realDist; - + this.body.facing = angle; } } @@ -930,7 +937,7 @@ class io_snake extends IO { this.body.y += Math.sin(this.body.velocity.direction) * this.body.size * 20; // Clamp scale to [45, 75] // Attempts to get the bullets to intersect with the cursor - this.waveHorizontalScale = util.clamp(util.getDistance(this.body.master.master.control.target, {x: 0, y: 0}) / Math.PI, 45, 75); + this.waveHorizontalScale = util.clamp(util.getDistance(this.body.master.master.control.target, { x: 0, y: 0 }) / Math.PI, 45, 75); } think(input) { // Define a sin wave for the bullet to follow @@ -960,7 +967,7 @@ class io_disableOnOverride extends IO { this.initialAlpha = this.body.alpha; this.targetAlpha = this.initialAlpha; } - + this.pacify = (this.body.parent.master.autoOverride || this.body.parent.master.master.autoOverride); if (this.pacify && !this.lastPacify) { this.targetAlpha = 0; @@ -984,12 +991,12 @@ class io_disableOnOverride extends IO { class io_scaleWithMaster extends IO { constructor(body) { super(body); - let handler = ({body: b}) => { + let handler = ({ body: b }) => { this.sizeFactor = b.size / b.master.size; }; this.body.definitionEvents.push({ event: 'define', handler, once: false }); this.body.on('define', handler, false); - + this.storedSize = 0; } think(input) { diff --git a/server/modules/live/entity.js b/server/modules/live/entity.js index d2ac6736a..24bf20805 100644 --- a/server/modules/live/entity.js +++ b/server/modules/live/entity.js @@ -57,7 +57,7 @@ class Gun extends EventEmitter { this.destroyOldestChild = info.PROPERTIES.DESTROY_OLDEST_CHILD ?? false; if (this.destroyOldestChild) this.maxChildren++; this.shootOnDeath = info.PROPERTIES.SHOOT_ON_DEATH ?? false; - this.stack = info.PROPERTIES.STACK_GUN ?? true ; + this.stack = info.PROPERTIES.STACK_GUN ?? true; this.identifier = info.PROPERTIES.IDENTIFIER ?? null; if (info.PROPERTIES.TYPE != null) { this.canShoot = true; @@ -108,7 +108,7 @@ class Gun extends EventEmitter { } live() { if (!this.canShoot || this.body.master.invuln) return; - + // Iterate recoil this.recoil(); @@ -133,19 +133,19 @@ class Gun extends EventEmitter { // Repeatedly check for shoot permission to prevent ultra low reload guns from exceeding the child limit in 1 tick shootPermission = this.checkShootPermission(); } - // If we're not shooting, only cycle up to where we'll have the proper firing delay + // If we're not shooting, only cycle up to where we'll have the proper firing delay } else if (this.cycleTimer > this.maxCycleTimer) { this.cycleTimer = this.maxCycleTimer; } } checkShootPermission() { let shootPermission = this.maxChildren - ? this.maxChildren > + ? this.maxChildren > this.children.length * this.childrenLimitFactor - : this.body.maxChildren - ? this.body.maxChildren > - this.body.children.length * this.childrenLimitFactor - : true; + : this.body.maxChildren + ? this.body.maxChildren > + this.body.children.length * this.childrenLimitFactor + : true; // Handle destroying oldest child if (this.destroyOldestChild && !shootPermission) { @@ -169,7 +169,7 @@ class Gun extends EventEmitter { // If told to, create an independent entity if (this.independentChildren) { this.defineIndependentBullet(bullet); - // Else make a regular bullet + // Else make a regular bullet } else { this.defineBullet(bullet); } @@ -195,16 +195,16 @@ class Gun extends EventEmitter { let offsetAngle = this.offsetDirection + this.angle + this.body.facing, gunlength = this.length + Config.bulletSpawnOffset * this.width * this.shootSettings.size / 2, - // Calculate offset of gun base and gun end based + // Calculate offset of gun base and gun end based offsetBaseX = this.offset * Math.cos(offsetAngle), offsetBaseY = this.offset * Math.sin(offsetAngle), offsetEndX = gunlength * Math.cos(this.facing), offsetEndY = gunlength * Math.sin(this.facing), - // Combine offsets to get final values + // Combine offsets to get final values offsetFinalX = offsetBaseX + offsetEndX, offsetFinalY = offsetBaseY + offsetEndY; - + return [offsetFinalX, offsetFinalY] } createBullet(spawnX, spawnY) { @@ -226,7 +226,7 @@ class Gun extends EventEmitter { let velocityMagnitude = this.negativeRecoil * this.shootSettings.speed * this.bulletSkills.spd * (shudder + 1) * Config.runSpeed, velocityDirection = this.angle + this.body.facing + spread, velocity = new Vector(velocityMagnitude * Math.cos(velocityDirection), velocityMagnitude * Math.sin(velocityDirection)); - + // Apply velocity inheritance if (this.body.velocity.length) { let extraBoost = @@ -242,16 +242,16 @@ class Gun extends EventEmitter { // Spawn bullet spawnX = this.body.x + this.body.size * spawnX - velocity.x, - spawnY = this.body.y + this.body.size * spawnY - velocity.y; - + spawnY = this.body.y + this.body.size * spawnY - velocity.y; + // Independent children if (this.independentChildren) { - let bullet = new Entity({x: spawnX, y: spawnY}); + let bullet = new Entity({ x: spawnX, y: spawnY }); return bullet; } // Dependent children - let bullet = new Entity({x: spawnX, y: spawnY}, this.master.master); + let bullet = new Entity({ x: spawnX, y: spawnY }, this.master.master); bullet.velocity = velocity; return bullet; } @@ -273,7 +273,7 @@ class Gun extends EventEmitter { defineBullet(bullet) { // Set bullet source bullet.source = this.body; - + // Define bullet based on natural properties and skills this.bulletType.SIZE = (this.body.size * this.width * this.shootSettings.size) / 2; bullet.define(this.bulletType); @@ -298,7 +298,7 @@ class Gun extends EventEmitter { if (!bullet.settings.necroTypes) { return; } - + // Set all necroType gun references to parent gun for (let shape of bullet.settings.necroTypes) { bullet.settings.necroDefineGuns[shape] = this; @@ -329,7 +329,7 @@ class Gun extends EventEmitter { // Pre-flatten bullet types to save on doing the same define() sequence a million times this.bulletType = Array.isArray(type) ? type : [type]; // Preset BODY because not all definitions have BODY defined when flattened - let flattenedType = {BODY: {}}; + let flattenedType = { BODY: {} }; for (let type of this.bulletType) { type = ensureIsClass(type); util.flattenDefinition(flattenedType, type); @@ -467,12 +467,12 @@ class Gun extends EventEmitter { } getPhotoInfo() { return { - ...this.lastShot, + ...this.lastShot, color: this.color.compiled, alpha: this.alpha, strokeWidth: this.strokeWidth, - borderless: this.borderless, - drawFill: this.drawFill, + borderless: this.borderless, + drawFill: this.drawFill, drawAbove: this.drawAbove, length: this.length, width: this.width, @@ -721,7 +721,6 @@ class Entity extends EventEmitter { this.isInGrid = false; this.removeFromGrid = () => { if (this.isInGrid) { - grid.removeObject(this); this.isInGrid = false; } }; @@ -729,7 +728,6 @@ class Entity extends EventEmitter { if (!mockupsLoaded) return; if (!this.collidingBond && this.bond != null) return; if (!this.isInGrid) { - grid.addObject(this); this.isInGrid = true; } }; @@ -766,8 +764,8 @@ class Entity extends EventEmitter { yMin: 0, yMax: room.height, }, - // Define it - this.SIZE = 1; + // Define it + this.SIZE = 1; this.sizeMultiplier = 1; this.define("genericEntity"); // Initalize physics and collision @@ -806,41 +804,27 @@ class Entity extends EventEmitter { this.globalStore = {}; this.store = {}; // This is for collisions - this.AABB_data = {}; - this.AABB_savedSize = 0; + this.minX = 0; + this.minY = 0; + this.maxX = 0; + this.maxY = 0; this.collidingBond = false this.updateAABB = (active) => { - if (!this.collidingBond && this.bond != null) return 0; - if (!active) { - this.AABB_data.active = false; - return 0; - } - if (this.isPlayer && !this.isDead()) this.refreshBodyAttributes(); - this.antiNaN.update(); - // Get bounds - let x1 = Math.min(this.x, this.x + this.velocity.x + this.accel.x) - this.realSize - 5; - let y1 = Math.min(this.y, this.y + this.velocity.y + this.accel.y) - this.realSize - 5; - let x2 = Math.max(this.x, this.x + this.velocity.x + this.accel.x) + this.realSize + 5; - let y2 = Math.max(this.y, this.y + this.velocity.y + this.accel.y) + this.realSize + 5; - let size = Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1)); - let sizeDiff = this.AABB_savedSize / size; - // Update data - this.AABB_data = { - min: [x1, y1], - max: [x2, y2], - active: true, - size: size, - }; - // Update grid if needed - if (sizeDiff > Math.SQRT2 || sizeDiff < Math.SQRT1_2) { - this.removeFromGrid(); - this.addToGrid(); - this.AABB_savedSize = size; + if (!active || (!this.collidingBond && this.bond != null)) { + this.isInGrid = false; + } else { + this.isInGrid = true; + if (this.isPlayer && !this.isDead()) { + this.refreshBodyAttributes(); + } + this.minX = this.x - this.size; + this.minY = this.y - this.size; + this.maxX = this.x + this.size; + this.maxY = this.y + this.size; } }; - this.getAABB = () => this.AABB_data; this.updateAABB(true); - entities.push(this); + entities.set(this.id, this); for (let v of views) v.add(this); this.activation.update(); Events.emit('spawn', this); @@ -1212,7 +1196,7 @@ class Entity extends EventEmitter { let savedFacing = host.facing; let savedSize = host.SIZE; - + host.controllers = []; host.define("genericEntity"); gun.defineBullet(host); @@ -1728,7 +1712,7 @@ class Entity extends EventEmitter { } } destroyAllChildren() { - for (let instance of entities) { + for (let instance of entities.values()) { if ( instance.settings.clearOnMasterUpgrade && instance.master.id === this.id @@ -2038,18 +2022,19 @@ class Entity extends EventEmitter { return 0; } if (this.damageReceived > 0) { - let damageInflictor = [] - let damageTool = [] - - for (let i = 0; i < this.collisionArray.length; i++) { - let instance = this.collisionArray[i]; + const damageInflictor = []; + const damageTool = []; + for (const instance of this.collisionArray) { if (instance.type === 'wall' || !instance.damage) continue; - damageInflictor.push(instance.master) - damageTool.push(instance) + damageInflictor.push(instance.master); + damageTool.push(instance); } - this.emit('damage', { body: this, damageInflictor, damageTool }); + this.emit('damage', { + body: this, + damageInflictor, + damageTool + }); } - // Life-limiting effects if (this.settings.diesAtRange) { this.range -= 1 / Config.runSpeed; if (this.range < 0) { @@ -2057,185 +2042,141 @@ class Entity extends EventEmitter { } } if (this.settings.diesAtLowSpeed) { - if ( - !this.collisionArray.length && - this.velocity.length < this.topSpeed / 2 - ) { + if (!this.collisionArray.length && this.velocity.length < this.topSpeed / 2) { this.health.amount -= this.health.getDamage(1 / Config.runSpeed); } } - // Shield regen and damage - if (this.shield.max) { - if (this.damageReceived) { - let shieldDamage = this.shield.getDamage(this.damageReceived); - this.damageReceived -= shieldDamage; - this.shield.amount -= shieldDamage; - } + if (this.shield.max && this.damageReceived > 0) { + const shieldDamage = this.shield.getDamage(this.damageReceived); + this.damageReceived -= shieldDamage; + this.shield.amount -= shieldDamage; } - // Health damage - if (this.damageReceived) { - let healthDamage = this.health.getDamage(this.damageReceived); + if (this.damageReceived > 0) { + const healthDamage = this.health.getDamage(this.damageReceived); this.blend.amount = 1; this.health.amount -= healthDamage; } this.damageReceived = 0; - // Check for death - if (this.isDead()) { - - this.emit('dead'); - - //Shoot on death - for (let i = 0; i < this.guns.length; i++) { - let gun = this.guns[i]; - if (gun.shootOnDeath && gun.body != null) { - gun.fire(); - } - } - - // MEMORY LEAKS ARE BAD!!!! - for (let i = 0; i < this.turrets.length; i++) { - this.turrets[i].kill(); - } - - // Initalize message arrays - let killers = [], - killTools = [], - notJustFood = false; - // If I'm a tank, call me a nameless player - let name = this.master.name == "" - ? this.master.type === "tank" - ? "an unnamed " + this.label : this.master.type === "miniboss" - ? "a visiting " + this.label : this.label.substring(0, 3) == 'The' - ? this.label : util.addArticle(this.label) - : this.master.name + "'s " + this.label; - // Calculate the jackpot - let jackpot = util.getJackpot(this.skill.score) / this.collisionArray.length; - // Now for each of the things that kill me... - for (let i = 0; i < this.collisionArray.length; i++) { - let instance = this.collisionArray[i]; - if (instance.type === 'wall' || !instance.damage) continue; - if (instance.master.settings.acceptsScore) { - // If it's not food, give its master the score - if (instance.master.type === "tank" || instance.master.type === "miniboss") { - notJustFood = true; - } - instance.master.skill.score += jackpot; - killers.push(instance.master); // And keep track of who killed me - } else if (instance.settings.acceptsScore) { - instance.skill.score += jackpot; - } - killTools.push(instance); // Keep track of what actually killed me - } - // Remove duplicates - killers = killers.filter((elem, index, self) => index == self.indexOf(elem)); - this.emit('death', { body: this, killers, killTools }); - killers.forEach((e) => e.emit('kill', { body: e, entity: this })); - // If there's no valid killers (you were killed by food), change the message to be more passive - let killText = "You have been killed by ", - doISendAText = this.settings.givesKillMessage; - - for (let i = 0; i < killers.length; i++) { - let instance = killers[i]; - - switch (this.type) { - case "tank": - killers.length > 1 ? instance.killCount.assists++ : instance.killCount.solo++; - break; - - case "food": - case "crasher": - instance.killCount.polygons++; - break - - case "miniboss": - instance.killCount.bosses++; - break; + if (!this.isDead()) { + return 0; + } + this.emit('dead'); + for (const gun of this.guns) { + if (gun.shootOnDeath && gun.body != null) { + gun.fire(); + } + } + for (const turret of this.turrets) { + turret.kill(); + } + const killerSet = new Set(); + const killTools = []; + let notJustFood = false; + const jackpot = this.collisionArray.length > 0 ? util.getJackpot(this.skill.score) / this.collisionArray.length : util.getJackpot(this.skill.score); + for (const instance of this.collisionArray) { + if (instance.type === 'wall' || !instance.damage) continue; + const master = instance.master; + if (master.settings.acceptsScore) { + if (master.type === "tank" || master.type === "miniboss") { + notJustFood = true; } - - this.killCount.killers.push(instance.index); - }; - // Add the killers to our death message, also send them a message - if (notJustFood) { - for (let i = 0; i < killers.length; i++) { - let instance = killers[i]; - if (instance.master.type !== "food" && instance.master.type !== "crasher") { - killText += instance.name == "" ? killText == "" ? "An unnamed player" : "an unnamed player" : instance.name; - killText += " and "; - } - // Only if we give messages + master.skill.score += jackpot; + killerSet.add(master); + } else if (instance.settings.acceptsScore) { + instance.skill.score += jackpot; + } + killTools.push(instance); + } + const killers = [...killerSet]; + this.emit('death', { + body: this, + killers, + killTools + }); + killers.forEach((e) => e.emit('kill', { + body: e, + entity: this + })); + const doISendAText = this.settings.givesKillMessage; + for (const killer of killers) { + switch (this.type) { + case "tank": + killers.length > 1 ? killer.killCount.assists++ : killer.killCount.solo++; + break; + case "food": + case "crasher": + killer.killCount.polygons++; + break; + case "miniboss": + killer.killCount.bosses++; + break; + } + this.killCount.killers.push(killer.index); + } + let killText = "You have been killed by "; + if (notJustFood) { + const killerNames = []; + for (const killer of killers) { + if (killer.master.type !== "food" && killer.master.type !== "crasher") { + killerNames.push(killer.name || "an unnamed player"); if (doISendAText) { - instance.sendMessage("You killed " + name + (killers.length > 1 ? " (with some help)." : ".")); + const message = `You killed ${this.master.name || util.addArticle(this.label)}${killers.length > 1 ? " (with some help)." : "."}`; + killer.sendMessage(message); } if (this.settings.killMessage) { - instance.sendMessage("You " + this.settings.killMessage + " " + name + (killers.length > 1 ? " (with some help)." : ".")); + const message = `You ${this.settings.killMessage} ${this.master.name || util.addArticle(this.label)}${killers.length > 1 ? " (with some help)." : "."}`; + killer.sendMessage(message); } } - // Prepare the next part of the next - killText = killText.slice(0, -4) + "killed you with "; - } - // Broadcast - if (this.settings.broadcastMessage) { - sockets.broadcast(this.settings.broadcastMessage); - } - if (this.settings.defeatMessage) { - let text = util.addArticle(this.label, true); - if (notJustFood) { - text += " has been defeated by"; - for (let { name } of killers) { - text += " "; - text += name === "" ? "an unnamed player" : name; - text += " and"; - } - text = text.slice(0, -4); - text += "!"; - } else { - text += " fought a polygon... and the polygon won."; - } - sockets.broadcast(text); } - - // instead of "a Machine Gunner Bullet and a Machine Gunner Bullet and a Machine Gunner Bullet", - // make it say " 3 Machine Gunner Bullets" - let killCounts = {}; - for (let { label } of killTools) { - if (!killCounts[label]) killCounts[label] = 0; - killCounts[label]++; - } - let killCountEntries = Object.entries(killCounts).map(([name, count], i) => name); - for (let i = 0; i < killCountEntries.length; i++) { - killText += (killCounts[killCountEntries[i]] == 1) ? util.addArticle(killTools[i].label) : killCounts[killCountEntries[i]] + ' ' + killCountEntries[i] + 's'; - killText += i < killCountEntries.length - 2 ? ', ' : ' and '; + killText += killerNames.join(' and ') + " with "; + } + const toolCounts = new Map(); + for (const { + label + } + of killTools) { + toolCounts.set(label, (toolCounts.get(label) || 0) + 1); + } + const toolStrings = []; + for (const [label, count] of toolCounts.entries()) { + toolStrings.push(count === 1 ? util.addArticle(label) : `${count} ${label}s`); + } + if (toolStrings.length > 0) { + if (toolStrings.length === 1) { + killText += toolStrings[0]; + } else { + killText += toolStrings.slice(0, -1).join(', ') + ' and ' + toolStrings.slice(-1); } - - // Prepare it and clear the collision array. - killText = killText.slice(0, -5); - if (killText === "You have been kille") { - killText = "You have died a stupid death"; - } - if (!this.dontSendDeathMessage) { - this.sendMessage(killText + "."); - } - // If I'm the leader, broadcast it: - if (this.id === room.topPlayerID) { - let usurptText = this.name === "" ? "The leader" : this.name; - if (notJustFood) { - usurptText += " has been usurped by"; - for (let i = 0; i < killers.length; i++) { - usurptText += " "; - usurptText += killers[i].name === "" ? "an unnamed player" : killers[i].name; - usurptText += " and"; - } - usurptText = usurptText.slice(0, -4) + "!"; - } else { - usurptText += " fought a polygon... and the polygon won."; - } - sockets.broadcast(usurptText); + } else if (killText === "You have been killed by ") { + killText = "You have died a stupid death"; + } + if (!this.dontSendDeathMessage) { + this.sendMessage(killText.trim() + "."); + } + if (this.settings.broadcastMessage) { + sockets.broadcast(this.settings.broadcastMessage); + } + if (this.settings.defeatMessage) { + let text = util.addArticle(this.label, true); + if (notJustFood) { + text += " has been defeated by " + killers.map(k => k.name || "an unnamed player").join(" and ") + "!"; + } else { + text += " fought a polygon... and the polygon won."; + } + sockets.broadcast(text); + } + if (this.id === room.topPlayerID) { + let usurptText = (this.name || "The leader"); + if (notJustFood) { + usurptText += " has been usurped by " + killers.map(k => k.name || "an unnamed player").join(" and ") + "!"; + } else { + usurptText += " fought a polygon... and the polygon won."; } - this.setKillers(killers); - // Kill it - return 1; + sockets.broadcast(usurptText); } - return 0; + this.setKillers(killers); + return 1; } protect() { entitiesToAvoid.push(this); @@ -2254,47 +2195,50 @@ class Entity extends EventEmitter { this.health.amount = -100; } destroy() { - // Remove from the protected entities list if (this.isProtected) { - util.remove(entitiesToAvoid, entitiesToAvoid.indexOf(this)); + const index = entitiesToAvoid.indexOf(this); + if (index !== -1) { + entitiesToAvoid[index] = entitiesToAvoid[entitiesToAvoid.length - 1]; + entitiesToAvoid.pop(); + } } - // Remove from minimap - let i = minimap.findIndex(entry => entry[0] === this.id); - if (i != -1) { - util.remove(minimap, i); + const minimapIndex = minimap.findIndex(entry => entry[0] === this.id); + if (minimapIndex !== -1) { + minimap[minimapIndex] = minimap[minimap.length - 1]; + minimap.pop(); } - // Remove this from views - for (let view of views) { + for (const view of views) { view.remove(this); } - // Remove from parent lists if needed - if (this.parent != null) { - util.remove(this.parent.children, this.parent.children.indexOf(this)); + if (this.parent) { + const children = this.parent.children; + const index = children.indexOf(this); + if (index !== -1) { + children[index] = children[children.length - 1]; + children.pop(); + } } - // Kill all of its children - for (let instance of entities) { - if (instance.source.id === this.id) { + for (const instance of entities.values()) { + if (instance.source?.id === this.id) { if (instance.settings.persistsAfterDeath) { instance.source = instance; } else { instance.kill(); } } - if (instance.parent && instance.parent.id === this.id) { + if (instance.parent?.id === this.id) { instance.parent = null; } - if (instance.master.id === this.id) { + if (instance.master?.id === this.id) { instance.kill(); - instance.master = instance; } } - // Remove everything bound to it - for (let i = 0; i < this.turrets.length; i++) { - this.turrets[i].destroy(); + for (const turret of this.turrets) { + turret.destroy(); } - // Remove from the collision grid this.removeFromGrid(); this.isGhost = true; + entities.delete(this.id); } isDead() { return this.health.amount <= 0; diff --git a/server/modules/live/entitySubFunctions.js b/server/modules/live/entitySubFunctions.js index 7934129da..6ee7c94dd 100644 --- a/server/modules/live/entitySubFunctions.js +++ b/server/modules/live/entitySubFunctions.js @@ -190,7 +190,7 @@ class HealthType { damageToMax); } } - regenerate(boost = false) { + regenerate(boost = 0) { boost /= 2; let cons = 5; switch (this.type) { @@ -234,8 +234,6 @@ const dirtyCheck = function (p, r) { return false }; -const purgeEntities = () => entities = entities.filter(e => !e.isGhost); - let remapTarget = (i, ref, self) => { if (i.target == null || !(i.main || i.alt)) return undefined; return { @@ -261,4 +259,4 @@ lazyRealSizes = new Proxy(lazyRealSizes, { } }); -module.exports = { skcnv, Skill, HealthType, dirtyCheck, purgeEntities, remapTarget, lazyRealSizes }; \ No newline at end of file +module.exports = { skcnv, Skill, HealthType, dirtyCheck, remapTarget, lazyRealSizes }; \ No newline at end of file diff --git a/server/modules/network/sockets.js b/server/modules/network/sockets.js index 9a826a64f..c17481435 100644 --- a/server/modules/network/sockets.js +++ b/server/modules/network/sockets.js @@ -446,8 +446,7 @@ function incoming(message, socket) { case "1": //suicide squad if (player.body != null && !player.body.underControl) { - for (let i = 0; i < entities.length; i++) { - let instance = entities[i]; + for (const instance of entities.values()) { if (instance.settings.clearOnMasterUpgrade && instance.master.id === player.body.id) { instance.kill(); } @@ -458,8 +457,7 @@ function incoming(message, socket) { case "A": if (player.body != null) return 1; let possible = [] - for (let i = 0; i < entities.length; i++) { - let entry = entities[i]; + for (const entry of entities.values()) { if (entry.type === "miniboss") possible.push(entry); if (entry.isDominator || entry.isMothership || entry.isArenaCloser) possible.push(entry); if (Config.MODE === "tdm" && socket.rememberedTeam === entry.team && entry.type === "tank" && entry.bond == null) possible.push(entry); @@ -523,9 +521,12 @@ function incoming(message, socket) { player.body.sendMessage("You are now controlling the mothership."); player.body.sendMessage("Press F to relinquish control of the mothership."); } else if (Config.DOMINATOR_LOOP) { - let dominators = entities.map((entry) => { - if (entry.isDominator && entry.team === player.body.team && !entry.underControl) return entry; - }).filter(x=>x); + const dominators = []; + for (const entity of entities.values()) { + if (entity.isDominator && entity.team === player.body.team && !entity.underControl) { + dominators.push(entity); + } + } if (!dominators.length) { player.body.sendMessage("There are no dominators available that are on your team or already controlled by an player."); return 1; @@ -1179,9 +1180,9 @@ class View { this.lastVisibleUpdate = camera.lastUpdate; // And update the nearby list this.nearby = [] - for (let i = 0; i < entities.length; i++) { - if (check(this.socket.camera, entities[i])) { - this.nearby.push(entities[i]); + for (const entity of entities.values()) { + if (check(this.socket.camera, entity)) { + this.nearby.push(entity); } } } @@ -1291,7 +1292,7 @@ const Delta = class { // Deltas let minimapAll = new Delta(5, args => { let all = []; - for (let my of entities) { + for (let my of entities.values()) { if (my.allowedOnMinimap && ( my.alwaysShowOnMinimap || (my.type === "wall" && my.alpha > 0.2) || @@ -1314,7 +1315,7 @@ let minimapAll = new Delta(5, args => { }); let minimapTeams = new Delta(3, args => { let all = []; - for (let my of entities) + for (let my of entities.values()) if (my.type === "tank" && my.team === args[0] && my.master === my && my.allowedOnMinimap) { all.push({ id: my.id, @@ -1342,7 +1343,7 @@ let leaderboard = new Delta(7, args => { team }); } - for (let instance of entities) { + for (let instance of entities.values()) { if (Config.MOTHERSHIP_LOOP) { if (instance.isMothership) list.push(instance); } else if (Config.TAG) { diff --git a/server/modules/physics/hashgrid.js b/server/modules/physics/hashgrid.js new file mode 100644 index 000000000..e6489a541 --- /dev/null +++ b/server/modules/physics/hashgrid.js @@ -0,0 +1,51 @@ +module.exports = class HashGrid { + static stride = 1 << 16; + + cells = new Map(); + constructor(cellSize) { + this.cellSize = cellSize; + } + + insert(entity, minX, minY, maxX, maxY) { + const endX = maxX >> this.cellSize; + const endY = maxY >> this.cellSize; + for (let x = minX >> this.cellSize; x <= endX; x++) { + for (let y = minY >> this.cellSize; y <= endY; y++) { + const key = x + y * HashGrid.stride; + const cell = this.cells.get(key); + if (cell === undefined) { + this.cells.set(key, [entity]); + } else { + cell.push(entity); + } + } + } + } + + query(minX, minY, maxX, maxY) { + const cells = this.cells; + const cellSize = this.cellSize; + const stride = HashGrid.stride; + const output = new Set(); + const endX = maxX >> cellSize; + const endY = maxY >> cellSize; + for (let x = minX >> cellSize; x <= endX; x++) { + for (let y = minY >> cellSize; y <= endY; y++) { + const key = x + y * stride; + const cell = cells.get(key); + if (cell !== undefined) { + for (const entity of cell) { + if (entity.minX < maxX && entity.maxX > minX && entity.minY < maxY && entity.maxY > minY) { + output.add(entity); + } + } + } + } + } + return output; + } + + clear() { + this.cells.clear(); + } +} diff --git a/server/modules/physics/quadTree.js b/server/modules/physics/quadTree.js deleted file mode 100644 index b08d38ed5..000000000 --- a/server/modules/physics/quadTree.js +++ /dev/null @@ -1,152 +0,0 @@ -class CollisionGrid { - constructor(width, height, gridSize = 100) { - this.totalInstances = []; - this.grid = []; - this.outOfBoundObjects = []; - this.width = width; - this.height = height; - this.gridSize = gridSize; - } - intoCells() { - for (let i = 0; i < this.gridSize; i++) { - let row = []; - for (let j = 0; j < this.gridSize; j++) row.push([]); - this.grid.push(row); - } - this.grid; - } - addToGrid(instance) { - let toAdd = { - x: instance.x, - y: instance.y, - vx: instance.vx, - vy: instance.vy, - size: instance.size, - id: instance.id - }; - let cellX = Math.floor(toAdd.x * this.gridSize / this.height); - let cellY = Math.floor(toAdd.y * this.gridSize / this.height); - this.totalInstances.push(instance); - if (!this.grid[cellY] || !this.grid[cellY][cellX]) return this.outOfBoundObjects.push(toAdd); - this.grid[cellY][cellX].push(toAdd); - } - getCell(instance) { - let cellX = Math.floor(instance.x * this.gridSize / this.height); - let cellY = Math.floor(instance.y * this.gridSize / this.height); - if (!this.grid[cellY]) return this.outOfBoundObjects; - if (!this.grid[cellY][cellX]) return this.outOfBoundObjects; - return this.grid[cellY][cellX]; - } - reset(list) { - this.totalInstances = []; - this.grid = []; - this.outOfBoundObjects = []; - this.intoCells(); - for (let i = 0; i < list.length; i++) this.addToGrid(list[i]); - } - hitDetection(instance, other) { - return Math.sqrt(Math.pow(other.x - instance.x, 2) + Math.pow(other.y - instance.y, 2)) < (instance.size + other.size); - } - queryForCollisionPairs() { - let pairs = []; - let i = 0; - for (let i = 0; i < this.totalInstances.length; i++) { - let instance = this.totalInstances[i]; - let cell = this.getCell(instance); - if (cell.length > 1) { - for (let j = 0; j < cell.length; j++) { - let other = cell[j]; - let pair = instance.id > other.id ? [other.id, instance.id] : [instance.id, other.id]; - let hit = this.hitDetection(instance, other); - if (!pairs.includes(pair) && hit && instance.id !== other.id) pairs.push(pair); - } - } - } - return pairs; - } -} - -class QuadTree { - constructor(bounds, max_objects, max_levels, level) { - this.maxObjects = max_objects || 25; - this.maxLevels = max_levels || 5; - this.level = level || 0; - this.bounds = bounds; - this.objects = []; - this.branches = []; - } - split() { - let nextLevel = this.level + 1; - let subWidth = this.bounds.width / 2; - let subHeight = this.bounds.height / 2; - let x = this.bounds.x; - let y = this.bounds.y; - this.branches.push(new QuadTree({ x: x + subWidth, y: y , width: subWidth, height: subHeight }, this.maxObjects, this.maxLevels, nextLevel)); - this.branches.push(new QuadTree({ x: x , y: y , width: subWidth, height: subHeight }, this.maxObjects, this.maxLevels, nextLevel)); - this.branches.push(new QuadTree({ x: x , y: y + subHeight, width: subWidth, height: subHeight }, this.maxObjects, this.maxLevels, nextLevel)); - this.branches.push(new QuadTree({ x: x + subWidth, y: y + subHeight, width: subWidth, height: subHeight }, this.maxObjects, this.maxLevels, nextLevel)); - } - getBranches(object) { - let output = []; - let midY = this.bounds.x + (this.bounds.width / 2); - let midX = this.bounds.y + (this.bounds.height / 2); - let north = object.y < midX; - let west = object.x < midY; - let east = object.x + object.size > midY; - let south = object.y + object.size > midX; - if (north && east) output.push(0); - if (west && north) output.push(1); - if (west && south) output.push(2); - if (east && south) output.push(3); - return output; - } - insert(object) { - let i = 0; - let cells; - if (this.branches.length) { - cells = this.getBranches(object); - for (i = 0; i < cells.length; i++) this.branches[cells[i]].insert(object); - return; - } - this.objects.push(object); - if (this.objects.length > this.maxObjects && this.level < this.maxLevels) { - if (!this.branches.length) this.split(); - for (i = 0; i < this.objects.length; i++) { - cells = this.getBranches(this.objects[i]); - for (let j = 0; j < cells.length; j++) this.branches[cells[j]].insert(this.objects[i]); - } - this.objects = []; - } - } - retrieve(object) { - let cells = this.getBranches(object); - let output = this.objects; - if (this.branches.length) - for (let i = 0; i < cells.length; i++) output = output.concat(this.branches[cells[i]].retrieve(object)); - output = output.filter((item, index) => output.indexOf(item) >= index); - return output; - } - hitDetection(object, other) { - return Math.sqrt(Math.pow(other.x - object.x, 2) + Math.pow(other.y - object.y, 2)) < (object.size + other.size); - } - queryForCollisionPairs(object) { - let closeBy = this.retrieve(object); - let pairs = []; - for (let i = 0; i < closeBy.length; i++) { - let other = closeBy[i]; - let hit = this.hitDetection(object, other); - let pair = object.size > other.size ? [object.id, other.id] : [other.id, object.id]; - if (hit && !pairs.includes(pair) && object.id !== other.id) pairs.push(pair); - } - return pairs; - } - clear() { - this.objects = []; - if (this.branches.length) { - for (let i = 0; i < this.branches.length; i++) this.branches[i].clear(); - this.branches = []; - } - } -} - -module.exports = { CollisionGrid, QuadTree }; \ No newline at end of file diff --git a/server/modules/setup/mockups.js b/server/modules/setup/mockups.js index 536c17c63..b8a500c87 100644 --- a/server/modules/setup/mockups.js +++ b/server/modules/setup/mockups.js @@ -239,9 +239,6 @@ for (let k in Class) { } } -// Remove them -purgeEntities(); - let mockupsLoadEndTime = performance.now(); console.log("Finished compiling " + mockupData.length + " classes into mockups."); console.log("Mockups generated in " + util.rounder(mockupsLoadEndTime - mockupsLoadStartTime, 3) + " milliseconds.\n"); diff --git a/server/modules/setup/room.js b/server/modules/setup/room.js index e729df0de..90bb68590 100644 --- a/server/modules/setup/room.js +++ b/server/modules/setup/room.js @@ -17,7 +17,7 @@ for (let filename of Config.ROOM_SETUP) { global.room = { lastCycle: undefined, - cycleSpeed: 1000 / 30, + cycleSpeed: 1000 / 40, regenerateTick: Config.REGENERATE_TICK, setup: importedRoom, xgrid: importedRoom[0].length, @@ -63,7 +63,7 @@ room.random = function() { }; }; room.getAt = location => { - if (!room.isInRoom(location)) return undefined; + if (!room.isInRoom(location)) return null; let a = Math.floor(location.y / room.tileWidth); let b = Math.floor(location.x / room.tileHeight); return room.setup[a][b]; @@ -102,27 +102,4 @@ for (let y in room.setup) { let tile = room.setup[y][x] = new TileEntity(room.setup[y][x], { x, y }); tile.init(tile); } -} - -function roomLoop() { - for (let i = 0; i < entities.length; i++) { - let entity = entities[i], - tile = room.getAt(entity); - if (tile) tile.entities.push(entity); - } - - for (let y = 0; y < room.setup.length; y++) { - for (let x = 0; x < room.setup[y].length; x++) { - let tile = room.setup[y][x]; - tile.tick(tile); - tile.entities = []; - } - } - - if (room.sendColorsToClient) { - room.sendColorsToClient = false; - sockets.broadcastRoom(); - } -} - -module.exports = { roomLoop }; \ No newline at end of file +} \ No newline at end of file diff --git a/server/modules/setup/tiles/portal.js b/server/modules/setup/tiles/portal.js index 42cb17ab3..6b6afd2cf 100644 --- a/server/modules/setup/tiles/portal.js +++ b/server/modules/setup/tiles/portal.js @@ -52,7 +52,7 @@ portal = new Tile({ entity.protect() //also don't forget to bring her kids along the ride - for (let o of entities) { + for (let o of entities.values()) { if (o.id !== entity.id && o.master.master.id === entity.id && (o.type === "drone" || o.type === "minion" || o.type === "satellite")) { o.velocity.x += entity.velocity.x; o.velocity.y += entity.velocity.y;