@supalosa/chronodivide-bot 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -46
- package/dist/bot/bot.js +27 -183
- package/dist/bot/logic/awareness.js +122 -0
- package/dist/bot/logic/building/basicGroundUnit.js +8 -6
- package/dist/bot/logic/building/building.js +6 -3
- package/dist/bot/logic/building/harvester.js +1 -1
- package/dist/bot/logic/building/queueController.js +4 -21
- package/dist/bot/logic/common/scout.js +10 -0
- package/dist/bot/logic/knowledge.js +1 -0
- package/dist/bot/logic/map/map.js +6 -0
- package/dist/bot/logic/map/sector.js +6 -1
- package/dist/bot/logic/mission/basicMission.js +1 -5
- package/dist/bot/logic/mission/expansionMission.js +22 -4
- package/dist/bot/logic/mission/mission.js +49 -2
- package/dist/bot/logic/mission/missionController.js +67 -34
- package/dist/bot/logic/mission/missionFactories.js +10 -0
- package/dist/bot/logic/mission/missions/attackMission.js +109 -0
- package/dist/bot/logic/mission/missions/defenceMission.js +62 -0
- package/dist/bot/logic/mission/missions/expansionMission.js +24 -0
- package/dist/bot/logic/mission/missions/oneTimeMission.js +26 -0
- package/dist/bot/logic/mission/missions/retreatMission.js +7 -0
- package/dist/bot/logic/mission/missions/scoutingMission.js +38 -0
- package/dist/bot/logic/squad/behaviours/attackSquad.js +82 -0
- package/dist/bot/logic/squad/behaviours/combatSquad.js +99 -0
- package/dist/bot/logic/squad/behaviours/common.js +37 -0
- package/dist/bot/logic/squad/behaviours/defenceSquad.js +48 -0
- package/dist/bot/logic/squad/behaviours/expansionSquad.js +42 -0
- package/dist/bot/logic/squad/behaviours/retreatSquad.js +32 -0
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js +38 -0
- package/dist/bot/logic/squad/behaviours/squadExpansion.js +26 -13
- package/dist/bot/logic/squad/squad.js +68 -15
- package/dist/bot/logic/squad/squadBehaviour.js +5 -5
- package/dist/bot/logic/squad/squadBehaviours.js +6 -0
- package/dist/bot/logic/squad/squadController.js +106 -15
- package/dist/exampleBot.js +22 -7
- package/package.json +29 -24
- package/src/bot/bot.ts +178 -378
- package/src/bot/logic/awareness.ts +220 -0
- package/src/bot/logic/building/ArtilleryUnit.ts +2 -2
- package/src/bot/logic/building/antiGroundStaticDefence.ts +2 -2
- package/src/bot/logic/building/basicAirUnit.ts +2 -2
- package/src/bot/logic/building/basicBuilding.ts +2 -2
- package/src/bot/logic/building/basicGroundUnit.ts +83 -78
- package/src/bot/logic/building/building.ts +125 -120
- package/src/bot/logic/building/harvester.ts +27 -27
- package/src/bot/logic/building/powerPlant.ts +1 -1
- package/src/bot/logic/building/queueController.ts +17 -38
- package/src/bot/logic/building/resourceCollectionBuilding.ts +1 -1
- package/src/bot/logic/common/scout.ts +12 -0
- package/src/bot/logic/map/map.ts +11 -3
- package/src/bot/logic/map/sector.ts +136 -130
- package/src/bot/logic/mission/mission.ts +83 -47
- package/src/bot/logic/mission/missionController.ts +103 -51
- package/src/bot/logic/mission/missionFactories.ts +46 -0
- package/src/bot/logic/mission/missions/attackMission.ts +152 -0
- package/src/bot/logic/mission/missions/defenceMission.ts +104 -0
- package/src/bot/logic/mission/missions/expansionMission.ts +49 -0
- package/src/bot/logic/mission/missions/oneTimeMission.ts +32 -0
- package/src/bot/logic/mission/missions/retreatMission.ts +9 -0
- package/src/bot/logic/mission/missions/scoutingMission.ts +59 -0
- package/src/bot/logic/squad/behaviours/combatSquad.ts +125 -0
- package/src/bot/logic/squad/behaviours/common.ts +37 -0
- package/src/bot/logic/squad/behaviours/expansionSquad.ts +59 -0
- package/src/bot/logic/squad/behaviours/retreatSquad.ts +46 -0
- package/src/bot/logic/squad/behaviours/scoutingSquad.ts +56 -0
- package/src/bot/logic/squad/squad.ts +163 -97
- package/src/bot/logic/squad/squadBehaviour.ts +61 -43
- package/src/bot/logic/squad/squadBehaviours.ts +8 -0
- package/src/bot/logic/squad/squadController.ts +190 -66
- package/src/exampleBot.ts +19 -4
- package/tsconfig.json +1 -1
- package/src/bot/logic/mission/basicMission.ts +0 -42
- package/src/bot/logic/mission/expansionMission.ts +0 -25
- package/src/bot/logic/squad/behaviours/squadExpansion.ts +0 -33
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import { MovementZone } from "@chronodivide/game-api";
|
|
3
|
+
import { grabCombatants, noop } from "../squadBehaviour.js";
|
|
4
|
+
import { getDistanceBetweenPoints } from "../../map/map.js";
|
|
5
|
+
import { manageAttackMicro, manageMoveMicro } from "./common.js";
|
|
6
|
+
const TARGET_UPDATE_INTERVAL_TICKS = 10;
|
|
7
|
+
const GRAB_INTERVAL_TICKS = 10;
|
|
8
|
+
const GRAB_RADIUS = 20;
|
|
9
|
+
// Units must be in a certain radius of the center of mass before attacking.
|
|
10
|
+
// This scales for number of units in the squad though.
|
|
11
|
+
const MIN_GATHER_RADIUS = 5;
|
|
12
|
+
// If the radius expands beyond this amount then we should switch back to gathering mode.
|
|
13
|
+
const MAX_GATHER_RADIUS = 15;
|
|
14
|
+
const GATHER_RATIO = 10;
|
|
15
|
+
var SquadState;
|
|
16
|
+
(function (SquadState) {
|
|
17
|
+
SquadState[SquadState["Gathering"] = 0] = "Gathering";
|
|
18
|
+
SquadState[SquadState["Attacking"] = 1] = "Attacking";
|
|
19
|
+
})(SquadState || (SquadState = {}));
|
|
20
|
+
export class CombatSquad {
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
* @param rallyArea the initial location to grab combatants
|
|
24
|
+
* @param targetArea
|
|
25
|
+
* @param radius
|
|
26
|
+
*/
|
|
27
|
+
constructor(rallyArea, targetArea, radius) {
|
|
28
|
+
this.rallyArea = rallyArea;
|
|
29
|
+
this.targetArea = targetArea;
|
|
30
|
+
this.radius = radius;
|
|
31
|
+
this.lastGrab = null;
|
|
32
|
+
this.lastCommand = null;
|
|
33
|
+
this.state = SquadState.Gathering;
|
|
34
|
+
}
|
|
35
|
+
setAttackArea(targetArea) {
|
|
36
|
+
this.targetArea = targetArea;
|
|
37
|
+
}
|
|
38
|
+
onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
|
|
39
|
+
if (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS) {
|
|
40
|
+
this.lastCommand = gameApi.getCurrentTick();
|
|
41
|
+
const centerOfMass = squad.getCenterOfMass();
|
|
42
|
+
const maxDistance = squad.getMaxDistanceToCenterOfMass();
|
|
43
|
+
const units = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant);
|
|
44
|
+
// Only use ground units for center of mass.
|
|
45
|
+
const groundUnits = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant &&
|
|
46
|
+
(r.rules.movementZone === MovementZone.Infantry ||
|
|
47
|
+
r.rules.movementZone === MovementZone.Normal ||
|
|
48
|
+
r.rules.movementZone === MovementZone.InfantryDestroyer));
|
|
49
|
+
if (this.state === SquadState.Gathering) {
|
|
50
|
+
const requiredGatherRadius = Math.sqrt(groundUnits.length) * GATHER_RATIO + MIN_GATHER_RADIUS;
|
|
51
|
+
if (centerOfMass &&
|
|
52
|
+
maxDistance &&
|
|
53
|
+
gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
|
|
54
|
+
maxDistance > requiredGatherRadius) {
|
|
55
|
+
units.forEach((unit) => {
|
|
56
|
+
manageMoveMicro(actionsApi, unit, centerOfMass);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.state = SquadState.Attacking;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const targetPoint = this.targetArea || playerData.startLocation;
|
|
65
|
+
const requiredGatherRadius = Math.sqrt(groundUnits.length) * GATHER_RATIO + MAX_GATHER_RADIUS;
|
|
66
|
+
if (centerOfMass &&
|
|
67
|
+
maxDistance &&
|
|
68
|
+
gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
|
|
69
|
+
maxDistance > requiredGatherRadius) {
|
|
70
|
+
// Switch back to gather mode.
|
|
71
|
+
this.state = SquadState.Gathering;
|
|
72
|
+
return noop();
|
|
73
|
+
}
|
|
74
|
+
for (const unit of units) {
|
|
75
|
+
if (unit.isIdle) {
|
|
76
|
+
const { rx: x, ry: y } = unit.tile;
|
|
77
|
+
const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
|
|
78
|
+
const nearbyHostiles = matchAwareness.getHostilesNearPoint(x, y, range * 2);
|
|
79
|
+
const closest = _.minBy(nearbyHostiles, ({ x: hX, y: hY }) => getDistanceBetweenPoints({ x, y }, { x: hX, y: hY }));
|
|
80
|
+
const closestUnit = closest ? gameApi.getUnitData(closest.unitId) ?? null : null;
|
|
81
|
+
if (closestUnit) {
|
|
82
|
+
manageAttackMicro(actionsApi, unit, closestUnit);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
manageMoveMicro(actionsApi, unit, targetPoint);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!this.lastGrab || gameApi.getCurrentTick() > this.lastGrab + GRAB_INTERVAL_TICKS) {
|
|
92
|
+
this.lastGrab = gameApi.getCurrentTick();
|
|
93
|
+
return grabCombatants(squad.getCenterOfMass() ?? this.rallyArea, GRAB_RADIUS);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return noop();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ObjectType, OrderType } from "@chronodivide/game-api";
|
|
2
|
+
import { getDistanceBetweenUnits } from "../../map/map.js";
|
|
3
|
+
// Micro methods
|
|
4
|
+
export function manageMoveMicro(actionsApi, attacker, attackPoint) {
|
|
5
|
+
if (attacker.name === "E1") {
|
|
6
|
+
if (!attacker.canMove) {
|
|
7
|
+
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
actionsApi.orderUnits([attacker.id], OrderType.Move, attackPoint.x, attackPoint.y);
|
|
11
|
+
}
|
|
12
|
+
export function manageAttackMicro(actionsApi, attacker, target) {
|
|
13
|
+
const distance = getDistanceBetweenUnits(attacker, target);
|
|
14
|
+
if (attacker.name === "E1") {
|
|
15
|
+
// Para (deployed weapon) range is 5.
|
|
16
|
+
const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;
|
|
17
|
+
if (attacker.canMove && distance <= deployedWeaponRange * 0.8) {
|
|
18
|
+
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
else if (!attacker.canMove && distance > deployedWeaponRange) {
|
|
22
|
+
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let targetData = target;
|
|
27
|
+
let orderType = OrderType.Attack;
|
|
28
|
+
const primaryWeaponRange = attacker.primaryWeapon?.maxRange || 5;
|
|
29
|
+
if (targetData?.type == ObjectType.Building && distance < primaryWeaponRange * 0.8) {
|
|
30
|
+
orderType = OrderType.Attack;
|
|
31
|
+
}
|
|
32
|
+
else if (targetData?.rules.canDisguise) {
|
|
33
|
+
// Special case for mirage tank/spy as otherwise they just sit next to it.
|
|
34
|
+
orderType = OrderType.Attack;
|
|
35
|
+
}
|
|
36
|
+
actionsApi.orderUnits([attacker.id], orderType, target.id);
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import { disband, grabCombatants } from "../squadBehaviour.js";
|
|
3
|
+
import { getDistanceBetween, getDistanceBetweenUnits } from "../../map/map.js";
|
|
4
|
+
import { manageAttackMicro } from "./common.js";
|
|
5
|
+
// If no enemies are seen in a circle IDLE_CHECK_RADIUS*radius for IDLE_COOLDOWN_TICKS ticks, the mission is disbanded.
|
|
6
|
+
const IDLE_CHECK_RADIUS_RATIO = 2;
|
|
7
|
+
const IDLE_COOLDOWN_TICKS = 15 * 30;
|
|
8
|
+
const GRAB_RADIUS = 2;
|
|
9
|
+
export class DefenceSquad {
|
|
10
|
+
constructor(defenceArea, radius) {
|
|
11
|
+
this.defenceArea = defenceArea;
|
|
12
|
+
this.radius = radius;
|
|
13
|
+
this.lastIdleCheck = null;
|
|
14
|
+
}
|
|
15
|
+
onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
|
|
16
|
+
const enemyUnits = gameApi.getVisibleUnits(playerData.name, "hostile", (r) => r.isSelectableCombatant);
|
|
17
|
+
const hasEnemiesInIdleCheckRadius = enemyUnits
|
|
18
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
19
|
+
.some((unit) => !!unit &&
|
|
20
|
+
unit.tile &&
|
|
21
|
+
getDistanceBetween(unit, this.defenceArea) < IDLE_CHECK_RADIUS_RATIO * this.radius);
|
|
22
|
+
if (this.lastIdleCheck === null) {
|
|
23
|
+
this.lastIdleCheck = gameApi.getCurrentTick();
|
|
24
|
+
}
|
|
25
|
+
else if (!hasEnemiesInIdleCheckRadius &&
|
|
26
|
+
gameApi.getCurrentTick() > this.lastIdleCheck + IDLE_COOLDOWN_TICKS) {
|
|
27
|
+
return disband();
|
|
28
|
+
}
|
|
29
|
+
const enemiesInRadius = enemyUnits
|
|
30
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
31
|
+
.filter((unit) => !!unit && unit.tile && getDistanceBetween(unit, this.defenceArea) < this.radius)
|
|
32
|
+
.map((unit) => unit);
|
|
33
|
+
const defenders = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant);
|
|
34
|
+
defenders.forEach((defender) => {
|
|
35
|
+
// Find closest attacking unit
|
|
36
|
+
if (defender.isIdle) {
|
|
37
|
+
const closestEnemy = _.minBy(enemiesInRadius.map((enemy) => ({
|
|
38
|
+
enemy,
|
|
39
|
+
distance: getDistanceBetweenUnits(defender, enemy),
|
|
40
|
+
})), "distance");
|
|
41
|
+
if (closestEnemy) {
|
|
42
|
+
manageAttackMicro(actionsApi, defender, closestEnemy.enemy);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return grabCombatants(this.defenceArea, this.radius * GRAB_RADIUS);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { OrderType } from "@chronodivide/game-api";
|
|
2
|
+
import { disband, noop, requestSpecificUnits, requestUnits } from "../squadBehaviour.js";
|
|
3
|
+
const DEPLOY_COOLDOWN_TICKS = 30;
|
|
4
|
+
// Expansion or initial base.
|
|
5
|
+
export class ExpansionSquad {
|
|
6
|
+
/**
|
|
7
|
+
* @param selectedMcv ID of the MCV to try to expand with. If that unit dies, the squad will disband. If no value is provided,
|
|
8
|
+
* the mission requests an MCV.
|
|
9
|
+
*/
|
|
10
|
+
constructor(selectedMcv) {
|
|
11
|
+
this.selectedMcv = selectedMcv;
|
|
12
|
+
this.hasAttemptedDeployWith = null;
|
|
13
|
+
}
|
|
14
|
+
;
|
|
15
|
+
onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
|
|
16
|
+
const mcvTypes = ["AMCV", "SMCV"];
|
|
17
|
+
const mcvs = squad.getUnitsOfTypes(gameApi, ...mcvTypes);
|
|
18
|
+
if (mcvs.length === 0) {
|
|
19
|
+
// Perhaps we deployed already (or the unit was destroyed), end the mission.
|
|
20
|
+
if (this.hasAttemptedDeployWith !== null) {
|
|
21
|
+
return disband();
|
|
22
|
+
}
|
|
23
|
+
// We need an mcv!
|
|
24
|
+
if (this.selectedMcv) {
|
|
25
|
+
return requestSpecificUnits([this.selectedMcv], 100);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return requestUnits(mcvTypes, 100);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (!this.hasAttemptedDeployWith ||
|
|
32
|
+
gameApi.getCurrentTick() > this.hasAttemptedDeployWith.gameTick + DEPLOY_COOLDOWN_TICKS) {
|
|
33
|
+
actionsApi.orderUnits(mcvs.map((mcv) => mcv.id), OrderType.DeploySelected);
|
|
34
|
+
// Add a cooldown to deploy attempts.
|
|
35
|
+
this.hasAttemptedDeployWith = {
|
|
36
|
+
unitId: mcvs[0].id,
|
|
37
|
+
gameTick: gameApi.getCurrentTick(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return noop();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { OrderType } from "@chronodivide/game-api";
|
|
2
|
+
import { disband, requestSpecificUnits } from "../squadBehaviour.js";
|
|
3
|
+
const SCOUT_MOVE_COOLDOWN_TICKS = 30;
|
|
4
|
+
export class RetreatSquad {
|
|
5
|
+
constructor(unitIds, retreatToPoint) {
|
|
6
|
+
this.unitIds = unitIds;
|
|
7
|
+
this.retreatToPoint = retreatToPoint;
|
|
8
|
+
this.hasRequestedUnits = false;
|
|
9
|
+
this.moveOrderSentAt = null;
|
|
10
|
+
this.createdAt = null;
|
|
11
|
+
}
|
|
12
|
+
onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
|
|
13
|
+
if (!this.createdAt) {
|
|
14
|
+
this.createdAt = gameApi.getCurrentTick();
|
|
15
|
+
}
|
|
16
|
+
if (squad.getUnitIds().length > 0) {
|
|
17
|
+
// Only send the order once we have managed to claim some units.
|
|
18
|
+
console.log(`Retreat squad ordered ${squad.getUnitIds()} to retreat`);
|
|
19
|
+
actionsApi.orderUnits(squad.getUnitIds(), OrderType.Move, this.retreatToPoint.x, this.retreatToPoint.y);
|
|
20
|
+
if (!this.moveOrderSentAt) {
|
|
21
|
+
this.moveOrderSentAt = gameApi.getCurrentTick();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if ((this.moveOrderSentAt && gameApi.getCurrentTick() > this.moveOrderSentAt + 60) ||
|
|
25
|
+
(this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240)) {
|
|
26
|
+
return disband();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return requestSpecificUnits(this.unitIds, 100);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { OrderType } from "@chronodivide/game-api";
|
|
2
|
+
import { disband, noop, requestUnits } from "../squadBehaviour.js";
|
|
3
|
+
import { getUnseenStartingLocations } from "../../common/scout.js";
|
|
4
|
+
const SCOUT_MOVE_COOLDOWN_TICKS = 30;
|
|
5
|
+
export class ScoutingSquad {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.scoutingWith = null;
|
|
8
|
+
}
|
|
9
|
+
onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
|
|
10
|
+
const scoutNames = ["ADOG", "DOG", "E1", "E2", "FV", "HTK"];
|
|
11
|
+
const scouts = squad.getUnitsOfTypes(gameApi, ...scoutNames);
|
|
12
|
+
if ((matchAwareness.getSectorCache().getOverallVisibility() || 0) > 0.9) {
|
|
13
|
+
return disband();
|
|
14
|
+
}
|
|
15
|
+
if (scouts.length === 0) {
|
|
16
|
+
this.scoutingWith = null;
|
|
17
|
+
return requestUnits(scoutNames, 100);
|
|
18
|
+
}
|
|
19
|
+
else if (!this.scoutingWith ||
|
|
20
|
+
gameApi.getCurrentTick() > this.scoutingWith.gameTick + SCOUT_MOVE_COOLDOWN_TICKS) {
|
|
21
|
+
const candidatePoints = getUnseenStartingLocations(gameApi, playerData);
|
|
22
|
+
scouts.forEach((unit) => {
|
|
23
|
+
if (candidatePoints.length > 0) {
|
|
24
|
+
if (unit?.isIdle) {
|
|
25
|
+
const scoutLocation = candidatePoints[Math.floor(gameApi.generateRandom() * candidatePoints.length)];
|
|
26
|
+
actionsApi.orderUnits([unit.id], OrderType.AttackMove, scoutLocation.x, scoutLocation.y);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Add a cooldown to scout attempts.
|
|
31
|
+
this.scoutingWith = {
|
|
32
|
+
unitId: scouts[0].id,
|
|
33
|
+
gameTick: gameApi.getCurrentTick(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return noop();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
import { SideType } from "@chronodivide/game-api";
|
|
1
|
+
import { OrderType, SideType } from "@chronodivide/game-api";
|
|
2
|
+
import { disband, noop, requestUnits } from "../squadBehaviour.js";
|
|
3
|
+
const DEPLOY_COOLDOWN_TICKS = 30;
|
|
2
4
|
// Expansion or initial base.
|
|
3
5
|
export class SquadExpansion {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
let myMcvName = playerData.country?.side == SideType.GDI ? "AMCV" : "SMCV";
|
|
7
|
-
return [
|
|
8
|
-
{
|
|
9
|
-
unitName: myMcvName,
|
|
10
|
-
priority: 10,
|
|
11
|
-
amount: 1,
|
|
12
|
-
},
|
|
13
|
-
];
|
|
6
|
+
constructor() {
|
|
7
|
+
this.hasAttemptedDeployWith = null;
|
|
14
8
|
}
|
|
15
|
-
onAiUpdate(gameApi, playerData, squad, threatData) {
|
|
16
|
-
|
|
9
|
+
onAiUpdate(gameApi, actionsApi, playerData, squad, threatData) {
|
|
10
|
+
let myMcvName = playerData.country?.side == SideType.GDI ? "AMCV" : "SMCV";
|
|
11
|
+
const mcvs = squad.getUnitsOfType(gameApi, myMcvName);
|
|
12
|
+
if (mcvs.length === 0) {
|
|
13
|
+
// Perhaps we deployed already (or the unit was destroyed), end the mission.
|
|
14
|
+
if (this.hasAttemptedDeployWith !== null) {
|
|
15
|
+
return disband();
|
|
16
|
+
}
|
|
17
|
+
// We need an mcv!
|
|
18
|
+
return requestUnits(myMcvName, 100);
|
|
19
|
+
}
|
|
20
|
+
else if (!this.hasAttemptedDeployWith ||
|
|
21
|
+
gameApi.getCurrentTick() > this.hasAttemptedDeployWith.gameTick + DEPLOY_COOLDOWN_TICKS) {
|
|
22
|
+
actionsApi.orderUnits(mcvs.map((mcv) => mcv.id), OrderType.DeploySelected);
|
|
23
|
+
// Add a cooldown to deploy attempts.
|
|
24
|
+
this.hasAttemptedDeployWith = {
|
|
25
|
+
unitId: mcvs[0].id,
|
|
26
|
+
gameTick: gameApi.getCurrentTick(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return noop();
|
|
17
30
|
}
|
|
18
31
|
}
|
|
@@ -1,28 +1,78 @@
|
|
|
1
|
+
import { disband } from "./squadBehaviour.js";
|
|
2
|
+
import { getDistanceBetweenPoints } from "../map/map.js";
|
|
3
|
+
import _ from "lodash";
|
|
1
4
|
export var SquadLiveness;
|
|
2
5
|
(function (SquadLiveness) {
|
|
3
6
|
SquadLiveness[SquadLiveness["SquadDead"] = 0] = "SquadDead";
|
|
4
7
|
SquadLiveness[SquadLiveness["SquadActive"] = 1] = "SquadActive";
|
|
5
8
|
})(SquadLiveness || (SquadLiveness = {}));
|
|
9
|
+
const calculateCenterOfMass = (unitTiles) => {
|
|
10
|
+
if (unitTiles.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
// TODO: use median here
|
|
14
|
+
const sums = unitTiles.reduce(({ x, y }, tile) => {
|
|
15
|
+
return {
|
|
16
|
+
x: x + (tile?.rx || 0),
|
|
17
|
+
y: y + (tile?.ry || 0),
|
|
18
|
+
};
|
|
19
|
+
}, { x: 0, y: 0 });
|
|
20
|
+
const centerOfMass = {
|
|
21
|
+
x: Math.round(sums.x / unitTiles.length),
|
|
22
|
+
y: Math.round(sums.y / unitTiles.length),
|
|
23
|
+
};
|
|
24
|
+
// max distance of units to the center of mass
|
|
25
|
+
const distances = unitTiles.map((tile) => getDistanceBetweenPoints({ x: tile.rx, y: tile.ry }, centerOfMass));
|
|
26
|
+
const maxDistance = _.max(distances);
|
|
27
|
+
return { centerOfMass, maxDistance };
|
|
28
|
+
};
|
|
6
29
|
export class Squad {
|
|
7
|
-
constructor(name, behaviour, mission,
|
|
30
|
+
constructor(name, behaviour, mission, killable = false) {
|
|
8
31
|
this.name = name;
|
|
9
32
|
this.behaviour = behaviour;
|
|
10
33
|
this.mission = mission;
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
34
|
+
this.killable = killable;
|
|
35
|
+
this.unitIds = [];
|
|
36
|
+
this.liveness = SquadLiveness.SquadActive;
|
|
37
|
+
this.lastLivenessUpdateTick = 0;
|
|
38
|
+
this.centerOfMass = null;
|
|
39
|
+
this.maxDistanceToCenterOfMass = null;
|
|
14
40
|
}
|
|
15
41
|
getName() {
|
|
16
42
|
return this.name;
|
|
17
43
|
}
|
|
18
|
-
|
|
44
|
+
getCenterOfMass() {
|
|
45
|
+
return this.centerOfMass;
|
|
46
|
+
}
|
|
47
|
+
getMaxDistanceToCenterOfMass() {
|
|
48
|
+
return this.maxDistanceToCenterOfMass;
|
|
49
|
+
}
|
|
50
|
+
onAiUpdate(gameApi, actionsApi, playerData, matchAwareness) {
|
|
19
51
|
this.updateLiveness(gameApi);
|
|
52
|
+
const movableUnitTiles = this.unitIds
|
|
53
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
54
|
+
.filter((unit) => unit?.canMove)
|
|
55
|
+
.map((unit) => unit?.tile)
|
|
56
|
+
.filter((tile) => !!tile);
|
|
57
|
+
const tileMetrics = calculateCenterOfMass(movableUnitTiles);
|
|
58
|
+
if (tileMetrics) {
|
|
59
|
+
this.centerOfMass = tileMetrics.centerOfMass;
|
|
60
|
+
this.maxDistanceToCenterOfMass = tileMetrics.maxDistance;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.centerOfMass = null;
|
|
64
|
+
this.maxDistanceToCenterOfMass = null;
|
|
65
|
+
}
|
|
20
66
|
if (this.mission && this.mission.isActive() == false) {
|
|
21
67
|
// Orphaned squad, might get picked up later.
|
|
22
|
-
this.mission.removeSquad(
|
|
23
|
-
this.mission =
|
|
68
|
+
this.mission.removeSquad();
|
|
69
|
+
this.mission = null;
|
|
70
|
+
return disband();
|
|
24
71
|
}
|
|
25
|
-
|
|
72
|
+
else if (!this.mission) {
|
|
73
|
+
return disband();
|
|
74
|
+
}
|
|
75
|
+
let outcome = this.behaviour.onAiUpdate(gameApi, actionsApi, playerData, this, matchAwareness);
|
|
26
76
|
return outcome;
|
|
27
77
|
}
|
|
28
78
|
getMission() {
|
|
@@ -30,7 +80,7 @@ export class Squad {
|
|
|
30
80
|
}
|
|
31
81
|
setMission(mission) {
|
|
32
82
|
if (this.mission != undefined && this.mission != mission) {
|
|
33
|
-
this.mission.removeSquad(
|
|
83
|
+
this.mission.removeSquad();
|
|
34
84
|
}
|
|
35
85
|
this.mission = mission;
|
|
36
86
|
}
|
|
@@ -43,25 +93,28 @@ export class Squad {
|
|
|
43
93
|
.filter((unit) => unit != null)
|
|
44
94
|
.map((unit) => unit);
|
|
45
95
|
}
|
|
46
|
-
|
|
96
|
+
getUnitsOfTypes(gameApi, ...names) {
|
|
47
97
|
return this.unitIds
|
|
48
98
|
.map((unitId) => gameApi.getUnitData(unitId))
|
|
49
|
-
.filter(
|
|
99
|
+
.filter((unit) => !!unit && names.includes(unit.name))
|
|
100
|
+
.map((unit) => unit);
|
|
101
|
+
}
|
|
102
|
+
getUnitsMatching(gameApi, filter) {
|
|
103
|
+
return this.unitIds
|
|
104
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
105
|
+
.filter((unit) => !!unit && filter(unit))
|
|
50
106
|
.map((unit) => unit);
|
|
51
107
|
}
|
|
52
108
|
removeUnit(unitIdToRemove) {
|
|
53
109
|
this.unitIds = this.unitIds.filter((unitId) => unitId != unitIdToRemove);
|
|
54
110
|
}
|
|
55
|
-
clearUnits() {
|
|
56
|
-
this.unitIds = [];
|
|
57
|
-
}
|
|
58
111
|
addUnit(unitIdToAdd) {
|
|
59
112
|
this.unitIds.push(unitIdToAdd);
|
|
60
113
|
}
|
|
61
114
|
updateLiveness(gameApi) {
|
|
62
115
|
this.unitIds = this.unitIds.filter((unitId) => gameApi.getUnitData(unitId));
|
|
63
116
|
this.lastLivenessUpdateTick = gameApi.getCurrentTick();
|
|
64
|
-
if (this.unitIds.length == 0) {
|
|
117
|
+
if (this.killable && this.unitIds.length == 0) {
|
|
65
118
|
if (this.liveness == SquadLiveness.SquadActive) {
|
|
66
119
|
this.liveness = SquadLiveness.SquadDead;
|
|
67
120
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export const noop = () => ({ type: "noop" });
|
|
2
|
+
export const disband = () => ({ type: "disband" });
|
|
3
|
+
export const requestUnits = (unitNames, priority) => ({ type: "request", unitNames, priority });
|
|
4
|
+
export const requestSpecificUnits = (unitIds, priority) => ({ type: "requestSpecific", unitIds, priority });
|
|
5
|
+
export const grabCombatants = (point, radius) => ({ type: "requestCombatants", point, radius });
|
|
@@ -1,43 +1,45 @@
|
|
|
1
1
|
// Meta-controller for forming and controlling squads.
|
|
2
2
|
import { SquadLiveness } from "./squad.js";
|
|
3
|
+
import { getDistanceBetween } from "../map/map.js";
|
|
3
4
|
export class SquadController {
|
|
4
|
-
constructor(
|
|
5
|
-
this.squads =
|
|
6
|
-
this.unitIdToSquad =
|
|
5
|
+
constructor() {
|
|
6
|
+
this.squads = [];
|
|
7
|
+
this.unitIdToSquad = new Map();
|
|
7
8
|
}
|
|
8
|
-
onAiUpdate(gameApi, playerData,
|
|
9
|
-
// Remove dead squads.
|
|
10
|
-
this.squads = this.squads.filter((squad) => squad.getLiveness()
|
|
9
|
+
onAiUpdate(gameApi, actionsApi, playerData, matchAwareness, logger) {
|
|
10
|
+
// Remove dead squads or those where the mission is dead.
|
|
11
|
+
this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
|
|
11
12
|
this.squads.sort((a, b) => a.getName().localeCompare(b.getName()));
|
|
12
13
|
// Check for units in multiple squads, this shouldn't happen.
|
|
13
14
|
this.unitIdToSquad = new Map();
|
|
14
15
|
this.squads.forEach((squad) => {
|
|
15
16
|
squad.getUnitIds().forEach((unitId) => {
|
|
16
17
|
if (this.unitIdToSquad.has(unitId)) {
|
|
17
|
-
|
|
18
|
+
logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
18
19
|
}
|
|
19
20
|
else {
|
|
20
21
|
this.unitIdToSquad.set(unitId, squad);
|
|
21
22
|
}
|
|
22
23
|
});
|
|
23
24
|
});
|
|
24
|
-
|
|
25
|
+
const squadActions = this.squads.map((squad) => {
|
|
25
26
|
return {
|
|
26
27
|
squad,
|
|
27
|
-
action: squad.onAiUpdate(gameApi, playerData,
|
|
28
|
+
action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
|
|
28
29
|
};
|
|
29
30
|
});
|
|
30
31
|
// Handle disbands and merges.
|
|
31
|
-
const isDisband = (a) => a.type
|
|
32
|
-
const isMerge = (a) => a.type
|
|
32
|
+
const isDisband = (a) => a.type === "disband";
|
|
33
|
+
const isMerge = (a) => a.type === "mergeInto";
|
|
33
34
|
let disbandedSquads = new Set();
|
|
34
35
|
squadActions
|
|
35
36
|
.filter((a) => isDisband(a.action))
|
|
36
37
|
.forEach((a) => {
|
|
38
|
+
logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
39
|
+
a.squad.getMission()?.removeSquad();
|
|
37
40
|
a.squad.getUnitIds().forEach((unitId) => {
|
|
38
41
|
this.unitIdToSquad.delete(unitId);
|
|
39
42
|
});
|
|
40
|
-
a.squad.clearUnits();
|
|
41
43
|
disbandedSquads.add(a.squad.getName());
|
|
42
44
|
});
|
|
43
45
|
squadActions
|
|
@@ -45,14 +47,103 @@ export class SquadController {
|
|
|
45
47
|
.forEach((a) => {
|
|
46
48
|
let mergeInto = a.action;
|
|
47
49
|
if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
|
|
48
|
-
|
|
50
|
+
logger(`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`);
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
a.squad.getUnitIds().forEach((unitId) => mergeInto.mergeInto.addUnit(unitId));
|
|
52
54
|
disbandedSquads.add(a.squad.getName());
|
|
53
55
|
});
|
|
54
56
|
// remove disbanded and merged squads.
|
|
55
|
-
this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
|
|
56
|
-
//
|
|
57
|
+
this.squads = this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
|
|
58
|
+
// Request specific units by ID
|
|
59
|
+
const isRequestSpecific = (a) => a.type === "requestSpecific";
|
|
60
|
+
const unitIdToHighestRequest = squadActions
|
|
61
|
+
.filter((a) => isRequestSpecific(a.action))
|
|
62
|
+
.reduce((prev, a) => {
|
|
63
|
+
const squadWithAction = a;
|
|
64
|
+
const { unitIds } = squadWithAction.action;
|
|
65
|
+
unitIds.forEach((unitId) => {
|
|
66
|
+
if (prev.hasOwnProperty(unitId)) {
|
|
67
|
+
if (prev[unitId].action.priority > prev[unitId].action.priority) {
|
|
68
|
+
prev[unitId] = squadWithAction;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
prev[unitId] = squadWithAction;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return prev;
|
|
76
|
+
}, {});
|
|
77
|
+
Object.entries(unitIdToHighestRequest).forEach(([id, request]) => {
|
|
78
|
+
const unitId = Number.parseInt(id);
|
|
79
|
+
const unit = gameApi.getUnitData(unitId);
|
|
80
|
+
const { squad: requestingSquad } = request;
|
|
81
|
+
const missionName = requestingSquad.getMission()?.getUniqueName();
|
|
82
|
+
if (!unit) {
|
|
83
|
+
logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!this.unitIdToSquad.has(unitId)) {
|
|
87
|
+
logger(`granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`);
|
|
88
|
+
this.addUnitToSquad(requestingSquad, unit);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Request units by type
|
|
92
|
+
const isRequest = (a) => a.type === "request";
|
|
93
|
+
const unitTypeToHighestRequest = squadActions
|
|
94
|
+
.filter((a) => isRequest(a.action))
|
|
95
|
+
.reduce((prev, a) => {
|
|
96
|
+
const squadWithAction = a;
|
|
97
|
+
const { unitNames } = squadWithAction.action;
|
|
98
|
+
unitNames.forEach((unitName) => {
|
|
99
|
+
if (prev.hasOwnProperty(unitName)) {
|
|
100
|
+
if (prev[unitName].action.priority > prev[unitName].action.priority) {
|
|
101
|
+
prev[unitName] = squadWithAction;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
prev[unitName] = squadWithAction;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return prev;
|
|
109
|
+
}, {});
|
|
110
|
+
// Request combat-capable units in an area
|
|
111
|
+
const isGrab = (a) => a.type === "requestCombatants";
|
|
112
|
+
const grabRequests = squadActions.filter((a) => isGrab(a.action));
|
|
113
|
+
// Find loose units
|
|
114
|
+
const unitIds = gameApi.getVisibleUnits(playerData.name, "self");
|
|
115
|
+
const freeUnits = unitIds
|
|
116
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
117
|
+
.filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
|
|
118
|
+
.map((unit) => unit);
|
|
119
|
+
freeUnits.forEach((freeUnit) => {
|
|
120
|
+
if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
|
|
121
|
+
const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
|
|
122
|
+
logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
123
|
+
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
124
|
+
delete unitTypeToHighestRequest[freeUnit.name];
|
|
125
|
+
}
|
|
126
|
+
else if (grabRequests.length > 0) {
|
|
127
|
+
grabRequests.some((request) => {
|
|
128
|
+
const { squad: requestingSquad } = request;
|
|
129
|
+
if (freeUnit.rules.isSelectableCombatant &&
|
|
130
|
+
getDistanceBetween(freeUnit, request.action.point) <= request.action.radius) {
|
|
131
|
+
logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()} via grab at ${request.action.point.x},${request.action.point.y}`);
|
|
132
|
+
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
addUnitToSquad(squad, unit) {
|
|
143
|
+
squad.addUnit(unit.id);
|
|
144
|
+
this.unitIdToSquad.set(unit.id, squad);
|
|
145
|
+
}
|
|
146
|
+
registerSquad(squad) {
|
|
147
|
+
this.squads.push(squad);
|
|
57
148
|
}
|
|
58
149
|
}
|