diff --git a/client/input.js b/client/input.js
index 9290a92f..9df3e897 100644
--- a/client/input.js
+++ b/client/input.js
@@ -71,6 +71,7 @@ window.setupInput = () => {
}
window.onkeydown = e => {
+ if (e.repeat) return;
window.input.flushInputHooks();
if(e.keyCode >= 112 && e.keyCode <= 130 && e.keyCode !== 113) return;
window.input.keyDown(e.keyCode);
diff --git a/client/loader.js b/client/loader.js
index ef71be2e..f6f000e0 100644
--- a/client/loader.js
+++ b/client/loader.js
@@ -257,6 +257,9 @@ Module.executeCommand = execCtx => {
if(!Module.commandDefinitions.find(({ id }) => id === tokens[0])) {
throw `${Module.executionCallbackMap[tokens]} for command ${cmd} is an invalid callback`;
}
+
+ if (Game.socket.readyState !== WebSocket.OPEN) return;
+
return Game.socket.send(new Uint8Array([
6,
...Encoder.encode(tokens[0]), 0,
diff --git a/package-lock.json b/package-lock.json
index ea810648..4f4b9f71 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1174,9 +1174,9 @@
}
},
"node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
diff --git a/package.json b/package.json
index 81372f5b..8ad8ca1f 100644
--- a/package.json
+++ b/package.json
@@ -13,11 +13,11 @@
],
"license": "AGPL-3.0-only",
"scripts": {
- "build": "npx tsup",
- "dev": "npx tsup --watch --onSuccess \"node lib/index.js\"",
- "check": "npx tsc --noEmit",
+ "check": "tsc --noEmit",
"start": "node index",
+ "build": "npm run check && tsup",
"server": "npm run build && npm run start",
+ "dev": "tsup --watch --onSuccess \"npm run check && npm run start\"",
"docker:build": "docker build --tag diepcustom .",
"docker:start": "docker run --pull never --rm --publish ${PORT:-8080}:8080 --env PORT=8080 --env DEV_PASSWORD_HASH --env SERVER_INFO --env NODE_ENV --init --interactive --tty diepcustom",
"docker": "npm run docker:build && npm run docker:start"
diff --git a/src/Client.ts b/src/Client.ts
index ecb3abf6..237da400 100644
--- a/src/Client.ts
+++ b/src/Client.ts
@@ -258,8 +258,7 @@ export default class Client {
this.inputs.mouse.y = util.constrain(mouseY, minY, maxY);
const player = camera.cameraData.values.player;
- if (!Entity.exists(player) || !(player instanceof TankBody)) return;
-
+ if (!Entity.exists(player) || !TankBody.isTank(player)) return;
// No AI
if (this.inputs.isPossessing && this.accessLevel !== config.AccessLevel.FullAccess) return;
@@ -346,7 +345,7 @@ export default class Client {
if (camera.cameraData.statsAvailable <= 0) return;
const player = camera.cameraData.values.player;
- if (!Entity.exists(player) || !(player instanceof TankBody)) return;
+ if (!TankBody.isTank(player)) return;
const definition = getTankById(player.currentTank);
if (!definition || !definition.stats.length) return;
@@ -366,7 +365,7 @@ export default class Client {
}
case ServerBound.TankUpgrade: {
const player = camera.cameraData.values.player;
- if (!Entity.exists(player) || !(player instanceof TankBody)) return;
+ if (!TankBody.isTank(player)) return;
const definition = getTankById(player.currentTank);
const tankId: Tank = r.vi() ^ TANK_XOR;
@@ -460,7 +459,7 @@ export default class Client {
ai.state = AIState.possessed;
// Silly workaround to change color of player when needed
- if (this.camera?.cameraData.values.player instanceof ObjectEntity) {
+ if (ObjectEntity.isObject(this.camera?.cameraData.values.player)) {
const color = this.camera.cameraData.values.player.styleData.values.color;
this.camera.cameraData.values.player.styleData.values.color = -1 as Color;
this.camera.cameraData.values.player.styleData.color = color;
@@ -480,7 +479,7 @@ export default class Client {
this.camera.cameraData.player = ai.owner;
this.camera.cameraData.movementSpeed = ai.movementSpeed;
- if (ai.owner instanceof TankBody) {
+ if (TankBody.isTank(ai.owner)) {
// If its a TankBody, set the stats, level, and tank to that of the TankBody
this.camera.cameraData.tank = ai.owner.cameraEntity.cameraData.values.tank;
this.camera.setLevel(ai.owner.cameraEntity.cameraData.values.level);
@@ -490,13 +489,13 @@ export default class Client {
for (let i = 0; i < StatCount; ++i) this.camera.cameraData.statNames[i as Stat] = ai.owner.cameraEntity.cameraData.statNames.values[i];
this.camera.cameraData.FOV = ai.owner.cameraEntity.cameraData.FOV;
- } else if (ai.owner instanceof AbstractBoss) {
+ } else if (AbstractBoss.isBoss(ai.owner)) {
this.camera.setLevel(75);
this.camera.cameraData.FOV = 0.35;
} else {
this.camera.setLevel(30);
}
-
+
this.camera.cameraData.statsAvailable = 0;
this.camera.cameraData.score = 0;
this.camera.entityState = EntityStateFlags.needsCreate | EntityStateFlags.needsDelete;
@@ -553,8 +552,6 @@ export default class Client {
camera.spectatee = null;
this.inputs.isPossessing = false;
this.inputs.movement.magnitude = 0;
-
- if (camera.cameraData.values.flags & CameraFlags.gameWaitingStart) camera.cameraData.values.flags &= ~CameraFlags.gameWaitingStart;
}
public tick(tick: number) {
diff --git a/src/Const/Achievements.json b/src/Const/Achievements.json
new file mode 100644
index 00000000..5f2f4c30
--- /dev/null
+++ b/src/Const/Achievements.json
@@ -0,0 +1,632 @@
+[
+ {
+ "name": "A moment to cherish forever",
+ "desc": "Destroy your first tank",
+ "conds": [
+ {
+ "event": "kill",
+ "tags": {
+ "victim.isTank": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Git gud",
+ "desc": "Destroy 10 tanks",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "threshold": 10,
+ "tags": {
+ "victim.isTank": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Gitting gud",
+ "desc": "Destroy 100 tanks",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "threshold": 100,
+ "tags": {
+ "victim.isTank": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Got gud",
+ "desc": "Destroy 1000 tanks",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "threshold": 1000,
+ "tags": {
+ "victim.isTank": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Starting out",
+ "desc": "Destroy something",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "threshold": 1
+ }
+ ]
+ },
+ {
+ "name": "Getting good",
+ "desc": "Destroy 500 things",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "threshold": 500
+ }
+ ]
+ },
+ {
+ "name": "Destroy EVERYTHING",
+ "desc": "Destroy 10000 things",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "threshold": 10000
+ }
+ ]
+ },
+ {
+ "name": "Gotta start somewhere",
+ "desc": "Destroy 10 squares",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "tags": {
+ "victim.arenaMobID": "square"
+ },
+ "threshold": 10
+ }
+ ]
+ },
+ {
+ "name": "Square hater",
+ "desc": "Destroy 500 squares",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "tags": {
+ "victim.arenaMobID": "square"
+ },
+ "threshold": 500
+ }
+ ]
+ },
+ {
+ "name": "These squares gotta go",
+ "desc": "Destroy 10000 squares",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "tags": {
+ "victim.arenaMobID": "square"
+ },
+ "threshold": 10000
+ }
+ ]
+ },
+ {
+ "name": "It hurts when I do this",
+ "desc": "Ram into something",
+ "conds": [
+ {
+ "event": "kill",
+ "tags": {
+ "weapon.isTank": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Who needs bullets anyway",
+ "desc": "Ram into 100 things",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "tags": {
+ "weapon.isTank": true
+ },
+ "threshold": 100
+ }
+ ]
+ },
+ {
+ "name": "Look mom, no cannons!",
+ "desc": "Ram into 3000 things",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "tags": {
+ "weapon.isTank": true
+ },
+ "threshold": 3000
+ }
+ ]
+ },
+ {
+ "name": "They ain't a real tank",
+ "desc": "Destroy 100 factory drones",
+ "conds": [
+ {
+ "type": "counter",
+ "event": "kill",
+ "tags": {
+ "victim.arenaMobID": "factoryDrone"
+ },
+ "threshold": 100
+ }
+ ]
+ },
+ {
+ "name": "2fast4u",
+ "desc": "Upgrade Movement Speed to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 0,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Ratatatatatatatata",
+ "desc": "Upgrade Reload to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 1,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "More dangerous than it looks",
+ "desc": "Upgrade Bullet Damage to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 2,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "There's no stopping it!",
+ "desc": "Upgrade Bullet Penetration to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 3,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Mach 4",
+ "desc": "Upgrade Bullet Speed to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 4,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Don't touch me",
+ "desc": "Upgrade Body Damage to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 5,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Indestructible",
+ "desc": "Upgrade Max Health to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 6,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Self-repairing",
+ "desc": "Upgrade Health Regen to its maximum value",
+ "conds": [
+ {
+ "event": "statUpgraded",
+ "tags": {
+ "id": 7,
+ "isMaxLevel": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Fire power",
+ "desc": "Upgrade to Twin",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 1
+ }
+ }
+ ]
+ },
+ {
+ "name": "Eat those bullets",
+ "desc": "Upgrade to Machine Gun",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 7
+ }
+ }
+ ]
+ },
+ {
+ "name": "Snipin'",
+ "desc": "Upgrade to Sniper",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 6
+ }
+ }
+ ]
+ },
+ {
+ "name": "Ain't no one sneaking up on ME",
+ "desc": "Upgrade to Flank Guard",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 8
+ }
+ }
+ ]
+ },
+ {
+ "name": "Three at the same time",
+ "desc": "Upgrade to Triple Shot",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 3
+ }
+ }
+ ]
+ },
+ {
+ "name": "I've got places to be",
+ "desc": "Upgrade to Tri-Angle",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 9
+ }
+ }
+ ]
+ },
+ {
+ "name": "BOOM, you're dead",
+ "desc": "Upgrade to Destroyer",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 10
+ }
+ }
+ ]
+ },
+ {
+ "name": "Drones are love",
+ "desc": "Upgrade to Overseer",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 11
+ }
+ }
+ ]
+ },
+ {
+ "name": "C + E",
+ "desc": "Upgrade to Quad Tank",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 4
+ }
+ }
+ ]
+ },
+ {
+ "name": "Now you really ain't sneaking up on me",
+ "desc": "Upgrade to Twin Flank",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 13
+ }
+ }
+ ]
+ },
+ {
+ "name": "Insert uncreative achievement name here",
+ "desc": "Upgrade to Assassin",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 15
+ }
+ }
+ ]
+ },
+ {
+ "name": "Huntin'",
+ "desc": "Upgrade to Hunter",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 19
+ }
+ }
+ ]
+ },
+ {
+ "name": "Eat those pellets!",
+ "desc": "Upgrade to Gunner",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 20
+ }
+ }
+ ]
+ },
+ {
+ "name": "BUILD A WALL",
+ "desc": "Upgrade to Trapper",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 31
+ }
+ }
+ ]
+ },
+ {
+ "name": "Can't bother using both hands to play?",
+ "desc": "Upgrade to Auto 3",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 41
+ }
+ }
+ ]
+ },
+ {
+ "name": "Where did my cannon go?",
+ "desc": "Upgrade to Smasher",
+ "conds": [
+ {
+ "event": "classChange",
+ "tags": {
+ "class": 36
+ }
+ }
+ ]
+ },
+ {
+ "name": "Try some other classes too",
+ "desc": "Upgrade to Sniper 100 times",
+ "conds": [
+ {
+ "event": "classChange",
+ "type": "counter",
+ "threshold": 100,
+ "tags": {
+ "class": 6
+ }
+ }
+ ]
+ },
+ {
+ "name": "Drones are life",
+ "desc": "Upgrade to Overseer 100 times",
+ "conds": [
+ {
+ "event": "classChange",
+ "type": "counter",
+ "threshold": 100,
+ "tags": {
+ "class": 11
+ }
+ }
+ ]
+ },
+ {
+ "name": "That was tough",
+ "desc": "Kill a boss",
+ "conds": [
+ {
+ "event": "kill",
+ "tags": {
+ "victim.isBoss": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Boss Hunter",
+ "desc": "Kill 10 bosses",
+ "conds": [
+ {
+ "event": "kill",
+ "type": "counter",
+ "threshold": 10,
+ "tags": {
+ "victim.isBoss": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "Eh, you're trying",
+ "desc": "Reach 1k points",
+ "conds": [
+ {
+ "event": "score",
+ "tags": {
+ "total": ">=1000"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Starting to git gud",
+ "desc": "Reach 10k points",
+ "conds": [
+ {
+ "event": "score",
+ "tags": {
+ "total": ">=10000"
+ }
+ }
+ ]
+ },
+ {
+ "name": "You aren't that bad at this",
+ "desc": "Reach 100k points",
+ "conds": [
+ {
+ "event": "score",
+ "tags": {
+ "total": ">=100000"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Okay you're pretty good",
+ "desc": "Reach 1m points",
+ "conds": [
+ {
+ "event": "score",
+ "tags": {
+ "total": ">=1000000"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Jackpot!",
+ "desc": "Get 20k points in a single kill",
+ "conds": [
+ {
+ "event": "score",
+ "tags": {
+ "delta": ">=20000"
+ }
+ }
+ ]
+ },
+ {
+ "name": "LAAAAAAAAAAAAG",
+ "desc": "Play with over 1000 ms of latency",
+ "conds": [
+ {
+ "event": "latency",
+ "tags": {
+ "value": ">=1000"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Shiny!",
+ "desc": "???",
+ "conds": [
+ {
+ "event": "kill",
+ "tags": {
+ "victim.isShiny": true
+ }
+ }
+ ]
+ },
+ {
+ "name": "There are other classes?",
+ "desc": "Get to level 45 as a basic tank",
+ "conds": [
+ {
+ "event": "levelUp",
+ "tags": {
+ "level": 45,
+ "class": 0
+ }
+ }
+ ]
+ }
+]
diff --git a/src/Const/Commands.ts b/src/Const/Commands.ts
index 3f4542ef..a153b08f 100644
--- a/src/Const/Commands.ts
+++ b/src/Const/Commands.ts
@@ -1,3 +1,21 @@
+/*
+ DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
+ Copyright (C) 2022 ABCxFF (github.com/ABCxFF)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see
+*/
+
import Client from "../Client"
import { AccessLevel, maxPlayerLevel } from "../config";
import AbstractBoss from "../Entity/Boss/AbstractBoss";
@@ -169,14 +187,14 @@ export const commandCallbacks = {
game_set_tank: (client: Client, tankNameArg: string) => {
const tankDef = getTankByName(tankNameArg);
const player = client.camera?.cameraData.player;
- if (!tankDef || !Entity.exists(player) || !(player instanceof TankBody)) return;
+ if (!tankDef || !Entity.exists(player) || !TankBody.isTank(player)) return;
if (tankDef.flags.devOnly && client.accessLevel !== AccessLevel.FullAccess) return;
player.setTank(tankDef.id);
},
game_set_level: (client: Client, levelArg: string) => {
const level = parseInt(levelArg);
const player = client.camera?.cameraData.player;
- if (isNaN(level) || !Entity.exists(player) || !(player instanceof TankBody)) return;
+ if (isNaN(level) || !Entity.exists(player) || !TankBody.isTank(player)) return;
const finalLevel = client.accessLevel == AccessLevel.FullAccess ? level : Math.min(maxPlayerLevel, level);
client.camera?.setLevel(finalLevel);
},
@@ -184,7 +202,7 @@ export const commandCallbacks = {
const score = parseInt(scoreArg);
const camera = client.camera?.cameraData;
const player = client.camera?.cameraData.player;
- if (isNaN(score) || score > Number.MAX_SAFE_INTEGER || score < Number.MIN_SAFE_INTEGER || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
+ if (isNaN(score) || score > Number.MAX_SAFE_INTEGER || score < Number.MIN_SAFE_INTEGER || !Entity.exists(player) || !TankBody.isTank(player) || !camera) return;
camera.score = score;
},
game_set_stat_max: (client: Client, statIdArg: string, statMaxArg: string) => {
@@ -192,7 +210,7 @@ export const commandCallbacks = {
const statMax = parseInt(statMaxArg);
const camera = client.camera?.cameraData;
const player = client.camera?.cameraData.player;
- if (statId < 0 || statId >= StatCount || isNaN(statId) || isNaN(statMax) || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
+ if (statId < 0 || statId >= StatCount || isNaN(statId) || isNaN(statMax) || !Entity.exists(player) || !TankBody.isTank(player) || !camera) return;
const clampedStatMax = Math.max(statMax, 0);
camera.statLimits[statId as Stat] = clampedStatMax;
camera.statLevels[statId as Stat] = Math.min(camera.statLevels[statId as Stat], clampedStatMax);
@@ -202,19 +220,19 @@ export const commandCallbacks = {
const statPoints = parseInt(statPointsArg);
const camera = client.camera?.cameraData;
const player = client.camera?.cameraData.player;
- if (statId < 0 || statId >= StatCount || isNaN(statId) || isNaN(statPoints) || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
+ if (statId < 0 || statId >= StatCount || isNaN(statId) || isNaN(statPoints) || !Entity.exists(player) || !TankBody.isTank(player) || !camera) return;
camera.statLevels[statId as Stat] = statPoints;
},
game_add_upgrade_points: (client: Client, pointsArg: string) => {
const points = parseInt(pointsArg);
const camera = client.camera?.cameraData;
const player = client.camera?.cameraData.player;
- if (isNaN(points) || points > Number.MAX_SAFE_INTEGER || points < Number.MIN_SAFE_INTEGER || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
+ if (isNaN(points) || points > Number.MAX_SAFE_INTEGER || points < Number.MIN_SAFE_INTEGER || !Entity.exists(player) || !TankBody.isTank(player) || !camera) return;
camera.statsAvailable += points;
},
game_teleport: (client: Client, xArg: string, yArg: string) => {
const player = client.camera?.cameraData.player;
- if (!Entity.exists(player) || !(player instanceof ObjectEntity)) return;
+ if (!Entity.exists(player) || !ObjectEntity.isObject(player)) return;
const x = xArg.match(RELATIVE_POS_REGEX) ? player.positionData.x + parseInt(xArg.slice(1) || "0", 10) : parseInt(xArg, 10);
const y = yArg.match(RELATIVE_POS_REGEX) ? player.positionData.y + parseInt(yArg.slice(1) || "0", 10) : parseInt(yArg, 10);
if (isNaN(x) || isNaN(y)) return;
@@ -244,7 +262,7 @@ export const commandCallbacks = {
},
game_godmode: (client: Client, activeArg?: string) => {
const player = client.camera?.cameraData.player;
- if (!Entity.exists(player) || !(player instanceof TankBody)) return;
+ if (!Entity.exists(player) || !TankBody.isTank(player)) return;
switch (activeArg) {
case "on":
@@ -281,7 +299,7 @@ export const commandCallbacks = {
let y = parseInt(yArg || "0", 10);
const player = client.camera?.cameraData.player;
- if (Entity.exists(player) && player instanceof ObjectEntity) {
+ if (Entity.exists(player) && ObjectEntity.isObject(player)) {
if (xArg && xArg.match(RELATIVE_POS_REGEX)) {
x = player.positionData.x + parseInt(xArg.slice(1) || "0", 10);
}
@@ -325,7 +343,7 @@ export const commandCallbacks = {
const entity = game.entities.inner[id];
if (
Entity.exists(entity) &&
- entity instanceof LivingEntity &&
+ LivingEntity.isLive(entity) &&
entity !== client.camera?.cameraData.player &&
!(entity.physicsData.values.flags & PhysicsFlags.showsOnMap)
) entity.destroy();
diff --git a/src/Const/Enums.ts b/src/Const/Enums.ts
index 8383d0f1..09502f9e 100644
--- a/src/Const/Enums.ts
+++ b/src/Const/Enums.ts
@@ -281,6 +281,17 @@ export const enum NameFlags {
highlightedName = 1 << 1
}
+/**
+ * Entity type flags.
+ */
+export const enum EntityTags {
+ isShape = 1 << 0,
+ isTank = 1 << 1,
+ isDominator = 1 << 2,
+ isBoss = 1 << 3,
+ isShiny = 1 << 4,
+}
+
/**
* Credits to CX for discovering this.
* This is not fully correct but it works up to the decimal (float rounding likely causes this).
diff --git a/src/Entity/Boss/AbstractBoss.ts b/src/Entity/Boss/AbstractBoss.ts
index 1390f1bf..7bb3b5c1 100644
--- a/src/Entity/Boss/AbstractBoss.ts
+++ b/src/Entity/Boss/AbstractBoss.ts
@@ -17,14 +17,16 @@
*/
import GameServer from "../../Game";
+import ObjectEntity from "../Object";
+import LivingEntity from "../Live";
import Barrel from "../Tank/Barrel";
+import TankBody from "../Tank/TankBody";
-import { ClientBound, Color, PositionFlags, NameFlags } from "../../Const/Enums";
+import { ClientBound, Color, PositionFlags, NameFlags, EntityTags } from "../../Const/Enums";
import { VectorAbstract } from "../../Physics/Vector";
import { AI, AIState, Inputs } from "../AI";
import { NameGroup } from "../../Native/FieldGroups";
-import LivingEntity from "../Live";
-import TankBody from "../Tank/TankBody";
+import { Entity } from "../../Native/Entity";
import { CameraEntity } from "../../Native/Camera";
@@ -137,6 +139,14 @@ export default class AbstractBoss extends LivingEntity {
this.reloadTime = 15 * Math.pow(0.914, 7);
this.healthData.values.health = this.healthData.values.maxHealth = 3000;
+
+ this.entityTags |= EntityTags.isBoss;
+ }
+
+ public static isBoss(entity: Entity | null | undefined): entity is AbstractBoss {
+ if (!ObjectEntity.isObject(entity)) return false;
+
+ return !!(entity.entityTags & EntityTags.isBoss);
}
public get sizeFactor() {
@@ -153,13 +163,10 @@ export default class AbstractBoss extends LivingEntity {
* Will set game.arena.boss to null, so that the next boss can spawn
*/
public onDeath(killer: LivingEntity) {
- // Reset arena.boss
- if (this.game.arena.boss === this) this.game.arena.boss = null;
-
let killerName: string;
- if ((killer.nameData && killer.nameData.values.name && !(killer.nameData.values.flags & NameFlags.hiddenName))) {
- killerName = killer.nameData.values.name; // in Diep.io, it should only show the name in notification if it is visible above the killer entity for whatever reason
+ if (TankBody.isTank(killer) || AbstractBoss.isBoss(killer)) {
+ killerName = killer.nameData.values.name;
} else {
killerName = "an unnamed tank";
}
diff --git a/src/Entity/Boss/Defender.ts b/src/Entity/Boss/Defender.ts
index f846d298..65fca6b0 100644
--- a/src/Entity/Boss/Defender.ts
+++ b/src/Entity/Boss/Defender.ts
@@ -112,8 +112,8 @@ export default class Defender extends AbstractBoss {
const angle = base.ai.inputs.mouse.angle = PI2 * (i / count);
- base.positionData.values.y = this.physicsData.values.size * Math.sin(angle) * offset
- base.positionData.values.x = this.physicsData.values.size * Math.cos(angle) * offset
+ base.positionData.values.y = this.physicsData.values.size * Math.sin(angle) * offset;
+ base.positionData.values.x = this.physicsData.values.size * Math.cos(angle) * offset;
base.physicsData.values.flags |= PositionFlags.absoluteRotation;
diff --git a/src/Entity/Boss/FallenBooster.ts b/src/Entity/Boss/FallenBooster.ts
index 8d81ab9a..86baa06a 100644
--- a/src/Entity/Boss/FallenBooster.ts
+++ b/src/Entity/Boss/FallenBooster.ts
@@ -34,7 +34,7 @@ export default class FallenBooster extends AbstractBoss {
public constructor(game: GameServer) {
super(game);
- this.nameData.values.name = 'Fallen Booster';
+ this.nameData.values.name = "Fallen Booster";
for (const barrelDefinition of TankDefinitions[Tank.Booster].barrels) {
const def = Object.assign({}, barrelDefinition, {});
diff --git a/src/Entity/Boss/FallenOverlord.ts b/src/Entity/Boss/FallenOverlord.ts
index f8ad78c8..7589f087 100644
--- a/src/Entity/Boss/FallenOverlord.ts
+++ b/src/Entity/Boss/FallenOverlord.ts
@@ -31,7 +31,7 @@ export default class FallenOverlord extends AbstractBoss {
public constructor(game: GameServer) {
super(game);
- this.nameData.values.name = 'Fallen Overlord';
+ this.nameData.values.name = "Fallen Overlord";
for (const barrelDefinition of TankDefinitions[Tank.Overlord].barrels) {
diff --git a/src/Entity/Boss/Guardian.ts b/src/Entity/Boss/Guardian.ts
index 7a04794d..a4f94cf9 100644
--- a/src/Entity/Boss/Guardian.ts
+++ b/src/Entity/Boss/Guardian.ts
@@ -59,7 +59,7 @@ export default class Guardian extends AbstractBoss {
public constructor(game: GameServer) {
super(game);
- this.nameData.values.name = 'Guardian';
+ this.nameData.values.name = "Guardian";
this.altName = 'Guardian of the Pentagons';
this.styleData.values.color = Color.EnemyCrasher;
this.relationsData.values.team = this.game.arena;
diff --git a/src/Entity/Boss/Summoner.ts b/src/Entity/Boss/Summoner.ts
index af88ba8c..56332f64 100644
--- a/src/Entity/Boss/Summoner.ts
+++ b/src/Entity/Boss/Summoner.ts
@@ -65,7 +65,7 @@ export default class Summoner extends AbstractBoss {
public constructor(game: GameServer) {
super(game);
- this.nameData.values.name = 'Summoner';
+ this.nameData.values.name = "Summoner";
this.styleData.values.color = Color.EnemySquare;
this.relationsData.values.team = this.game.arena;
this.physicsData.values.size = SUMMONER_SIZE * Math.SQRT1_2;
diff --git a/src/Entity/Live.ts b/src/Entity/Live.ts
index 33af33d7..845144e5 100644
--- a/src/Entity/Live.ts
+++ b/src/Entity/Live.ts
@@ -18,7 +18,8 @@
import ObjectEntity from "./Object";
-import { StyleFlags } from "../Const/Enums";
+import { Entity } from "../Native/Entity";
+import { StyleFlags, EntityTags } from "../Const/Enums";
import { HealthGroup } from "../Native/FieldGroups";
/**
@@ -57,6 +58,12 @@ export default class LivingEntity extends ObjectEntity {
super.destroy(animate);
}
+
+ public static isLive(entity: Entity | null | undefined): entity is LivingEntity {
+ if (!ObjectEntity.isObject(entity)) return false;
+
+ return !!entity.healthData;
+ }
/** Applies damage to two entity after colliding with eachother. */
public static handleCollision(entity1: LivingEntity, entity2: LivingEntity) {
@@ -104,11 +111,11 @@ export default class LivingEntity extends ObjectEntity {
this.healthData.health = 0;
let killer: ObjectEntity = source;
- while (killer.relationsData.values.owner instanceof ObjectEntity && killer.relationsData.values.owner.hash !== 0) {
+ while (ObjectEntity.isObject(killer.relationsData.values.owner) && killer.relationsData.values.owner.hash !== 0) {
killer = killer.relationsData.values.owner;
}
- if (killer instanceof LivingEntity) {
+ if (LivingEntity.isLive(killer)) {
this.onDeath(killer);
}
@@ -129,7 +136,7 @@ export default class LivingEntity extends ObjectEntity {
if (this.healthData.values.health <= 0) {
this.destroy(true);
- this.damagedEntities = [];
+ this.damagedEntities.length = 0;
return;
}
@@ -147,7 +154,7 @@ export default class LivingEntity extends ObjectEntity {
this.healthData.health = this.healthData.values.maxHealth;
}
- this.damagedEntities = [];
+ this.damagedEntities.length = 0;
}
public tick(tick: number) {
diff --git a/src/Entity/Misc/Boss/FallenAC.ts b/src/Entity/Misc/Boss/FallenAC.ts
index 168d1df7..43634641 100644
--- a/src/Entity/Misc/Boss/FallenAC.ts
+++ b/src/Entity/Misc/Boss/FallenAC.ts
@@ -31,7 +31,7 @@ export default class FallenAC extends AbstractBoss {
public constructor(game: GameServer) {
super(game);
- this.nameData.values.name = 'Fallen Arena Closer';
+ this.nameData.values.name = "Fallen Arena Closer";
this.movementSpeed = 20;
for (const barrelDefinition of TankDefinitions[Tank.ArenaCloser].barrels) {
this.barrels.push(new Barrel(this, barrelDefinition));
diff --git a/src/Entity/Misc/Boss/FallenMegaTrapper.ts b/src/Entity/Misc/Boss/FallenMegaTrapper.ts
index 258627e9..e0699456 100644
--- a/src/Entity/Misc/Boss/FallenMegaTrapper.ts
+++ b/src/Entity/Misc/Boss/FallenMegaTrapper.ts
@@ -25,7 +25,7 @@ import { Tank } from "../../../Const/Enums";
import { AIState } from "../../AI";
/**
- * Class which represents the boss "FallenBooster"
+ * Class which represents the boss "Fallen Mega Trapper"
*/
export default class FallenMegaTrapper extends AbstractBoss {
/** The speed to maintain during movement. */
@@ -34,10 +34,10 @@ export default class FallenMegaTrapper extends AbstractBoss {
public constructor(game: GameServer) {
super(game);
- this.nameData.values.name = 'Fallen Mega Trapper';
+ this.nameData.values.name = "Fallen Mega Trapper";
for (const barrelDefinition of TankDefinitions[Tank.MegaTrapper].barrels) {
- const def = Object.assign({}, barrelDefinition, {reload: 4});
+ const def = Object.assign({}, barrelDefinition, { reload: 4 });
def.bullet = Object.assign({}, def.bullet, { speed: 1.7, damage: 20, health: 20, });
this.barrels.push(new Barrel(this, def));
}
diff --git a/src/Entity/Misc/Boss/FallenSpike.ts b/src/Entity/Misc/Boss/FallenSpike.ts
index f5a0d4de..71b9251a 100644
--- a/src/Entity/Misc/Boss/FallenSpike.ts
+++ b/src/Entity/Misc/Boss/FallenSpike.ts
@@ -31,7 +31,7 @@ export default class FallenSpike extends AbstractBoss {
this.movementSpeed = 3.0;
- this.nameData.values.name = 'Fallen Spike';
+ this.nameData.values.name = "Fallen Spike";
// Sharp
this.damagePerTick *= 2;
diff --git a/src/Entity/Misc/Dominator.ts b/src/Entity/Misc/Dominator.ts
index 0e7afd73..1fcf709e 100644
--- a/src/Entity/Misc/Dominator.ts
+++ b/src/Entity/Misc/Dominator.ts
@@ -16,10 +16,12 @@
along with this program. If not, see
*/
-import { Color, ColorsHexCode, NameFlags, StyleFlags, Tank, ClientBound } from "../../Const/Enums";
+import { Color, ColorsHexCode, NameFlags, StyleFlags, EntityTags, Tank, ClientBound } from "../../Const/Enums";
import ArenaEntity from "../../Native/Arena";
import ClientCamera, { CameraEntity } from "../../Native/Camera";
+import { Entity } from "../../Native/Entity";
import { AI, AIState, Inputs } from "../AI";
+import ObjectEntity from "../Object";
import LivingEntity from "../Live";
import Bullet from "../Tank/Projectile/Bullet";
import TankBody from "../Tank/TankBody";
@@ -97,13 +99,23 @@ export default class Dominator extends TankBody {
this.styleData.values.flags ^= StyleFlags.isFlashing;
this.damageReduction = 1.0;
}
+
+ this.entityTags |= EntityTags.isDominator;
+ }
+
+ public static isDominator(entity: Entity | null | undefined): entity is Dominator {
+ if (!ObjectEntity.isObject(entity)) return false;
+
+ return !!(entity.entityTags & EntityTags.isDominator);
}
public onDeath(killer: LivingEntity) {
- if (this.relationsData.values.team === this.game.arena && killer.relationsData.values.team instanceof TeamEntity) {
- const killerTeam = killer.relationsData.values.team;
- this.relationsData.team = killerTeam || this.game.arena;
- this.styleData.color = this.relationsData.team.teamData?.teamColor || killer.styleData.values.color;
+ const killerTeam = killer.relationsData.values.team;
+
+ if (TeamEntity.isTeam(killerTeam) && this.relationsData.values.team === this.game.arena) { // Only proper teams should capture doms
+ // capture neutral dominator
+ this.relationsData.team = killerTeam;
+ this.styleData.color = killerTeam.teamData.values.teamColor
this.game.broadcast()
.u8(ClientBound.Notification)
.stringNT(`The ${this.prefix}${this.nameData.values.name} is now controlled by ${killerTeam.teamName}`)
diff --git a/src/Entity/Misc/MazeWall.ts b/src/Entity/Misc/MazeWall.ts
index 74716ff8..bd36b2d7 100644
--- a/src/Entity/Misc/MazeWall.ts
+++ b/src/Entity/Misc/MazeWall.ts
@@ -24,6 +24,25 @@ import { PhysicsFlags, Color } from "../../Const/Enums";
* Only used for maze walls and nothing else.
*/
export default class MazeWall extends ObjectEntity {
+ public static newFromBounds(
+ game: GameServer,
+ minX: number,
+ minY: number,
+ maxX: number,
+ maxY: number
+ ): MazeWall {
+ if (minX > maxX) [minX, maxX] = [maxX, minX];
+ if (minY > maxY) [minY, maxY] = [maxY, minY];
+
+ const width = maxX - minX;
+ const height = maxY - minY;
+
+ const centerX = (minX + maxX) / 2;
+ const centerY = (minY + maxY) / 2;
+
+ return new MazeWall(game, centerX, centerY, width, height);
+ }
+
public constructor(game: GameServer, x: number, y: number, width: number, height: number) {
super(game);
@@ -32,8 +51,8 @@ export default class MazeWall extends ObjectEntity {
this.positionData.values.x = x;
this.positionData.values.y = y;
- this.physicsData.values.width = width;
- this.physicsData.values.size = height;
+ this.physicsData.values.width = height;
+ this.physicsData.values.size = width;
this.physicsData.values.sides = 2;
this.physicsData.values.flags |= PhysicsFlags.isSolidWall;
this.physicsData.values.pushFactor = 2;
@@ -46,4 +65,6 @@ export default class MazeWall extends ObjectEntity {
}
public tick(tick: number) {} // No need to tick walls
+
+ public applyPhysics() {} // These never move, so no need to do this either
}
diff --git a/src/Entity/Misc/Mothership.ts b/src/Entity/Misc/Mothership.ts
index 8156c112..81519551 100644
--- a/src/Entity/Misc/Mothership.ts
+++ b/src/Entity/Misc/Mothership.ts
@@ -72,24 +72,24 @@ export default class Mothership extends TankBody {
const def = (this.definition = Object.assign({}, this.definition));
// 418 is what the normal health increase for stat/level would be, so we just subtract it and force it 7k
- def.maxHealth = 7008 - 418;
+ def.maxHealth = 7000 - 418;
}
public onDeath(killer: Live): void {
if ((this.game.arena.state >= ArenaState.OVER)) return; // Do not send a defeat notification if the game has been won already
const team = this.relationsData.values.team;
- const teamIsATeam = team instanceof TeamEntity;
+ const teamIsATeam = TeamEntity.isTeam(team);
const killerTeam = killer.relationsData.values.team;
- const killerTeamIsATeam = killerTeam instanceof TeamEntity;
+ const killerTeamIsATeam = TeamEntity.isTeam(killerTeam);
// UNCOMMENT TO ALLOW SOLO KILLS
// if (!killerTeamIsATeam) return;
this.game.broadcast()
.u8(ClientBound.Notification)
// If mothership has a team name, use it, otherwise just say has destroyed a mothership
- .stringNT(`${killerTeamIsATeam ? killerTeam.teamName : (killer.nameData?.values.name || "an unnamed tank")} has destroyed ${teamIsATeam ? team.teamName + "'s" : "a"} Mothership!`)
+ .stringNT(`${killerTeamIsATeam ? (killerTeam.teamName || "a mysterious group") : (killer.nameData?.values.name || "an unnamed tank")} has destroyed ${teamIsATeam ? team.teamName + "'s" : "a"} Mothership!`)
.u32(killerTeamIsATeam ? ColorsHexCode[killerTeam.teamData.values.teamColor] : 0x000000)
.float(-1)
.stringNT("").send();
diff --git a/src/Entity/Misc/TeamEntity.ts b/src/Entity/Misc/TeamEntity.ts
index 391a29d4..23f5e9bd 100644
--- a/src/Entity/Misc/TeamEntity.ts
+++ b/src/Entity/Misc/TeamEntity.ts
@@ -37,7 +37,7 @@ export const ColorsTeamName: { [K in Color]?: string } = {
[Color.EnemyTriangle]: "TRIANGLE",
[Color.EnemyPentagon]: "PENTAGON",
[Color.EnemyCrasher]: "CRASHER",
- [Color.Neutral]: "a mysterious group",
+ [Color.Neutral]: "NEUTRAL",
[Color.ScoreboardBar]: "SCOREBOARD",
[Color.Box]: "MAZE",
[Color.EnemyTank]: "ENEMY",
@@ -58,4 +58,10 @@ export class TeamEntity extends Entity implements TeamGroupEntity {
this.teamData.values.teamColor = color;
this.teamName = name;
}
+
+ public static isTeam(entity: Entity | null | undefined): entity is TeamEntity {
+ if (!entity) return false;
+
+ return !!entity.teamData;
+ }
}
diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts
index 498be88e..071a82ca 100644
--- a/src/Entity/Object.ts
+++ b/src/Entity/Object.ts
@@ -22,7 +22,7 @@ import Vector from "../Physics/Vector";
import { PhysicsGroup, PositionGroup, RelationsGroup, StyleGroup } from "../Native/FieldGroups";
import { Entity } from "../Native/Entity";
-import { PositionFlags, PhysicsFlags } from "../Const/Enums";
+import { PositionFlags, PhysicsFlags, EntityTags } from "../Const/Enums";
/**
* The animator for how entities delete (the opacity and size fade out).
@@ -70,10 +70,13 @@ class DeletionAnimation {
export default class ObjectEntity extends Entity {
/** Always existant relations field group. Present in all objects. */
public relationsData: RelationsGroup = new RelationsGroup(this);
+
/** Always existant physics field group. Present in all objects. */
public physicsData: PhysicsGroup = new PhysicsGroup(this);
+
/** Always existant position field group. Present in all objects. */
public positionData: PositionGroup = new PositionGroup(this);
+
/** Always existant style field group. Present in all objects. */
public styleData: StyleGroup = new StyleGroup(this);
@@ -92,6 +95,12 @@ export default class ObjectEntity extends Entity {
/** Used to determine the parent of all parents. */
public rootParent: ObjectEntity = this;
+ /** Entity tags. */
+ public entityTags: number = 0;
+
+ /** Entity type ID. */
+ public arenaMobID: string = ""
+
/** Velocity used for physics. */
public velocity = new Vector();
@@ -108,6 +117,12 @@ export default class ObjectEntity extends Entity {
this.styleData.zIndex = game.entities.zIndex++;
}
+
+ public static isObject(entity: Entity | null | undefined): entity is ObjectEntity {
+ if (!entity) return false;
+
+ return !!entity.physicsData;
+ }
/** Receives collision pairs from CollisionManager and applies kb */
public static handleCollision(objA: ObjectEntity, objB: ObjectEntity) {
@@ -222,11 +237,18 @@ export default class ObjectEntity extends Entity {
super.delete();
}
- /** @deprecated Applies acceleration to the object. */
+ /**
+ * DEPRECATED:
+ * Please switch to calling `addVelocity()` instead.
+ *
+ * > Applies acceleration to the object
+ * @deprecated
+ */
public addAcceleration(angle: number, acceleration: number) {
this.addVelocity(angle, acceleration);
}
+ /** Adds to the velocity of the object. */
public addVelocity(angle: number, magnitude: number) {
this.velocity.add(Vector.fromPolar(angle, magnitude));
}
@@ -284,20 +306,20 @@ export default class ObjectEntity extends Entity {
const relB = Math.sin(kbAngle + entity.positionData.values.angle) / entity.physicsData.values.width;
if (Math.abs(relA) <= Math.abs(relB)) {
if (relB < 0) {
- this.addAcceleration(Math.PI * 3 / 2, kbMagnitude);
+ this.addVelocity(Math.PI * 3 / 2, kbMagnitude);
} else {
- this.addAcceleration(Math.PI * 1 / 2, kbMagnitude);
+ this.addVelocity(Math.PI * 1 / 2, kbMagnitude);
}
} else {
if (relA < 0) {
- this.addAcceleration(Math.PI, kbMagnitude);
+ this.addVelocity(Math.PI, kbMagnitude);
} else {
- this.addAcceleration(0, kbMagnitude);
+ this.addVelocity(0, kbMagnitude);
}
}
}
} else {
- this.addAcceleration(kbAngle, kbMagnitude);
+ this.addVelocity(kbAngle, kbMagnitude);
}
}
@@ -323,7 +345,7 @@ export default class ObjectEntity extends Entity {
let par = 0;
let entity: ObjectEntity = this;
- while (entity.relationsData.values.parent instanceof ObjectEntity) {
+ while (ObjectEntity.isObject(entity.relationsData.values.parent)) {
if (!(entity.relationsData.values.parent.positionData.values.flags & PositionFlags.absoluteRotation)) pos.angle += entity.positionData.values.angle;
entity = entity.relationsData.values.parent;
px += entity.positionData.values.x;
@@ -346,24 +368,35 @@ export default class ObjectEntity extends Entity {
this.game.entities.globalEntities.push(this.id);
}
+ /** Keeps the object within the arena bounds. */
+ protected keepInArena() {
+ const arena = this.game.arena.arenaData;
+ const padding = this.game.arena.ARENA_PADDING;
+
+ if (this.positionData.values.x < arena.values.leftX - padding) {
+ this.positionData.x = arena.values.leftX - padding;
+ } else if (this.positionData.values.x > arena.values.rightX + padding) {
+ this.positionData.x = arena.values.rightX + padding;
+ }
+
+ if (this.positionData.values.y < arena.values.topY - padding) {
+ this.positionData.y = arena.values.topY - padding;
+ } else if (this.positionData.values.y > arena.values.bottomY + padding) {
+ this.positionData.y = arena.values.bottomY + padding;
+ }
+ }
+
public tick(tick: number) {
this.deletionAnimation?.tick();
for (let i = 0; i < this.children.length; ++i) this.children[i].tick(tick);
// Keep things in the arena
- if (!(this.physicsData.values.flags & PhysicsFlags.canEscapeArena) && this.isPhysical) {
- const arena = this.game.arena;
- xPos: {
- if (this.positionData.values.x < arena.arenaData.values.leftX - arena.ARENA_PADDING) this.positionData.x = arena.arenaData.values.leftX - arena.ARENA_PADDING;
- else if (this.positionData.values.x > arena.arenaData.values.rightX + arena.ARENA_PADDING) this.positionData.x = arena.arenaData.values.rightX + arena.ARENA_PADDING;
- else break xPos;
- }
- yPos: {
- if (this.positionData.values.y < arena.arenaData.values.topY - arena.ARENA_PADDING) this.positionData.y = arena.arenaData.values.topY - arena.ARENA_PADDING;
- else if (this.positionData.values.y > arena.arenaData.values.bottomY + arena.ARENA_PADDING) this.positionData.y = arena.arenaData.values.bottomY + arena.ARENA_PADDING;
- else break yPos;
- }
+ if (
+ this.isPhysical &&
+ !(this.physicsData.values.flags & PhysicsFlags.canEscapeArena)
+ ) {
+ this.keepInArena();
}
}
}
diff --git a/src/Entity/Shape/AbstractShape.ts b/src/Entity/Shape/AbstractShape.ts
index 727f5934..fc4e3502 100644
--- a/src/Entity/Shape/AbstractShape.ts
+++ b/src/Entity/Shape/AbstractShape.ts
@@ -17,9 +17,11 @@
*/
import GameServer from "../../Game";
+import ObjectEntity from "../Object";
import LivingEntity from "../Live";
-import { Color, PositionFlags, NameFlags } from "../../Const/Enums";
+import { Entity } from "../../Native/Entity";
+import { Color, PositionFlags, NameFlags, EntityTags } from "../../Const/Enums";
import { NameGroup } from "../../Native/FieldGroups";
import { AI } from "../AI";
import { normalizeAngle, PI2 } from "../../util";
@@ -74,6 +76,14 @@ export default class AbstractShape extends LivingEntity {
this.orbitAngle = this.positionData.values.angle = (Math.random() * PI2);
this.maxDamageMultiplier = 4.0;
+
+ this.entityTags |= EntityTags.isShape;
+ }
+
+ public static isShape(entity: Entity | null | undefined): entity is AbstractShape {
+ if (!ObjectEntity.isObject(entity)) return false;
+
+ return !!(entity.entityTags & EntityTags.isShape);
}
protected turnTo(angle: number) {
diff --git a/src/Entity/Shape/Crasher.ts b/src/Entity/Shape/Crasher.ts
index 24838228..55f119b1 100644
--- a/src/Entity/Shape/Crasher.ts
+++ b/src/Entity/Shape/Crasher.ts
@@ -59,6 +59,8 @@ export default class Crasher extends AbstractShape {
this.ai.viewRange = 2000;
this.ai.aimSpeed = (this.ai.movementSpeed = this.targettingSpeed);
this.ai['_findTargetInterval'] = tps;
+
+ this.arenaMobID = "crasher";
}
tick(tick: number) {
diff --git a/src/Entity/Shape/Pentagon.ts b/src/Entity/Shape/Pentagon.ts
index 1774b835..dc42d431 100644
--- a/src/Entity/Shape/Pentagon.ts
+++ b/src/Entity/Shape/Pentagon.ts
@@ -19,7 +19,8 @@
import GameServer from "../../Game";
import AbstractShape from "./AbstractShape";
-import { Color } from "../../Const/Enums";
+import { Color, EntityTags } from "../../Const/Enums";
+import { shinyChance } from "../../config";
/**
* Pentagon entity class.
@@ -32,7 +33,7 @@ export default class Pentagon extends AbstractShape {
protected static BASE_ORBIT = AbstractShape.BASE_ORBIT / 2;
protected static BASE_VELOCITY = AbstractShape.BASE_VELOCITY / 2;
- public constructor(game: GameServer, isAlpha=false, shiny=(Math.random() < 0.000001) && !isAlpha) {
+ public constructor(game: GameServer, isAlpha=false, shiny=(Math.random() < shinyChance) && !isAlpha) {
super(game);
this.nameData.values.name = isAlpha ? "Alpha Pentagon" : "Pentagon";
@@ -54,6 +55,9 @@ export default class Pentagon extends AbstractShape {
if (shiny) {
this.scoreReward *= 100;
this.healthData.values.health = this.healthData.values.maxHealth *= 10;
+ this.entityTags |= EntityTags.isShiny;
}
+
+ this.arenaMobID = this.isAlpha ? "alphaPentagon" : "pentagon";
}
-}
\ No newline at end of file
+}
diff --git a/src/Entity/Shape/Square.ts b/src/Entity/Shape/Square.ts
index 8b1667fe..11967bb0 100644
--- a/src/Entity/Shape/Square.ts
+++ b/src/Entity/Shape/Square.ts
@@ -19,11 +19,13 @@
import GameServer from "../../Game";
import AbstractShape from "./AbstractShape";
-import { Color } from "../../Const/Enums";
+import { Color, EntityTags } from "../../Const/Enums";
+import { shinyChance } from "../../config";
export default class Square extends AbstractShape {
- public constructor(game: GameServer, shiny=Math.random() < 0.000001) {
+ public constructor(game: GameServer, shiny=Math.random() < shinyChance) {
super(game);
+
this.nameData.values.name = "Square";
this.healthData.values.health = this.healthData.values.maxHealth = 10;
this.physicsData.values.size = 55 * Math.SQRT1_2;
@@ -37,6 +39,9 @@ export default class Square extends AbstractShape {
if (shiny) {
this.scoreReward *= 100;
this.healthData.values.health = this.healthData.values.maxHealth *= 10;
+ this.entityTags |= EntityTags.isShiny;
}
+
+ this.arenaMobID = "square";
}
}
diff --git a/src/Entity/Shape/Triangle.ts b/src/Entity/Shape/Triangle.ts
index c88c14d6..03e10a6e 100644
--- a/src/Entity/Shape/Triangle.ts
+++ b/src/Entity/Shape/Triangle.ts
@@ -19,10 +19,11 @@
import GameServer from "../../Game";
import AbstractShape from "./AbstractShape";
-import { Color } from "../../Const/Enums";
+import { Color, EntityTags } from "../../Const/Enums";
+import { shinyChance } from "../../config";
export default class Triangle extends AbstractShape {
- public constructor(game: GameServer, shiny=Math.random() < 0.000001) {
+ public constructor(game: GameServer, shiny=Math.random() < shinyChance) {
super(game);
this.nameData.values.name = "Triangle";
@@ -38,6 +39,9 @@ export default class Triangle extends AbstractShape {
if (shiny) {
this.scoreReward *= 100;
this.healthData.values.health = this.healthData.values.maxHealth *= 10;
+ this.entityTags |= EntityTags.isShiny;
}
+
+ this.arenaMobID = "triangle";
}
}
diff --git a/src/Entity/Tank/Addons.ts b/src/Entity/Tank/Addons.ts
index 095b22b4..2e86c14f 100644
--- a/src/Entity/Tank/Addons.ts
+++ b/src/Entity/Tank/Addons.ts
@@ -189,7 +189,7 @@ export class GuardObject extends ObjectEntity implements BarrelBase {
* Spreads onKill to owner
*/
public onKill(killedEntity: LivingEntity) {
- if (!(this.owner instanceof LivingEntity)) return;
+ if (!LivingEntity.isLive(this.owner)) return;
this.owner.onKill(killedEntity);
}
diff --git a/src/Entity/Tank/Barrel.ts b/src/Entity/Tank/Barrel.ts
index 314bfecd..e3e72f98 100644
--- a/src/Entity/Tank/Barrel.ts
+++ b/src/Entity/Tank/Barrel.ts
@@ -58,7 +58,7 @@ export class ShootCycle {
const reloadTime = this.barrelEntity.tank.reloadTime * this.barrelEntity.definition.reload;
if (reloadTime !== this.reloadTime) {
this.pos *= reloadTime / this.reloadTime;
- this.reloadTime = reloadTime;
+ this.reloadTime = this.barrelEntity.barrelData.reloadTime = reloadTime;
}
const alwaysShoot = (this.barrelEntity.definition.forceFire) || (this.barrelEntity.definition.bullet.type === 'drone') || (this.barrelEntity.definition.bullet.type === 'minion');
@@ -154,11 +154,11 @@ export default class Barrel extends ObjectEntity {
const scatterAngle = (Math.PI / 180) * this.definition.bullet.scatterRate * (Math.random() - .5) * 10;
let angle = this.definition.angle + scatterAngle + this.tank.positionData.values.angle;
- this.rootParent.addAcceleration(angle + Math.PI, this.definition.recoil * 2);
+ this.rootParent.addVelocity(angle + Math.PI, this.definition.recoil * 2);
let tankDefinition: TankDefinition | null = null;
- if (this.rootParent instanceof TankBody) tankDefinition = this.rootParent.definition;
+ if (TankBody.isTank(this.rootParent)) tankDefinition = this.rootParent.definition;
let projectile: ObjectEntity | null = null;
diff --git a/src/Entity/Tank/Projectile/Bullet.ts b/src/Entity/Tank/Projectile/Bullet.ts
index 45c19a2d..ffc3a29c 100644
--- a/src/Entity/Tank/Projectile/Bullet.ts
+++ b/src/Entity/Tank/Projectile/Bullet.ts
@@ -110,7 +110,7 @@ export default class Bullet extends LivingEntity {
public tick(tick: number) {
super.tick(tick);
- if (tick === this.spawnTick + 1) this.addAcceleration(this.movementAngle, this.baseSpeed);
+ if (tick === this.spawnTick + 1) this.addVelocity(this.movementAngle, this.baseSpeed);
else this.maintainVelocity(this.usePosAngle ? this.positionData.values.angle : this.movementAngle, this.baseAccel);
if (tick - this.spawnTick >= this.lifeLength) this.destroy(true);
diff --git a/src/Entity/Tank/Projectile/Minion.ts b/src/Entity/Tank/Projectile/Minion.ts
index 809edab9..629bd2dd 100644
--- a/src/Entity/Tank/Projectile/Minion.ts
+++ b/src/Entity/Tank/Projectile/Minion.ts
@@ -89,6 +89,8 @@ export default class Minion extends Drone implements BarrelBase {
this.minionBarrel = new Barrel(this, MinionBarrelDefinition);
this.ai.movementSpeed = this.ai.aimSpeed = this.baseAccel;
+
+ this.arenaMobID = "factoryDrone";
}
public get sizeFactor() {
diff --git a/src/Entity/Tank/Projectile/NecromancerSquare.ts b/src/Entity/Tank/Projectile/NecromancerSquare.ts
index 6edf67e4..87b69dd2 100644
--- a/src/Entity/Tank/Projectile/NecromancerSquare.ts
+++ b/src/Entity/Tank/Projectile/NecromancerSquare.ts
@@ -67,6 +67,9 @@ export default class NecromancerSquare extends Drone {
sunchip.damagePerTick *= shapeDamagePerTick / 2;
sunchip.healthData.values.maxHealth = (sunchip.healthData.values.health *= (shapeDamagePerTick / 2));
+
+ sunchip.scoreReward = shape.scoreReward;
+
return sunchip;
}
-}
\ No newline at end of file
+}
diff --git a/src/Entity/Tank/Projectile/Skimmer.ts b/src/Entity/Tank/Projectile/Skimmer.ts
index 16509ea0..c48f66fc 100644
--- a/src/Entity/Tank/Projectile/Skimmer.ts
+++ b/src/Entity/Tank/Projectile/Skimmer.ts
@@ -29,7 +29,7 @@ import { CameraEntity } from "../../../Native/Camera";
* Barrel definition for the skimmer skimmer's barrel.
*/
const SkimmerBarrelDefinition: BarrelDefinition = {
- angle: Math.PI / 2,
+ angle: 0,
offset: 0,
size: 70,
width: 42,
@@ -63,8 +63,10 @@ export default class Skimmer extends Bullet implements BarrelBase {
/** The camera entity (used as team) of the skimmer. */
public cameraEntity: CameraEntity;
+
/** The reload time of the skimmer's barrel. */
public reloadTime = 15;
+
/** The inputs for when to shoot or not. (skimmer) */
public inputs: Inputs;
diff --git a/src/Entity/Tank/TankBody.ts b/src/Entity/Tank/TankBody.ts
index 6f78be41..b69fe4ac 100644
--- a/src/Entity/Tank/TankBody.ts
+++ b/src/Entity/Tank/TankBody.ts
@@ -21,13 +21,13 @@ import * as util from "../../util";
import type GameServer from "../../Game";
import type { CameraEntity } from "../../Native/Camera";
-import Square from "../Shape/Square";
+import AbstractShape from "../Shape/AbstractShape";
import NecromancerSquare from "./Projectile/NecromancerSquare";
import LivingEntity from "../Live";
import ObjectEntity from "../Object";
import Barrel from "./Barrel";
-import { Color, StyleFlags, StatCount, Tank, CameraFlags, Stat, InputFlags, PhysicsFlags, PositionFlags, NameFlags, HealthFlags } from "../../Const/Enums";
+import { Color, StyleFlags, StatCount, Tank, CameraFlags, Stat, InputFlags, PhysicsFlags, PositionFlags, NameFlags, HealthFlags, EntityTags } from "../../Const/Enums";
import { Entity } from "../../Native/Entity";
import { NameGroup, ScoreGroup } from "../../Native/FieldGroups";
import { Addon, AddonById } from "./Addons";
@@ -100,6 +100,14 @@ export default class TankBody extends LivingEntity implements BarrelBase {
this.damagePerTick = 5;
this.maxDamageMultiplier = 6;
this.setTank(Tank.Basic);
+
+ this.entityTags |= EntityTags.isTank;
+ }
+
+ public static isTank(entity: Entity | null | undefined): entity is TankBody {
+ if (!ObjectEntity.isObject(entity)) return false;
+
+ return !!(entity.entityTags & EntityTags.isTank);
}
/** The active change in size from the base size to the current. Contributes to barrel and addon sizes. */
@@ -188,13 +196,13 @@ export default class TankBody extends LivingEntity implements BarrelBase {
// TODO(ABC):
// This is actually not how necromancers claim squares.
- if (entity instanceof Square && this.definition.flags.canClaimSquares && this.barrels.length) {
+ if (entity.arenaMobID === "square" && this.definition.flags.canClaimSquares && this.barrels.length) {
// If can claim, pick a random barrel that has drones it can still shoot, then shoot
const MAX_DRONES_PER_BARREL = 11 + this.cameraEntity.cameraData.values.statLevels.values[Stat.Reload];
const barrelsToShoot = this.barrels.filter((e) => e.definition.bullet.type === "necrodrone" && e.droneCount < MAX_DRONES_PER_BARREL);
if (barrelsToShoot.length) {
- const barrelToShoot = barrelsToShoot[~~(Math.random()*barrelsToShoot.length)];
+ const barrelToShoot = util.randomFrom(barrelsToShoot);
// No destroy it on the next tick to make it look more like the way diep does it.
entity.destroy(true);
@@ -204,7 +212,7 @@ export default class TankBody extends LivingEntity implements BarrelBase {
entity.healthData.flags = HealthFlags.hiddenHealthbar
}
- const sunchip = NecromancerSquare.fromShape(barrelToShoot, this, this.definition, entity);
+ const sunchip = NecromancerSquare.fromShape(barrelToShoot, this, this.definition, entity as AbstractShape);
}
}
}
diff --git a/src/Game.ts b/src/Game.ts
index 06ff1871..0772e387 100644
--- a/src/Game.ts
+++ b/src/Game.ts
@@ -23,21 +23,15 @@ import EntityManager from "./Native/Manager";
import Client from "./Client";
import ArenaEntity from "./Native/Arena";
import FFAArena from "./Gamemodes/FFA";
+import SurvivalArena from "./Gamemodes/Survival";
import Teams2Arena from "./Gamemodes/Team2";
-import SandboxArena from "./Gamemodes/Sandbox";
-import { ClientBound } from "./Const/Enums";
import Teams4Arena from "./Gamemodes/Team4";
import DominationArena from "./Gamemodes/Domination";
+import TagArena from "./Gamemodes/Tag";
import MothershipArena from "./Gamemodes/Mothership";
-import TestingArena from "./Gamemodes/Misc/Testing";
-import SpikeboxArena from "./Gamemodes/Misc/Spikebox";
-import DominationTestingArena from "./Gamemodes/Misc/DomTest";
-import JungleArena from "./Gamemodes/Misc/Jungle";
-import FactoryTestArena from "./Gamemodes/Misc/FactoryTest";
-import BallArena from "./Gamemodes/Misc/Ball";
import MazeArena from "./Gamemodes/Maze";
-import TagArena from "./Gamemodes/Tag";
-import SurvivalArena from "./Gamemodes/Survival";
+import SandboxArena from "./Gamemodes/Sandbox";
+import { ClientBound } from "./Const/Enums";
/**
* WriterStream that broadcasts to all of the game's WebSockets.
@@ -61,17 +55,17 @@ class WSSWriterStream extends Writer {
/** @deprecated */
-type DiepGamemodeID = "ffa" | "sandbox" | "teams" | "4teams" | "mot" | "dom" | "maze" | "tag" | "survival";
+type DiepGamemodeID = "ffa" | "survival" | "teams" | "4teams" | "dom" | "tag" | "mot" | "maze" | "sandbox";
const GamemodeToArenaClass: Record = {
"ffa": FFAArena,
+ "survival": SurvivalArena,
"teams": Teams2Arena,
"4teams": Teams4Arena,
- "sandbox": SandboxArena,
"dom": DominationArena,
- "survival": SurvivalArena,
"tag": TagArena,
"mot": MothershipArena,
- "maze": MazeArena
+ "maze": MazeArena,
+ "sandbox": SandboxArena
}
/**
@@ -219,14 +213,13 @@ export default class GameServer {
/** Ticks the game. */
private tickLoop() {
this.tick += 1;
-
- this.entities.collisionManager.preTick(this.tick);
+ this.entities.preTick(this.tick);
// process inputs before ticking entities for lower input latency
for (const client of this.clients) client.tick(this.tick);
this.entities.tick(this.tick);
- this.entities.collisionManager.postTick(this.tick);
+ this.entities.postTick(this.tick);
}
}
diff --git a/src/Gamemodes/Benchmark/Crashers.ts b/src/Gamemodes/Benchmark/Crashers.ts
index 2c6f7b54..650accda 100644
--- a/src/Gamemodes/Benchmark/Crashers.ts
+++ b/src/Gamemodes/Benchmark/Crashers.ts
@@ -18,7 +18,7 @@
import AbstractShape from "../../Entity/Shape/AbstractShape";
import Crasher from "../../Entity/Shape/Crasher";
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import GameServer from "../../Game";
import ArenaEntity, { ArenaState } from "../../Native/Arena";
@@ -29,7 +29,7 @@ class ManyCrashersManager extends ShapeManager {
protected spawnShape(): AbstractShape {
const shape = new Crasher(this.arena.game, Math.random() < 0.3);
- const loc = this.arena.findSpawnLocation(false);
+ const loc = this.arena.findSpawnLocation();
shape.positionData.values.x = loc.x;
shape.positionData.values.y = loc.y;
return shape;
diff --git a/src/Gamemodes/Benchmark/HugeMap.ts b/src/Gamemodes/Benchmark/HugeMap.ts
index 2dffb8cf..035be0a1 100644
--- a/src/Gamemodes/Benchmark/HugeMap.ts
+++ b/src/Gamemodes/Benchmark/HugeMap.ts
@@ -16,7 +16,7 @@
along with this program. If not, see
*/
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import GameServer from "../../Game";
import ArenaEntity, { ArenaState } from "../../Native/Arena";
diff --git a/src/Gamemodes/Benchmark/Players.ts b/src/Gamemodes/Benchmark/Players.ts
index 698f90d7..43bb4646 100644
--- a/src/Gamemodes/Benchmark/Players.ts
+++ b/src/Gamemodes/Benchmark/Players.ts
@@ -20,7 +20,7 @@ import { DevTank } from "../../Const/DevTankDefinitions";
import { InputFlags, Stat, Tank } from "../../Const/Enums";
import { Inputs } from "../../Entity/AI";
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import TankBody from "../../Entity/Tank/TankBody";
import GameServer from "../../Game";
import ArenaEntity, { ArenaState } from "../../Native/Arena";
diff --git a/src/Gamemodes/Benchmark/ShapeDense.ts b/src/Gamemodes/Benchmark/ShapeDense.ts
index faeee251..5fda34f0 100644
--- a/src/Gamemodes/Benchmark/ShapeDense.ts
+++ b/src/Gamemodes/Benchmark/ShapeDense.ts
@@ -16,7 +16,7 @@
along with this program. If not, see
*/
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import GameServer from "../../Game";
import ArenaEntity, { ArenaState } from "../../Native/Arena";
diff --git a/src/Gamemodes/Domination.ts b/src/Gamemodes/Domination.ts
index 8a111eab..7596d200 100644
--- a/src/Gamemodes/Domination.ts
+++ b/src/Gamemodes/Domination.ts
@@ -25,7 +25,7 @@ import TankBody from "../Entity/Tank/TankBody";
import GameServer from "../Game";
import ArenaEntity, { ArenaState } from "../Native/Arena";
import { Entity } from "../Native/Entity";
-
+import { randomFrom } from "../util";
const arenaSize = 11150;
const baseSize = arenaSize / (3 + 1/3); // 3345, must scale with arena size
@@ -88,7 +88,7 @@ export default class DominationArena extends ArenaEntity {
const xOffset = (Math.random() - 0.5) * baseSize,
yOffset = (Math.random() - 0.5) * baseSize;
- const team = this.playerTeamMap.get(client) || this.teams[~~(Math.random() * this.teams.length)];
+ const team = this.playerTeamMap.get(client) || randomFrom(this.teams);
const teamBase: TeamBase = this.game.entities.inner.find((entity) => entity instanceof TeamBase && entity.relationsData.values.team === team) as TeamBase;
tank.relationsData.values.team = teamBase.relationsData.values.team;
diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts
index 2a4c3ecb..fa354002 100644
--- a/src/Gamemodes/Maze.ts
+++ b/src/Gamemodes/Maze.ts
@@ -15,12 +15,12 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see
*/
-import ArenaEntity, { ArenaState } from "../Native/Arena";
+
+import ArenaEntity from "../Native/Arena";
import GameServer from "../Game";
-import MazeWall from "../Entity/Misc/MazeWall";
-import { VectorAbstract } from "../Physics/Vector";
+import MazeGenerator, { MazeGeneratorConfig } from "../Misc/MazeGenerator";
-import ShapeManager from "../Entity/Shape/Manager";
+import ShapeManager from "../Misc/ShapeManager";
/**
* Manage shape count
@@ -33,215 +33,45 @@ export class MazeShapeManager extends ShapeManager {
return Math.floor(12.5 * ratio);
*/
-
+
return 1300;
}
}
-// constss.
-const CELL_SIZE = 635;
const GRID_SIZE = 40;
-const ARENA_SIZE = CELL_SIZE * GRID_SIZE;
-const SEED_AMOUNT = Math.floor(Math.random() * 30) + 30;
-const TURN_CHANCE = 0.2;
-const BRANCH_CHANCE = 0.2;
-const TERMINATION_CHANCE = 0.2;
+const CELL_SIZE = 635;
+const ARENA_SIZE = GRID_SIZE * CELL_SIZE;
+
+const config: MazeGeneratorConfig = {
+ size: GRID_SIZE,
+ baseSeedCount: 45,
+ seedCountVariation: 30,
+ turnChance: 0.2,
+ branchChance: 0.2,
+ terminationChance: 0.2
+}
-/**
- * Maze Gamemode Arena
- *
- * Implementation details:
- * Maze map generator by damocles
- * - Added into codebase on December 3rd 2022
- */
export default class MazeArena extends ArenaEntity {
static override GAMEMODE_ID: string = "maze";
protected shapes: ShapeManager = new MazeShapeManager(this);
- /** Stores all the "seed"s */
- private SEEDS: VectorAbstract[] = [];
- /** Stores all the "wall"s, contains cell based coords */
- private WALLS: (VectorAbstract & {width: number, height: number})[] = [];
- /** Rolled out matrix of the grid */
- private MAZE: Uint8Array = new Uint8Array(GRID_SIZE * GRID_SIZE);
+ public mazeGenerator: MazeGenerator = new MazeGenerator(config);
public constructor(game: GameServer) {
super(game);
+
this.updateBounds(ARENA_SIZE, ARENA_SIZE);
- this.allowBoss = false;
- this._buildMaze();
- }
- /** Creates a maze wall from cell coords */
- private _buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) {
- const scaledW = gridW * CELL_SIZE;
- const scaledH = gridH * CELL_SIZE;
- const scaledX = gridX * CELL_SIZE - ARENA_SIZE / 2 + (scaledW / 2);
- const scaledY = gridY * CELL_SIZE - ARENA_SIZE / 2 + (scaledH / 2);
- new MazeWall(this.game, scaledX, scaledY, scaledH, scaledW);
- }
- /** Allows for easier (x, y) based getting of maze cells */
- private _get(x: number, y: number): number {
- return this.MAZE[y * GRID_SIZE + x];
- }
- /** Allows for easier (x, y) based setting of maze cells */
- private _set(x: number, y: number, value: number): number {
- return this.MAZE[y * GRID_SIZE + x] = value;
- }
- /** Converts MAZE grid into an array of set and unset bits for ease of use */
- private _mapValues(): [x: number, y: number, value: number][] {
- const values: [x: number, y: number, value: number][] = Array(this.MAZE.length);
- for (let i = 0; i < this.MAZE.length; ++i) values[i] = [i % GRID_SIZE, Math.floor(i / GRID_SIZE), this.MAZE[i]];
- return values;
- }
- /** Builds the maze */
- protected _buildMaze() {
- // Plant some seeds
- for (let i = 0; i < 10000; i++) {
- // Stop if we exceed our maximum seed amount
- if (this.SEEDS.length >= SEED_AMOUNT) break;
- // Attempt a seed planting
- let seed: VectorAbstract = {
- x: Math.floor((Math.random() * GRID_SIZE) - 1),
- y: Math.floor((Math.random() * GRID_SIZE) - 1),
- };
- // Check if our seed is valid (is 3 GU away from another seed, and is not on the border)
- if (this.SEEDS.some(a => (Math.abs(seed.x - a.x) <= 3 && Math.abs(seed.y - a.y) <= 3))) continue;
- if (seed.x <= 0 || seed.y <= 0 || seed.x >= GRID_SIZE - 1 || seed.y >= GRID_SIZE - 1) continue;
- // Push it to the pending seeds and set its grid to a wall cell
- this.SEEDS.push(seed);
- this._set(seed.x, seed.y, 1);
- }
- const direction: number[][] = [
- [-1, 0], [1, 0], // left and right
- [0, -1], [0, 1], // up and down
- ];
- // Let it grow!
- for (let seed of this.SEEDS) {
- // Select a direction we want to head in
- let dir: number[] = direction[Math.floor(Math.random() * 4)];
- let termination = 1;
- // Now we can start to grow
- while (termination >= TERMINATION_CHANCE) {
- // Choose the next termination chance
- termination = Math.random();
- // Get the direction we're going in
- let [x, y] = dir;
- // Move forward in that direction, and set that grid to a wall cell
- seed.x += x;
- seed.y += y;
- if (seed.x <= 0 || seed.y <= 0 || seed.x >= GRID_SIZE - 1 || seed.y >= GRID_SIZE - 1) break;
- this._set(seed.x, seed.y, 1);
- // Now lets see if we want to branch or turn
- if (Math.random() <= BRANCH_CHANCE) {
- // If the seeds exceeds 75, then we're going to stop creating branches in order to avoid making a massive maze tumor(s)
- if (this.SEEDS.length > 75) continue;
- // Get which side we want the branch to be on (left or right if moving up or down, and up and down if moving left or right)
- let [ xx, yy ] = direction.filter(a => a.every((b, c) => b !== dir[c]))[Math.floor(Math.random() * 2)];
- // Create the seed
- let newSeed = {
- x: seed.x + xx,
- y: seed.y + yy,
- };
- // Push the seed and set its grid to a maze zone
- this.SEEDS.push(newSeed);
- this._set(seed.x, seed.y, 1);
- } else if (Math.random() <= TURN_CHANCE) {
- // Get which side we want to turn to (left or right if moving up or down, and up and down if moving left or right)
- dir = direction.filter(a => a.every((b, c) => b !== dir[c]))[Math.floor(Math.random() * 2)];
- }
- }
- }
- // Now lets attempt to add some singular walls around the arena
- for (let i = 0; i < 10; i++) {
- // Attempt to place it
- let seed = {
- x: Math.floor((Math.random() * GRID_SIZE) - 1),
- y: Math.floor((Math.random() * GRID_SIZE) - 1),
- };
- // Check if our sprinkle is valid (is 3 GU away from another wall, and is not on the border)
- if (this._mapValues().some(([x, y, r]) => r === 1 && (Math.abs(seed.x - x) <= 3 && Math.abs(seed.y - y) <= 3))) continue;
- if (seed.x <= 0 || seed.y <= 0 || seed.x >= GRID_SIZE - 1 || seed.y >= GRID_SIZE - 1) continue;
- // Set its grid to a wall cell
- this._set(seed.x, seed.y, 1);
- }
- // Now it's time to fill in the inaccessible pockets
- // Start at the top left
- let queue: number[][] = [[0, 0]];
- this._set(0, 0, 2);
- let checkedIndices = new Set([0]);
- // Now lets cycle through the whole map
- for (let i = 0; i < 3000 && queue.length > 0; i++) {
- let next = queue.shift();
- if (next == null) break;
- let [x, y] = next;
- // Get what the coordinates of what lies to the side of our cell
- for (let [nx, ny] of [
- [x - 1, y], // left
- [x + 1, y], // right
- [x, y - 1], // top
- [x, y + 1], // bottom
- ]) {
- // If its a wall ignore it
- if (this._get(nx, ny) !== 0) continue;
- let i = ny * GRID_SIZE + nx;
- // Check if we've already checked this cell
- if (checkedIndices.has(i)) continue;
- // Add it to the checked cells if we haven't already
- checkedIndices.add(i);
- // Add it to the next cycle to check
- queue.push([nx, ny]);
- // Set its grid to an accessible cell
- this._set(nx, ny, 2);
- }
- }
- // Cycle through all areas of the map
- for (let x = 0; x < GRID_SIZE; x++) {
- for (let y = 0; y < GRID_SIZE; y++) {
- // If we're not a wall, ignore the cell and move on
- if (this._get(x, y) === 2) continue;
- // Define our properties
- let chunk = { x, y, width: 0, height: 1 };
- // Loop through adjacent cells and see how long we should be
- while (this._get(x + chunk.width, y) !== 2) {
- this._set(x + chunk.width, y, 2);
- chunk.width++;
- }
- // Now lets see if we need to be t h i c c
- outer: while (true) {
- // Check the row below to see if we can still make a box
- for (let i = 0; i < chunk.width; i++)
- // Stop if we can't
- if (this._get(x + i, y + chunk.height) === 2) break outer;
- // If we can, remove the line of cells from the map and increase the height of the block
- for (let i = 0; i < chunk.width; i++)
- this._set(x + i, y + chunk.height, 2);
- chunk.height++;
- }
- this.WALLS.push(chunk);
- }
- }
- // Create the walls!
- for (let {x, y, width, height} of this.WALLS)
- this._buildWallFromGridCoord(x, y, width, height);
+
+ this.mazeGenerator.generate();
+ this.mazeGenerator.placeWalls(this);
+
+ this.bossManager = null; // Disables boss spawning
}
public isValidSpawnLocation(x: number, y: number): boolean {
+ const { gridX, gridY } = this.mazeGenerator.getGridCell(this, x, y);
// Should never spawn inside walls
- for (let wall of this.WALLS) {
- const wallX = wall.x * CELL_SIZE - ARENA_SIZE / 2;
- const wallY = wall.y * CELL_SIZE - ARENA_SIZE / 2;
- const wallW = wall.width * CELL_SIZE;
- const wallH = wall.height * CELL_SIZE;
- if (
- x >= wallX &&
- x <= wallX + wallW &&
- y >= wallY &&
- y <= wallY + wallH
- ) {
- return false;
- }
- }
- return true;
+ return this.mazeGenerator.isCellOccupied(gridX, gridY) === false;
}
}
diff --git a/src/Gamemodes/Misc/DomTest.ts b/src/Gamemodes/Misc/DomTest.ts
index 7fb05917..8a9d47ae 100644
--- a/src/Gamemodes/Misc/DomTest.ts
+++ b/src/Gamemodes/Misc/DomTest.ts
@@ -19,7 +19,7 @@
import GameServer from "../../Game";
import ArenaEntity from "../../Native/Arena";
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import { NameFlags, Tank } from "../../Const/Enums";
import Dominator from "../../Entity/Misc/Dominator";
import TeamBase from "../../Entity/Misc/TeamBase";
diff --git a/src/Gamemodes/Misc/FactoryTest.ts b/src/Gamemodes/Misc/FactoryTest.ts
index 0ec01150..1208d7ed 100644
--- a/src/Gamemodes/Misc/FactoryTest.ts
+++ b/src/Gamemodes/Misc/FactoryTest.ts
@@ -19,7 +19,7 @@
import GameServer from "../../Game";
import ArenaEntity from "../../Native/Arena";
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import { Inputs } from "../../Entity/AI";
import { CameraEntity } from "../../Native/Camera";
import TankBody from "../../Entity/Tank/TankBody";
@@ -77,7 +77,7 @@ export default class FactoryTestArena extends ArenaEntity {
tank.positionData.values.x = x + (Math.cos(shootAngle) * barrel.physicsData.values.size * 0.5) - Math.sin(shootAngle) * barrel.definition.offset * this.nimdac.sizeFactor;
tank.positionData.values.y = y + (Math.sin(shootAngle) * barrel.physicsData.values.size * 0.5) + Math.cos(shootAngle) * barrel.definition.offset * this.nimdac.sizeFactor;
- tank.addAcceleration(shootAngle, 40);
+ tank.addVelocity(shootAngle, 40);
}
public tick(tick: number) {
diff --git a/src/Gamemodes/Misc/Jungle.ts b/src/Gamemodes/Misc/Jungle.ts
index 50dc3385..156a9f06 100644
--- a/src/Gamemodes/Misc/Jungle.ts
+++ b/src/Gamemodes/Misc/Jungle.ts
@@ -19,7 +19,7 @@
import GameServer from "../../Game";
import ArenaEntity from "../../Native/Arena";
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import TankBody from "../../Entity/Tank/TankBody";
import AbstractShape from "../../Entity/Shape/AbstractShape";
import Client from "../../Client";
diff --git a/src/Gamemodes/Misc/Spikebox.ts b/src/Gamemodes/Misc/Spikebox.ts
index 0dbed65e..50153c2d 100644
--- a/src/Gamemodes/Misc/Spikebox.ts
+++ b/src/Gamemodes/Misc/Spikebox.ts
@@ -19,7 +19,7 @@
import GameServer from "../../Game";
import ArenaEntity from "../../Native/Arena";
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import { Inputs } from "../../Entity/AI";
import { CameraEntity } from "../../Native/Camera";
import TankBody from "../../Entity/Tank/TankBody";
diff --git a/src/Gamemodes/Misc/Testing.ts b/src/Gamemodes/Misc/Testing.ts
index 0fbf7087..2276ae0f 100644
--- a/src/Gamemodes/Misc/Testing.ts
+++ b/src/Gamemodes/Misc/Testing.ts
@@ -19,7 +19,7 @@
import GameServer from "../../Game";
import ArenaEntity from "../../Native/Arena";
-import ShapeManager from "../../Entity/Shape/Manager";
+import ShapeManager from "../../Misc/ShapeManager";
import TankBody from "../../Entity/Tank/TankBody";
import { CameraEntity } from "../../Native/Camera";
import { Inputs } from "../../Entity/AI";
diff --git a/src/Gamemodes/Mothership.ts b/src/Gamemodes/Mothership.ts
index 6fde254d..b830ff16 100644
--- a/src/Gamemodes/Mothership.ts
+++ b/src/Gamemodes/Mothership.ts
@@ -24,7 +24,7 @@ import TankBody from "../Entity/Tank/TankBody";
import GameServer from "../Game";
import ArenaEntity, { ArenaState } from "../Native/Arena";
import { Entity } from "../Native/Entity";
-import { PI2 } from "../util";
+import { PI2, randomFrom } from "../util";
const arenaSize = 11150;
const TEAM_COLORS = [Color.TeamBlue, Color.TeamRed];
@@ -70,8 +70,8 @@ export default class MothershipArena extends ArenaEntity {
public spawnPlayer(tank: TankBody, client: Client) {
if (!this.motherships.length && !this.playerTeamMotMap.has(client)) {
- const team = this.teams[~~(Math.random()*this.teams.length)];
- const { x, y } = this.findSpawnLocation();
+ const team = randomFrom(this.teams);
+ const { x, y } = this.findPlayerSpawnLocation();
tank.positionData.values.x = x;
tank.positionData.values.y = y;
@@ -80,22 +80,17 @@ export default class MothershipArena extends ArenaEntity {
return;
}
- const mothership = this.playerTeamMotMap.get(client) || this.motherships[~~(Math.random() * this.motherships.length)];
+ const mothership = this.playerTeamMotMap.get(client) || randomFrom(this.motherships);
this.playerTeamMotMap.set(client, mothership);
tank.relationsData.values.team = mothership.relationsData.values.team;
tank.styleData.values.color = mothership.styleData.values.color;
// TODO: Possess mothership if its unpossessed
- if (Entity.exists(mothership)) {
- tank.positionData.values.x = mothership.positionData.values.x;
- tank.positionData.values.y = mothership.positionData.values.y;
- } else {
- const { x, y } = this.findSpawnLocation();
+ const { x, y } = this.findPlayerSpawnLocation();
- tank.positionData.values.x = x;
- tank.positionData.values.y = y;
- }
+ tank.positionData.values.x = x;
+ tank.positionData.values.y = y;
if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team;
}
@@ -106,7 +101,7 @@ export default class MothershipArena extends ArenaEntity {
for (let i = 0; i < length; ++i) {
const mothership = this.motherships[i];
const team = mothership.relationsData.values.team;
- const isTeamATeam = team instanceof TeamEntity;
+ const isTeamATeam = TeamEntity.isTeam(team);
if (mothership.styleData.values.color === Color.Tank) this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = Color.ScoreboardBar;
else this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = mothership.styleData.values.color;
this.arenaData.values.scoreboardNames[i as ValidScoreboardIndex] = isTeamATeam ? team.teamName : `Mothership ${i+1}`;
diff --git a/src/Gamemodes/Sandbox.ts b/src/Gamemodes/Sandbox.ts
index 31661a09..f04770d4 100644
--- a/src/Gamemodes/Sandbox.ts
+++ b/src/Gamemodes/Sandbox.ts
@@ -19,7 +19,7 @@
import GameServer from "../Game";
import ArenaEntity, { ArenaState } from "../Native/Arena";
-import ShapeManager from "../Entity/Shape/Manager";
+import ShapeManager from "../Misc/ShapeManager";
import { ArenaFlags } from "../Const/Enums";
/**
@@ -47,15 +47,19 @@ export default class SandboxArena extends ArenaEntity {
public constructor(game: GameServer) {
super(game);
- this.updateBounds(2500, 2500);
this.arenaData.values.flags |= ArenaFlags.canUseCheats;
this.state = ArenaState.OPEN; // Sandbox should start instantly, no countdown
- // const w1 = new MazeWall(this.game, 0, 0, 500, 500);
+
+ this.setSandboxArenaSize(0);
+ }
+
+ public setSandboxArenaSize(playerCount: number) {
+ const arenaSize = Math.floor(25 * Math.sqrt(Math.max(playerCount, 1))) * 100;
+ this.updateBounds(arenaSize, arenaSize);
}
public tick(tick: number) {
- const arenaSize = Math.floor(25 * Math.sqrt(Math.max(this.game.clients.size, 1))) * 100;
- if (this.width !== arenaSize || this.height !== arenaSize) this.updateBounds(arenaSize, arenaSize);
+ this.setSandboxArenaSize(this.game.clients.size);
super.tick(tick);
}
diff --git a/src/Gamemodes/Survival.ts b/src/Gamemodes/Survival.ts
index 7d2ea765..e1669fcd 100644
--- a/src/Gamemodes/Survival.ts
+++ b/src/Gamemodes/Survival.ts
@@ -22,11 +22,11 @@ import ArenaEntity, { ArenaState } from "../Native/Arena";
import { Entity } from "../Native/Entity";
import TankBody from "../Entity/Tank/TankBody";
-import ShapeManager from "../Entity/Shape/Manager";
+import ShapeManager from "../Misc/ShapeManager";
import { ArenaFlags, ClientBound } from "../Const/Enums";
import { tps, countdownTicks, scoreboardUpdateInterval } from "../config";
-const MIN_PLAYERS = 4;
+const MIN_PLAYERS = 4; // 6 in Diep.io
/**
* Manage shape count
@@ -51,9 +51,10 @@ export default class SurvivalArena extends ArenaEntity {
super(game);
this.shapeScoreRewardMultiplier = 3.0;
- this.updateBounds(2500, 2500);
this.arenaData.values.flags &= ~ArenaFlags.gameReadyStart;
this.arenaData.values.playersNeeded = MIN_PLAYERS;
+
+ this.setSurvivalArenaSize(0);
}
public updateArenaState() {
diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts
index 3c82d99e..07265074 100644
--- a/src/Gamemodes/Tag.ts
+++ b/src/Gamemodes/Tag.ts
@@ -30,20 +30,19 @@ import { tps, scoreboardUpdateInterval } from "../config";
import TeamBase from "../Entity/Misc/TeamBase"
import Dominator from "../Entity/Misc/Dominator"
-import ShapeManager from "../Entity/Shape/Manager";
+import ShapeManager from "../Misc/ShapeManager";
-const arenaSize = 11150;
const TEAM_COLORS = [Color.TeamBlue, Color.TeamRed, Color.TeamPurple, Color.TeamGreen];
const MIN_PLAYERS = TEAM_COLORS.length * 1; // It is higher in the official servers, though we do not have enough players for that (4 players per team)
+const ARENA_SIZE = 11150;
+
const SHRINK_AMOUNT = 100;
const SHRINK_INTERVAL = 15 * tps;
const MIN_SIZE = 6600;
const ENABLE_DOMINATOR = false;
-shuffleArray(TEAM_COLORS); // Randomize leading team
-
/**
* Manage shape count
*/
@@ -74,8 +73,9 @@ export default class TagArena extends ArenaEntity {
this.shapeScoreRewardMultiplier = 3.0;
this.arenaData.values.flags |= ArenaFlags.hiddenScores;
-
- for (const teamColor of TEAM_COLORS) {
+ const teamOrder = TEAM_COLORS.slice();
+ shuffleArray(teamOrder);
+ for (const teamColor of teamOrder) {
const team = new TeamEntity(this.game, teamColor);
this.teams.push(team);
}
@@ -85,11 +85,10 @@ export default class TagArena extends ArenaEntity {
new Dominator(this, new TeamBase(game, this, 0, 0, domBaseSize, domBaseSize, false));
}
- this.updateBounds(arenaSize * 2, arenaSize * 2);
+ this.updateBounds(ARENA_SIZE * 2, ARENA_SIZE * 2);
}
public spawnPlayer(tank: TankBody, client: Client) {
- this.updateArenaState();
const deathMixin = tank.onDeath.bind(tank);
tank.onDeath = (killer: LivingEntity) => {
deathMixin(killer);
@@ -109,7 +108,7 @@ export default class TagArena extends ArenaEntity {
if (!this.playerTeamMap.has(client)) {
const team = this.getAlivePlayers().length <= MIN_PLAYERS ? this.teams[this.teams.length - 1] :
this.teams[0]; // If there are not enough players to start the game, choose the team with least players. Otherwise choose the one with highest player count
- const { x, y } = this.findSpawnLocation();
+ const { x, y } = this.findPlayerSpawnLocation();
tank.positionData.values.x = x;
tank.positionData.values.y = y;
@@ -127,7 +126,7 @@ export default class TagArena extends ArenaEntity {
tank.relationsData.values.team = team;
tank.styleData.values.color = team.teamData.values.teamColor;
- const { x, y } = this.findSpawnLocation();
+ const { x, y } = this.findPlayerSpawnLocation();
tank.positionData.values.x = x;
tank.positionData.values.y = y;
@@ -195,4 +194,3 @@ export default class TagArena extends ArenaEntity {
}
}
}
-
diff --git a/src/Gamemodes/Team2.ts b/src/Gamemodes/Team2.ts
index 45f28467..0e0b7368 100644
--- a/src/Gamemodes/Team2.ts
+++ b/src/Gamemodes/Team2.ts
@@ -25,6 +25,7 @@ import TankBody from "../Entity/Tank/TankBody";
import { TeamEntity } from "../Entity/Misc/TeamEntity";
import { Color } from "../Const/Enums";
+import { randomFrom } from "../util";
const arenaSize = 11150;
const baseWidth = arenaSize / (3 + 1/3) * 0.6; // 2007
@@ -39,13 +40,6 @@ export default class Teams2Arena extends ArenaEntity {
public blueTeamBase: TeamBase;
/** Red Team entity */
public redTeamBase: TeamBase;
- // /** Limits shape count 100 */
- // protected shapes: ShapeManager = new class extends ShapeManager {
- // protected get wantedShapes() {
- // return 64;
- // }
- // }(this);
-
/** Maps clients to their teams */
public playerTeamMap: WeakMap = new WeakMap();
@@ -61,7 +55,7 @@ export default class Teams2Arena extends ArenaEntity {
const xOffset = (Math.random() - 0.5) * baseWidth;
- const base = this.playerTeamMap.get(client) || [this.blueTeamBase, this.redTeamBase][0|Math.random()*2];
+ const base = this.playerTeamMap.get(client) || randomFrom([this.blueTeamBase, this.redTeamBase]);
tank.relationsData.values.team = base.relationsData.values.team;
tank.styleData.values.color = base.styleData.values.color;
tank.positionData.values.x = base.positionData.values.x + xOffset;
@@ -69,4 +63,4 @@ export default class Teams2Arena extends ArenaEntity {
if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team;
}
-}
+}
\ No newline at end of file
diff --git a/src/Gamemodes/Team4.ts b/src/Gamemodes/Team4.ts
index e151c202..3e15a2a4 100644
--- a/src/Gamemodes/Team4.ts
+++ b/src/Gamemodes/Team4.ts
@@ -25,6 +25,7 @@ import TankBody from "../Entity/Tank/TankBody";
import { TeamEntity } from "../Entity/Misc/TeamEntity";
import { Color } from "../Const/Enums";
+import { randomFrom } from "../util";
const arenaSize = 11150;
const baseSize = arenaSize / (3 + 1/3); // 3345
@@ -63,7 +64,7 @@ export default class Teams4Arena extends ArenaEntity {
const xOffset = (Math.random() - 0.5) * baseSize,
yOffset = (Math.random() - 0.5) * baseSize;
- const base = this.playerTeamMap.get(client) || [this.blueTeamBase, this.redTeamBase, this.greenTeamBase, this.purpleTeamBase][0|Math.random()*4];
+ const base = this.playerTeamMap.get(client) || randomFrom([this.blueTeamBase, this.redTeamBase, this.greenTeamBase, this.purpleTeamBase]);
tank.relationsData.values.team = base.relationsData.values.team;
tank.styleData.values.color = base.styleData.values.color;
tank.positionData.values.x = base.positionData.values.x + xOffset;
diff --git a/src/Misc/BossManager.ts b/src/Misc/BossManager.ts
new file mode 100644
index 00000000..1324d2a5
--- /dev/null
+++ b/src/Misc/BossManager.ts
@@ -0,0 +1,76 @@
+/*
+ DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
+ Copyright (C) 2022 ABCxFF (github.com/ABCxFF)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see
+*/
+
+import ArenaEntity from "../Native/Arena";
+
+import AbstractBoss from "../Entity/Boss/AbstractBoss";
+import Guardian from "../Entity/Boss/Guardian";
+import Summoner from "../Entity/Boss/Summoner";
+import FallenOverlord from "../Entity/Boss/FallenOverlord";
+import FallenBooster from "../Entity/Boss/FallenBooster";
+import Defender from "../Entity/Boss/Defender";
+
+import { bossSpawningInterval } from "../config";
+import { VectorAbstract } from "../Physics/Vector";
+
+/**
+ * Manages boss spawning.
+ */
+export default class BossManager {
+ /** The arena to spawn bosses in. */
+ public arena: ArenaEntity;
+ /** The current boss spawned into the game */
+ public boss: AbstractBoss | null = null;
+ /** A random boss will be selected out of these. */
+ public bossClasses: typeof AbstractBoss[] = [Guardian, Summoner, FallenOverlord, FallenBooster, Defender];
+
+ public constructor(arena: ArenaEntity) {
+ this.arena = arena;
+ }
+
+ public findBossSpawnLocation(): VectorAbstract {
+ const width = this.arena.width / 2;
+ const height = this.arena.height / 2;
+
+ const pos = this.arena.findSpawnLocation(width, height);
+ return pos;
+ }
+
+ public spawnBoss() {
+ const TBoss = this.bossClasses[Math.floor(Math.random() * this.bossClasses.length)];
+ this.boss = new TBoss(this.arena.game);
+
+ const { x, y } = this.findBossSpawnLocation();
+
+ this.boss.positionData.values.x = x;
+ this.boss.positionData.values.y = y;
+
+ const deleteMixin = this.boss.delete.bind(this.boss);
+ this.boss.delete = () => {
+ deleteMixin();
+ // Reset arena boss
+ this.boss = null;
+ }
+ }
+
+ public tick(tick: number) {
+ if (tick >= 1 && (tick % bossSpawningInterval) === 0 && !this.boss) {
+ this.spawnBoss();
+ }
+ }
+}
diff --git a/src/Misc/MazeGenerator.ts b/src/Misc/MazeGenerator.ts
new file mode 100644
index 00000000..aae11c37
--- /dev/null
+++ b/src/Misc/MazeGenerator.ts
@@ -0,0 +1,302 @@
+import ArenaEntity from "../Native/Arena";
+import MazeWall from "../Entity/Misc/MazeWall";
+import { VectorAbstract } from "../Physics/Vector";
+
+export interface MazeGeneratorConfig {
+ /**
+ * Size of the grid
+ * - size=7 implies a 7x7 grid
+ */
+ size: number,
+
+ /**
+ * Amount of "wall seeds" to plant initially
+ */
+ baseSeedCount: number,
+ /**
+ * Variation in the amount of "wall seeds" to plant initially
+ * - Actual seeds planted = baseSeedCount + random(-0.5, 0.5) * seedCountVariation
+ */
+ seedCountVariation: number,
+ /**
+ * Chance of turning when growing a wall seed
+ */
+ turnChance: number,
+ /**
+ * Chance of branching when growing a wall seed
+ */
+ branchChance: number,
+ /**
+ * Chance of terminating when growing a wall seed
+ */
+ terminationChance: number
+}
+
+interface GridWall {
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+}
+
+const MAZE_CELL_EMPTY = 0;
+const MAZE_CELL_WALL = 1;
+const MAZE_CELL_ACCESSIBLE = 2;
+const MAZE_CELL_PLACED_WALL = 3;
+
+/**
+ * Implementation details:
+ * Maze map generator by damocles
+ * - Added into codebase on Saturday 3rd of December 2022
+ * - Split into its own file on Wednesday 3rd of December 2025
+ */
+export default class MazeGenerator {
+ /** The variables that affect maze generation */
+ public config: MazeGeneratorConfig;
+ /** Rolled out matrix of the grid */
+ public maze: Uint8Array;
+
+ public constructor(config: MazeGeneratorConfig) {
+ this.config = config;
+
+ this.maze = new Uint8Array(config.size * config.size);
+ }
+
+ /** Builds the maze */
+ public generate() {
+ interface Seed {
+ x: number,
+ y: number,
+ }
+
+ const seeds: Seed[] = [];
+
+ const seedCount = this.config.baseSeedCount + Math.floor((Math.random() - 0.5) * this.config.seedCountVariation);
+ const maxSeedCount = this.config.baseSeedCount + this.config.seedCountVariation;
+ // Plant some seeds
+ for (let i = 0; i < 10000; i++) {
+ // Stop if we exceed our maximum seed count
+ if (seeds.length >= seedCount) break;
+ // Attempt a seed planting
+ let seed: VectorAbstract = {
+ x: Math.floor((Math.random() * this.config.size) - 1),
+ y: Math.floor((Math.random() * this.config.size) - 1),
+ };
+ // Check if our seed is valid (is 3 GU away from another seed, and is not on the border)
+ if (seeds.some(a => (Math.abs(seed.x - a.x) <= 3 && Math.abs(seed.y - a.y) <= 3))) continue;
+ if (seed.x <= 0 || seed.y <= 0 || seed.x >= this.config.size - 1 || seed.y >= this.config.size - 1) continue;
+ // Push it to the pending seeds and set its grid to a wall cell
+ seeds.push(seed);
+
+ this.set(seed.x, seed.y, MAZE_CELL_WALL);
+ }
+ const direction: number[][] = [
+ [-1, 0], [1, 0], // left and right
+ [0, -1], [0, 1], // up and down
+ ];
+ // Let it grow!
+ for (let seed of seeds) {
+ // Select a direction we want to head in
+ let dir: number[] = direction[Math.floor(Math.random() * 4)];
+ let termination = 1;
+ // Now we can start to grow
+ while (termination >= this.config.terminationChance) {
+ // Choose the next termination chance
+ termination = Math.random();
+ // Get the direction we're going in
+ let [x, y] = dir;
+ // Move forward in that direction, and set that grid to a wall cell
+ seed.x += x;
+ seed.y += y;
+ if (seed.x <= 0 || seed.y <= 0 || seed.x >= this.config.size - 1 || seed.y >= this.config.size - 1) break;
+ this.set(seed.x, seed.y, MAZE_CELL_WALL);
+ // Now lets see if we want to branch or turn
+ if (Math.random() <= this.config.branchChance) {
+ // If the seeds exceeds maxSeedCount, then we're going to stop creating branches in order to avoid making a massive maze tumor(s)
+ if (seeds.length > maxSeedCount) continue;
+ // Get which side we want the branch to be on (left or right if moving up or down, and up and down if moving left or right)
+ let [ xx, yy ] = direction.filter(a => a.every((b, c) => b !== dir[c]))[Math.floor(Math.random() * 2)];
+ // Create the seed
+ let newSeed = {
+ x: seed.x + xx,
+ y: seed.y + yy,
+ };
+ // Push the seed and set its grid to a maze zone
+ seeds.push(newSeed);
+ this.set(seed.x, seed.y, MAZE_CELL_WALL);
+ } else if (Math.random() <= this.config.turnChance) {
+ // Get which side we want to turn to (left or right if moving up or down, and up and down if moving left or right)
+ dir = direction.filter(a => a.every((b, c) => b !== dir[c]))[Math.floor(Math.random() * 2)];
+ }
+ }
+ }
+ // Now lets attempt to add some singular walls around the arena
+ for (let i = 0; i < 10; i++) {
+ // Attempt to place it
+ let seed = {
+ x: Math.floor((Math.random() * this.config.size) - 1),
+ y: Math.floor((Math.random() * this.config.size) - 1),
+ };
+ // Check if our sprinkle is valid (is 3 GU away from another wall, and is not on the border)
+ if (this.mapValues().some(([x, y, r]) => r === 1 && (Math.abs(seed.x - x) <= 3 && Math.abs(seed.y - y) <= 3))) continue;
+ if (seed.x <= 0 || seed.y <= 0 || seed.x >= this.config.size - 1 || seed.y >= this.config.size - 1) continue;
+ // Set its grid to a wall cell
+ this.set(seed.x, seed.y, MAZE_CELL_WALL);
+ }
+ // Now it's time to fill in the inaccessible pockets
+ // Start at the top left
+ let queue: number[][] = [[0, 0]];
+ this.set(0, 0, MAZE_CELL_ACCESSIBLE);
+ let checkedIndices = new Set([0]);
+ // Now lets cycle through the whole map
+ for (let i = 0; i < 3000 && queue.length > 0; i++) {
+ let next = queue.shift();
+ if (next == null) break;
+ let [x, y] = next;
+ // Get what the coordinates of what lies to the side of our cell
+ for (let [nx, ny] of [
+ [x - 1, y], // left
+ [x + 1, y], // right
+ [x, y - 1], // top
+ [x, y + 1], // bottom
+ ]) {
+ if (nx < 0 || ny < 0 || nx >= this.config.size || ny >= this.config.size) continue;
+ // If its not empty ignore it
+ if (this.get(nx, ny) !== MAZE_CELL_EMPTY) continue;
+ let i = ny * this.config.size + nx;
+ // Check if we've already checked this cell
+ if (checkedIndices.has(i)) continue;
+ // Add it to the checked cells if we haven't already
+ checkedIndices.add(i);
+ // Add it to the next cycle to check
+ queue.push([nx, ny]);
+ // Set its grid to an accessible cell
+ this.set(nx, ny, MAZE_CELL_ACCESSIBLE);
+ }
+ }
+
+ for (const [x, y, value] of this.mapValues()) {
+ // If we are a wall or accessible cell, ignore us
+ if (value === MAZE_CELL_WALL || value === MAZE_CELL_ACCESSIBLE) continue;
+ // Otherwise, we are an inaccessible empty cell, so we need to convert ourselves to a wall
+ this.set(x, y, MAZE_CELL_WALL);
+ }
+ }
+
+ protected convertToWalls(): GridWall[] {
+ // Unplace any walls
+ for (const [x, y, value] of this.mapValues()) {
+ if (value !== MAZE_CELL_PLACED_WALL) continue;
+ this.set(x, y, MAZE_CELL_WALL);
+ }
+
+ const walls: GridWall[] = [];
+
+ // Cycle through all areas of the map
+ for (let x = 0; x < this.config.size; x++) {
+ for (let y = 0; y < this.config.size; y++) {
+ // If we're not a wall, ignore the cell and move on
+ if (this.get(x, y) !== MAZE_CELL_WALL) continue;
+ // Define our properties
+ const chunk: GridWall = { x, y, width: 0, height: 1 };
+ // Loop through adjacent cells and see how long we should be
+ while (this.get(x + chunk.width, y) === MAZE_CELL_WALL) {
+ this.set(x + chunk.width, y, MAZE_CELL_PLACED_WALL);
+ chunk.width++;
+ }
+ // Now lets see if we need to be t h i c c
+ outer: while (true) {
+ // Check the row below to see if we can still make a box
+ for (let i = 0; i < chunk.width; i++)
+ // Stop if we can't
+ if (this.get(x + i, y + chunk.height) !== MAZE_CELL_WALL) break outer;
+ // If we can, remove the line of cells from the map and increase the height of the block
+ for (let i = 0; i < chunk.width; i++)
+ this.set(x + i, y + chunk.height, MAZE_CELL_PLACED_WALL);
+ chunk.height++;
+ }
+ walls.push(chunk);
+ }
+ }
+ return walls;
+ }
+
+ public placeWalls(arena: ArenaEntity) {
+ const walls = this.convertToWalls();
+
+ for (const wall of walls) {
+ this.buildWallFromGridCoord(arena, wall.x, wall.y, wall.width, wall.height);
+ }
+ }
+
+ /** Creates a maze wall from cell coords */
+ protected buildWallFromGridCoord(
+ arena: ArenaEntity,
+ gridX: number,
+ gridY: number,
+ gridW: number,
+ gridH: number,
+ ): MazeWall {
+ const { x: minX, y: minY } = this.scaleGridToArenaPosition(arena, gridX, gridY);
+ const { x: maxX, y: maxY } = this.scaleGridToArenaPosition(arena, gridX + gridW, gridY + gridH);
+ return MazeWall.newFromBounds(arena.game, minX, minY, maxX, maxY);
+ }
+
+ /** Allows for easier (x, y) based getting of maze cells */
+ protected get(x: number, y: number): number {
+ return this.maze[y * this.config.size + x];
+ }
+
+ /** Checks if a cell is occupied on grid */
+ public isCellOccupied(x: number, y: number): boolean {
+ return this.get(x, y) === MAZE_CELL_PLACED_WALL;
+ }
+
+ /** Allows for easier (x, y) based setting of maze cells */
+ protected set(x: number, y: number, value: number): number {
+ return this.maze[y * this.config.size + x] = value;
+ }
+ /** Converts MAZE grid into an array of set and unset bits for ease of use */
+ protected mapValues(): [x: number, y: number, value: number][] {
+ const values: [x: number, y: number, value: number][] = Array(this.maze.length);
+ for (let i = 0; i < this.maze.length; ++i) values[i] = [i % this.config.size, Math.floor(i / this.config.size), this.maze[i]];
+ return values;
+ }
+
+ public scaleArenaToGridPosition(
+ arena: ArenaEntity,
+ x: number,
+ y: number,
+ ): { gridX: number, gridY: number } {
+ const gridCellWidth = arena.width / this.config.size;
+ const gridCellHeight = arena.height / this.config.size;
+ const gridX = (x + arena.width / 2) / gridCellWidth;
+ const gridY = (y + arena.height / 2) / gridCellHeight;
+ return { gridX, gridY };
+ }
+
+ public getGridCell(
+ arena: ArenaEntity,
+ x: number,
+ y: number,
+ ): { gridX: number, gridY: number } {
+ const { gridX, gridY } = this.scaleArenaToGridPosition(arena, x, y);
+
+ return {
+ gridX: Math.floor(gridX),
+ gridY: Math.floor(gridY),
+ }
+ }
+
+ public scaleGridToArenaPosition(
+ arena: ArenaEntity,
+ gridX: number,
+ gridY: number,
+ ): { x: number, y: number } {
+ const gridCellWidth = arena.width / this.config.size;
+ const gridCellHeight = arena.height / this.config.size;
+ const x = gridX * gridCellWidth + arena.arenaData.values.leftX;
+ const y = gridY * gridCellHeight + arena.arenaData.values.topY;
+ return { x, y };
+ }
+}
diff --git a/src/Entity/Shape/Manager.ts b/src/Misc/ShapeManager.ts
similarity index 91%
rename from src/Entity/Shape/Manager.ts
rename to src/Misc/ShapeManager.ts
index 680849ed..3ba52e5b 100644
--- a/src/Entity/Shape/Manager.ts
+++ b/src/Misc/ShapeManager.ts
@@ -16,15 +16,15 @@
along with this program. If not, see
*/
-import ArenaEntity from "../../Native/Arena";
-import GameServer from "../../Game";
-
-import Crasher from "./Crasher";
-import Pentagon from "./Pentagon";
-import Triangle from "./Triangle";
-import Square from "./Square";
-import AbstractShape from "./AbstractShape";
-import { removeFast } from "../../util";
+import ArenaEntity from "../Native/Arena";
+import GameServer from "../Game";
+
+import Crasher from "../Entity/Shape/Crasher";
+import Pentagon from "../Entity/Shape/Pentagon";
+import Triangle from "../Entity/Shape/Triangle";
+import Square from "../Entity/Shape/Square";
+import AbstractShape from "../Entity/Shape/AbstractShape";
+import { removeFast } from "../util";
/**
* Used to balance out shape count in the arena, as well
diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts
index 0397495c..80f4ff43 100644
--- a/src/Native/Arena.ts
+++ b/src/Native/Arena.ts
@@ -17,9 +17,11 @@
*/
import GameServer from "../Game";
-import ShapeManager from "../Entity/Shape/Manager";
+import ShapeManager from "../Misc/ShapeManager";
+import BossManager from "../Misc/BossManager";
import TankBody from "../Entity/Tank/TankBody";
import ArenaCloser from "../Entity/Misc/ArenaCloser";
+import AbstractBoss from "../Entity/Boss/AbstractBoss";
import { VectorAbstract } from "../Physics/Vector";
import { ArenaGroup, TeamGroup } from "./FieldGroups";
@@ -30,13 +32,6 @@ import { TeamGroupEntity } from "../Entity/Misc/TeamEntity";
import Client from "../Client";
-import AbstractBoss from "../Entity/Boss/AbstractBoss";
-import Guardian from "../Entity/Boss/Guardian";
-import Summoner from "../Entity/Boss/Summoner";
-import FallenOverlord from "../Entity/Boss/FallenOverlord";
-import FallenBooster from "../Entity/Boss/FallenBooster";
-import Defender from "../Entity/Boss/Defender";
-
import { countdownTicks, bossSpawningInterval, scoreboardUpdateInterval } from "../config";
export const enum ArenaState {
@@ -76,11 +71,8 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
public shapeScoreRewardMultiplier: number = 1;
- /** Enable or disable natural boss spawning */
- public allowBoss: boolean = true;
-
- /** The current boss spawned into the game */
- public boss: AbstractBoss | null = null;
+ /** The boss spawner. Set to null in gamemode file to disable boss spawning. */
+ public bossManager: BossManager | null = new BossManager(this);
/** Scoreboard leader */
public leader: TankBody | null = null;
@@ -134,27 +126,24 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
}
/**
- * Finds a spawnable location on the map.
+ * Finds a spawnable location on the map within the given width and height.
*/
- public findSpawnLocation(isPlayer: boolean=false): VectorAbstract {
+ public findSpawnLocation(width: number = this.width, height: number = this.height): VectorAbstract {
const pos = {
- x: ~~(Math.random() * this.width - this.width / 2),
- y: ~~(Math.random() * this.height - this.height / 2),
+ x: ~~(Math.random() * width - width / 2),
+ y: ~~(Math.random() * height - height / 2),
}
for (let i = 0; i < 20; ++i) {
- if (
- !this.isValidSpawnLocation(pos.x, pos.y) ||
- isPlayer && Math.max(pos.x, pos.y) < this.arenaData.values.rightX / 2 && Math.min(pos.x, pos.y) > this.arenaData.values.leftX / 2
- ) {
- pos.x = ~~(Math.random() * this.width - this.width / 2);
- pos.y = ~~(Math.random() * this.height - this.height / 2);
+ if (!this.isValidSpawnLocation(pos.x, pos.y)) {
+ pos.x = ~~(Math.random() * width - width / 2);
+ pos.y = ~~(Math.random() * height - height / 2);
continue;
}
// If there is any tank within 1000 units, find a new position
const entity = this.game.entities.collisionManager.getFirstMatch(pos.x, pos.y, 1000, 1000, (entity) => {
- if (!(entity instanceof TankBody)) return false;
+ if (!TankBody.isTank(entity) || !AbstractBoss.isBoss(entity)) return false;
const dX = entity.positionData.values.x - pos.x;
const dY = entity.positionData.values.y - pos.y;
@@ -163,8 +152,8 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
});
if (entity) {
- pos.x = ~~(Math.random() * this.width - this.width / 2);
- pos.y = ~~(Math.random() * this.height - this.height / 2);
+ pos.x = ~~(Math.random() * width - width / 2);
+ pos.y = ~~(Math.random() * height - height / 2);
continue;
}
@@ -174,6 +163,21 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
return pos;
}
+ public findPlayerSpawnLocation(): VectorAbstract {
+ let pos = this.findSpawnLocation();
+ for (let i = 0; i < 20; ++i) {
+ if (
+ Math.max(pos.x, pos.y) < this.arenaData.values.rightX / 2 &&
+ Math.min(pos.x, pos.y) > this.arenaData.values.leftX / 2
+ ) {
+ pos = this.findSpawnLocation(); // Players spawn away from the center
+ continue;
+ }
+ break;
+ }
+ return pos;
+ }
+
/** Checks if players or shapes can spawn at the given coordinates. */
public isValidSpawnLocation(x: number, y: number): boolean {
// Override in gamemode files
@@ -252,6 +256,10 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
// Otherwise, proceed as usual
client.createAndSpawnPlayer(name);
+ if (camera.cameraData.values.flags & CameraFlags.gameWaitingStart) { // Hide countdown screen
+ camera.cameraData.values.flags &= ~CameraFlags.gameWaitingStart;
+ }
+
// Remove this client from waiting list once this is done
this.game.clientsAwaitingSpawn.delete(client);
}
@@ -263,7 +271,7 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
for (const client of this.game.clients) {
const entity = client.camera?.cameraData.values.player;
- if (Entity.exists(entity) && entity instanceof TankBody) players.push(entity);
+ if (Entity.exists(entity) && TankBody.isTank(entity)) players.push(entity);
}
return players;
}
@@ -297,7 +305,7 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
* Allows the arena to decide how players are spawned into the game.
*/
public spawnPlayer(tank: TankBody, client: Client) {
- const { x, y } = this.findSpawnLocation(true);
+ const { x, y } = this.findPlayerSpawnLocation();
tank.positionData.values.x = x;
tank.positionData.values.y = y;
@@ -333,18 +341,6 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
this.state = ArenaState.OPEN;
}
- /** Spawns the boss into the arena */
- protected spawnBoss() {
- const TBoss = [Guardian, Summoner, FallenOverlord, FallenBooster, Defender]
- [~~(Math.random() * 5)];
-
- this.boss = new TBoss(this.game);
-
- const { x, y } = this.game.arena.findSpawnLocation();
- this.boss.positionData.values.x = x;
- this.boss.positionData.values.y = y;
- }
-
public tick(tick: number) {
this.shapes.tick();
this.updateArenaState();
@@ -355,8 +351,6 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity {
this.arenaData.leaderY = this.leader.positionData.values.y;
}
- if (this.allowBoss && this.game.tick >= 1 && (this.game.tick % bossSpawningInterval) === 0 && !this.boss) {
- this.spawnBoss();
- }
+ this.bossManager?.tick(tick);
}
}
diff --git a/src/Native/Camera.ts b/src/Native/Camera.ts
index 85dd8dce..fbb72a01 100644
--- a/src/Native/Camera.ts
+++ b/src/Native/Camera.ts
@@ -54,7 +54,7 @@ export class CameraEntity extends Entity {
this.cameraData.score = levelToScore(level);
const player = this.cameraData.values.player;
- if (Entity.exists(player) && player instanceof TankBody) {
+ if (TankBody.isTank(player)) {
player.scoreData.score = this.cameraData.values.score;
player.scoreReward = this.cameraData.values.score;
}
@@ -80,12 +80,12 @@ export class CameraEntity extends Entity {
public tick(tick: number) {
if (Entity.exists(this.cameraData.values.player)) {
const focus = this.cameraData.values.player;
- if (!(this.cameraData.values.flags & CameraFlags.usesCameraCoords) && focus instanceof ObjectEntity) {
+ if (!(this.cameraData.values.flags & CameraFlags.usesCameraCoords) && ObjectEntity.isObject(focus)) {
this.cameraData.cameraX = focus.rootParent.positionData.values.x;
this.cameraData.cameraY = focus.rootParent.positionData.values.y;
}
- if (this.cameraData.values.player instanceof TankBody) {
+ if (TankBody.isTank(this.cameraData.values.player)) {
// Update player related data
const player = this.cameraData.values.player as TankBody;
@@ -204,7 +204,7 @@ export default class ClientCamera extends CameraEntity {
entity.positionData.values.x + width > l &&
entity.positionData.values.y - size < b
) {
- if (entity !== this.cameraData.values.player &&!(entity.styleData.values.opacity === 0 && !entity.deletionAnimation)) {
+ if (entity !== this.cameraData.values.player && entity.styleData.values.opacity !== 0) { // Invisible tanks shouldn't be sent
entitiesInRange.push(entity);
}
}
@@ -217,11 +217,11 @@ export default class ClientCamera extends CameraEntity {
if (!entitiesInRange.includes(entity)) entitiesInRange.push(entity);
}
- if (Entity.exists(this.cameraData.values.player) && this.cameraData.values.player instanceof ObjectEntity) entitiesInRange.push(this.cameraData.values.player);
+ if (Entity.exists(this.cameraData.values.player)) entitiesInRange.push(this.cameraData.values.player as ObjectEntity);
for (let i = 0; i < this.view.length; ++i) {
const entity = this.view[i]
- if (entity instanceof ObjectEntity) {
+ if (ObjectEntity.isObject(entity)) {
// TODO(speed)
// Orphan children must be destroyed
if (!entitiesInRange.includes(entity.rootParent)) {
@@ -260,10 +260,10 @@ export default class ClientCamera extends CameraEntity {
const entity = entities.inner[id];
if (!entity) continue;
- if (entity instanceof CameraEntity) continue;
- creations.push(entity);
+ if (entity.cameraData) continue;
+ creations.push(entity);
this.addToView(entity);
}
}
@@ -273,7 +273,7 @@ export default class ClientCamera extends CameraEntity {
creations.push(entity);
this.addToView(entity);
- if (entity instanceof ObjectEntity) {
+ if (ObjectEntity.isObject(entity)) {
if (entity.children.length && !entity.isChild) {
// add any of its children
this.view.push.apply(this.view, entity.children);
@@ -319,7 +319,7 @@ export default class ClientCamera extends CameraEntity {
public tick(tick: number) {
super.tick(tick);
- if (!Entity.exists(this.cameraData.values.player) || !(this.cameraData.values.player instanceof TankBody)) {
+ if (!Entity.exists(this.cameraData.values.player)) {
if (Entity.exists(this.spectatee)) {
const pos = this.spectatee.rootParent.positionData.values;
this.cameraData.cameraX = pos.x;
@@ -327,7 +327,6 @@ export default class ClientCamera extends CameraEntity {
this.cameraData.flags |= CameraFlags.usesCameraCoords;
}
}
-
// always last
this.updateView(tick);
}
diff --git a/src/Native/Entity.TEMPLATE b/src/Native/Entity.TEMPLATE
index dba0ffcf..1991138f 100644
--- a/src/Native/Entity.TEMPLATE
+++ b/src/Native/Entity.TEMPLATE
@@ -41,7 +41,9 @@ export class Entity {
* Determines if the first parameter is an entity and not a deleted one.
*/
public static exists(entity: Entity | null | undefined): entity is Entity {
- return entity instanceof Entity && entity.hash !== 0
+ if (!entity) return false;
+
+ return (entity as Entity).hash !== 0;
}
/**
diff --git a/src/Native/Entity.ts b/src/Native/Entity.ts
index 6be14e07..2af06e19 100644
--- a/src/Native/Entity.ts
+++ b/src/Native/Entity.ts
@@ -40,7 +40,9 @@ export class Entity {
* Determines if the first parameter is an entity and not a deleted one.
*/
public static exists(entity: Entity | null | undefined): entity is Entity {
- return entity instanceof Entity && entity.hash !== 0
+ if (!entity) return false;
+
+ return (entity as Entity).hash !== 0;
}
/**
diff --git a/src/Native/Manager.ts b/src/Native/Manager.ts
index 11e8686c..aa32cc11 100644
--- a/src/Native/Manager.ts
+++ b/src/Native/Manager.ts
@@ -17,7 +17,9 @@
*/
import GameServer from "../Game";
+
import ObjectEntity from "../Entity/Object";
+import LivingEntity from "../Entity/Live";
import CollisionManager from "../Physics/CollisionManager";
import HashGrid from "../Physics/HashGrid";
@@ -26,7 +28,7 @@ import { CameraEntity } from "./Camera";
import { Entity } from "./Entity";
import { AI } from "../Entity/AI";
import { removeFast } from "../util";
-import LivingEntity from "../Entity/Live";
+import { EntityTags } from "../Const/Enums";
/**
* Manages all entities in the game.
@@ -68,14 +70,23 @@ export default class EntityManager {
for (let id = 0; id <= lastId; ++id) {
if (this.inner[id]) continue;
+ // We found a free id
entity.id = id;
entity.hash = entity.preservedHash = this.hashTable[id] += 1;
this.inner[id] = entity;
-
+ // Classify entity so that we know what to send to client
if (this.collisionManager && entity instanceof ObjectEntity) {
- } else if (entity instanceof CameraEntity) this.cameras.push(id);
- else this.otherEntities.push(id);
+ // ObjectEntitys need no special handling here
+ // they added to collisionManager in preTick
+ } else if (entity instanceof CameraEntity) {
+ // CameraEntitys entities go into the camera list
+ this.cameras.push(id);
+ } else {
+ // Anything else is stored in otherEntities
+ // (this will be removed soon as it is practically unused)
+ this.otherEntities.push(id);
+ }
if (this.lastId < id) this.lastId = entity.id;
@@ -92,9 +103,9 @@ export default class EntityManager {
if (!entity) throw new RangeError("Deleting entity that isn't in the game?");
entity.hash = 0;
- if (this.collisionManager && entity instanceof ObjectEntity) {
+ if (this.collisionManager && ObjectEntity.isObject(entity)) {
// Nothing I guess
- } else if (entity instanceof CameraEntity) removeFast(this.cameras, this.cameras.indexOf(id));
+ } else if (entity.cameraData) removeFast(this.cameras, this.cameras.indexOf(id));
else removeFast(this.otherEntities, this.otherEntities.indexOf(id));
// TODO(speed)[not super important]:
@@ -126,46 +137,59 @@ export default class EntityManager {
ObjectEntity.handleCollision(entityA, entityB);
if (
- entityA instanceof LivingEntity &&
- entityB instanceof LivingEntity
+ LivingEntity.isLive(entityA) &&
+ LivingEntity.isLive(entityB)
) {
LivingEntity.handleCollision(entityA, entityB);
}
}.bind(this);
+
+ public preTick(tick: number) {
+ this.collisionManager.preTick(tick);
- /** Ticks all entities in the game. */
- public tick(tick: number) {
while (!this.inner[this.lastId] && this.lastId >= 0) {
this.lastId -= 1;
}
- scanner: for (let id = 0; id <= this.lastId; ++id) {
+ for (let id = 0; id <= this.lastId; ++id) {
const entity = this.inner[id];
if (!Entity.exists(entity)) continue;
- if (entity instanceof ObjectEntity && entity.isPhysical) {
+ if (ObjectEntity.isObject(entity) && entity.isPhysical) {
this.collisionManager.insert(entity);
}
}
+ }
- this.collisionManager.forEachCollisionPair(this.handleCollision)
+ public postTick(tick: number) {
+ this.collisionManager.postTick(tick);
for (let id = 0; id <= this.lastId; ++id) {
const entity = this.inner[id];
- if (entity && entity instanceof ObjectEntity && entity.isPhysical) {
- entity.applyPhysics();
+ if (entity) {
+ entity.wipeState();
}
}
+ }
+
+ /** Ticks all entities in the game. */
+ public tick(tick: number) {
+ this.collisionManager.forEachCollisionPair(this.handleCollision);
for (let id = 0; id <= this.lastId; ++id) {
- const entity = this.inner[id];
+ const entity = this.inner[id] as ObjectEntity;
- if (!Entity.exists(entity)) continue;
+ // Alternatively, Entity.exists(entity), though this is probably faster.
+ if (!entity || entity.hash === 0) continue;
+
+ if (entity.isPhysical) {
+ entity.applyPhysics();
+ }
- if (!(entity instanceof CameraEntity)) {
- if (!(entity instanceof ObjectEntity) || !entity.isChild) entity.tick(tick);
+ if (!entity.isChild) {
+ entity.tick(tick);
}
}
@@ -180,13 +204,5 @@ export default class EntityManager {
for (let i = 0; i < this.cameras.length; ++i) {
(this.inner[this.cameras[i]] as CameraEntity).tick(tick);
}
-
- for (let id = 0; id <= this.lastId; ++id) {
- const entity = this.inner[id];
-
- if (entity) {
- entity.wipeState();
- }
- }
}
}
diff --git a/src/Physics/HashGrid.ts b/src/Physics/HashGrid.ts
index cb18343e..de444af5 100644
--- a/src/Physics/HashGrid.ts
+++ b/src/Physics/HashGrid.ts
@@ -234,7 +234,6 @@ export default class HashGrid implements CollisionManager {
// Ensure (x, y) -> x.id < y.id
callback(entityB, entityA);
}
-
}
}
}
diff --git a/src/config.ts b/src/config.ts
index 295e88e0..0d44cdb5 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -41,11 +41,14 @@ export const writtenBufferChunkSize = Buffer.poolSize || 2048;
export const host: string = process.env.SERVER_INFO || "unknown";
/** Runtime mode. */
-export const mode: string = process.env.NODE_ENV || "development";
+export const mode: string = process.env.NODE_ENV || "production";
/** How long the countdown should last until the game is started. By default it is 10 seconds. Set to 0 if you wish to disable this. */
export const countdownTicks = 10 * tps;
+/** Chance for a shape to spawn as shiny (green) */
+export const shinyChance: number = 1 / 1_000_000;
+
/** Is hosting a rest api */
export const enableApi: boolean = true;
@@ -80,7 +83,7 @@ export const hashGridCellSize: number = 7;
export const bossSpawningInterval = 45 * 60 * tps;
/** Amount of TICKs before the scoreboard update */
-export const scoreboardUpdateInterval = 0.5 * tps;
+export const scoreboardUpdateInterval = 1 * tps;
/** Hashed (sha256) dev password */
export const devPasswordHash: string | undefined = process.env.DEV_PASSWORD_HASH;
diff --git a/src/util.ts b/src/util.ts
index 3a3b4c2e..1fc10421 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -24,11 +24,13 @@ import { doVerboseLogs } from "./config";
export const log = (...args: any[]) => {
console.log(`[${Date().split(" ")[4]}]`, ...args)
}
+
/** Logs data prefixed with the Date in a yellow format. */
export const warn = (...args: any[]) => {
args = args.map(s => typeof s === "string" ? chalk.yellow(s) : s);
console.log(chalk.yellow(`[${Date().split(" ")[4]}] WARNING: `), ...args);
}
+
/** Logs a raw object. */
export const inspectLog = (object: any, c = 14) => {
console.log(inspect(object, false, c, true));
@@ -46,7 +48,7 @@ export const removeFast = (array: any[], index: number) => {
}
/**
- * Self explanatory
+ * Shuffles an array in place.
*/
export const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i >= 1; i--) {
@@ -55,10 +57,17 @@ export const shuffleArray = (array: any[]) => {
}
}
+/**
+ * Picks a random element from an array.
+ */
+export const randomFrom = (array: T[]): T => {
+ return array[Math.floor(Math.random() * array.length)];
+}
+
/**
* Contrains a value between bounds
*/
-export const constrain = (value: number, min: number, max: number) => {
+export const constrain = (value: number, min: number, max: number): number => {
return Math.max(min, Math.min(max, value));
}
@@ -68,7 +77,7 @@ export const PI2 = Math.PI * 2;
/**
* Normalize angle (ex: 4π-> 0π, 3π -> 1π)
*/
-export const normalizeAngle = (angle: number) => {
+export const normalizeAngle = (angle: number): number => {
return ((angle % PI2) + PI2) % PI2;
}
diff --git a/tsup.config.ts b/tsup.config.ts
index 630c0582..f1ef81cc 100644
--- a/tsup.config.ts
+++ b/tsup.config.ts
@@ -9,5 +9,6 @@ export default defineConfig({
sourcemap: false,
minify: false,
skipNodeModulesBundle: true,
+ onSuccess: 'npm run check',
external: ['uWebSockets.js']
})
\ No newline at end of file