From e9d5391b27bad4a7029da6d651f612bdbd914705 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 19:06:12 +0200 Subject: [PATCH 01/17] factory test --- src/Client.ts | 4 +-- src/Entity/Misc/Mothership.ts | 2 +- src/Entity/Misc/TeamBase.ts | 4 ++- src/Entity/Misc/TeamEntity.ts | 18 ++++++++++++ src/Entity/Object.ts | 30 ++++++++++++++++++- src/Gamemodes/Domination.ts | 32 ++++++++++---------- src/Gamemodes/Mothership.ts | 42 +++++++++++++------------- src/Gamemodes/Survival.ts | 4 +-- src/Gamemodes/Tag.ts | 32 +++++++------------- src/Gamemodes/Team2.ts | 39 ++++++++++++++++--------- src/Gamemodes/Team4.ts | 55 +++++++++++++++++++++-------------- src/Native/Arena.ts | 49 +++++++++++++++++++++++++++---- src/Native/Camera.ts | 6 ++-- src/config.ts | 7 +++-- 14 files changed, 212 insertions(+), 112 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index 237da400..08df2ad2 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -324,11 +324,11 @@ export default class Client { this.setHasCheated(true); player.destroy(); - player.onKill(player); player.onDeath(player); + player.onKill(player); } } - + return; } case ServerBound.Spawn: { diff --git a/src/Entity/Misc/Mothership.ts b/src/Entity/Misc/Mothership.ts index 81519551..145cf335 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 || "a mysterious group") : (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 || "a mysterious group") + "'s" : "a"} Mothership!`) .u32(killerTeamIsATeam ? ColorsHexCode[killerTeam.teamData.values.teamColor] : 0x000000) .float(-1) .stringNT("").send(); diff --git a/src/Entity/Misc/TeamBase.ts b/src/Entity/Misc/TeamBase.ts index 9b4304f7..7437f171 100644 --- a/src/Entity/Misc/TeamBase.ts +++ b/src/Entity/Misc/TeamBase.ts @@ -19,7 +19,7 @@ import GameServer from "../../Game"; import { HealthFlags, PhysicsFlags, StyleFlags } from "../../Const/Enums"; -import { TeamGroupEntity } from "./TeamEntity"; +import { TeamEntity, TeamGroupEntity } from "./TeamEntity"; import LivingEntity from "../Live"; import BaseDrones from "./BaseDrones"; /** @@ -34,6 +34,8 @@ export default class TeamBase extends LivingEntity { this.relationsData.values.team = team; + if (team instanceof TeamEntity) team.base = this; + this.positionData.values.x = x; this.positionData.values.y = y; diff --git a/src/Entity/Misc/TeamEntity.ts b/src/Entity/Misc/TeamEntity.ts index 23f5e9bd..bb995a76 100644 --- a/src/Entity/Misc/TeamEntity.ts +++ b/src/Entity/Misc/TeamEntity.ts @@ -17,6 +17,9 @@ */ import GameServer from "../../Game"; +import TeamBase from "./TeamBase"; +import ObjectEntity from "../Object"; +import TankBody from "../Tank/TankBody"; import { Color } from "../../Const/Enums"; import { Entity } from "../../Native/Entity"; @@ -51,6 +54,9 @@ export class TeamEntity extends Entity implements TeamGroupEntity { /** Used for notifications in team based gamemodes */ public teamName: string; + + /** The team's spawn base. */ + public base: TeamBase | null = null; public constructor(game: GameServer, color: Color, name: string = ColorsTeamName[color] || "UNKNOWN") { super(game); @@ -64,4 +70,16 @@ export class TeamEntity extends Entity implements TeamGroupEntity { return !!entity.teamData; } + + public static setTeam(team: TeamGroupEntity, entity: ObjectEntity) { + if (!Entity.exists(entity)) return; + + entity.relationsData.values.team = team; + + entity.styleData.values.color = team.teamData.values.teamColor; + + if (TankBody.isTank(entity)) { + entity.cameraEntity.relationsData.values.team = team; + } + } } diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index 071a82ca..959a8f49 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -18,7 +18,7 @@ import * as util from "../util"; import GameServer from "../Game"; -import Vector from "../Physics/Vector"; +import Vector, { VectorAbstract } from "../Physics/Vector"; import { PhysicsGroup, PositionGroup, RelationsGroup, StyleGroup } from "../Native/FieldGroups"; import { Entity } from "../Native/Entity"; @@ -204,6 +204,34 @@ export default class ObjectEntity extends Entity { return dX*dX + dY*dY <= rSum*rSum; } } + + /** Returns a random position inside the given entity. */ + public static getRandomPosition(entity: ObjectEntity): VectorAbstract { + if (entity.isChild) util.warn("Attached entities are not fully supported by physics engine for now"); + + const pos = { + x: entity.positionData.values.x, + y: entity.positionData.values.y + } + + const isRect = entity.physicsData.values.sides === 2; + + if (isRect) { // Rectangular hitbox + const xOffset = (Math.random() - 0.5) * entity.physicsData.values.size, + yOffset = (Math.random() - 0.5) * entity.physicsData.values.width; + + pos.x += xOffset; + pos.y += yOffset; + } else { // Circular hitbox + const radius = Math.random() * entity.physicsData.values.size; + const angle = Math.random() * util.PI2; + + pos.x += Math.cos(angle) * radius; + pos.y += Math.sin(angle) * radius; + } + + return pos; + } /** Calls the deletion animation, unless animate is set to false, in that case it instantly deletes. */ public destroy(animate = true) { diff --git a/src/Gamemodes/Domination.ts b/src/Gamemodes/Domination.ts index 7596d200..df71c9d5 100644 --- a/src/Gamemodes/Domination.ts +++ b/src/Gamemodes/Domination.ts @@ -18,6 +18,7 @@ import Client from "../Client"; import { Color, ColorsHexCode, ArenaFlags, ValidScoreboardIndex, ClientBound } from "../Const/Enums"; +import ObjectEntity from "../Entity/Object"; import Dominator from "../Entity/Misc/Dominator"; import TeamBase from "../Entity/Misc/TeamBase"; import { TeamEntity } from "../Entity/Misc/TeamEntity"; @@ -81,26 +82,25 @@ export default class DominationArena extends ArenaEntity { NE.prefix = "NE "; this.dominators.push(SE, SW, NW, NE); } - - public spawnPlayer(tank: TankBody, client: Client) { - tank.positionData.values.y = arenaSize * Math.random() - arenaSize; - - const xOffset = (Math.random() - 0.5) * baseSize, - yOffset = (Math.random() - 0.5) * baseSize; - - 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; - tank.styleData.values.color = teamBase.styleData.values.color; - tank.positionData.values.x = teamBase.positionData.values.x + xOffset; - tank.positionData.values.y = teamBase.positionData.values.y + yOffset; + + public decideTeam(client: Client): TeamEntity { + const team = this.playerTeamMap.get(client) || randomFrom(this.teams); this.playerTeamMap.set(client, team); - if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team; + return team; + } + + public spawnPlayer(tank: TankBody, client: Client) { + const team = this.decideTeam(client); + const teamBase = team.base as TeamBase; + TeamEntity.setTeam(team, tank); + + const pos = ObjectEntity.getRandomPosition(teamBase); + tank.positionData.values.x = pos.x; + tank.positionData.values.y = pos.y; } - public getTeamDominatorCount(team: TeamEntity) { + public getTeamDominatorCount(team: TeamEntity): number { let doms: number = 0; for (const dominator of this.dominators) { if (dominator.relationsData.values.team === team) doms++; diff --git a/src/Gamemodes/Mothership.ts b/src/Gamemodes/Mothership.ts index b830ff16..373cf722 100644 --- a/src/Gamemodes/Mothership.ts +++ b/src/Gamemodes/Mothership.ts @@ -37,14 +37,18 @@ export default class MothershipArena extends ArenaEntity { /** All team entities in game */ public teams: TeamEntity[] = []; + /** Motherships in game */ public motherships: Mothership[] = []; - /** Maps clients to their mothership */ - public playerTeamMotMap: WeakMap = new WeakMap(); + /** Maps clients to their team */ + public playerTeamMap: WeakMap = new WeakMap(); public constructor(game: GameServer) { super(game); + + this.updateBounds(arenaSize * 2, arenaSize * 2); + this.shapeScoreRewardMultiplier = 3.0; this.arenaData.values.flags |= ArenaFlags.hiddenScores; @@ -59,41 +63,34 @@ export default class MothershipArena extends ArenaEntity { mot.relationsData.values.team = team; mot.styleData.values.color = team.teamData.values.teamColor; - mot.positionData.values.x = Math.cos(randAngle) * arenaSize * 0.75; - mot.positionData.values.y = Math.sin(randAngle) * arenaSize * 0.75; + mot.positionData.values.x = Math.cos(randAngle) * arenaSize * 0.8; + mot.positionData.values.y = Math.sin(randAngle) * arenaSize * 0.8; randAngle += PI2 / TEAM_COLORS.length; } + } + + public decideTeam(client: Client): TeamEntity { + const team = this.playerTeamMap.get(client) || randomFrom(this.teams); + this.playerTeamMap.set(client, team); - this.updateBounds(arenaSize * 2, arenaSize * 2); + return team; } public spawnPlayer(tank: TankBody, client: Client) { - if (!this.motherships.length && !this.playerTeamMotMap.has(client)) { - const team = randomFrom(this.teams); - const { x, y } = this.findPlayerSpawnLocation(); - - tank.positionData.values.x = x; - tank.positionData.values.y = y; - tank.relationsData.values.team = team; - tank.styleData.values.color = team.teamData.teamColor; - return; - } - - const mothership = this.playerTeamMotMap.get(client) || randomFrom(this.motherships); - this.playerTeamMotMap.set(client, mothership); + const team = this.decideTeam(client); + TeamEntity.setTeam(team, tank); - tank.relationsData.values.team = mothership.relationsData.values.team; - tank.styleData.values.color = mothership.styleData.values.color; + const success = this.attemptFactorySpawn(tank); + if (success) return; // This player was spawned from a factory instead // TODO: Possess mothership if its unpossessed const { x, y } = this.findPlayerSpawnLocation(); tank.positionData.values.x = x; tank.positionData.values.y = y; - - if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team; } + public updateScoreboard() { this.motherships.sort((m1, m2) => m2.healthData.values.health - m1.healthData.values.health); @@ -113,6 +110,7 @@ export default class MothershipArena extends ArenaEntity { this.arenaData.scoreboardAmount = length; } + public updateArenaState() { // backwards to preserve for (let i = this.motherships.length; i --> 0;) { diff --git a/src/Gamemodes/Survival.ts b/src/Gamemodes/Survival.ts index e1669fcd..61033508 100644 --- a/src/Gamemodes/Survival.ts +++ b/src/Gamemodes/Survival.ts @@ -24,7 +24,7 @@ import TankBody from "../Entity/Tank/TankBody"; import ShapeManager from "../Misc/ShapeManager"; import { ArenaFlags, ClientBound } from "../Const/Enums"; -import { tps, countdownTicks, scoreboardUpdateInterval } from "../config"; +import { tps, countdownDuration, scoreboardUpdateInterval } from "../config"; const MIN_PLAYERS = 4; // 6 in Diep.io @@ -104,7 +104,7 @@ export default class SurvivalArena extends ArenaEntity { if (this.arenaData.values.playersNeeded <= 0) { this.arenaData.flags |= ArenaFlags.gameReadyStart; } else { - this.arenaData.ticksUntilStart = countdownTicks; // Reset countdown + this.arenaData.ticksUntilStart = countdownDuration; // Reset countdown if (this.arenaData.flags & ArenaFlags.gameReadyStart) this.arenaData.flags &= ~ArenaFlags.gameReadyStart; } } diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts index 07265074..055f195d 100644 --- a/src/Gamemodes/Tag.ts +++ b/src/Gamemodes/Tag.ts @@ -87,6 +87,13 @@ export default class TagArena extends ArenaEntity { this.updateBounds(ARENA_SIZE * 2, ARENA_SIZE * 2); } + + public decideTeam(client: Client): TeamEntity { + const team = this.playerTeamMap.get(client) || (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 + this.playerTeamMap.set(client, team); + + return team; + } public spawnPlayer(tank: TankBody, client: Client) { const deathMixin = tank.onDeath.bind(tank); @@ -105,33 +112,16 @@ export default class TagArena extends ArenaEntity { else this.playerTeamMap.set(client, team); } - 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.findPlayerSpawnLocation(); - - tank.positionData.values.x = x; - tank.positionData.values.y = y; - tank.relationsData.values.team = team; - tank.styleData.values.color = team.teamData.teamColor; - - this.playerTeamMap.set(client, team) - return; - } - - const team = this.playerTeamMap.get(client) || this.teams[0]; + const team = this.decideTeam(client); + TeamEntity.setTeam(team, tank); - this.playerTeamMap.set(client, team); - - tank.relationsData.values.team = team; - tank.styleData.values.color = team.teamData.values.teamColor; + const success = this.attemptFactorySpawn(tank); + if (success) return; // This player was spawned from a factory instead const { x, y } = this.findPlayerSpawnLocation(); tank.positionData.values.x = x; tank.positionData.values.y = y; - - if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team; } public updateScoreboard() { diff --git a/src/Gamemodes/Team2.ts b/src/Gamemodes/Team2.ts index 0e0b7368..922de548 100644 --- a/src/Gamemodes/Team2.ts +++ b/src/Gamemodes/Team2.ts @@ -20,6 +20,7 @@ import GameServer from "../Game"; import ArenaEntity from "../Native/Arena"; import Client from "../Client"; +import ObjectEntity from "../Entity/Object"; import TeamBase from "../Entity/Misc/TeamBase"; import TankBody from "../Entity/Tank/TankBody"; @@ -37,30 +38,40 @@ export default class Teams2Arena extends ArenaEntity { static override GAMEMODE_ID: string = "teams"; /** Blue Team entity */ - public blueTeamBase: TeamBase; + public blueTeamEntity: TeamEntity; /** Red Team entity */ - public redTeamBase: TeamBase; + public redTeamEntity: TeamEntity; /** Maps clients to their teams */ - public playerTeamMap: WeakMap = new WeakMap(); + public playerTeamMap: WeakMap = new WeakMap(); public constructor(game: GameServer) { super(game); 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.blueTeamEntity = new TeamEntity(this.game, Color.TeamBlue); + this.redTeamEntity = new TeamEntity(this.game, Color.TeamRed); + + new TeamBase(game, this.blueTeamEntity, -arenaSize + baseWidth / 2, 0, arenaSize * 2, baseWidth, true, 12, 2); + new TeamBase(game, this.redTeamEntity, arenaSize - baseWidth / 2, 0, arenaSize * 2, baseWidth, true, 12, 2); } - public spawnPlayer(tank: TankBody, client: Client) { - tank.positionData.values.y = 2 * arenaSize * Math.random() - arenaSize; + public decideTeam(client: Client): TeamEntity { + const team = this.playerTeamMap.get(client) || randomFrom([this.blueTeamEntity, this.redTeamEntity]); + this.playerTeamMap.set(client, team); - const xOffset = (Math.random() - 0.5) * baseWidth; + return team; + } + + public spawnPlayer(tank: TankBody, client: Client) { + const team = this.decideTeam(client); + TeamEntity.setTeam(team, tank); - 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; - this.playerTeamMap.set(client, base); + const success = this.attemptFactorySpawn(tank); + if (success) return; // This player was spawned from a factory instead - if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team; + const base = team.base as TeamBase; + const pos = ObjectEntity.getRandomPosition(base); + tank.positionData.x = pos.x; + tank.positionData.y = pos.y; } } \ No newline at end of file diff --git a/src/Gamemodes/Team4.ts b/src/Gamemodes/Team4.ts index 3e15a2a4..eb96bdc5 100644 --- a/src/Gamemodes/Team4.ts +++ b/src/Gamemodes/Team4.ts @@ -20,6 +20,7 @@ import GameServer from "../Game"; import ArenaEntity from "../Native/Arena"; import Client from "../Client"; +import ObjectEntity from "../Entity/Object"; import TeamBase from "../Entity/Misc/TeamBase"; import TankBody from "../Entity/Tank/TankBody"; @@ -36,41 +37,51 @@ const baseSize = arenaSize / (3 + 1/3); // 3345 export default class Teams4Arena extends ArenaEntity { static override GAMEMODE_ID: string = "4teams"; - /** Blue TeamBASEentity */ - public blueTeamBase: TeamBase; + /** Blue Team entity */ + public blueTeamEntity: TeamEntity; /** Red TeamBASE entity */ - public redTeamBase: TeamBase; + public redTeamEntity: TeamEntity; /** Green TeamBASE entity */ - public greenTeamBase: TeamBase; + public greenTeamEntity: TeamEntity; /** Purple TeamBASE entity */ - public purpleTeamBase: TeamBase; + public purpleTeamEntity: TeamEntity; /** Maps clients to their teams */ - public playerTeamMap: WeakMap = new WeakMap(); + public playerTeamMap: WeakMap = new WeakMap(); public constructor(game: GameServer) { super(game); + this.updateBounds(arenaSize * 2, arenaSize * 2); - this.blueTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamBlue), -arenaSize + baseSize / 2, -arenaSize + baseSize / 2, baseSize, baseSize); - this.redTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamRed), arenaSize - baseSize / 2, arenaSize - baseSize / 2, baseSize, baseSize); - this.greenTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamGreen), -arenaSize + baseSize / 2, arenaSize - baseSize / 2, baseSize, baseSize); - this.purpleTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamPurple), arenaSize - baseSize / 2, -arenaSize + baseSize / 2, baseSize, baseSize); + this.blueTeamEntity = new TeamEntity(this.game, Color.TeamBlue); + this.redTeamEntity = new TeamEntity(this.game, Color.TeamRed); + this.purpleTeamEntity = new TeamEntity(this.game, Color.TeamPurple); + this.greenTeamEntity = new TeamEntity(this.game, Color.TeamGreen); + + new TeamBase(game, this.blueTeamEntity, -arenaSize + baseSize / 2, -arenaSize + baseSize / 2, baseSize, baseSize); + new TeamBase(game, this.redTeamEntity, arenaSize - baseSize / 2, arenaSize - baseSize / 2, baseSize, baseSize); + new TeamBase(game, this.purpleTeamEntity, -arenaSize + baseSize / 2, arenaSize - baseSize / 2, baseSize, baseSize); + new TeamBase(game, this.greenTeamEntity, arenaSize - baseSize / 2, -arenaSize + baseSize / 2, baseSize, baseSize); } - public spawnPlayer(tank: TankBody, client: Client) { - tank.positionData.values.y = arenaSize * Math.random() - arenaSize; + public decideTeam(client: Client): TeamEntity { + const team = this.playerTeamMap.get(client) || randomFrom([this.blueTeamEntity, this.redTeamEntity, this.purpleTeamEntity, this.greenTeamEntity]); + this.playerTeamMap.set(client, team); - const xOffset = (Math.random() - 0.5) * baseSize, - yOffset = (Math.random() - 0.5) * baseSize; + return team; + } + + public spawnPlayer(tank: TankBody, client: Client) { + const team = this.decideTeam(client); + TeamEntity.setTeam(team, tank); - 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; - tank.positionData.values.y = base.positionData.values.y + yOffset; - this.playerTeamMap.set(client, base); - - if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team; + const success = this.attemptFactorySpawn(tank); + if (success) return; // This player was spawned from a factory instead + + const base = team.base as TeamBase; + const pos = ObjectEntity.getRandomPosition(base); + tank.positionData.x = pos.x; + tank.positionData.y = pos.y; } } diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 80f4ff43..b4176a9a 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -26,13 +26,13 @@ import AbstractBoss from "../Entity/Boss/AbstractBoss"; import { VectorAbstract } from "../Physics/Vector"; import { ArenaGroup, TeamGroup } from "./FieldGroups"; import { Entity } from "./Entity"; -import { Color, ArenaFlags, CameraFlags, ValidScoreboardIndex } from "../Const/Enums"; -import { PI2, saveToLog } from "../util"; -import { TeamGroupEntity } from "../Entity/Misc/TeamEntity"; +import { Color, ArenaFlags, CameraFlags, Tank, ValidScoreboardIndex } from "../Const/Enums"; +import { PI2, randomFrom, saveToLog } from "../util"; +import { TeamEntity, TeamGroupEntity } from "../Entity/Misc/TeamEntity"; import Client from "../Client"; -import { countdownTicks, bossSpawningInterval, scoreboardUpdateInterval } from "../config"; +import { countdownDuration, bossSpawningInterval, factorySpawnChance, scoreboardUpdateInterval } from "../config"; export const enum ArenaState { /** Countdown, waiting for players screen */ @@ -95,7 +95,7 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity { this.arenaData.values.flags = ArenaFlags.gameReadyStart; this.arenaData.values.playersNeeded = 0; - this.arenaData.values.ticksUntilStart = countdownTicks; + this.arenaData.values.ticksUntilStart = countdownDuration; this.teamData.values.teamColor = Color.Neutral; } @@ -305,11 +305,50 @@ 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 success = this.attemptFactorySpawn(tank); + if (success) return; // This player was spawned from a factory instead + const { x, y } = this.findPlayerSpawnLocation(); tank.positionData.values.x = x; tank.positionData.values.y = y; } + + attemptFactorySpawn(tank: TankBody) { + if (Math.random() > factorySpawnChance) return false; + + const team = tank.relationsData.values.team; + if (!TeamEntity.isTeam(team)) return false; + + const teammates = this.getTeamPlayers(team); + const factories: TankBody[] = []; + + for (const teammate of teammates) { + if (teammate.currentTank === Tank.Factory && !teammate.deletionAnimation) { + factories.push(teammate); + } + } + + if (factories.length === 0) { // No factories on this team, spawn as usual + const { x, y } = this.findPlayerSpawnLocation(); + tank.positionData.values.x = x; + tank.positionData.values.y = y; + + return false; + } + + const factory = randomFrom(factories); + + const { x, y } = factory.getWorldPosition(); + const barrel = factory.barrels[0]; + const shootAngle = barrel.definition.angle + factory.positionData.values.angle; + + tank.positionData.values.x = x + (Math.cos(shootAngle) * barrel.physicsData.values.size) - Math.sin(shootAngle) * barrel.definition.offset * factory.sizeFactor; + tank.positionData.values.y = y + (Math.sin(shootAngle) * barrel.physicsData.values.size) + Math.cos(shootAngle) * barrel.definition.offset * factory.sizeFactor; + tank.addVelocity(shootAngle, 25); + + return true; + } /** * Closes the arena. diff --git a/src/Native/Camera.ts b/src/Native/Camera.ts index fbb72a01..364934d5 100644 --- a/src/Native/Camera.ts +++ b/src/Native/Camera.ts @@ -37,6 +37,9 @@ import { maxPlayerLevel } from "../config"; export class CameraEntity extends Entity { /** Always existant camera field group. Present in all GUI/camera entities. */ public cameraData: CameraGroup = new CameraGroup(this); + + /** Always existant relations field group. Present in all GUI/camera entities. */ + public relationsData: RelationsGroup = new RelationsGroup(this); /** The current size of the tank the camera is in charge of. Calculated with level stuff */ public sizeFactor: number = 1; @@ -122,9 +125,6 @@ export default class ClientCamera extends CameraEntity { /** All entities in the view of the camera. Represented by id. */ private view: Entity[] = []; - /** Always existant relations field group. Present in all GUI/camera entities. */ - public relationsData: RelationsGroup = new RelationsGroup(this); - /** Calculates the amount of stats available at a specific level. */ public static calculateStatCount(level: number) { if (level <= 0) return 0; diff --git a/src/config.ts b/src/config.ts index 0d44cdb5..c86f8f6e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -44,11 +44,14 @@ 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 = 10 * tps; +export const countdownDuration = 10 * tps; -/** Chance for a shape to spawn as shiny (green) */ +/** Chance for a shape to spawn as shiny (green). */ export const shinyChance: number = 1 / 1_000_000; +/** Chance for a player to spawn out of an allied factory. */ +export const factorySpawnChance: number = 0.5; + /** Is hosting a rest api */ export const enableApi: boolean = true; From 5376660639ec774c3a51e6c35b2cdf515acd16be 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, 14 Dec 2025 16:16:20 +0200 Subject: [PATCH 02/17] Update Arena.ts --- src/Native/Arena.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index b4176a9a..8a75a82d 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -314,7 +314,7 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity { tank.positionData.values.y = y; } - attemptFactorySpawn(tank: TankBody) { + public attemptFactorySpawn(tank: TankBody) { if (Math.random() > factorySpawnChance) return false; const team = tank.relationsData.values.team; From 9f91b9206080de753c2df0a1433b1fd1737c3aea 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, 14 Dec 2025 21:07:04 +0200 Subject: [PATCH 03/17] Fix boss death notif --- src/Entity/Boss/AbstractBoss.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Entity/Boss/AbstractBoss.ts b/src/Entity/Boss/AbstractBoss.ts index 7bb3b5c1..914dfef0 100644 --- a/src/Entity/Boss/AbstractBoss.ts +++ b/src/Entity/Boss/AbstractBoss.ts @@ -163,12 +163,10 @@ export default class AbstractBoss extends LivingEntity { * Will set game.arena.boss to null, so that the next boss can spawn */ public onDeath(killer: LivingEntity) { - let killerName: string; + let killerName: string = "an unnamed tank"; if (TankBody.isTank(killer) || AbstractBoss.isBoss(killer)) { - killerName = killer.nameData.values.name; - } else { - killerName = "an unnamed tank"; + killerName = killer.nameData.values.name || "an unnamed tank"; } this.game.broadcast() From 838e2ac0c9cfd69c0fb1774772b2037c4f4a08b6 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, 15 Dec 2025 01:16:11 +0200 Subject: [PATCH 04/17] broadcast msg function --- src/Game.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Game.ts b/src/Game.ts index 0772e387..646c608a 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -147,7 +147,7 @@ export default class GameServer { this.arena = new ArenaClass(this); this._tickInterval = setInterval(() => { - if (this.clients.size) this.tickLoop(); + if (this.clients.size) this.tickLoop(); // Don't tick empty games }, config.mspt); } @@ -159,6 +159,10 @@ export default class GameServer { public broadcastPlayerCount() { this.broadcast().vu(ClientBound.PlayerCount).vu(GameServer.globalPlayerCount).send(); } + /** Sends a notification to all clients connected to this game server. */ + public broadcastMessage(text: string, color = 0x000000, time = 5000, id = "") { + this.broadcast().u8(ClientBound.Notification).stringNT(text).u32(color).float(time).stringNT(id).send(); + } /** Ends the game instance. */ public end() { From 8ebcf3a53201507bb199957a91ef2896c4b5af8f 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, 15 Dec 2025 05:37:05 +0200 Subject: [PATCH 05/17] whoops --- src/Native/Arena.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 8a75a82d..66525dbc 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -329,14 +329,8 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity { } } - if (factories.length === 0) { // No factories on this team, spawn as usual - const { x, y } = this.findPlayerSpawnLocation(); - tank.positionData.values.x = x; - tank.positionData.values.y = y; + if (factories.length === 0) return false; // No available factories on this team, spawn as usual - return false; - } - const factory = randomFrom(factories); const { x, y } = factory.getWorldPosition(); From a2f5668d7287ea561a06000c02bcb6d57aa9023d 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, 15 Dec 2025 05:41:29 +0200 Subject: [PATCH 06/17] typescript --- src/Native/Arena.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 66525dbc..1a925a36 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -314,7 +314,7 @@ export default class ArenaEntity extends Entity implements TeamGroupEntity { tank.positionData.values.y = y; } - public attemptFactorySpawn(tank: TankBody) { + public attemptFactorySpawn(tank: TankBody): boolean { if (Math.random() > factorySpawnChance) return false; const team = tank.relationsData.values.team; From 2a9eb0da6886f741707b5b72565129616a648ed6 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, 15 Dec 2025 05:49:44 +0200 Subject: [PATCH 07/17] safety --- src/Gamemodes/Domination.ts | 4 +++- src/Gamemodes/Team2.ts | 4 +++- src/Gamemodes/Team4.ts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Gamemodes/Domination.ts b/src/Gamemodes/Domination.ts index df71c9d5..e4c50349 100644 --- a/src/Gamemodes/Domination.ts +++ b/src/Gamemodes/Domination.ts @@ -92,9 +92,11 @@ export default class DominationArena extends ArenaEntity { public spawnPlayer(tank: TankBody, client: Client) { const team = this.decideTeam(client); - const teamBase = team.base as TeamBase; TeamEntity.setTeam(team, tank); + const teamBase = team.base; + if (!teamBase) return super.spawnPlayer(tank, client); + const pos = ObjectEntity.getRandomPosition(teamBase); tank.positionData.values.x = pos.x; tank.positionData.values.y = pos.y; diff --git a/src/Gamemodes/Team2.ts b/src/Gamemodes/Team2.ts index 922de548..96d8c3ac 100644 --- a/src/Gamemodes/Team2.ts +++ b/src/Gamemodes/Team2.ts @@ -69,7 +69,9 @@ export default class Teams2Arena extends ArenaEntity { const success = this.attemptFactorySpawn(tank); if (success) return; // This player was spawned from a factory instead - const base = team.base as TeamBase; + const base = team.base; + if (!base) return super.spawnPlayer(tank, client); + const pos = ObjectEntity.getRandomPosition(base); tank.positionData.x = pos.x; tank.positionData.y = pos.y; diff --git a/src/Gamemodes/Team4.ts b/src/Gamemodes/Team4.ts index eb96bdc5..eea98764 100644 --- a/src/Gamemodes/Team4.ts +++ b/src/Gamemodes/Team4.ts @@ -79,7 +79,9 @@ export default class Teams4Arena extends ArenaEntity { const success = this.attemptFactorySpawn(tank); if (success) return; // This player was spawned from a factory instead - const base = team.base as TeamBase; + const base = team.base; + if (!base) return super.spawnPlayer(tank, client); + const pos = ObjectEntity.getRandomPosition(base); tank.positionData.x = pos.x; tank.positionData.y = pos.y; From c8cc8ed70f8eaf74680bc93162e9621493b43aed 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, 15 Dec 2025 15:29:32 +0200 Subject: [PATCH 08/17] fix crash --- src/Const/Commands.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Const/Commands.ts b/src/Const/Commands.ts index a153b08f..2ba3d7ab 100644 --- a/src/Const/Commands.ts +++ b/src/Const/Commands.ts @@ -233,9 +233,14 @@ export const commandCallbacks = { game_teleport: (client: Client, xArg: string, yArg: string) => { const player = client.camera?.cameraData.player; if (!Entity.exists(player) || !ObjectEntity.isObject(player)) return; + + if (!xArg || !yArg) 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; + player.positionData.x = x; player.positionData.y = y; player.setVelocity(0, 0); From 2984146c0fdaf590007adfe71a1ec101986e2aa8 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, 15 Dec 2025 18:14:23 +0200 Subject: [PATCH 09/17] update color properly --- src/Entity/Misc/TeamEntity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Misc/TeamEntity.ts b/src/Entity/Misc/TeamEntity.ts index bb995a76..0a460ab6 100644 --- a/src/Entity/Misc/TeamEntity.ts +++ b/src/Entity/Misc/TeamEntity.ts @@ -76,7 +76,7 @@ export class TeamEntity extends Entity implements TeamGroupEntity { entity.relationsData.values.team = team; - entity.styleData.values.color = team.teamData.values.teamColor; + entity.styleData.color = team.teamData.values.teamColor; if (TankBody.isTank(entity)) { entity.cameraEntity.relationsData.values.team = team; From d190a074d799dbb27f98288b2fabf397752b1405 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, 15 Dec 2025 18:21:15 +0200 Subject: [PATCH 10/17] admin fun cmd --- src/Const/Commands.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Const/Commands.ts b/src/Const/Commands.ts index 2ba3d7ab..72aae7ee 100644 --- a/src/Const/Commands.ts +++ b/src/Const/Commands.ts @@ -18,28 +18,36 @@ import Client from "../Client" import { AccessLevel, maxPlayerLevel } from "../config"; + +import ObjectEntity from "../Entity/Object"; +import LivingEntity from "../Entity/Live"; + import AbstractBoss from "../Entity/Boss/AbstractBoss"; import Defender from "../Entity/Boss/Defender"; import FallenBooster from "../Entity/Boss/FallenBooster"; import FallenOverlord from "../Entity/Boss/FallenOverlord"; import Guardian from "../Entity/Boss/Guardian"; import Summoner from "../Entity/Boss/Summoner"; -import LivingEntity from "../Entity/Live"; + import ArenaCloser from "../Entity/Misc/ArenaCloser"; import FallenAC from "../Entity/Misc/Boss/FallenAC"; -import Mothership from "../Entity/Misc/Mothership"; import FallenSpike from "../Entity/Misc/Boss/FallenSpike"; import FallenMegaTrapper from "../Entity/Misc/Boss/FallenMegaTrapper"; + +import Mothership from "../Entity/Misc/Mothership"; import Dominator from "../Entity/Misc/Dominator"; -import ObjectEntity from "../Entity/Object"; + import AbstractShape from "../Entity/Shape/AbstractShape"; import Crasher from "../Entity/Shape/Crasher"; import Pentagon from "../Entity/Shape/Pentagon"; import Square from "../Entity/Shape/Square"; import Triangle from "../Entity/Shape/Triangle"; + import AutoTurret from "../Entity/Tank/AutoTurret"; import Bullet from "../Entity/Tank/Projectile/Bullet"; import TankBody from "../Entity/Tank/TankBody"; + +import { TeamEntity } from "../Entity/Misc/TeamEntity"; import { AIState } from "../Entity/AI"; import { Entity, EntityStateFlags } from "../Native/Entity"; import { saveToVLog } from "../util"; @@ -60,6 +68,7 @@ export const enum CommandID { gameGodmode = "game_godmode", gameAnnounce = "game_announce", gameGoldenName = "game_golden_name", + gameNeutral = "game_neutral", adminSummon = "admin_summon", adminKillAll = "admin_kill_all", adminKillEntity = "admin_kill_entity", @@ -154,6 +163,12 @@ export const commandDefinitions = { description: "Toggles the golden nickname color that appears upon using cheats", permissionLevel: AccessLevel.FullAccess, isCheat: false + }, + game_neutral: { + id: CommandID.gameNeutral, + description: "Sets your tank's team to the neutral team", + permissionLevel: AccessLevel.FullAccess, + isCheat: false }, admin_summon: { id: CommandID.adminSummon, @@ -298,6 +313,15 @@ export const commandCallbacks = { game_golden_name: (client: Client, activeArg?: string) => { client.setHasCheated(!client.hasCheated()); }, + game_neutral: (client: Client) => { + const team = client.camera?.game.arena; + const player = client.camera?.cameraData.values.player; + + if (!team || !player) return; + if (!ObjectEntity.isObject(player)) return; + + TeamEntity.setTeam(team, player); + }, admin_summon: (client: Client, entityArg: string, countArg?: string, xArg?: string, yArg?: string) => { const count = countArg ? parseInt(countArg) : 1; let x = parseInt(xArg || "0", 10); From 255230b517ed2b7242b4d8c23c7ceaf129e0dfa6 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, 16 Dec 2025 00:07:45 +0200 Subject: [PATCH 11/17] tag cleanup --- src/Gamemodes/Tag.ts | 55 ++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/Gamemodes/Tag.ts b/src/Gamemodes/Tag.ts index 055f195d..9133f9b7 100644 --- a/src/Gamemodes/Tag.ts +++ b/src/Gamemodes/Tag.ts @@ -48,7 +48,8 @@ const ENABLE_DOMINATOR = false; */ export class TagShapeManager extends ShapeManager { protected get wantedShapes() { - const ratio = Math.ceil(Math.pow(this.game.arena.width / 2500, 2)); + const size = (this.game.arena.width + this.game.arena.height) / 2; + const ratio = Math.ceil(Math.pow(size / 2500, 2)); return Math.floor(12.5 * ratio); } @@ -61,9 +62,12 @@ export default class TagArena extends ArenaEntity { static override GAMEMODE_ID: string = "tag"; protected shapes: ShapeManager = new TagShapeManager(this); - + /** All team entities in game */ public teams: TeamEntity[] = []; + + /** Maps teams to their total score. */ + public teamScoreMap: Map = new Map(); /** Maps clients to their team */ public playerTeamMap: WeakMap = new WeakMap(); @@ -75,6 +79,7 @@ export default class TagArena extends ArenaEntity { this.arenaData.values.flags |= ArenaFlags.hiddenScores; const teamOrder = TEAM_COLORS.slice(); shuffleArray(teamOrder); + for (const teamColor of teamOrder) { const team = new TeamEntity(this.game, teamColor); this.teams.push(team); @@ -115,6 +120,8 @@ export default class TagArena extends ArenaEntity { const team = this.decideTeam(client); TeamEntity.setTeam(team, tank); + this.updateTeamScores(); // update team counts + const success = this.attemptFactorySpawn(tank); if (success) return; // This player was spawned from a factory instead @@ -128,9 +135,9 @@ export default class TagArena extends ArenaEntity { const length = Math.min(10, this.teams.length); for (let i = 0; i < length; ++i) { const team = this.teams[i]; - const playerCount = this.getTeamPlayers(team).length; - if (team.teamData.values.teamColor === Color.Tank) this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = Color.ScoreboardBar; - else this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = team.teamData.values.teamColor; + const playerCount = this.getTeamScore(team); + + this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = team.teamData.values.teamColor; this.arenaData.values.scoreboardNames[i as ValidScoreboardIndex] = team.teamName; this.arenaData.values.scoreboardTanks[i as ValidScoreboardIndex] = -1; this.arenaData.values.scoreboardScores[i as ValidScoreboardIndex] = playerCount; @@ -139,24 +146,38 @@ export default class TagArena extends ArenaEntity { this.arenaData.scoreboardAmount = Math.min(10, length); } + + public getTeamScore(team: TeamEntity): number { + return this.teamScoreMap.get(team) || 0; + } + + public updateTeamScores() { + for (let i = 0; i < this.teams.length; ++i) { + const team = this.teams[i]; + + this.teamScoreMap.set(team, this.getTeamPlayers(team).length); + } + this.teams.sort((t1, t2) => this.getTeamScore(t2) - this.getTeamScore(t1)); + } public updateArenaState() { - this.teams.sort((t1, t2) => this.getTeamPlayers(t2).length - this.getTeamPlayers(t1).length); + this.updateTeamScores(); + const length = Math.min(10, this.teams.length); const arenaPlayerCount = this.getAlivePlayers().length; // Only count alive players for win condition - const leaderTeam = this.teams[0]; // Most players are in this team + const leaderTeam = this.teams[0]; // Most players are on this team + for (let i = 0; i < length; ++i) { const team = this.teams[i]; if (this.getTeamPlayers(leaderTeam).length === arenaPlayerCount && arenaPlayerCount >= MIN_PLAYERS) { // If all alive players are in the leading team, it has won since all other team's players have died if (this.state === ArenaState.OPEN) { - this.game.broadcast() - .u8(ClientBound.Notification) - .stringNT(`${leaderTeam.teamName} HAS WON THE GAME!`) - .u32(ColorsHexCode[leaderTeam.teamData.values.teamColor]) - .float(-1) - .stringNT("").send(); - + this.game.broadcastMessage( + `${leaderTeam.teamName} HAS WON THE GAME!`, + ColorsHexCode[leaderTeam.teamData.values.teamColor], + -1 + ) + this.state = ArenaState.OVER; setTimeout(() => { this.close(); @@ -178,8 +199,12 @@ export default class TagArena extends ArenaEntity { if ((this.game.tick % scoreboardUpdateInterval) === 0) { this.updateScoreboard(); } + } + + public tick(tick: number) { + super.tick(tick); - if ((this.game.tick % SHRINK_INTERVAL) === 0 && this.width > MIN_SIZE) { + if ((tick % SHRINK_INTERVAL) === 0 && this.width > MIN_SIZE) { this.updateBounds(this.width - SHRINK_AMOUNT, this.height - SHRINK_AMOUNT); } } From f3eb97efbeb1d2be179267942a5caace66284648 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, 20 Dec 2025 14:56:19 +0200 Subject: [PATCH 12/17] Crash fix --- src/Entity/Object.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index 959a8f49..25e82b10 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -259,7 +259,11 @@ export default class ObjectEntity extends Entity { if (this.physicsData.values.flags & PhysicsFlags.showsOnMap) { const globalEntities = this.game.entities.globalEntities; - util.removeFast(globalEntities, globalEntities.indexOf(this.id)); + const id = this.id; + + if (!globalEntities.includes(id)) return; + + util.removeFast(globalEntities, globalEntities.indexOf(id)); } super.delete(); From 569eca5095bfb0fff9c9dc2bdbad0add9bac432d 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, 27 Dec 2025 16:16:20 +0200 Subject: [PATCH 13/17] speedup AI slightly --- src/Entity/AI.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Entity/AI.ts b/src/Entity/AI.ts index 8850ec2e..97d760ed 100644 --- a/src/Entity/AI.ts +++ b/src/Entity/AI.ts @@ -88,7 +88,7 @@ export class AI { /** The current game. */ public game: GameServer; /** The AI's target. */ - public target: Entity & { positionData: PositionGroup, physicsData: PhysicsGroup, relationsData: RelationsGroup, velocity: Vector } | null = null; + public target: ObjectEntity | null = null; /** The speed at which the ai's owner can move. */ public movementSpeed = 1; /** The speed at which the ai can reach the target. */ @@ -168,9 +168,9 @@ export class AI { chunk ^= bitValue; const id = 32 * i + bitIdx; - const entity = this.game.entities.inner[id] as ObjectEntity; + const entity = this.game.entities.inner[id]; if (!entity || entity.hash === 0) continue; - if (!entity.positionData || !entity.relationsData || !entity.physicsData) continue; + if (!ObjectEntity.isObject(entity)) continue; if (!entity.isPhysical) continue; // Check if the target is living @@ -197,7 +197,7 @@ export class AI { } } - return this.target = closestEntity as Entity & { positionData: PositionGroup, physicsData: PhysicsGroup, relationsData: RelationsGroup, velocity: Vector }; + return this.target = closestEntity; } /** Aims and predicts at the target. */ From b547697163a5267e65fac988d2ecbf98d42d6437 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, 27 Dec 2025 16:29:36 +0200 Subject: [PATCH 14/17] move getRandomPosition to util --- src/Entity/Object.ts | 28 ---------------------------- src/Gamemodes/Domination.ts | 4 ++-- src/Gamemodes/Team2.ts | 4 ++-- src/Gamemodes/Team4.ts | 4 ++-- src/util.ts | 31 +++++++++++++++++++++++++++++-- 5 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index 25e82b10..b029dff7 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -204,34 +204,6 @@ export default class ObjectEntity extends Entity { return dX*dX + dY*dY <= rSum*rSum; } } - - /** Returns a random position inside the given entity. */ - public static getRandomPosition(entity: ObjectEntity): VectorAbstract { - if (entity.isChild) util.warn("Attached entities are not fully supported by physics engine for now"); - - const pos = { - x: entity.positionData.values.x, - y: entity.positionData.values.y - } - - const isRect = entity.physicsData.values.sides === 2; - - if (isRect) { // Rectangular hitbox - const xOffset = (Math.random() - 0.5) * entity.physicsData.values.size, - yOffset = (Math.random() - 0.5) * entity.physicsData.values.width; - - pos.x += xOffset; - pos.y += yOffset; - } else { // Circular hitbox - const radius = Math.random() * entity.physicsData.values.size; - const angle = Math.random() * util.PI2; - - pos.x += Math.cos(angle) * radius; - pos.y += Math.sin(angle) * radius; - } - - return pos; - } /** Calls the deletion animation, unless animate is set to false, in that case it instantly deletes. */ public destroy(animate = true) { diff --git a/src/Gamemodes/Domination.ts b/src/Gamemodes/Domination.ts index e4c50349..ae0a4529 100644 --- a/src/Gamemodes/Domination.ts +++ b/src/Gamemodes/Domination.ts @@ -26,7 +26,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"; +import { randomFrom, getRandomPosition } from "../util"; const arenaSize = 11150; const baseSize = arenaSize / (3 + 1/3); // 3345, must scale with arena size @@ -97,7 +97,7 @@ export default class DominationArena extends ArenaEntity { const teamBase = team.base; if (!teamBase) return super.spawnPlayer(tank, client); - const pos = ObjectEntity.getRandomPosition(teamBase); + const pos = getRandomPosition(teamBase); tank.positionData.values.x = pos.x; tank.positionData.values.y = pos.y; } diff --git a/src/Gamemodes/Team2.ts b/src/Gamemodes/Team2.ts index 96d8c3ac..ff2179f4 100644 --- a/src/Gamemodes/Team2.ts +++ b/src/Gamemodes/Team2.ts @@ -26,7 +26,7 @@ import TankBody from "../Entity/Tank/TankBody"; import { TeamEntity } from "../Entity/Misc/TeamEntity"; import { Color } from "../Const/Enums"; -import { randomFrom } from "../util"; +import { randomFrom, getRandomPosition } from "../util"; const arenaSize = 11150; const baseWidth = arenaSize / (3 + 1/3) * 0.6; // 2007 @@ -72,7 +72,7 @@ export default class Teams2Arena extends ArenaEntity { const base = team.base; if (!base) return super.spawnPlayer(tank, client); - const pos = ObjectEntity.getRandomPosition(base); + const pos = getRandomPosition(base); tank.positionData.x = pos.x; tank.positionData.y = pos.y; } diff --git a/src/Gamemodes/Team4.ts b/src/Gamemodes/Team4.ts index eea98764..e56b8305 100644 --- a/src/Gamemodes/Team4.ts +++ b/src/Gamemodes/Team4.ts @@ -26,7 +26,7 @@ import TankBody from "../Entity/Tank/TankBody"; import { TeamEntity } from "../Entity/Misc/TeamEntity"; import { Color } from "../Const/Enums"; -import { randomFrom } from "../util"; +import { randomFrom, getRandomPosition } from "../util"; const arenaSize = 11150; const baseSize = arenaSize / (3 + 1/3); // 3345 @@ -82,7 +82,7 @@ export default class Teams4Arena extends ArenaEntity { const base = team.base; if (!base) return super.spawnPlayer(tank, client); - const pos = ObjectEntity.getRandomPosition(base); + const pos = getRandomPosition(base); tank.positionData.x = pos.x; tank.positionData.y = pos.y; } diff --git a/src/util.ts b/src/util.ts index 1fc10421..4270ba05 100644 --- a/src/util.ts +++ b/src/util.ts @@ -19,6 +19,8 @@ import chalk from "chalk"; import { inspect } from "util"; import { doVerboseLogs } from "./config"; +import { VectorAbstract } from "./Physics/Vector"; +import ObjectEntity from "./Entity/Object"; /** Logs data prefixed with the Date. */ export const log = (...args: any[]) => { @@ -71,16 +73,41 @@ export const constrain = (value: number, min: number, max: number): number => { return Math.max(min, Math.min(max, value)); } -/** 2π */ +/** 2PI / TAU */ export const PI2 = Math.PI * 2; /** - * Normalize angle (ex: 4π-> 0π, 3π -> 1π) + * Normalize angle (ex: 4PI-> 0PI, 3PI -> 1PI) */ export const normalizeAngle = (angle: number): number => { return ((angle % PI2) + PI2) % PI2; } +/** + * Returns a random position inside the given entity. Might not work correctly with attach entities + */ +export const getRandomPosition = (entity: ObjectEntity): VectorAbstract => { + const pos = entity.getWorldPosition(); + + const isRect = entity.physicsData.values.sides === 2; + + if (isRect) { // Rectangular hitbox + const xOffset = (Math.random() - 0.5) * entity.physicsData.values.size, + yOffset = (Math.random() - 0.5) * entity.physicsData.values.width; + + pos.x += xOffset; + pos.y += yOffset; + } else { // Circular hitbox + const radius = Math.random() * entity.physicsData.values.size; + const angle = Math.random() * PI2; + + pos.x += Math.cos(angle) * radius; + pos.y += Math.sin(angle) * radius; + } + + return pos; +} + /** * Logs - Used to have a webhook log here */ From e1001553e3fd7566dc8b09de6c3966d79a12ef9d 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, 28 Dec 2025 12:41:07 +0200 Subject: [PATCH 15/17] Fix maze wall team --- src/Entity/Misc/MazeWall.ts | 13 ++++++------ src/Entity/Object.ts | 4 ++-- src/Entity/Tank/Barrel.ts | 30 ++++++++++++++-------------- src/Entity/Tank/Projectile/Bullet.ts | 2 +- src/Misc/MazeGenerator.ts | 2 +- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Entity/Misc/MazeWall.ts b/src/Entity/Misc/MazeWall.ts index bd36b2d7..c4f23483 100644 --- a/src/Entity/Misc/MazeWall.ts +++ b/src/Entity/Misc/MazeWall.ts @@ -16,16 +16,17 @@ along with this program. If not, see */ -import GameServer from "../../Game"; +import ArenaEntity from "../../Native/Arena"; import ObjectEntity from "../Object"; 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, + arena: ArenaEntity, minX: number, minY: number, maxX: number, @@ -40,11 +41,11 @@ export default class MazeWall extends ObjectEntity { const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; - return new MazeWall(game, centerX, centerY, width, height); + return new MazeWall(arena, centerX, centerY, width, height); } - public constructor(game: GameServer, x: number, y: number, width: number, height: number) { - super(game); + public constructor(arena: ArenaEntity, x: number, y: number, width: number, height: number) { + super(arena.game); this.setGlobalEntity(); @@ -58,7 +59,7 @@ export default class MazeWall extends ObjectEntity { this.physicsData.values.pushFactor = 2; this.physicsData.values.absorbtionFactor = 0; - this.relationsData.values.team = this.game.arena; + this.relationsData.values.team = arena; this.styleData.values.borderWidth = 10; this.styleData.values.color = Color.Box; diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index b029dff7..44c45c1a 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -95,11 +95,11 @@ export default class ObjectEntity extends Entity { /** Used to determine the parent of all parents. */ public rootParent: ObjectEntity = this; - /** Entity tags. */ + /** Entity bit flag tags. */ public entityTags: number = 0; /** Entity type ID. */ - public arenaMobID: string = "" + public arenaMobID: string | null = null; /** Velocity used for physics. */ public velocity = new Vector(); diff --git a/src/Entity/Tank/Barrel.ts b/src/Entity/Tank/Barrel.ts index 792fdc23..d160d6a9 100644 --- a/src/Entity/Tank/Barrel.ts +++ b/src/Entity/Tank/Barrel.ts @@ -1,5 +1,5 @@ /* - DiepCustom - custom tank game server that shares diep.io's WebSocket protocol + 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 @@ -38,7 +38,7 @@ import { BarrelAddon, BarrelAddonById } from "./BarrelAddons"; import { Swarm } from "./Projectile/Swarm"; import NecromancerSquare from "./Projectile/NecromancerSquare"; /** - * Class that determines when barrels can shoot, and when they can't. + * Class that determines when barrels can shoot, and when they can"t. */ export class ShootCycle { /** The barrel this cycle is keeping track of. */ @@ -61,7 +61,7 @@ export class ShootCycle { 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'); + const alwaysShoot = (this.barrelEntity.definition.forceFire) || (this.barrelEntity.definition.bullet.type === "drone") || (this.barrelEntity.definition.bullet.type === "minion"); if (this.pos >= reloadTime) { // When its not shooting dont shoot, unless its a drone @@ -70,7 +70,7 @@ export class ShootCycle { return; } // When it runs out of drones, dont shoot - if (typeof this.barrelEntity.definition.droneCount === 'number' && this.barrelEntity.droneCount >= this.barrelEntity.definition.droneCount) { + if (typeof this.barrelEntity.definition.droneCount === "number" && this.barrelEntity.droneCount >= this.barrelEntity.definition.droneCount) { this.pos = reloadTime; return; } @@ -105,7 +105,7 @@ export default class Barrel extends ObjectEntity { /** Number of drones that this barrel shot that are still alive. */ public droneCount = 0; - /** The barrel's addons */ + /** The barrel"s addons */ public addons: BarrelAddon[] = []; /** Always existant barrel field group, present on all barrels. */ @@ -169,32 +169,32 @@ export default class Barrel extends ObjectEntity { case "rocket": new Rocket(this, this.tank, tankDefinition, angle); break; - case 'bullet': { + case "bullet": { projectile = new Bullet(this, this.tank, tankDefinition, angle); if (tankDefinition && (tankDefinition.id === Tank.ArenaCloser || tankDefinition.id === DevTank.Squirrel)) projectile.positionData.flags |= PositionFlags.canMoveThroughWalls; break; } - case 'trap': + case "trap": projectile = new Trap(this, this.tank, tankDefinition, angle); break; - case 'drone': + case "drone": projectile = new Drone(this, this.tank, tankDefinition, angle); break; - case 'necrodrone': + case "necrodrone": projectile = new NecromancerSquare(this, this.tank, tankDefinition, angle); break; - case 'swarm': + case "swarm": projectile = new Swarm(this, this.tank, tankDefinition, angle); break; - case 'minion': + case "minion": projectile = new Minion(this, this.tank, tankDefinition, angle); break; - case 'flame': + case "flame": projectile = new Flame(this, this.tank, tankDefinition, angle); break; - case 'wall': { - const w = projectile = new MazeWall(this.game, Math.round(this.tank.inputs.mouse.x / 50) * 50, Math.round(this.tank.inputs.mouse.y / 50) * 50, 250, 250); + case "wall": { + const w = projectile = new MazeWall(this.game.arena, Math.round(this.tank.inputs.mouse.x / 50) * 50, Math.round(this.tank.inputs.mouse.y / 50) * 50, 250, 250); setTimeout(() => { w.delete(); }, 60 * 1000); @@ -204,7 +204,7 @@ export default class Barrel extends ObjectEntity { projectile = new CrocSkimmer(this, this.tank, tankDefinition, angle); break; default: - util.log('Ignoring attempt to spawn projectile of type ' + this.definition.bullet.type); + util.log("Ignoring attempt to spawn projectile of type " + this.definition.bullet.type); break; } diff --git a/src/Entity/Tank/Projectile/Bullet.ts b/src/Entity/Tank/Projectile/Bullet.ts index ffc3a29c..be587d73 100644 --- a/src/Entity/Tank/Projectile/Bullet.ts +++ b/src/Entity/Tank/Projectile/Bullet.ts @@ -73,7 +73,7 @@ export default class Bullet extends LivingEntity { this.physicsData.values.sides = 1; this.physicsData.values.flags |= PhysicsFlags.noOwnTeamCollision | PhysicsFlags.canEscapeArena; - if (tank.positionData.values.flags & PositionFlags.canMoveThroughWalls || this.relationsData.values.team === this.game.arena) this.positionData.values.flags |= PositionFlags.canMoveThroughWalls + if (tank.positionData.values.flags & PositionFlags.canMoveThroughWalls) this.positionData.values.flags |= PositionFlags.canMoveThroughWalls this.physicsData.values.size = (barrel.physicsData.values.width / 2) * bulletDefinition.sizeRatio; this.styleData.values.color = tank.rootParent.styleData.values.color; this.styleData.values.flags |= StyleFlags.hasNoDmgIndicator; diff --git a/src/Misc/MazeGenerator.ts b/src/Misc/MazeGenerator.ts index aae11c37..69d86b3a 100644 --- a/src/Misc/MazeGenerator.ts +++ b/src/Misc/MazeGenerator.ts @@ -239,7 +239,7 @@ export default class MazeGenerator { ): 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); + return MazeWall.newFromBounds(arena, minX, minY, maxX, maxY); } /** Allows for easier (x, y) based getting of maze cells */ From e4eb26f5a3ef98ee26abcee3ad413cdd292d36d6 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, 29 Dec 2025 17:12:14 +0200 Subject: [PATCH 16/17] factory spawn chance tweak --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index c86f8f6e..faffafdf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,7 +50,7 @@ export const countdownDuration = 10 * tps; export const shinyChance: number = 1 / 1_000_000; /** Chance for a player to spawn out of an allied factory. */ -export const factorySpawnChance: number = 0.5; +export const factorySpawnChance: number = 0.05; /** Is hosting a rest api */ export const enableApi: boolean = true; From d12f298688dcdc4c910aec34b954d962fa0cabfc 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, 4 Jan 2026 03:58:29 +0200 Subject: [PATCH 17/17] Fix spawns --- src/Native/Arena.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Native/Arena.ts b/src/Native/Arena.ts index 1a925a36..32f4950b 100644 --- a/src/Native/Arena.ts +++ b/src/Native/Arena.ts @@ -143,7 +143,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 (!TankBody.isTank(entity) || !AbstractBoss.isBoss(entity)) 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;