Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 76 additions & 19 deletions src/Entity/AI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand All @@ -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
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -225,6 +262,7 @@ export class AI {
this.inputs.movement.magnitude = 1;
return;
}

if (this.doAimPrediction) {
const delta = {
x: pos.x - ownerPos.x,
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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();
Expand Down
8 changes: 6 additions & 2 deletions src/Entity/Boss/AbstractBoss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;

Expand All @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/Entity/Boss/FallenBooster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
13 changes: 13 additions & 0 deletions src/Entity/Object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/Entity/Shape/AbstractShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -77,6 +77,8 @@ export default class AbstractShape extends LivingEntity {

this.maxDamageMultiplier = 4.0;

this.aiPriority = PriorityLevel.Passive;

this.entityTags |= EntityTags.isShape;
}

Expand Down
7 changes: 5 additions & 2 deletions src/Entity/Shape/Crasher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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";
}
Expand Down
15 changes: 11 additions & 4 deletions src/Entity/Tank/Projectile/Drone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -130,8 +139,6 @@ export default class Drone extends Bullet {
this.restCycle = false
}



if (this.canControlDrones && inputs.attemptingRepel()) {
this.positionData.angle += Math.PI;
}
Expand All @@ -141,4 +148,4 @@ export default class Drone extends Bullet {

this.tickMixin(tick);
}
}
}
Loading
Loading