From e18e8e669efb6d3a7cf50cbc219961590e5dc39b Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:40:15 +0300 Subject: [PATCH 01/65] Fix reload animation when reload stat is upgraded --- src/Entity/Tank/Barrel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Tank/Barrel.ts b/src/Entity/Tank/Barrel.ts index 314bfecd..e76a41cb 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'); From 7caef81f5a6bac8fce0f665692a35ebd6ff89fba Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 22 Oct 2025 05:49:17 +0300 Subject: [PATCH 02/65] Add EntityManager pretick and posttick functions --- src/Game.ts | 5 ++--- src/Native/Manager.ts | 30 +++++++++++++++++++----------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index 06ff1871..fcf6840c 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -219,14 +219,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/Native/Manager.ts b/src/Native/Manager.ts index 11e8686c..723593d2 100644 --- a/src/Native/Manager.ts +++ b/src/Native/Manager.ts @@ -132,14 +132,15 @@ export default class EntityManager { 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; @@ -148,7 +149,22 @@ export default class EntityManager { this.collisionManager.insert(entity); } } + } + + public postTick(tick: number) { + this.collisionManager.postTick(tick); + + for (let id = 0; id <= this.lastId; ++id) { + const entity = this.inner[id]; + + 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) { @@ -180,13 +196,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(); - } - } } } From aeaa4e1235fbcc5cde81c4718ce6c2bc9c72d3b8 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:00:07 +0300 Subject: [PATCH 03/65] Remove unused gamemode imports --- src/Game.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index fcf6840c..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 } /** From 27aff6c80ae78c3f2d606afb6c0f13b5727be075 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:03:58 -0400 Subject: [PATCH 04/65] fix: enforce types pre-build --- package.json | 6 +++--- tsup.config.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) 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/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 From 50e76d1bea219f9263a52e7743f4699db97682c4 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:26:02 -0400 Subject: [PATCH 05/65] docs: add addAccel deprecation notice --- src/Entity/Object.ts | 19 +++++++++++++------ src/Entity/Tank/Barrel.ts | 2 +- src/Entity/Tank/Projectile/Bullet.ts | 2 +- src/Gamemodes/Misc/FactoryTest.ts | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index 498be88e..ba7f1370 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -222,11 +222,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 +291,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); } } diff --git a/src/Entity/Tank/Barrel.ts b/src/Entity/Tank/Barrel.ts index e76a41cb..fe322db6 100644 --- a/src/Entity/Tank/Barrel.ts +++ b/src/Entity/Tank/Barrel.ts @@ -154,7 +154,7 @@ 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; 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/Gamemodes/Misc/FactoryTest.ts b/src/Gamemodes/Misc/FactoryTest.ts index 0ec01150..a87f24de 100644 --- a/src/Gamemodes/Misc/FactoryTest.ts +++ b/src/Gamemodes/Misc/FactoryTest.ts @@ -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) { From d09036e6798f1440d4e8e4ebe0c0571ad1eaf113 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:26:10 -0400 Subject: [PATCH 06/65] feat: keepInArena sub method --- src/Entity/Object.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index ba7f1370..8a1f0cd1 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -353,24 +353,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(); } } } From d92dbe6ee6c86c364a6d967912a9844d4dd4d9ee Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:41:32 +0300 Subject: [PATCH 07/65] chore: code formatting --- src/Entity/Boss/Defender.ts | 4 ++-- src/Entity/Boss/FallenBooster.ts | 2 +- src/Entity/Boss/FallenOverlord.ts | 2 +- src/Entity/Boss/Guardian.ts | 2 +- src/Entity/Boss/Summoner.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) 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..b5003405 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; From 723d89e7726f06d6819ff6fe536e34cd84e3c410 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:42:22 +0300 Subject: [PATCH 08/65] Typo --- src/Entity/Boss/FallenBooster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Boss/FallenBooster.ts b/src/Entity/Boss/FallenBooster.ts index b5003405..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, {}); From b10329002c05515c11911200268578e26f5b41b1 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:42:55 +0300 Subject: [PATCH 09/65] Formatting --- src/Entity/Misc/Boss/FallenAC.ts | 2 +- src/Entity/Misc/Boss/FallenMegaTrapper.ts | 6 +++--- src/Entity/Misc/Boss/FallenSpike.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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; From 16887eec436d7acd76091419be28f69b66f05d75 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:59:54 +0300 Subject: [PATCH 10/65] Remove code from old version --- src/Client.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index ecb3abf6..ef98ab18 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -553,8 +553,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) { From 6f560646a3b14661ae88c66c347e570957759ffd Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 22 Oct 2025 20:00:21 +0300 Subject: [PATCH 11/65] Let manageCountdown deal with this instead Hide countdown screen when game is waiting to start. --- src/Native/Arena.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 0397495c..1e8e29f3 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -252,6 +252,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); } From b95da7fda8d4dbb91d0cecd7bba11122b9914f00 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 23 Oct 2025 06:55:29 +0300 Subject: [PATCH 12/65] Disable countdown for debug mode --- src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 295e88e0..50563310 100644 --- a/src/config.ts +++ b/src/config.ts @@ -41,10 +41,10 @@ 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; +export const countdownTicks = (mode === "development" ? 0 : 10) * tps; /** Is hosting a rest api */ export const enableApi: boolean = true; From 3745219b6ea2109ae748e247fce5060fbb487a6a Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:28:48 +0300 Subject: [PATCH 13/65] Fix skimmer bullet angles --- src/Entity/Tank/Projectile/Skimmer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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; From a97ad1db9cec8374859cbdbde4099862687129b5 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:40:16 +0300 Subject: [PATCH 14/65] Cleanup arena scaling --- src/Gamemodes/Sandbox.ts | 12 ++++++++---- src/Gamemodes/Survival.ts | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Gamemodes/Sandbox.ts b/src/Gamemodes/Sandbox.ts index 31661a09..b36e2cdb 100644 --- a/src/Gamemodes/Sandbox.ts +++ b/src/Gamemodes/Sandbox.ts @@ -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..595cff58 100644 --- a/src/Gamemodes/Survival.ts +++ b/src/Gamemodes/Survival.ts @@ -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() { From d57639b8fb1f05b13e262700c39424dc6bb41438 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:45:55 +0200 Subject: [PATCH 15/65] Mothership HP tiny fix --- src/Entity/Misc/Mothership.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Misc/Mothership.ts b/src/Entity/Misc/Mothership.ts index 8156c112..49037c8a 100644 --- a/src/Entity/Misc/Mothership.ts +++ b/src/Entity/Misc/Mothership.ts @@ -72,7 +72,7 @@ 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 { From 8faed7b7b5876732ea0934ae0ad6cf9dbe5e3f77 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:47:40 +0200 Subject: [PATCH 16/65] Comment --- src/Gamemodes/Survival.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gamemodes/Survival.ts b/src/Gamemodes/Survival.ts index 595cff58..32cf9656 100644 --- a/src/Gamemodes/Survival.ts +++ b/src/Gamemodes/Survival.ts @@ -26,7 +26,7 @@ import ShapeManager from "../Entity/Shape/Manager"; 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 From 7fc81ec8a7388c00aee3a287d84358c6593e1f9f Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Mon, 3 Nov 2025 00:45:44 +0200 Subject: [PATCH 17/65] Remove broken call --- src/Gamemodes/Tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts index 3c82d99e..5695d7d6 100644 --- a/src/Gamemodes/Tag.ts +++ b/src/Gamemodes/Tag.ts @@ -89,7 +89,6 @@ export default class TagArena extends ArenaEntity { } public spawnPlayer(tank: TankBody, client: Client) { - this.updateArenaState(); const deathMixin = tank.onDeath.bind(tank); tank.onDeath = (killer: LivingEntity) => { deathMixin(killer); @@ -196,3 +195,4 @@ export default class TagArena extends ArenaEntity { } } + From 549d4f33b9ef76ea106081c58f4852f64c129c5a Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:35:56 +0200 Subject: [PATCH 18/65] Remove unused shape manager in 2 teams --- src/Gamemodes/Team2.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Gamemodes/Team2.ts b/src/Gamemodes/Team2.ts index 45f28467..6e6521a7 100644 --- a/src/Gamemodes/Team2.ts +++ b/src/Gamemodes/Team2.ts @@ -39,13 +39,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(); From 214668687eda3ea5b5452bd281f33e8ed50800bc Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:49:21 +0200 Subject: [PATCH 19/65] speedup maze walls slightly --- src/Entity/Misc/MazeWall.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Entity/Misc/MazeWall.ts b/src/Entity/Misc/MazeWall.ts index 74716ff8..73d49396 100644 --- a/src/Entity/Misc/MazeWall.ts +++ b/src/Entity/Misc/MazeWall.ts @@ -46,4 +46,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 } From 7cc7a3bdfa64d562405b910e7b151be60e7dc051 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:26:13 -0500 Subject: [PATCH 20/65] fix: ignore repeated keys to prevent client fps drop --- client/input.js | 1 + 1 file changed, 1 insertion(+) 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); From f5c563dad48b27c7c9c1aec536e6521e6e3cd832 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:12:37 +0200 Subject: [PATCH 21/65] Maze generator is now its own file --- src/Systems/MazeGenerator.ts | 212 +++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/Systems/MazeGenerator.ts diff --git a/src/Systems/MazeGenerator.ts b/src/Systems/MazeGenerator.ts new file mode 100644 index 00000000..c19ec363 --- /dev/null +++ b/src/Systems/MazeGenerator.ts @@ -0,0 +1,212 @@ +import ArenaEntity from "../Native/Arena"; +import MazeWall from "../Entity/Misc/MazeWall"; +import { VectorAbstract } from "../Physics/Vector"; + +export interface MazeGeneratorConfig { + CELL_SIZE: number, + GRID_SIZE: number, + SEED_AMOUNT: number, + TURN_CHANCE: number, + BRANCH_CHANCE: number, + TERMINATION_CHANCE: number +} + +/** + * 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 { + public arena: ArenaEntity; + + public config: MazeGeneratorConfig; + + /** Stores all the "seed"s */ + public SEEDS: VectorAbstract[] = []; + /** Stores all the "wall"s, contains cell based coords */ + public WALLS: (VectorAbstract & {width: number, height: number})[] = []; + /** Rolled out matrix of the grid */ + public MAZE: Uint8Array; + + constructor(arena: ArenaEntity, config: MazeGeneratorConfig) { + this.arena = arena; + + this.config = config; + + this.MAZE = new Uint8Array(config.GRID_SIZE * config.GRID_SIZE); + } + /** Creates a maze wall from cell coords */ + public _buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) { + const scaledW = gridW * this.config.CELL_SIZE; + const scaledH = gridH * this.config.CELL_SIZE; + const scaledX = gridX * this.config.CELL_SIZE - this.arena.width / 2 + (scaledW / 2); + const scaledY = gridY * this.config.CELL_SIZE - this.arena.height / 2 + (scaledH / 2); + new MazeWall(this.arena.game, scaledX, scaledY, scaledH, scaledW); + } + /** Allows for easier (x, y) based getting of maze cells */ + public _get(x: number, y: number): number { + return this.MAZE[y * this.config.GRID_SIZE + x]; + } + /** Allows for easier (x, y) based setting of maze cells */ + public _set(x: number, y: number, value: number): number { + return this.MAZE[y * this.config.GRID_SIZE + x] = value; + } + /** Converts MAZE grid into an array of set and unset bits for ease of use */ + public _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.GRID_SIZE, Math.floor(i / this.config.GRID_SIZE), this.MAZE[i]]; + return values; + } + /** Builds the maze */ + public buildMaze() { + // Plant some seeds + for (let i = 0; i < 10000; i++) { + // Stop if we exceed our maximum seed amount + if (this.SEEDS.length >= this.config.SEED_AMOUNT) break; + // Attempt a seed planting + let seed: VectorAbstract = { + x: Math.floor((Math.random() * this.config.GRID_SIZE) - 1), + y: Math.floor((Math.random() * this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.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 >= this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.GRID_SIZE - 1) break; + this._set(seed.x, seed.y, 1); + // Now lets see if we want to branch or turn + if (Math.random() <= this.config.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() <= this.config.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() * this.config.GRID_SIZE) - 1), + y: Math.floor((Math.random() * this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.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 * this.config.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 < this.config.GRID_SIZE; x++) { + for (let y = 0; y < this.config.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); + } + } + public isInWall(x: number, y: number): boolean { + const width = this.arena.width / 2 + const height = this.arena.height / 2 + for (const wall of this.WALLS) { + const wallX = wall.x * this.config.CELL_SIZE - width; + const wallY = wall.y * this.config.CELL_SIZE - height; + const wallW = wall.width * this.config.CELL_SIZE; + const wallH = wall.height * this.config.CELL_SIZE; + if ( + x >= wallX && + x <= wallX + wallW && + y >= wallY && + y <= wallY + wallH + ) { + return true; + } + } + return false; + } +} \ No newline at end of file From 5e346fa83faaeafe28b13b0e6ba8c8b1843310d9 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:13:22 +0200 Subject: [PATCH 22/65] Maze cleanup --- src/Gamemodes/Maze.ts | 214 ++++-------------------------------------- 1 file changed, 19 insertions(+), 195 deletions(-) diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts index 2a4c3ecb..aa15ee9f 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -15,10 +15,9 @@ 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 "../Systems/MazeGenerator"; import ShapeManager from "../Entity/Shape/Manager"; @@ -33,215 +32,40 @@ 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 config: MazeGeneratorConfig = { + CELL_SIZE: 635, + GRID_SIZE: 40, + SEED_AMOUNT: Math.floor(Math.random() * 30) + 30, + TURN_CHANCE: 0.2, + BRANCH_CHANCE: 0.2, + TERMINATION_CHANCE: 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(this, config); public constructor(game: GameServer) { super(game); - this.updateBounds(ARENA_SIZE, ARENA_SIZE); + + const arenaSize = config.CELL_SIZE * config.GRID_SIZE + this.updateBounds(arenaSize, arenaSize); + + this.mazeGenerator.buildMaze(); + 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); } public isValidSpawnLocation(x: number, y: number): boolean { // 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.isInWall(x, y); } } From 5d1aebd994944f0a07ba1bf1246aba599bfa33e3 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:13:13 +0200 Subject: [PATCH 23/65] comments --- src/Systems/MazeGenerator.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Systems/MazeGenerator.ts b/src/Systems/MazeGenerator.ts index c19ec363..3a480455 100644 --- a/src/Systems/MazeGenerator.ts +++ b/src/Systems/MazeGenerator.ts @@ -18,10 +18,10 @@ export interface MazeGeneratorConfig { * - Split into its own file on Wednesday 3rd of December 2025 */ export default class MazeGenerator { + /** The arena to place the walls in */ public arena: ArenaEntity; - + /** The variables that affect maze generation */ public config: MazeGeneratorConfig; - /** Stores all the "seed"s */ public SEEDS: VectorAbstract[] = []; /** Stores all the "wall"s, contains cell based coords */ @@ -190,9 +190,12 @@ export default class MazeGenerator { this._buildWallFromGridCoord(x, y, width, height); } } + + /** Checks if the given coordinates overlap with any wall */ public isInWall(x: number, y: number): boolean { - const width = this.arena.width / 2 - const height = this.arena.height / 2 + const width = this.arena.width / 2; + const height = this.arena.height / 2; + for (const wall of this.WALLS) { const wallX = wall.x * this.config.CELL_SIZE - width; const wallY = wall.y * this.config.CELL_SIZE - height; @@ -209,4 +212,4 @@ export default class MazeGenerator { } return false; } -} \ No newline at end of file +} From 9fb08c1cebdd35e75d3e45bbea4420b12ed0badd Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:16:33 +0200 Subject: [PATCH 24/65] init --- src/Client.ts | 15 +++++++-------- src/Const/Enums.ts | 17 +++++++++++++++-- src/Entity/Boss/AbstractBoss.ts | 20 +++++++++++++++----- src/Entity/Live.ts | 9 ++++++++- src/Entity/Misc/Dominator.ts | 10 ++++++---- src/Entity/Misc/Mothership.ts | 4 ++-- src/Entity/Misc/TeamEntity.ts | 6 ++++++ src/Entity/Object.ts | 17 ++++++++++++++++- src/Entity/Shape/AbstractShape.ts | 12 +++++++++++- src/Entity/Shape/Crasher.ts | 2 ++ src/Entity/Shape/Pentagon.ts | 8 ++++++-- src/Entity/Shape/Square.ts | 9 +++++++-- src/Entity/Shape/Triangle.ts | 8 ++++++-- src/Entity/Tank/Addons.ts | 2 +- src/Entity/Tank/Barrel.ts | 2 +- src/Entity/Tank/Projectile/Minion.ts | 2 ++ src/Entity/Tank/TankBody.ts | 18 +++++++++++++----- src/Gamemodes/Maze.ts | 1 + src/Gamemodes/Mothership.ts | 2 +- src/Gamemodes/Team2.ts | 20 ++++++++++++++++++++ src/Native/Arena.ts | 4 ++-- src/Native/Camera.ts | 21 ++++++++++----------- src/Native/Entity.TEMPLATE | 4 +++- src/Native/Entity.ts | 4 +++- src/Native/Manager.ts | 23 +++++++++++++---------- src/Physics/HashGrid.ts | 1 - src/Systems/MazeGenerator.ts | 2 +- src/config.ts | 5 ++++- src/index.ts | 6 +++--- 29 files changed, 185 insertions(+), 69 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index ef98ab18..090d46d9 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 (!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; diff --git a/src/Const/Enums.ts b/src/Const/Enums.ts index 8383d0f1..6f08502f 100644 --- a/src/Const/Enums.ts +++ b/src/Const/Enums.ts @@ -40,8 +40,9 @@ export const enum Color { EnemyTank = 15, NecromancerSquare = 16, Fallen = 17, + EnemyHexagon = 18, - kMaxColors = 18 + kMaxColors = 19 } /** @@ -66,6 +67,7 @@ export const ColorsHexCode: Record = { [Color.EnemyTank]: 0xF14E54, [Color.NecromancerSquare]: 0xFCC376, [Color.Fallen]: 0xC0C0C0, + [Color.EnemyHexagon]: 0xFCAD76, [Color.kMaxColors]: 0x000000 } @@ -281,6 +283,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). @@ -306,4 +319,4 @@ export function levelToScore(level: number): number { if (level <= 0) return 0; return levelToScoreTable[level - 1]; -} +} \ No newline at end of file diff --git a/src/Entity/Boss/AbstractBoss.ts b/src/Entity/Boss/AbstractBoss.ts index 1390f1bf..37e0a124 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() { @@ -158,8 +168,8 @@ export default class AbstractBoss extends LivingEntity { 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)) { + killerName = killer.nameData.values.name; } else { killerName = "an unnamed tank"; } diff --git a/src/Entity/Live.ts b/src/Entity/Live.ts index 33af33d7..7cb8b5c8 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) { diff --git a/src/Entity/Misc/Dominator.ts b/src/Entity/Misc/Dominator.ts index 0e7afd73..72048534 100644 --- a/src/Entity/Misc/Dominator.ts +++ b/src/Entity/Misc/Dominator.ts @@ -100,10 +100,12 @@ export default class Dominator extends TankBody { } 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/Mothership.ts b/src/Entity/Misc/Mothership.ts index 49037c8a..8f64e6d5 100644 --- a/src/Entity/Misc/Mothership.ts +++ b/src/Entity/Misc/Mothership.ts @@ -79,10 +79,10 @@ export default class Mothership extends TankBody { 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; diff --git a/src/Entity/Misc/TeamEntity.ts b/src/Entity/Misc/TeamEntity.ts index 391a29d4..ba47bea8 100644 --- a/src/Entity/Misc/TeamEntity.ts +++ b/src/Entity/Misc/TeamEntity.ts @@ -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 8a1f0cd1..0d5b2586 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) { 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..2d5fda57 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 fe322db6..e3e72f98 100644 --- a/src/Entity/Tank/Barrel.ts +++ b/src/Entity/Tank/Barrel.ts @@ -158,7 +158,7 @@ export default class Barrel extends ObjectEntity { 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/Minion.ts b/src/Entity/Tank/Projectile/Minion.ts index 809edab9..583a31de 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/TankBody.ts b/src/Entity/Tank/TankBody.ts index 6f78be41..8c27639d 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,7 +196,7 @@ 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); @@ -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); } } } @@ -366,4 +374,4 @@ export default class TankBody extends LivingEntity implements BarrelBase { y: 0 }); } -} +} \ No newline at end of file diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts index aa15ee9f..af2a385c 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -15,6 +15,7 @@ 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 GameServer from "../Game"; import MazeGenerator, { MazeGeneratorConfig } from "../Systems/MazeGenerator"; diff --git a/src/Gamemodes/Mothership.ts b/src/Gamemodes/Mothership.ts index 6fde254d..94b1c9d9 100644 --- a/src/Gamemodes/Mothership.ts +++ b/src/Gamemodes/Mothership.ts @@ -106,7 +106,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/Team2.ts b/src/Gamemodes/Team2.ts index 6e6521a7..dfeba8f9 100644 --- a/src/Gamemodes/Team2.ts +++ b/src/Gamemodes/Team2.ts @@ -26,9 +26,20 @@ import TankBody from "../Entity/Tank/TankBody"; import { TeamEntity } from "../Entity/Misc/TeamEntity"; import { Color } from "../Const/Enums"; +import MazeGenerator, { MazeGeneratorConfig } from "../Systems/MazeGenerator"; + const arenaSize = 11150; const baseWidth = arenaSize / (3 + 1/3) * 0.6; // 2007 +const config: MazeGeneratorConfig = { + CELL_SIZE: arenaSize * 2 / 40, + GRID_SIZE: 40, + SEED_AMOUNT: Math.floor(Math.random() * 30) + 30, + TURN_CHANCE: 0.2, + BRANCH_CHANCE: 0.2, + TERMINATION_CHANCE: 0.2 +} + /** * Teams2 Gamemode Arena */ @@ -39,6 +50,9 @@ export default class Teams2Arena extends ArenaEntity { public blueTeamBase: TeamBase; /** Red Team entity */ public redTeamBase: TeamBase; + + public mazeGenerator: MazeGenerator = new MazeGenerator(this, config); + /** Maps clients to their teams */ public playerTeamMap: WeakMap = new WeakMap(); @@ -47,6 +61,7 @@ export default class Teams2Arena extends ArenaEntity { this.updateBounds(arenaSize * 2, arenaSize * 2); this.blueTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamBlue), -arenaSize + baseWidth / 2, 0, arenaSize * 2, baseWidth, true, 12, 2); this.redTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamRed), arenaSize - baseWidth / 2, 0, arenaSize * 2, baseWidth, true, 12, 2); + this.mazeGenerator.buildMaze(); } public spawnPlayer(tank: TankBody, client: Client) { @@ -62,4 +77,9 @@ export default class Teams2Arena extends ArenaEntity { if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team; } + + public isValidSpawnLocation(x: number, y: number): boolean { + // Should never spawn inside walls + return !this.mazeGenerator.isInWall(x, y); + } } diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 1e8e29f3..5d13cc4a 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -154,7 +154,7 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity { // 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; @@ -267,7 +267,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; } 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 723593d2..3dfdaff1 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. @@ -71,9 +73,10 @@ export default class EntityManager { entity.id = id; entity.hash = entity.preservedHash = this.hashTable[id] += 1; this.inner[id] = entity; - + // TODO figure out why instanceof is neccessary for this if (this.collisionManager && entity instanceof ObjectEntity) { + // Nothing } else if (entity instanceof CameraEntity) this.cameras.push(id); else this.otherEntities.push(id); @@ -92,9 +95,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,8 +129,8 @@ 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); } @@ -145,7 +148,7 @@ export default class EntityManager { if (!Entity.exists(entity)) continue; - if (entity instanceof ObjectEntity && entity.isPhysical) { + if (ObjectEntity.isObject(entity) && entity.isPhysical) { this.collisionManager.insert(entity); } } @@ -170,7 +173,7 @@ export default class EntityManager { for (let id = 0; id <= this.lastId; ++id) { const entity = this.inner[id]; - if (entity && entity instanceof ObjectEntity && entity.isPhysical) { + if (entity && ObjectEntity.isObject(entity) && entity.isPhysical) { entity.applyPhysics(); } } @@ -180,8 +183,8 @@ export default class EntityManager { if (!Entity.exists(entity)) continue; - if (!(entity instanceof CameraEntity)) { - if (!(entity instanceof ObjectEntity) || !entity.isChild) entity.tick(tick); + if (!(entity.cameraData)) { + if (!(ObjectEntity.isObject(entity)) || !entity.isChild) entity.tick(tick); } } 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/Systems/MazeGenerator.ts b/src/Systems/MazeGenerator.ts index 3a480455..162e1e39 100644 --- a/src/Systems/MazeGenerator.ts +++ b/src/Systems/MazeGenerator.ts @@ -212,4 +212,4 @@ export default class MazeGenerator { } return false; } -} +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 50563310..0c1a435d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -44,7 +44,10 @@ export const host: string = process.env.SERVER_INFO || "unknown"; 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 = (mode === "development" ? 0 : 10) * tps; +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; diff --git a/src/index.ts b/src/index.ts index 7a981a30..3639f740 100644 --- a/src/index.ts +++ b/src/index.ts @@ -155,10 +155,10 @@ app.listen(PORT, (success) => { // RULES(0): No two game servers should share the same endpoint // // NOTES(0): As of now, both servers run on the same process (and thread) here - const ffa = new GameServer(FFAArena, "FFA"); - const sbx = new GameServer(SandboxArena, "Sandbox"); + const ffa = new GameServer("maze", "Maze"); + const sbx = new GameServer("teams", "Sandbox"); - games.push(ffa, sbx); + games.push(ffa); util.saveToLog("Servers up", "All servers booted up.", 0x37F554); util.log("Dumping endpoint -> gamemode routing table"); From 8b2d4e334af027418eac6a8906ebec8fab04b4f6 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:18:01 +0200 Subject: [PATCH 25/65] remove this --- src/Const/Enums.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Const/Enums.ts b/src/Const/Enums.ts index 6f08502f..5512e7fb 100644 --- a/src/Const/Enums.ts +++ b/src/Const/Enums.ts @@ -40,9 +40,8 @@ export const enum Color { EnemyTank = 15, NecromancerSquare = 16, Fallen = 17, - EnemyHexagon = 18, - kMaxColors = 19 + kMaxColors = 18 } /** @@ -67,7 +66,6 @@ export const ColorsHexCode: Record = { [Color.EnemyTank]: 0xF14E54, [Color.NecromancerSquare]: 0xFCC376, [Color.Fallen]: 0xC0C0C0, - [Color.EnemyHexagon]: 0xFCAD76, [Color.kMaxColors]: 0x000000 } @@ -319,4 +317,4 @@ export function levelToScore(level: number): number { if (level <= 0) return 0; return levelToScoreTable[level - 1]; -} \ No newline at end of file +} From 69d8bad891afb0aa2f7820762978ebf2a6afdcd3 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:18:50 +0200 Subject: [PATCH 26/65] restore default gamemodes --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3639f740..7a981a30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -155,10 +155,10 @@ app.listen(PORT, (success) => { // RULES(0): No two game servers should share the same endpoint // // NOTES(0): As of now, both servers run on the same process (and thread) here - const ffa = new GameServer("maze", "Maze"); - const sbx = new GameServer("teams", "Sandbox"); + const ffa = new GameServer(FFAArena, "FFA"); + const sbx = new GameServer(SandboxArena, "Sandbox"); - games.push(ffa); + games.push(ffa, sbx); util.saveToLog("Servers up", "All servers booted up.", 0x37F554); util.log("Dumping endpoint -> gamemode routing table"); From 5f07e8d0d3c09a97ddcc4310f50f93eb9c04beb7 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:26:36 +0200 Subject: [PATCH 27/65] fix O key spam --- src/Client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.ts b/src/Client.ts index 090d46d9..237da400 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -258,7 +258,7 @@ export default class Client { this.inputs.mouse.y = util.constrain(mouseY, minY, maxY); const player = camera.cameraData.values.player; - if (!TankBody.isTank(player)) return; + if (!Entity.exists(player) || !TankBody.isTank(player)) return; // No AI if (this.inputs.isPossessing && this.accessLevel !== config.AccessLevel.FullAccess) return; From 0f07d80556b388826c4c2357966b98e3f3fa79da Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 05:54:47 +0200 Subject: [PATCH 28/65] Rename "Systems" to "Misc" --- src/Systems/MazeGenerator.ts | 215 ----------------------------------- 1 file changed, 215 deletions(-) delete mode 100644 src/Systems/MazeGenerator.ts diff --git a/src/Systems/MazeGenerator.ts b/src/Systems/MazeGenerator.ts deleted file mode 100644 index 162e1e39..00000000 --- a/src/Systems/MazeGenerator.ts +++ /dev/null @@ -1,215 +0,0 @@ -import ArenaEntity from "../Native/Arena"; -import MazeWall from "../Entity/Misc/MazeWall"; -import { VectorAbstract } from "../Physics/Vector"; - -export interface MazeGeneratorConfig { - CELL_SIZE: number, - GRID_SIZE: number, - SEED_AMOUNT: number, - TURN_CHANCE: number, - BRANCH_CHANCE: number, - TERMINATION_CHANCE: number -} - -/** - * 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 arena to place the walls in */ - public arena: ArenaEntity; - /** The variables that affect maze generation */ - public config: MazeGeneratorConfig; - /** Stores all the "seed"s */ - public SEEDS: VectorAbstract[] = []; - /** Stores all the "wall"s, contains cell based coords */ - public WALLS: (VectorAbstract & {width: number, height: number})[] = []; - /** Rolled out matrix of the grid */ - public MAZE: Uint8Array; - - constructor(arena: ArenaEntity, config: MazeGeneratorConfig) { - this.arena = arena; - - this.config = config; - - this.MAZE = new Uint8Array(config.GRID_SIZE * config.GRID_SIZE); - } - /** Creates a maze wall from cell coords */ - public _buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) { - const scaledW = gridW * this.config.CELL_SIZE; - const scaledH = gridH * this.config.CELL_SIZE; - const scaledX = gridX * this.config.CELL_SIZE - this.arena.width / 2 + (scaledW / 2); - const scaledY = gridY * this.config.CELL_SIZE - this.arena.height / 2 + (scaledH / 2); - new MazeWall(this.arena.game, scaledX, scaledY, scaledH, scaledW); - } - /** Allows for easier (x, y) based getting of maze cells */ - public _get(x: number, y: number): number { - return this.MAZE[y * this.config.GRID_SIZE + x]; - } - /** Allows for easier (x, y) based setting of maze cells */ - public _set(x: number, y: number, value: number): number { - return this.MAZE[y * this.config.GRID_SIZE + x] = value; - } - /** Converts MAZE grid into an array of set and unset bits for ease of use */ - public _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.GRID_SIZE, Math.floor(i / this.config.GRID_SIZE), this.MAZE[i]]; - return values; - } - /** Builds the maze */ - public buildMaze() { - // Plant some seeds - for (let i = 0; i < 10000; i++) { - // Stop if we exceed our maximum seed amount - if (this.SEEDS.length >= this.config.SEED_AMOUNT) break; - // Attempt a seed planting - let seed: VectorAbstract = { - x: Math.floor((Math.random() * this.config.GRID_SIZE) - 1), - y: Math.floor((Math.random() * this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.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 >= this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.GRID_SIZE - 1) break; - this._set(seed.x, seed.y, 1); - // Now lets see if we want to branch or turn - if (Math.random() <= this.config.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() <= this.config.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() * this.config.GRID_SIZE) - 1), - y: Math.floor((Math.random() * this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.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 * this.config.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 < this.config.GRID_SIZE; x++) { - for (let y = 0; y < this.config.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); - } - } - - /** Checks if the given coordinates overlap with any wall */ - public isInWall(x: number, y: number): boolean { - const width = this.arena.width / 2; - const height = this.arena.height / 2; - - for (const wall of this.WALLS) { - const wallX = wall.x * this.config.CELL_SIZE - width; - const wallY = wall.y * this.config.CELL_SIZE - height; - const wallW = wall.width * this.config.CELL_SIZE; - const wallH = wall.height * this.config.CELL_SIZE; - if ( - x >= wallX && - x <= wallX + wallW && - y >= wallY && - y <= wallY + wallH - ) { - return true; - } - } - return false; - } -} \ No newline at end of file From 06e1073d6dea0bcaf04fac2f2e2cef44a4c2d760 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 05:56:37 +0200 Subject: [PATCH 29/65] Add misc folder, moved shape manager there --- src/Gamemodes/Benchmark/Crashers.ts | 2 +- src/Gamemodes/Benchmark/HugeMap.ts | 2 +- src/Gamemodes/Benchmark/Players.ts | 2 +- src/Gamemodes/Benchmark/ShapeDense.ts | 2 +- src/Gamemodes/Maze.ts | 4 +- src/Gamemodes/Misc/DomTest.ts | 2 +- src/Gamemodes/Misc/FactoryTest.ts | 2 +- src/Gamemodes/Misc/Jungle.ts | 2 +- src/Gamemodes/Misc/Spikebox.ts | 2 +- src/Gamemodes/Misc/Testing.ts | 2 +- src/Gamemodes/Sandbox.ts | 2 +- src/Gamemodes/Survival.ts | 2 +- src/Gamemodes/Tag.ts | 2 +- src/Gamemodes/Team2.ts | 22 +-- src/Misc/MazeGenerator.ts | 215 ++++++++++++++++++++++++++ src/Misc/ShapeManager.ts | 119 ++++++++++++++ src/Native/Arena.ts | 2 +- 17 files changed, 350 insertions(+), 36 deletions(-) create mode 100644 src/Misc/MazeGenerator.ts create mode 100644 src/Misc/ShapeManager.ts diff --git a/src/Gamemodes/Benchmark/Crashers.ts b/src/Gamemodes/Benchmark/Crashers.ts index 2c6f7b54..e0060541 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"; 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/Maze.ts b/src/Gamemodes/Maze.ts index af2a385c..769b3f9f 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -18,9 +18,9 @@ import ArenaEntity from "../Native/Arena"; import GameServer from "../Game"; -import MazeGenerator, { MazeGeneratorConfig } from "../Systems/MazeGenerator"; +import MazeGenerator, { MazeGeneratorConfig } from "../Misc/MazeGenerator"; -import ShapeManager from "../Entity/Shape/Manager"; +import ShapeManager from "../Misc/ShapeManager"; /** * Manage shape count 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 a87f24de..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"; 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/Sandbox.ts b/src/Gamemodes/Sandbox.ts index b36e2cdb..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"; /** diff --git a/src/Gamemodes/Survival.ts b/src/Gamemodes/Survival.ts index 32cf9656..e1669fcd 100644 --- a/src/Gamemodes/Survival.ts +++ b/src/Gamemodes/Survival.ts @@ -22,7 +22,7 @@ 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"; diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts index 5695d7d6..1afae8f9 100644 --- a/src/Gamemodes/Tag.ts +++ b/src/Gamemodes/Tag.ts @@ -30,7 +30,7 @@ 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]; diff --git a/src/Gamemodes/Team2.ts b/src/Gamemodes/Team2.ts index dfeba8f9..5b52e2b5 100644 --- a/src/Gamemodes/Team2.ts +++ b/src/Gamemodes/Team2.ts @@ -26,20 +26,9 @@ import TankBody from "../Entity/Tank/TankBody"; import { TeamEntity } from "../Entity/Misc/TeamEntity"; import { Color } from "../Const/Enums"; -import MazeGenerator, { MazeGeneratorConfig } from "../Systems/MazeGenerator"; - const arenaSize = 11150; const baseWidth = arenaSize / (3 + 1/3) * 0.6; // 2007 -const config: MazeGeneratorConfig = { - CELL_SIZE: arenaSize * 2 / 40, - GRID_SIZE: 40, - SEED_AMOUNT: Math.floor(Math.random() * 30) + 30, - TURN_CHANCE: 0.2, - BRANCH_CHANCE: 0.2, - TERMINATION_CHANCE: 0.2 -} - /** * Teams2 Gamemode Arena */ @@ -50,9 +39,6 @@ export default class Teams2Arena extends ArenaEntity { public blueTeamBase: TeamBase; /** Red Team entity */ public redTeamBase: TeamBase; - - public mazeGenerator: MazeGenerator = new MazeGenerator(this, config); - /** Maps clients to their teams */ public playerTeamMap: WeakMap = new WeakMap(); @@ -61,7 +47,6 @@ export default class Teams2Arena extends ArenaEntity { this.updateBounds(arenaSize * 2, arenaSize * 2); this.blueTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamBlue), -arenaSize + baseWidth / 2, 0, arenaSize * 2, baseWidth, true, 12, 2); this.redTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamRed), arenaSize - baseWidth / 2, 0, arenaSize * 2, baseWidth, true, 12, 2); - this.mazeGenerator.buildMaze(); } public spawnPlayer(tank: TankBody, client: Client) { @@ -77,9 +62,4 @@ export default class Teams2Arena extends ArenaEntity { if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team; } - - public isValidSpawnLocation(x: number, y: number): boolean { - // Should never spawn inside walls - return !this.mazeGenerator.isInWall(x, y); - } -} +} \ No newline at end of file diff --git a/src/Misc/MazeGenerator.ts b/src/Misc/MazeGenerator.ts new file mode 100644 index 00000000..162e1e39 --- /dev/null +++ b/src/Misc/MazeGenerator.ts @@ -0,0 +1,215 @@ +import ArenaEntity from "../Native/Arena"; +import MazeWall from "../Entity/Misc/MazeWall"; +import { VectorAbstract } from "../Physics/Vector"; + +export interface MazeGeneratorConfig { + CELL_SIZE: number, + GRID_SIZE: number, + SEED_AMOUNT: number, + TURN_CHANCE: number, + BRANCH_CHANCE: number, + TERMINATION_CHANCE: number +} + +/** + * 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 arena to place the walls in */ + public arena: ArenaEntity; + /** The variables that affect maze generation */ + public config: MazeGeneratorConfig; + /** Stores all the "seed"s */ + public SEEDS: VectorAbstract[] = []; + /** Stores all the "wall"s, contains cell based coords */ + public WALLS: (VectorAbstract & {width: number, height: number})[] = []; + /** Rolled out matrix of the grid */ + public MAZE: Uint8Array; + + constructor(arena: ArenaEntity, config: MazeGeneratorConfig) { + this.arena = arena; + + this.config = config; + + this.MAZE = new Uint8Array(config.GRID_SIZE * config.GRID_SIZE); + } + /** Creates a maze wall from cell coords */ + public _buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) { + const scaledW = gridW * this.config.CELL_SIZE; + const scaledH = gridH * this.config.CELL_SIZE; + const scaledX = gridX * this.config.CELL_SIZE - this.arena.width / 2 + (scaledW / 2); + const scaledY = gridY * this.config.CELL_SIZE - this.arena.height / 2 + (scaledH / 2); + new MazeWall(this.arena.game, scaledX, scaledY, scaledH, scaledW); + } + /** Allows for easier (x, y) based getting of maze cells */ + public _get(x: number, y: number): number { + return this.MAZE[y * this.config.GRID_SIZE + x]; + } + /** Allows for easier (x, y) based setting of maze cells */ + public _set(x: number, y: number, value: number): number { + return this.MAZE[y * this.config.GRID_SIZE + x] = value; + } + /** Converts MAZE grid into an array of set and unset bits for ease of use */ + public _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.GRID_SIZE, Math.floor(i / this.config.GRID_SIZE), this.MAZE[i]]; + return values; + } + /** Builds the maze */ + public buildMaze() { + // Plant some seeds + for (let i = 0; i < 10000; i++) { + // Stop if we exceed our maximum seed amount + if (this.SEEDS.length >= this.config.SEED_AMOUNT) break; + // Attempt a seed planting + let seed: VectorAbstract = { + x: Math.floor((Math.random() * this.config.GRID_SIZE) - 1), + y: Math.floor((Math.random() * this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.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 >= this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.GRID_SIZE - 1) break; + this._set(seed.x, seed.y, 1); + // Now lets see if we want to branch or turn + if (Math.random() <= this.config.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() <= this.config.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() * this.config.GRID_SIZE) - 1), + y: Math.floor((Math.random() * this.config.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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.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 * this.config.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 < this.config.GRID_SIZE; x++) { + for (let y = 0; y < this.config.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); + } + } + + /** Checks if the given coordinates overlap with any wall */ + public isInWall(x: number, y: number): boolean { + const width = this.arena.width / 2; + const height = this.arena.height / 2; + + for (const wall of this.WALLS) { + const wallX = wall.x * this.config.CELL_SIZE - width; + const wallY = wall.y * this.config.CELL_SIZE - height; + const wallW = wall.width * this.config.CELL_SIZE; + const wallH = wall.height * this.config.CELL_SIZE; + if ( + x >= wallX && + x <= wallX + wallW && + y >= wallY && + y <= wallY + wallH + ) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/Misc/ShapeManager.ts b/src/Misc/ShapeManager.ts new file mode 100644 index 00000000..3ba52e5b --- /dev/null +++ b/src/Misc/ShapeManager.ts @@ -0,0 +1,119 @@ +/* + 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 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 + * as determines where each type of shape spawns around the arena. + */ + +export default class ShapeManager { + /** Current game server */ + protected game: GameServer; + /** Arena whose shapes are being managed */ + protected arena: ArenaEntity; + /** Stores all shapes */ + protected shapes: AbstractShape[] = []; + + public constructor(arena: ArenaEntity) { + this.arena = arena; + this.game = arena.game; + } + + /** + * Spawns a shape in a random location on the map. + * Determines shape type by the random position chosen. + */ + protected spawnShape(): AbstractShape { + let shape: AbstractShape; + const {x, y} = this.arena.findSpawnLocation(); + const rightX = this.arena.arenaData.values.rightX; + const leftX = this.arena.arenaData.values.leftX; + if (Math.max(x, y) < rightX / 10 && Math.min(x, y) > leftX / 10) { + // Pentagon Nest + shape = new Pentagon(this.game, Math.random() <= 0.05); + + shape.positionData.values.x = x; + shape.positionData.values.y = y; + shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; + } else if (Math.max(x, y) < rightX / 5 && Math.min(x, y) > leftX / 5) { + // Crasher Zone + const isBig = Math.random() < .2; + + shape = new Crasher(this.game, isBig); + + shape.positionData.values.x = x; + shape.positionData.values.y = y; + shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; + } else { + // Fields of Shapes + const rand = Math.random(); + if (rand < .04) { + shape = new Pentagon(this.game); + + shape.positionData.values.x = x; + shape.positionData.values.y = y; + shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; + } else if (rand < .20) { // < 16% + shape = new Triangle(this.game); + + shape.positionData.values.x = x; + shape.positionData.values.y = y; + shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; + } else { // if rand < 80% + shape = new Square(this.game); + + shape.positionData.values.x = x; + shape.positionData.values.y = y; + shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; + } + } + + shape.scoreReward *= this.arena.shapeScoreRewardMultiplier; + + return shape; + } + + /** Kills all shapes in the arena */ + public killAll() { + for(let i = 0; i < this.shapes.length; ++i) { + this.shapes[i]?.delete(); + } + } + + protected get wantedShapes() { + return 1000; + } + + public tick() { + for (let i = this.wantedShapes; i --> 0;) { + const shape = this.shapes[i]; + // Alternatively, Entity.exists(shape), though this is probably faster + if (!shape || shape.hash === 0) this.shapes[i] = this.spawnShape(); + } + } +} diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 5d13cc4a..5984245b 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -17,7 +17,7 @@ */ import GameServer from "../Game"; -import ShapeManager from "../Entity/Shape/Manager"; +import ShapeManager from "../Misc/ShapeManager"; import TankBody from "../Entity/Tank/TankBody"; import ArenaCloser from "../Entity/Misc/ArenaCloser"; From b395e50b6fabb3d1072cdd95da85480297439061 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 05:57:15 +0200 Subject: [PATCH 30/65] remove old shapemanager --- src/Entity/Shape/Manager.ts | 119 ------------------------------------ 1 file changed, 119 deletions(-) delete mode 100644 src/Entity/Shape/Manager.ts diff --git a/src/Entity/Shape/Manager.ts b/src/Entity/Shape/Manager.ts deleted file mode 100644 index 680849ed..00000000 --- a/src/Entity/Shape/Manager.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - 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 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"; - -/** - * Used to balance out shape count in the arena, as well - * as determines where each type of shape spawns around the arena. - */ - -export default class ShapeManager { - /** Current game server */ - protected game: GameServer; - /** Arena whose shapes are being managed */ - protected arena: ArenaEntity; - /** Stores all shapes */ - protected shapes: AbstractShape[] = []; - - public constructor(arena: ArenaEntity) { - this.arena = arena; - this.game = arena.game; - } - - /** - * Spawns a shape in a random location on the map. - * Determines shape type by the random position chosen. - */ - protected spawnShape(): AbstractShape { - let shape: AbstractShape; - const {x, y} = this.arena.findSpawnLocation(); - const rightX = this.arena.arenaData.values.rightX; - const leftX = this.arena.arenaData.values.leftX; - if (Math.max(x, y) < rightX / 10 && Math.min(x, y) > leftX / 10) { - // Pentagon Nest - shape = new Pentagon(this.game, Math.random() <= 0.05); - - shape.positionData.values.x = x; - shape.positionData.values.y = y; - shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; - } else if (Math.max(x, y) < rightX / 5 && Math.min(x, y) > leftX / 5) { - // Crasher Zone - const isBig = Math.random() < .2; - - shape = new Crasher(this.game, isBig); - - shape.positionData.values.x = x; - shape.positionData.values.y = y; - shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; - } else { - // Fields of Shapes - const rand = Math.random(); - if (rand < .04) { - shape = new Pentagon(this.game); - - shape.positionData.values.x = x; - shape.positionData.values.y = y; - shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; - } else if (rand < .20) { // < 16% - shape = new Triangle(this.game); - - shape.positionData.values.x = x; - shape.positionData.values.y = y; - shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; - } else { // if rand < 80% - shape = new Square(this.game); - - shape.positionData.values.x = x; - shape.positionData.values.y = y; - shape.relationsData.values.owner = shape.relationsData.values.team = this.arena; - } - } - - shape.scoreReward *= this.arena.shapeScoreRewardMultiplier; - - return shape; - } - - /** Kills all shapes in the arena */ - public killAll() { - for(let i = 0; i < this.shapes.length; ++i) { - this.shapes[i]?.delete(); - } - } - - protected get wantedShapes() { - return 1000; - } - - public tick() { - for (let i = this.wantedShapes; i --> 0;) { - const shape = this.shapes[i]; - // Alternatively, Entity.exists(shape), though this is probably faster - if (!shape || shape.hash === 0) this.shapes[i] = this.spawnShape(); - } - } -} From c734d041e19ed7207a43d4e07244310398248098 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:11:33 +0200 Subject: [PATCH 31/65] Style --- src/Gamemodes/Maze.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts index 769b3f9f..4e9482a9 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -39,12 +39,12 @@ export class MazeShapeManager extends ShapeManager { } const config: MazeGeneratorConfig = { - CELL_SIZE: 635, - GRID_SIZE: 40, - SEED_AMOUNT: Math.floor(Math.random() * 30) + 30, - TURN_CHANCE: 0.2, - BRANCH_CHANCE: 0.2, - TERMINATION_CHANCE: 0.2 + cellSize: 635, + gridSize: 40, + seedAmount: Math.floor(Math.random() * 30) + 30, + turnChance: 0.2, + branchChance: 0.2, + terminationChance: 0.2 } export default class MazeArena extends ArenaEntity { @@ -57,7 +57,7 @@ export default class MazeArena extends ArenaEntity { public constructor(game: GameServer) { super(game); - const arenaSize = config.CELL_SIZE * config.GRID_SIZE + const arenaSize = config.cellSize * config.gridSize; this.updateBounds(arenaSize, arenaSize); this.mazeGenerator.buildMaze(); From 66473e475b548de8a133866ee33f9144bedbabc6 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:11:59 +0200 Subject: [PATCH 32/65] variable names --- src/Misc/MazeGenerator.ts | 128 +++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/src/Misc/MazeGenerator.ts b/src/Misc/MazeGenerator.ts index 162e1e39..1733e963 100644 --- a/src/Misc/MazeGenerator.ts +++ b/src/Misc/MazeGenerator.ts @@ -3,17 +3,17 @@ import MazeWall from "../Entity/Misc/MazeWall"; import { VectorAbstract } from "../Physics/Vector"; export interface MazeGeneratorConfig { - CELL_SIZE: number, - GRID_SIZE: number, - SEED_AMOUNT: number, - TURN_CHANCE: number, - BRANCH_CHANCE: number, - TERMINATION_CHANCE: number + cellSize: number, + gridSize: number, + seedAmount: number, + turnChance: number, + branchChance: number, + terminationChance: number } /** * Implementation details: - * Maze map generator by damocles + * 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 */ @@ -23,39 +23,39 @@ export default class MazeGenerator { /** The variables that affect maze generation */ public config: MazeGeneratorConfig; /** Stores all the "seed"s */ - public SEEDS: VectorAbstract[] = []; + public seeds: VectorAbstract[] = []; /** Stores all the "wall"s, contains cell based coords */ - public WALLS: (VectorAbstract & {width: number, height: number})[] = []; + public walls: (VectorAbstract & {width: number, height: number})[] = []; /** Rolled out matrix of the grid */ - public MAZE: Uint8Array; + public maze: Uint8Array; constructor(arena: ArenaEntity, config: MazeGeneratorConfig) { this.arena = arena; this.config = config; - this.MAZE = new Uint8Array(config.GRID_SIZE * config.GRID_SIZE); + this.maze = new Uint8Array(config.gridSize * config.gridSize); } /** Creates a maze wall from cell coords */ - public _buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) { - const scaledW = gridW * this.config.CELL_SIZE; - const scaledH = gridH * this.config.CELL_SIZE; - const scaledX = gridX * this.config.CELL_SIZE - this.arena.width / 2 + (scaledW / 2); - const scaledY = gridY * this.config.CELL_SIZE - this.arena.height / 2 + (scaledH / 2); + public buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) { + const scaledW = gridW * this.config.cellSize; + const scaledH = gridH * this.config.cellSize; + const scaledX = gridX * this.config.cellSize - this.arena.width / 2 + (scaledW / 2); + const scaledY = gridY * this.config.cellSize - this.arena.height / 2 + (scaledH / 2); new MazeWall(this.arena.game, scaledX, scaledY, scaledH, scaledW); } /** Allows for easier (x, y) based getting of maze cells */ - public _get(x: number, y: number): number { - return this.MAZE[y * this.config.GRID_SIZE + x]; + public get(x: number, y: number): number { + return this.maze[y * this.config.gridSize + x]; } /** Allows for easier (x, y) based setting of maze cells */ - public _set(x: number, y: number, value: number): number { - return this.MAZE[y * this.config.GRID_SIZE + x] = value; + public set(x: number, y: number, value: number): number { + return this.maze[y * this.config.gridSize + x] = value; } /** Converts MAZE grid into an array of set and unset bits for ease of use */ - public _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.GRID_SIZE, Math.floor(i / this.config.GRID_SIZE), this.MAZE[i]]; + public 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.gridSize, Math.floor(i / this.config.gridSize), this.maze[i]]; return values; } /** Builds the maze */ @@ -63,30 +63,30 @@ export default class MazeGenerator { // Plant some seeds for (let i = 0; i < 10000; i++) { // Stop if we exceed our maximum seed amount - if (this.SEEDS.length >= this.config.SEED_AMOUNT) break; + if (this.seeds.length >= this.config.seedAmount) break; // Attempt a seed planting let seed: VectorAbstract = { - x: Math.floor((Math.random() * this.config.GRID_SIZE) - 1), - y: Math.floor((Math.random() * this.config.GRID_SIZE) - 1), + x: Math.floor((Math.random() * this.config.gridSize) - 1), + y: Math.floor((Math.random() * this.config.gridSize) - 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 >= this.config.GRID_SIZE - 1 || seed.y >= this.config.GRID_SIZE - 1) continue; + 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 >= this.config.gridSize - 1 || seed.y >= this.config.gridSize - 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); + 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) { + 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 >= this.config.TERMINATION_CHANCE) { + while (termination >= this.config.terminationChance) { // Choose the next termination chance termination = Math.random(); // Get the direction we're going in @@ -94,12 +94,12 @@ export default class MazeGenerator { // 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.GRID_SIZE - 1 || seed.y >= this.config.GRID_SIZE - 1) break; - this._set(seed.x, seed.y, 1); + if (seed.x <= 0 || seed.y <= 0 || seed.x >= this.config.gridSize - 1 || seed.y >= this.config.gridSize - 1) break; + this.set(seed.x, seed.y, 1); // Now lets see if we want to branch or turn - if (Math.random() <= this.config.BRANCH_CHANCE) { + if (Math.random() <= this.config.branchChance) { // 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; + 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 @@ -108,9 +108,9 @@ export default class MazeGenerator { 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() <= this.config.TURN_CHANCE) { + this.seeds.push(newSeed); + this.set(seed.x, seed.y, 1); + } 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)]; } @@ -120,19 +120,19 @@ export default class MazeGenerator { for (let i = 0; i < 10; i++) { // Attempt to place it let seed = { - x: Math.floor((Math.random() * this.config.GRID_SIZE) - 1), - y: Math.floor((Math.random() * this.config.GRID_SIZE) - 1), + x: Math.floor((Math.random() * this.config.gridSize) - 1), + y: Math.floor((Math.random() * this.config.gridSize) - 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.GRID_SIZE - 1 || seed.y >= this.config.GRID_SIZE - 1) continue; + 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.gridSize - 1 || seed.y >= this.config.gridSize - 1) continue; // Set its grid to a wall cell - this._set(seed.x, seed.y, 1); + 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); + 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++) { @@ -147,8 +147,8 @@ export default class MazeGenerator { [x, y + 1], // bottom ]) { // If its a wall ignore it - if (this._get(nx, ny) !== 0) continue; - let i = ny * this.config.GRID_SIZE + nx; + if (this.get(nx, ny) !== 0) continue; + let i = ny * this.config.gridSize + 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 @@ -156,19 +156,19 @@ export default class MazeGenerator { // Add it to the next cycle to check queue.push([nx, ny]); // Set its grid to an accessible cell - this._set(nx, ny, 2); + this.set(nx, ny, 2); } } // Cycle through all areas of the map - for (let x = 0; x < this.config.GRID_SIZE; x++) { - for (let y = 0; y < this.config.GRID_SIZE; y++) { + for (let x = 0; x < this.config.gridSize; x++) { + for (let y = 0; y < this.config.gridSize; y++) { // If we're not a wall, ignore the cell and move on - if (this._get(x, y) === 2) continue; + 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); + 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 @@ -176,18 +176,18 @@ export default class MazeGenerator { // 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 (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); + this.set(x + i, y + chunk.height, 2); chunk.height++; } - this.WALLS.push(chunk); + this.walls.push(chunk); } } // Create the walls! - for (let {x, y, width, height} of this.WALLS) { - this._buildWallFromGridCoord(x, y, width, height); + for (let {x, y, width, height} of this.walls) { + this.buildWallFromGridCoord(x, y, width, height); } } @@ -196,11 +196,11 @@ export default class MazeGenerator { const width = this.arena.width / 2; const height = this.arena.height / 2; - for (const wall of this.WALLS) { - const wallX = wall.x * this.config.CELL_SIZE - width; - const wallY = wall.y * this.config.CELL_SIZE - height; - const wallW = wall.width * this.config.CELL_SIZE; - const wallH = wall.height * this.config.CELL_SIZE; + for (const wall of this.walls) { + const wallX = wall.x * this.config.cellSize - width; + const wallY = wall.y * this.config.cellSize - height; + const wallW = wall.width * this.config.cellSize; + const wallH = wall.height * this.config.cellSize; if ( x >= wallX && x <= wallX + wallW && @@ -212,4 +212,4 @@ export default class MazeGenerator { } return false; } -} \ No newline at end of file +} From 071483d236e39cbf215a21dda98ed74d745b9ba0 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:19:36 +0200 Subject: [PATCH 33/65] instanceof's last stand --- src/Const/Commands.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Const/Commands.ts b/src/Const/Commands.ts index 3f4542ef..cc833637 100644 --- a/src/Const/Commands.ts +++ b/src/Const/Commands.ts @@ -169,14 +169,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 +184,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 +192,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 +202,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 +244,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 +281,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 +325,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(); From c8b55393988eaaed8906e768b29434ce18193640 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:28:30 +0200 Subject: [PATCH 34/65] necromancer square gives score --- src/Entity/Tank/Projectile/NecromancerSquare.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 +} From ac2b6ff16cb323cca63a3c3254c605fb7d13eb63 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:49:15 +0200 Subject: [PATCH 35/65] tweaks --- src/Entity/Misc/Dominator.ts | 12 +++++++++++- src/Entity/Misc/Mothership.ts | 2 +- src/Entity/Misc/TeamEntity.ts | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Entity/Misc/Dominator.ts b/src/Entity/Misc/Dominator.ts index 72048534..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,6 +99,14 @@ 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) { diff --git a/src/Entity/Misc/Mothership.ts b/src/Entity/Misc/Mothership.ts index 8f64e6d5..81519551 100644 --- a/src/Entity/Misc/Mothership.ts +++ b/src/Entity/Misc/Mothership.ts @@ -89,7 +89,7 @@ export default class Mothership extends TankBody { 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 ba47bea8..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", @@ -62,6 +62,6 @@ export class TeamEntity extends Entity implements TeamGroupEntity { public static isTeam(entity: Entity | null | undefined): entity is TeamEntity { if (!entity) return false; - return !!entity.teamData + return !!entity.teamData; } } From 71fe1d0ac66ee287435d97acfa21b4440888cab1 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:17:42 +0200 Subject: [PATCH 36/65] license --- src/Const/Commands.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Const/Commands.ts b/src/Const/Commands.ts index cc833637..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"; From 5b7e19f78a4ec36c4086f678eabfc57e7ec58149 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 08:00:10 +0200 Subject: [PATCH 37/65] Update Minion.ts --- src/Entity/Tank/Projectile/Minion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Tank/Projectile/Minion.ts b/src/Entity/Tank/Projectile/Minion.ts index 583a31de..629bd2dd 100644 --- a/src/Entity/Tank/Projectile/Minion.ts +++ b/src/Entity/Tank/Projectile/Minion.ts @@ -90,7 +90,7 @@ 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"; + this.arenaMobID = "factoryDrone"; } public get sizeFactor() { From e18a1f157abda768d51653003ece7964cdebdb6c Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:16:32 +0200 Subject: [PATCH 38/65] Fix commands crashing client when disconnected --- client/loader.js | 3 +++ 1 file changed, 3 insertions(+) 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, From e41a432ece5df821ddcb0f615e9a3245ebe6edf8 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:57:44 +0200 Subject: [PATCH 39/65] Fix --- src/Entity/Object.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index 0d5b2586..071a82ca 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -345,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; @@ -393,8 +393,8 @@ export default class ObjectEntity extends Entity { // Keep things in the arena if ( - this.isPhysical - && !(this.physicsData.values.flags & PhysicsFlags.canEscapeArena) + this.isPhysical && + !(this.physicsData.values.flags & PhysicsFlags.canEscapeArena) ) { this.keepInArena(); } From f399031e171baa961f64840d4f4d84df9fa55030 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:33:51 +0200 Subject: [PATCH 40/65] Spawning cleanup --- src/Native/Arena.ts | 70 +++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 5984245b..80f4ff43 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -18,8 +18,10 @@ import GameServer from "../Game"; 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,21 +126,18 @@ 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; } @@ -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 @@ -301,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; @@ -337,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(); @@ -359,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); } } From f3f10f32c481c584180ced9e93f24674c417908e Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:34:09 +0200 Subject: [PATCH 41/65] Boss spawner class --- src/Misc/BossManager.ts | 81 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/Misc/BossManager.ts diff --git a/src/Misc/BossManager.ts b/src/Misc/BossManager.ts new file mode 100644 index 00000000..0cd1e1a1 --- /dev/null +++ b/src/Misc/BossManager.ts @@ -0,0 +1,81 @@ +/* + 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 TankBody from "../Entity/Tank/TankBody"; + +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 { + // TODO confirm this? seems accurate to me so far - X + 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 (this.arena.game.tick >= 1 && (this.arena.game.tick % bossSpawningInterval) !== 0 && !this.boss) { + this.spawnBoss(); + } + } +} \ No newline at end of file From 8ff66dda951b6bb361d59c4a6e20c91f89df85e9 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:34:55 +0200 Subject: [PATCH 42/65] Add files via upload --- src/Gamemodes/Benchmark/Crashers.ts | 2 +- src/Gamemodes/Maze.ts | 2 +- src/Gamemodes/Mothership.ts | 4 ++-- src/Gamemodes/Tag.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Gamemodes/Benchmark/Crashers.ts b/src/Gamemodes/Benchmark/Crashers.ts index e0060541..650accda 100644 --- a/src/Gamemodes/Benchmark/Crashers.ts +++ b/src/Gamemodes/Benchmark/Crashers.ts @@ -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/Maze.ts b/src/Gamemodes/Maze.ts index 4e9482a9..9667d2a1 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -62,7 +62,7 @@ export default class MazeArena extends ArenaEntity { this.mazeGenerator.buildMaze(); - this.allowBoss = false; + this.bossManager = null; // Disables boss spawning } public isValidSpawnLocation(x: number, y: number): boolean { diff --git a/src/Gamemodes/Mothership.ts b/src/Gamemodes/Mothership.ts index 94b1c9d9..2fd6089c 100644 --- a/src/Gamemodes/Mothership.ts +++ b/src/Gamemodes/Mothership.ts @@ -71,7 +71,7 @@ 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 { x, y } = this.findPlayerSpawnLocation(); tank.positionData.values.x = x; tank.positionData.values.y = y; @@ -91,7 +91,7 @@ export default class MothershipArena extends ArenaEntity { 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; diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts index 1afae8f9..46e82560 100644 --- a/src/Gamemodes/Tag.ts +++ b/src/Gamemodes/Tag.ts @@ -108,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; From 46305a60ce6e9eb380dd0335c11d374e4c37b155 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:37:46 +0200 Subject: [PATCH 43/65] fix --- src/Entity/Boss/AbstractBoss.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Entity/Boss/AbstractBoss.ts b/src/Entity/Boss/AbstractBoss.ts index 37e0a124..60ef3aab 100644 --- a/src/Entity/Boss/AbstractBoss.ts +++ b/src/Entity/Boss/AbstractBoss.ts @@ -163,9 +163,6 @@ 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 (TankBody.isTank(killer)) { From e975b581e7869ce156a7fe66e0114f7627b32b7b Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:43:29 +0200 Subject: [PATCH 44/65] remove testing code --- src/Misc/BossManager.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Misc/BossManager.ts b/src/Misc/BossManager.ts index 0cd1e1a1..d56d983e 100644 --- a/src/Misc/BossManager.ts +++ b/src/Misc/BossManager.ts @@ -74,8 +74,9 @@ export default class BossManager { } public tick(tick: number) { - if (this.arena.game.tick >= 1 && (this.arena.game.tick % bossSpawningInterval) !== 0 && !this.boss) { + if (this.arena.game.tick >= 1 && (this.arena.game.tick % bossSpawningInterval) === 0 && !this.boss) { this.spawnBoss(); } } -} \ No newline at end of file + +} From 3552b102df40cdecc6897ef71d66c56b8f160117 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:44:21 +0200 Subject: [PATCH 45/65] Update BossManager.ts --- src/Misc/BossManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Misc/BossManager.ts b/src/Misc/BossManager.ts index d56d983e..85938fd4 100644 --- a/src/Misc/BossManager.ts +++ b/src/Misc/BossManager.ts @@ -74,9 +74,10 @@ export default class BossManager { } public tick(tick: number) { - if (this.arena.game.tick >= 1 && (this.arena.game.tick % bossSpawningInterval) === 0 && !this.boss) { + if (tick >= 1 && (tick % bossSpawningInterval) === 0 && !this.boss) { this.spawnBoss(); } } } + From 282dc110de92aac1db8c66450e93139510fb7292 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:20:30 +0200 Subject: [PATCH 46/65] Update Live.ts --- src/Entity/Live.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Entity/Live.ts b/src/Entity/Live.ts index 7cb8b5c8..845144e5 100644 --- a/src/Entity/Live.ts +++ b/src/Entity/Live.ts @@ -111,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); } @@ -136,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; } @@ -154,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) { From 8d2d290bc405b2dd8006806028ebb4a83f82f406 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:39:16 +0200 Subject: [PATCH 47/65] Update Tag.ts --- src/Gamemodes/Tag.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts index 46e82560..59da2d37 100644 --- a/src/Gamemodes/Tag.ts +++ b/src/Gamemodes/Tag.ts @@ -126,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; @@ -194,5 +194,3 @@ export default class TagArena extends ArenaEntity { } } } - - From 0fa5bc9664155668988809cbd5b6e986b7fa3313 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:06:50 +0200 Subject: [PATCH 48/65] Achievements data for future --- src/Const/Achievements.json | 632 ++++++++++++++++++++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 src/Const/Achievements.json 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 + } + } + ] + } +] From 67ac47e670b16e5927bd5f7aeb6acc4f38afaef5 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:44:14 +0200 Subject: [PATCH 49/65] Remove unusded imports --- src/Misc/BossManager.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Misc/BossManager.ts b/src/Misc/BossManager.ts index 85938fd4..1324d2a5 100644 --- a/src/Misc/BossManager.ts +++ b/src/Misc/BossManager.ts @@ -25,8 +25,6 @@ import FallenOverlord from "../Entity/Boss/FallenOverlord"; import FallenBooster from "../Entity/Boss/FallenBooster"; import Defender from "../Entity/Boss/Defender"; -import TankBody from "../Entity/Tank/TankBody"; - import { bossSpawningInterval } from "../config"; import { VectorAbstract } from "../Physics/Vector"; @@ -46,12 +44,10 @@ export default class BossManager { } public findBossSpawnLocation(): VectorAbstract { - // TODO confirm this? seems accurate to me so far - X const width = this.arena.width / 2; const height = this.arena.height / 2; const pos = this.arena.findSpawnLocation(width, height); - return pos; } @@ -67,7 +63,6 @@ export default class BossManager { const deleteMixin = this.boss.delete.bind(this.boss); this.boss.delete = () => { deleteMixin(); - // Reset arena boss this.boss = null; } @@ -78,6 +73,4 @@ export default class BossManager { this.spawnBoss(); } } - } - From 60656481f3a27dcb6252cfb567d077f93827e4a0 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:17:17 +0200 Subject: [PATCH 50/65] typo --- src/Entity/Shape/Pentagon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Entity/Shape/Pentagon.ts b/src/Entity/Shape/Pentagon.ts index 2d5fda57..dc42d431 100644 --- a/src/Entity/Shape/Pentagon.ts +++ b/src/Entity/Shape/Pentagon.ts @@ -58,6 +58,6 @@ export default class Pentagon extends AbstractShape { this.entityTags |= EntityTags.isShiny; } - this.arenaMobID = this.isAlpha ? "alphapentagon" : "pentagon"; + this.arenaMobID = this.isAlpha ? "alphaPentagon" : "pentagon"; } -} \ No newline at end of file +} From 50616f4e1e3af6cc75e306b90c31aa71239ff097 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:21:28 +0200 Subject: [PATCH 51/65] scoreboard update interval fix --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 0c1a435d..0d44cdb5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -83,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; From 32d5a87942b36b024111042259b386a305b529fc Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Tue, 9 Dec 2025 02:41:34 +0200 Subject: [PATCH 52/65] util function tweaks --- src/util.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 3a3b4c2e..bdadd7a8 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,13 +48,22 @@ export const removeFast = (array: any[], index: number) => { } /** - * Self explanatory + * Self explanatory. */ export const shuffleArray = (array: any[]) => { for (let i = array.length - 1; i >= 1; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } + + return array; +} + +/** + * Picks a random element from an array. + */ +export const randomFrom = (array: any[]) => { + return array[Math.floor(Math.random() * array.length)]; } /** From f9ca75332e20cc727cbed94d9ff371f9fd339077 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Tue, 9 Dec 2025 02:42:24 +0200 Subject: [PATCH 53/65] Use util.randomFrom function instead of doing this manually --- src/Gamemodes/Domination.ts | 4 ++-- src/Gamemodes/Mothership.ts | 17 ++++++----------- src/Gamemodes/Team2.ts | 3 ++- src/Gamemodes/Team4.ts | 3 ++- 4 files changed, 12 insertions(+), 15 deletions(-) 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/Mothership.ts b/src/Gamemodes/Mothership.ts index 2fd6089c..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,7 +70,7 @@ 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 team = randomFrom(this.teams); const { x, y } = this.findPlayerSpawnLocation(); tank.positionData.values.x = x; @@ -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.findPlayerSpawnLocation(); + 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; } diff --git a/src/Gamemodes/Team2.ts b/src/Gamemodes/Team2.ts index 5b52e2b5..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 @@ -54,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; 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; From 6647dc6277c4973f95f59d185379ec16b115d1dc Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Tue, 9 Dec 2025 02:42:53 +0200 Subject: [PATCH 54/65] Update TankBody.ts --- src/Entity/Tank/TankBody.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Entity/Tank/TankBody.ts b/src/Entity/Tank/TankBody.ts index 8c27639d..b69fe4ac 100644 --- a/src/Entity/Tank/TankBody.ts +++ b/src/Entity/Tank/TankBody.ts @@ -202,7 +202,7 @@ export default class TankBody extends LivingEntity implements BarrelBase { 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); @@ -374,4 +374,4 @@ export default class TankBody extends LivingEntity implements BarrelBase { y: 0 }); } -} \ No newline at end of file +} From 046296aa6ab258fabe1d3d5a11f82ba5c7ed1859 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 07:58:58 -0500 Subject: [PATCH 55/65] chore: cleanup --- src/Entity/Misc/MazeWall.ts | 23 +++- src/Gamemodes/Maze.ts | 23 ++-- src/Misc/BossManager.ts | 1 - src/Misc/MazeGenerator.ts | 224 +++++++++++++++++++++++------------- src/Native/Manager.ts | 16 ++- 5 files changed, 194 insertions(+), 93 deletions(-) diff --git a/src/Entity/Misc/MazeWall.ts b/src/Entity/Misc/MazeWall.ts index 73d49396..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; diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts index 9667d2a1..51c53bd1 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -38,10 +38,14 @@ export class MazeShapeManager extends ShapeManager { } } +const GRID_SIZE = 40; +const CELL_SIZE = 635; +const ARENA_SIZE = GRID_SIZE * CELL_SIZE; + const config: MazeGeneratorConfig = { - cellSize: 635, - gridSize: 40, - seedAmount: Math.floor(Math.random() * 30) + 30, + size: GRID_SIZE, + baseSeedCount: 45, + seedCountVariation: 30, turnChance: 0.2, branchChance: 0.2, terminationChance: 0.2 @@ -52,21 +56,24 @@ export default class MazeArena extends ArenaEntity { protected shapes: ShapeManager = new MazeShapeManager(this); - public mazeGenerator: MazeGenerator = new MazeGenerator(this, config); + public mazeGenerator: MazeGenerator = new MazeGenerator(config); public constructor(game: GameServer) { super(game); - const arenaSize = config.cellSize * config.gridSize; - this.updateBounds(arenaSize, arenaSize); + this.updateBounds(ARENA_SIZE, ARENA_SIZE); - this.mazeGenerator.buildMaze(); + this.state = 0; + 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 - return !this.mazeGenerator.isInWall(x, y); + return this.mazeGenerator.isCellOccupied(gridX, gridY) === false; } } diff --git a/src/Misc/BossManager.ts b/src/Misc/BossManager.ts index 85938fd4..e6c89d04 100644 --- a/src/Misc/BossManager.ts +++ b/src/Misc/BossManager.ts @@ -46,7 +46,6 @@ export default class BossManager { } public findBossSpawnLocation(): VectorAbstract { - // TODO confirm this? seems accurate to me so far - X const width = this.arena.width / 2; const height = this.arena.height / 2; diff --git a/src/Misc/MazeGenerator.ts b/src/Misc/MazeGenerator.ts index 1733e963..f9f8ac64 100644 --- a/src/Misc/MazeGenerator.ts +++ b/src/Misc/MazeGenerator.ts @@ -3,14 +3,42 @@ import MazeWall from "../Entity/Misc/MazeWall"; import { VectorAbstract } from "../Physics/Vector"; export interface MazeGeneratorConfig { - cellSize: number, - gridSize: number, - seedAmount: number, + /** + * 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, +} + /** * Implementation details: * Maze map generator by damocles @@ -18,62 +46,43 @@ export interface MazeGeneratorConfig { * - Split into its own file on Wednesday 3rd of December 2025 */ export default class MazeGenerator { - /** The arena to place the walls in */ - public arena: ArenaEntity; /** The variables that affect maze generation */ public config: MazeGeneratorConfig; - /** Stores all the "seed"s */ - public seeds: VectorAbstract[] = []; - /** Stores all the "wall"s, contains cell based coords */ - public walls: (VectorAbstract & {width: number, height: number})[] = []; /** Rolled out matrix of the grid */ public maze: Uint8Array; - constructor(arena: ArenaEntity, config: MazeGeneratorConfig) { - this.arena = arena; - + public constructor(config: MazeGeneratorConfig) { this.config = config; - this.maze = new Uint8Array(config.gridSize * config.gridSize); - } - /** Creates a maze wall from cell coords */ - public buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) { - const scaledW = gridW * this.config.cellSize; - const scaledH = gridH * this.config.cellSize; - const scaledX = gridX * this.config.cellSize - this.arena.width / 2 + (scaledW / 2); - const scaledY = gridY * this.config.cellSize - this.arena.height / 2 + (scaledH / 2); - new MazeWall(this.arena.game, scaledX, scaledY, scaledH, scaledW); - } - /** Allows for easier (x, y) based getting of maze cells */ - public get(x: number, y: number): number { - return this.maze[y * this.config.gridSize + x]; - } - /** Allows for easier (x, y) based setting of maze cells */ - public set(x: number, y: number, value: number): number { - return this.maze[y * this.config.gridSize + x] = value; - } - /** Converts MAZE grid into an array of set and unset bits for ease of use */ - public 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.gridSize, Math.floor(i / this.config.gridSize), this.maze[i]]; - return values; + this.maze = new Uint8Array(config.size * config.size); } + /** Builds the maze */ - public buildMaze() { + 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 amount - if (this.seeds.length >= this.config.seedAmount) break; + // 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.gridSize) - 1), - y: Math.floor((Math.random() * this.config.gridSize) - 1), + 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 (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 >= this.config.gridSize - 1 || seed.y >= this.config.gridSize - 1) continue; + 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 - this.seeds.push(seed); + seeds.push(seed); + this.set(seed.x, seed.y, 1); } const direction: number[][] = [ @@ -81,7 +90,7 @@ export default class MazeGenerator { [0, -1], [0, 1], // up and down ]; // Let it grow! - for (let seed of this.seeds) { + 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; @@ -94,12 +103,12 @@ export default class MazeGenerator { // 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.gridSize - 1 || seed.y >= this.config.gridSize - 1) break; + 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, 1); // Now lets see if we want to branch or turn if (Math.random() <= this.config.branchChance) { - // 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; + // 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 @@ -108,7 +117,7 @@ export default class MazeGenerator { y: seed.y + yy, }; // Push the seed and set its grid to a maze zone - this.seeds.push(newSeed); + seeds.push(newSeed); this.set(seed.x, seed.y, 1); } 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) @@ -120,12 +129,12 @@ export default class MazeGenerator { for (let i = 0; i < 10; i++) { // Attempt to place it let seed = { - x: Math.floor((Math.random() * this.config.gridSize) - 1), - y: Math.floor((Math.random() * this.config.gridSize) - 1), + 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.gridSize - 1 || seed.y >= this.config.gridSize - 1) 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, 1); } @@ -148,7 +157,7 @@ export default class MazeGenerator { ]) { // If its a wall ignore it if (this.get(nx, ny) !== 0) continue; - let i = ny * this.config.gridSize + nx; + 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 @@ -159,13 +168,18 @@ export default class MazeGenerator { this.set(nx, ny, 2); } } + } + + protected convertToWalls(): GridWall[] { + const walls: GridWall[] = []; + // Cycle through all areas of the map - for (let x = 0; x < this.config.gridSize; x++) { - for (let y = 0; y < this.config.gridSize; y++) { + 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) === 2) continue; // Define our properties - let chunk = { x, y, width: 0, height: 1 }; + 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) !== 2) { this.set(x + chunk.width, y, 2); @@ -182,34 +196,88 @@ export default class MazeGenerator { this.set(x + i, y + chunk.height, 2); chunk.height++; } - this.walls.push(chunk); + walls.push(chunk); } } - // Create the walls! - for (let {x, y, width, height} of this.walls) { - this.buildWallFromGridCoord(x, y, width, height); + 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); } } - /** Checks if the given coordinates overlap with any wall */ - public isInWall(x: number, y: number): boolean { - const width = this.arena.width / 2; - const height = this.arena.height / 2; - - for (const wall of this.walls) { - const wallX = wall.x * this.config.cellSize - width; - const wallY = wall.y * this.config.cellSize - height; - const wallW = wall.width * this.config.cellSize; - const wallH = wall.height * this.config.cellSize; - if ( - x >= wallX && - x <= wallX + wallW && - y >= wallY && - y <= wallY + wallH - ) { - return true; - } + /** 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) !== 0; + } + + /** 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), } - return false; + } + + 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/Native/Manager.ts b/src/Native/Manager.ts index 3dfdaff1..7688b1b4 100644 --- a/src/Native/Manager.ts +++ b/src/Native/Manager.ts @@ -70,15 +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; - // TODO figure out why instanceof is neccessary for this + // Classify entity so that we know what to send to client if (this.collisionManager && entity instanceof ObjectEntity) { - // Nothing - } 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; From 1eabb82b544283730d92eac2ed7ea8047dd5952e Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:05:35 -0500 Subject: [PATCH 56/65] chore: add typings --- src/util.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/util.ts b/src/util.ts index bdadd7a8..1fc10421 100644 --- a/src/util.ts +++ b/src/util.ts @@ -48,28 +48,26 @@ 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--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } - - return array; } /** * Picks a random element from an array. */ -export const randomFrom = (array: any[]) => { +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)); } @@ -79,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; } From aaa7c272878b3c65bc3d520dacfa0fa52afac318 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:10:55 -0500 Subject: [PATCH 57/65] fix: shuffle team colors on tag start --- src/Gamemodes/Tag.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts index 59da2d37..07265074 100644 --- a/src/Gamemodes/Tag.ts +++ b/src/Gamemodes/Tag.ts @@ -32,18 +32,17 @@ import Dominator from "../Entity/Misc/Dominator" 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,7 +85,7 @@ 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) { From 2a67dad3db697135f9d41291e9329f4f62234893 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:11:47 -0500 Subject: [PATCH 58/65] chore: npm audit fix --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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": { From 9f160d7f6bb0f9ec94b4ea05040bda02f2f661d4 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:12:38 -0500 Subject: [PATCH 59/65] fix(maze): bring back countdown --- src/Gamemodes/Maze.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts index 51c53bd1..0dae8f49 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -63,7 +63,6 @@ export default class MazeArena extends ArenaEntity { this.updateBounds(ARENA_SIZE, ARENA_SIZE); - this.state = 0; this.mazeGenerator.generate(); this.mazeGenerator.placeWalls(this); From 3b0c9ba04ca3fd6a9a6714e38d60a292cb1ec7dc Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:23:54 -0500 Subject: [PATCH 60/65] fix: proper maze spawning --- src/Gamemodes/Maze.ts | 2 +- src/Misc/MazeGenerator.ts | 31 ++++++++++++++++++------------- src/index.ts | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts index 0dae8f49..ee610615 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -66,12 +66,12 @@ export default class MazeArena extends ArenaEntity { this.mazeGenerator.generate(); this.mazeGenerator.placeWalls(this); + this.state = 0 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 return this.mazeGenerator.isCellOccupied(gridX, gridY) === false; } diff --git a/src/Misc/MazeGenerator.ts b/src/Misc/MazeGenerator.ts index f9f8ac64..b37a08c1 100644 --- a/src/Misc/MazeGenerator.ts +++ b/src/Misc/MazeGenerator.ts @@ -39,6 +39,11 @@ interface GridWall { height: number, } +const MAZE_CELL_EMPTY = 0; +const MAZE_CELL_WALL = 1; +const MAZE_CELL_ACCESSIBLE = 2; +const MAZE_CELL_PLACED = 3; + /** * Implementation details: * Maze map generator by damocles @@ -83,7 +88,7 @@ export default class MazeGenerator { // Push it to the pending seeds and set its grid to a wall cell seeds.push(seed); - this.set(seed.x, seed.y, 1); + this.set(seed.x, seed.y, MAZE_CELL_WALL); } const direction: number[][] = [ [-1, 0], [1, 0], // left and right @@ -104,7 +109,7 @@ export default class MazeGenerator { 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, 1); + 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) @@ -118,7 +123,7 @@ export default class MazeGenerator { }; // Push the seed and set its grid to a maze zone seeds.push(newSeed); - this.set(seed.x, seed.y, 1); + 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)]; @@ -136,12 +141,12 @@ export default class MazeGenerator { 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, 1); + 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, 2); + 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++) { @@ -156,7 +161,7 @@ export default class MazeGenerator { [x, y + 1], // bottom ]) { // If its a wall ignore it - if (this.get(nx, ny) !== 0) continue; + 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; @@ -165,7 +170,7 @@ export default class MazeGenerator { // Add it to the next cycle to check queue.push([nx, ny]); // Set its grid to an accessible cell - this.set(nx, ny, 2); + this.set(nx, ny, MAZE_CELL_ACCESSIBLE); } } } @@ -177,12 +182,12 @@ export default class MazeGenerator { 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) === 2) continue; + 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) !== 2) { - this.set(x + chunk.width, y, 2); + while (this.get(x + chunk.width, y) === MAZE_CELL_WALL) { + this.set(x + chunk.width, y, MAZE_CELL_PLACED); chunk.width++; } // Now lets see if we need to be t h i c c @@ -190,10 +195,10 @@ export default class MazeGenerator { // 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 (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, 2); + this.set(x + i, y + chunk.height, MAZE_CELL_PLACED); chunk.height++; } walls.push(chunk); @@ -230,7 +235,7 @@ export default class MazeGenerator { /** Checks if a cell is occupied on grid */ public isCellOccupied(x: number, y: number): boolean { - return this.get(x, y) !== 0; + return this.get(x, y) === MAZE_CELL_PLACED; } /** Allows for easier (x, y) based setting of maze cells */ diff --git a/src/index.ts b/src/index.ts index 7a981a30..a518e96d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ import TankDefinitions from "./Const/TankDefinitions"; import { commandDefinitions } from "./Const/Commands"; import { ColorsHexCode } from "./Const/Enums"; -import FFAArena from "./Gamemodes/FFA"; +import FFAArena from "./Gamemodes/Maze"; import SandboxArena from "./Gamemodes/Sandbox"; const PORT = config.serverPort; From 4dae6fdf68058a71cbf48bc7873aea32aa355381 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:24:19 -0500 Subject: [PATCH 61/65] chore: typo --- src/Gamemodes/Maze.ts | 1 - src/index.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Gamemodes/Maze.ts b/src/Gamemodes/Maze.ts index ee610615..fa354002 100644 --- a/src/Gamemodes/Maze.ts +++ b/src/Gamemodes/Maze.ts @@ -66,7 +66,6 @@ export default class MazeArena extends ArenaEntity { this.mazeGenerator.generate(); this.mazeGenerator.placeWalls(this); - this.state = 0 this.bossManager = null; // Disables boss spawning } diff --git a/src/index.ts b/src/index.ts index a518e96d..7a981a30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ import TankDefinitions from "./Const/TankDefinitions"; import { commandDefinitions } from "./Const/Commands"; import { ColorsHexCode } from "./Const/Enums"; -import FFAArena from "./Gamemodes/Maze"; +import FFAArena from "./Gamemodes/FFA"; import SandboxArena from "./Gamemodes/Sandbox"; const PORT = config.serverPort; From 03fc08f6d25c32c6c6310d1d8470e4b435374e1f Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:35:34 +0200 Subject: [PATCH 62/65] space --- src/Const/Enums.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Const/Enums.ts b/src/Const/Enums.ts index 5512e7fb..09502f9e 100644 --- a/src/Const/Enums.ts +++ b/src/Const/Enums.ts @@ -289,7 +289,7 @@ export const enum EntityTags { isTank = 1 << 1, isDominator = 1 << 2, isBoss = 1 << 3, - isShiny= 1 << 4, + isShiny = 1 << 4, } /** From e680c5e0f1829b6c0e5f01ea847fb8f1311a087d Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:11:00 -0500 Subject: [PATCH 63/65] fix: maze --- src/Misc/MazeGenerator.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Misc/MazeGenerator.ts b/src/Misc/MazeGenerator.ts index b37a08c1..aae11c37 100644 --- a/src/Misc/MazeGenerator.ts +++ b/src/Misc/MazeGenerator.ts @@ -42,7 +42,7 @@ interface GridWall { const MAZE_CELL_EMPTY = 0; const MAZE_CELL_WALL = 1; const MAZE_CELL_ACCESSIBLE = 2; -const MAZE_CELL_PLACED = 3; +const MAZE_CELL_PLACED_WALL = 3; /** * Implementation details: @@ -160,7 +160,8 @@ export default class MazeGenerator { [x, y - 1], // top [x, y + 1], // bottom ]) { - // If its a wall ignore it + 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 @@ -173,9 +174,22 @@ export default class MazeGenerator { 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 @@ -187,7 +201,7 @@ export default class MazeGenerator { 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); + 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 @@ -198,7 +212,7 @@ export default class MazeGenerator { 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); + this.set(x + i, y + chunk.height, MAZE_CELL_PLACED_WALL); chunk.height++; } walls.push(chunk); @@ -235,7 +249,7 @@ export default class MazeGenerator { /** Checks if a cell is occupied on grid */ public isCellOccupied(x: number, y: number): boolean { - return this.get(x, y) === MAZE_CELL_PLACED; + return this.get(x, y) === MAZE_CELL_PLACED_WALL; } /** Allows for easier (x, y) based setting of maze cells */ From 81a5e0adc9d382dcc8c920caf8c73f75fa281ec0 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 13 Dec 2025 03:51:00 +0200 Subject: [PATCH 64/65] Update AbstractBoss.ts --- src/Entity/Boss/AbstractBoss.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Boss/AbstractBoss.ts b/src/Entity/Boss/AbstractBoss.ts index 60ef3aab..7bb3b5c1 100644 --- a/src/Entity/Boss/AbstractBoss.ts +++ b/src/Entity/Boss/AbstractBoss.ts @@ -165,7 +165,7 @@ export default class AbstractBoss extends LivingEntity { public onDeath(killer: LivingEntity) { let killerName: string; - if (TankBody.isTank(killer)) { + if (TankBody.isTank(killer) || AbstractBoss.isBoss(killer)) { killerName = killer.nameData.values.name; } else { killerName = "an unnamed tank"; From 7e7a6d3576aadc9a6e7a2c431e3375c7534441f7 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Sat, 13 Dec 2025 04:51:44 +0200 Subject: [PATCH 65/65] Entity manager optimization --- src/Native/Manager.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Native/Manager.ts b/src/Native/Manager.ts index 7688b1b4..aa32cc11 100644 --- a/src/Native/Manager.ts +++ b/src/Native/Manager.ts @@ -176,23 +176,20 @@ export default class EntityManager { /** Ticks all entities in the game. */ public tick(tick: number) { - this.collisionManager.forEachCollisionPair(this.handleCollision) + 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 && ObjectEntity.isObject(entity) && entity.isPhysical) { + // Alternatively, Entity.exists(entity), though this is probably faster. + if (!entity || entity.hash === 0) continue; + + if (entity.isPhysical) { entity.applyPhysics(); } - } - - for (let id = 0; id <= this.lastId; ++id) { - const entity = this.inner[id]; - - if (!Entity.exists(entity)) continue; - if (!(entity.cameraData)) { - if (!(ObjectEntity.isObject(entity)) || !entity.isChild) entity.tick(tick); + if (!entity.isChild) { + entity.tick(tick); } }