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