diff --git a/src/Entity/AI.ts b/src/Entity/AI.ts index 4b74c2d8..779d8ac3 100644 --- a/src/Entity/AI.ts +++ b/src/Entity/AI.ts @@ -19,12 +19,14 @@ import GameServer from "../Game"; import Vector, { VectorAbstract } from "../Physics/Vector"; import ObjectEntity from "./Object"; +import LivingEntity from "./Live"; +import TankBody from "./Tank/TankBody"; -import { InputFlags, PhysicsFlags } from "../Const/Enums"; +import { InputFlags, PhysicsFlags, EntityTags } from "../Const/Enums"; import { Entity } from "../Native/Entity"; import { PhysicsGroup, PositionGroup, RelationsGroup } from "../Native/FieldGroups"; import PackedEntitySet from "../Physics/PackedEntitySet"; - +import { tps } from "../config"; // Beware // The logic in this file is somewhat messed up @@ -39,6 +41,14 @@ export const enum AIState { possessed = 3 } +export const enum PriorityLevel { + Passive = 0, + Neutral = 1, + Hostile = 2 +} + +const TARGET_RESET_INTERVAL = 60 * tps; + /** * Inputs are the shared thing between AIs and Clients. Both use inputs * and both can replace eachother. @@ -95,11 +105,19 @@ export class AI { public aimSpeed = 1; /** If the AI should predict enemy's movements, and aim accordingly. */ public doAimPrediction: boolean = false; - + /** If the AI should stay near the owner, used by drones. */ + public stayAroundOwner: boolean = false; + /** If the AI should ignore all shapes. */ + public ignoreShapes: boolean = false; + /** The minimum player level that this AI can target. */ + public minPlayerLevel: number = 0; /** Optionally filter targets for health */ public targetFilterNonLiving = true; /** Target filter letting owner classes filter what can't be a target by position - false = not valid target */ public targetFilter: (possibleTargetPos: VectorAbstract) => boolean; + + /** The game tick that this AI found a valid target. */ + private targetResetTick: number = -1; /** Stores a per-AI hash used to optimize ticking */ private _aiHash: number; @@ -130,34 +148,36 @@ export class AI { return Entity.exists(this.target) ? this.target : (this.target = null); } - const rootPos = this.owner.rootParent.positionData.values; + const rootPos = this.owner.getWorldPosition(); const team = this.owner.relationsData.values.team; + const range = this.viewRange ** 2; + const maxRange = (this.viewRange * 1.5) ** 2; // TODO(speed): find a way to speed up if (Entity.exists(this.target)) { - // If the AI already has a valid target within view distance, it's not necessary to find a new one - // Make sure the target hasn't changed teams, and is existant (sides != 0) if (team !== this.target.relationsData.values.team && this.target.physicsData.values.sides !== 0) { // confirm its within range - const targetDistSq = (this.target.positionData.values.x - rootPos.x) ** 2 + (this.target.positionData.values.y - rootPos.y) ** 2; - if (this.targetFilter(this.target.positionData.values) && targetDistSq < (this.viewRange ** 2) * 2) return this.target; // this range is inaccurate i think + const pos = this.stayAroundOwner ? this.owner.getRootOwner().positionData.values : rootPos; + const targetDistSq = (this.target.positionData.values.x - pos.x) ** 2 + (this.target.positionData.values.y - pos.y) ** 2; + if (this.targetFilter(this.target.positionData.values) && targetDistSq < maxRange) { + return this.target; + } } } - // const entities = this.game.entities.inner.slice(0, this.game.entities.lastId); - const root = (this.owner.rootParent === this.owner && (this.owner.relationsData.values.owner as ObjectEntity)?.positionData) ? this.owner.relationsData.values.owner as ObjectEntity : this.owner.rootParent; const entities = this.viewRange === Infinity ? PackedEntitySet.FULL_SET : this.game.entities.collisionManager.retrieve( - root.positionData.values.x, root.positionData.values.y, + rootPos.x, rootPos.y, this.viewRange, this.viewRange ); - let closestEntity = null; - let closestDistSq = this.viewRange ** 2; + let chosenEntity = null; + let highestPriority = -1; + let closestDistSq = range; for (let i = 0; i < entities.data.length; ++i) { let chunk = entities.data[i]; @@ -175,6 +195,11 @@ export class AI { if (!entity.isPhysical) continue; // Check if the target is living if (this.targetFilterNonLiving && !entity.healthData) continue; + // Check if the target is a shape + if (this.ignoreShapes && entity.entityTags & EntityTags.isShape) continue; + // Check if the target is a tank and has enough levels + // We could use isTank here, but isObject deals with most of that already, and this code runs many times per tick + if (this.minPlayerLevel && entity.entityTags & EntityTags.isTank && (entity as TankBody).cameraEntity.cameraData.values.level < this.minPlayerLevel) continue; // Check if the target is a base if (entity.physicsData.values.flags & PhysicsFlags.isBase) continue; // Don't target entities who have an object owner @@ -186,23 +211,36 @@ export class AI { // Custom check if (!this.targetFilter(entity.positionData.values)) continue; + if (this.stayAroundOwner) { + const rootOwnerPos = this.owner.getRootOwner().positionData.values; + const dX = entity.positionData.values.x - rootOwnerPos.x; + const dY = entity.positionData.values.y - rootOwnerPos.y; + const distSq = dX * dX + dY * dY; + + if (distSq > maxRange) continue; + } + const dX = entity.positionData.values.x - rootPos.x; const dY = entity.positionData.values.y - rootPos.y; const distSq = dX * dX + dY * dY; - if (distSq < closestDistSq) { - closestEntity = entity; + const isBetter = entity.aiPriority > highestPriority || (entity.aiPriority === highestPriority && distSq < closestDistSq); + + if (isBetter) { + chosenEntity = entity; closestDistSq = distSq; + highestPriority = entity.aiPriority; + this.targetResetTick = tick + TARGET_RESET_INTERVAL; } } } - return this.target = closestEntity; + return this.target = chosenEntity; } /** Aims and predicts at the target. */ public aimAtTarget() { - if(!this.target) return; + if (!this.target) return; const movementSpeed = this.aimSpeed * 1.6; const ownerPos = this.owner.getWorldPosition(); @@ -213,7 +251,6 @@ export class AI { } if (movementSpeed <= 0.001) { // Pls no weirdness - this.inputs.movement.set({ x: pos.x - ownerPos.x, y: pos.y - ownerPos.y @@ -225,6 +262,7 @@ export class AI { this.inputs.movement.magnitude = 1; return; } + if (this.doAimPrediction) { const delta = { x: pos.x - ownerPos.x, @@ -252,7 +290,6 @@ export class AI { x: pos.x + offset * unitDistancePerp.x, y: pos.y + offset * unitDistancePerp.y }); - } else { this.inputs.mouse.set({ x: pos.x, @@ -263,6 +300,21 @@ export class AI { this.inputs.movement.magnitude = 1; this.inputs.movement.angle = Math.atan2(this.inputs.mouse.y - ownerPos.y, this.inputs.mouse.x - ownerPos.x); } + + public onDamage(source: LivingEntity, amount: number) { + if (this.target) return; + + const owner = source.getRootOwner(); + + const range = (this.viewRange * 1.5) ** 2; + const dX = owner.positionData.values.x - this.owner.positionData.values.x; + const dY = owner.positionData.values.y - this.owner.positionData.values.y; + const distSq = dX * dX + dY * dY; + + if (distSq < range) { + this.target = owner; + } + } public tick(tick: number) { // If its being posessed, but its possessor is deleted... then just restart; @@ -284,6 +336,11 @@ export class AI { y: Math.sin(angle) * 100 }); } else { + if (tick === this.targetResetTick) { + this.target = null; + return this.findTarget(tick); // Find another target next tick + } + this.state = AIState.hasTarget; this.inputs.flags |= InputFlags.leftclick; this.aimAtTarget(); diff --git a/src/Entity/Boss/AbstractBoss.ts b/src/Entity/Boss/AbstractBoss.ts index b858b202..9cbe8db0 100644 --- a/src/Entity/Boss/AbstractBoss.ts +++ b/src/Entity/Boss/AbstractBoss.ts @@ -24,7 +24,7 @@ import TankBody from "../Tank/TankBody"; import { ClientBound, Color, PositionFlags, NameFlags, EntityTags } from "../../Const/Enums"; import { VectorAbstract } from "../../Physics/Vector"; -import { AI, AIState, Inputs } from "../AI"; +import { AI, AIState, Inputs, PriorityLevel } from "../AI"; import { NameGroup } from "../../Native/FieldGroups"; import { Entity } from "../../Native/Entity"; import { CameraEntity } from "../../Native/Camera"; @@ -127,6 +127,7 @@ export default class AbstractBoss extends LivingEntity { this.ai = new AI(this); this.ai.viewRange = 2000; + this.ai.doAimPrediction = true; this.ai['_findTargetInterval'] = 0; this.inputs = this.ai.inputs; @@ -140,6 +141,8 @@ export default class AbstractBoss extends LivingEntity { this.healthData.values.health = this.healthData.values.maxHealth = 3000; + this.aiPriority = PriorityLevel.Hostile; + this.entityTags |= EntityTags.isBoss; } @@ -182,7 +185,8 @@ export default class AbstractBoss extends LivingEntity { if (this.inputs !== this.ai.inputs) this.inputs = this.ai.inputs; this.ai.movementSpeed = this.movementSpeed; - + this.ai.aimSpeed = this.barrels[0]?.bulletAccel ?? 1; + if (this.ai.state !== AIState.possessed) this.moveAroundMap(); else { const x = this.positionData.values.x, diff --git a/src/Entity/Boss/FallenBooster.ts b/src/Entity/Boss/FallenBooster.ts index 7f94b069..4212e5a1 100644 --- a/src/Entity/Boss/FallenBooster.ts +++ b/src/Entity/Boss/FallenBooster.ts @@ -41,6 +41,7 @@ export default class FallenBooster extends AbstractBoss { def.bullet = Object.assign({}, def.bullet, { speed: 1.7, health: 6.25, damage:def.bullet.damage * 0.8 }); this.barrels.push(new Barrel(this, def)); } + this.scale(Math.pow(1.01, 75 - 1)); // Level 75 size } diff --git a/src/Entity/Object.ts b/src/Entity/Object.ts index 63b94a0f..71d8def0 100644 --- a/src/Entity/Object.ts +++ b/src/Entity/Object.ts @@ -23,6 +23,7 @@ import Vector from "../Physics/Vector"; import { PhysicsGroup, PositionGroup, RelationsGroup, StyleGroup } from "../Native/FieldGroups"; import { Entity } from "../Native/Entity"; import { PositionFlags, PhysicsFlags, EntityTags } from "../Const/Enums"; +import { PriorityLevel } from "./AI"; /** * The animator for how entities delete (the opacity and size fade out). @@ -111,6 +112,9 @@ export default class ObjectEntity extends Entity { /** Percent of accel applied when dying. */ public deathAccelFactor = 0.9; + /** AI target priority for this entity, makes drones and turret prioritize this entity over others. */ + public aiPriority: PriorityLevel = PriorityLevel.Neutral; + /** For internal spatial hash grid */ private _queryId: number = -1; @@ -343,6 +347,15 @@ export default class ObjectEntity extends Entity { this.isPhysical = false; } + public getRootOwner(): ObjectEntity { + let owner: ObjectEntity = this; + while (ObjectEntity.isObject(owner.relationsData.values.owner) && owner.relationsData.values.owner.hash !== 0) { + owner = owner.relationsData.values.owner; + } + + return owner; + } + /** Returns the true world position (even for objects who have parents). */ public getWorldPosition(): Vector { let pos = new Vector(this.positionData.values.x, this.positionData.values.y); diff --git a/src/Entity/Shape/AbstractShape.ts b/src/Entity/Shape/AbstractShape.ts index fc4e3502..726701ae 100644 --- a/src/Entity/Shape/AbstractShape.ts +++ b/src/Entity/Shape/AbstractShape.ts @@ -23,7 +23,7 @@ import LivingEntity from "../Live"; import { Entity } from "../../Native/Entity"; import { Color, PositionFlags, NameFlags, EntityTags } from "../../Const/Enums"; import { NameGroup } from "../../Native/FieldGroups"; -import { AI } from "../AI"; +import { AI, PriorityLevel } from "../AI"; import { normalizeAngle, PI2 } from "../../util"; /** @@ -77,6 +77,8 @@ export default class AbstractShape extends LivingEntity { this.maxDamageMultiplier = 4.0; + this.aiPriority = PriorityLevel.Passive; + this.entityTags |= EntityTags.isShape; } diff --git a/src/Entity/Shape/Crasher.ts b/src/Entity/Shape/Crasher.ts index 55f119b1..ca0b4c12 100644 --- a/src/Entity/Shape/Crasher.ts +++ b/src/Entity/Shape/Crasher.ts @@ -21,7 +21,7 @@ import LivingEntity from "../Live"; import AbstractShape from "./AbstractShape"; import { Color, PositionFlags } from "../../Const/Enums"; -import { AI, AIState } from "../AI"; +import { AI, AIState, PriorityLevel } from "../AI"; import { tps } from "../../config"; /** @@ -57,8 +57,11 @@ export default class Crasher extends AbstractShape { this.ai = new AI(this); this.ai.viewRange = 2000; + this.ai.aimSpeed = (this.ai.movementSpeed = this.targettingSpeed); - this.ai['_findTargetInterval'] = tps; + this.ai["_findTargetInterval"] = tps; + + this.aiPriority = PriorityLevel.Neutral; this.arenaMobID = "crasher"; } diff --git a/src/Entity/Tank/Projectile/Drone.ts b/src/Entity/Tank/Projectile/Drone.ts index 909fb5ed..a7440b50 100644 --- a/src/Entity/Tank/Projectile/Drone.ts +++ b/src/Entity/Tank/Projectile/Drone.ts @@ -18,6 +18,8 @@ import Barrel from "../Barrel"; import Bullet from "./Bullet"; +import ObjectEntity from "../../Object"; +import LivingEntity from "../../Live"; import { PhysicsFlags, StyleFlags } from "../../../Const/Enums"; import { TankDefinition } from "../../../Const/TankDefinitions"; @@ -53,7 +55,8 @@ export default class Drone extends Bullet { this.ai = new AI(this); this.ai.viewRange = 900; - this.ai.targetFilter = (targetPos) => (targetPos.x - this.tank.positionData.values.x) ** 2 + (targetPos.y - this.tank.positionData.values.y) ** 2 <= this.ai.viewRange ** 2; // (1000 ** 2) 1000 radius + this.ai.stayAroundOwner = true; + //this.ai.ignoreShapes = this.barrelEntity.definition.ignoreShapes || false; this.canControlDrones = typeof this.barrelEntity.definition.canControlDrones === 'boolean' && this.barrelEntity.definition.canControlDrones; this.physicsData.values.sides = 3; if (this.physicsData.values.flags & PhysicsFlags.noOwnTeamCollision) this.physicsData.values.flags ^= PhysicsFlags.noOwnTeamCollision; @@ -93,6 +96,12 @@ export default class Drone extends Bullet { super.tick(tick); } + public receiveDamage(source: LivingEntity, amount: number): void { + this.ai.onDamage(source, amount); + + super.receiveDamage(source, amount); + } + public tick(tick: number) { const usingAI = !this.canControlDrones || this.tank.inputs.deleted || (!this.tank.inputs.attemptingShot() && !this.tank.inputs.attemptingRepel()); const inputs = !usingAI ? this.tank.inputs : this.ai.inputs; @@ -130,8 +139,6 @@ export default class Drone extends Bullet { this.restCycle = false } - - if (this.canControlDrones && inputs.attemptingRepel()) { this.positionData.angle += Math.PI; } @@ -141,4 +148,4 @@ export default class Drone extends Bullet { this.tickMixin(tick); } -} \ No newline at end of file +} diff --git a/src/Entity/Tank/Projectile/Minion.ts b/src/Entity/Tank/Projectile/Minion.ts index 0205892a..a7dca1f2 100644 --- a/src/Entity/Tank/Projectile/Minion.ts +++ b/src/Entity/Tank/Projectile/Minion.ts @@ -57,6 +57,7 @@ import { CameraEntity } from "../../../Native/Camera"; export default class Minion extends Drone implements BarrelBase { /** Size of the focus the minions orbit. */ public static FOCUS_RADIUS = 800 ** 2; + public static MIN_RADIUS = 300 ** 2; /** The minion's barrel */ private minionBarrel: Barrel; @@ -74,7 +75,7 @@ export default class Minion extends Drone implements BarrelBase { const bulletDefinition = barrel.definition.bullet; this.inputs = this.ai.inputs; - this.ai.viewRange = 900; + this.ai.viewRange = 1200; this.usePosAngle = false; this.physicsData.values.sides = 1; @@ -108,7 +109,7 @@ export default class Minion extends Drone implements BarrelBase { const dist = inputs.mouse.distanceToSQ(this.positionData.values); - if (dist < Minion.FOCUS_RADIUS / 7) { + if (dist < Minion.MIN_RADIUS) { this.movementAngle = this.positionData.values.angle + Math.PI; } else if (dist < Minion.FOCUS_RADIUS) { this.movementAngle = this.positionData.values.angle + Math.PI / 2; diff --git a/src/Entity/Tank/TankBody.ts b/src/Entity/Tank/TankBody.ts index 284e2ae9..2b325363 100644 --- a/src/Entity/Tank/TankBody.ts +++ b/src/Entity/Tank/TankBody.ts @@ -35,7 +35,7 @@ import { Addon, AddonById } from "./Addons"; import { getTankById, TankDefinition, visibilityRateDamage } from "../../Const/TankDefinitions"; import { sendAchievementEvent } from "../../Const/Achievements"; import { DevTank } from "../../Const/DevTankDefinitions"; -import { Inputs } from "../AI"; +import { Inputs, PriorityLevel } from "../AI"; import { ArenaState } from "../../Native/Arena"; import { AccessLevel, maxPlayerLevel } from "../../config"; @@ -101,6 +101,8 @@ export default class TankBody extends LivingEntity implements BarrelBase { this.damagePerTick = 5; this.maxDamageMultiplier = 6; this.setTank(Tank.Basic); + + this.aiPriority = PriorityLevel.Hostile; this.entityTags |= EntityTags.isTank; } @@ -212,13 +214,14 @@ export default class TankBody extends LivingEntity implements BarrelBase { const victimIsTank = TankBody.isTank(entity); sendAchievementEvent(client, "kill", { - "weapon.isTank": TankBody.isTank(weapon), + "weapon.isTank": weapon === this, "victim.arenaMobID": entity.arenaMobID, "victim.isTank": victimIsTank, "victim.isBoss": AbstractBoss.isBoss(entity), "victim.isShiny": !!(entity.entityTags & EntityTags.isShiny), - "class": this.currentTank, - "victim.class": victimIsTank ? entity.currentTank : -1 + // These exist but are unused by diep, uncomment if you need + //"class": this.currentTank, + //"victim.class": victimIsTank ? entity.currentTank : -1 }); } }