@supalosa/chronodivide-bot 0.1.1 → 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.
Files changed (74) hide show
  1. package/README.md +71 -46
  2. package/dist/bot/bot.js +27 -183
  3. package/dist/bot/logic/awareness.js +122 -0
  4. package/dist/bot/logic/building/basicGroundUnit.js +8 -6
  5. package/dist/bot/logic/building/building.js +6 -3
  6. package/dist/bot/logic/building/harvester.js +1 -1
  7. package/dist/bot/logic/building/queueController.js +4 -21
  8. package/dist/bot/logic/common/scout.js +10 -0
  9. package/dist/bot/logic/knowledge.js +1 -0
  10. package/dist/bot/logic/map/map.js +6 -0
  11. package/dist/bot/logic/map/sector.js +6 -1
  12. package/dist/bot/logic/mission/basicMission.js +1 -5
  13. package/dist/bot/logic/mission/expansionMission.js +22 -4
  14. package/dist/bot/logic/mission/mission.js +49 -2
  15. package/dist/bot/logic/mission/missionController.js +67 -34
  16. package/dist/bot/logic/mission/missionFactories.js +10 -0
  17. package/dist/bot/logic/mission/missions/attackMission.js +109 -0
  18. package/dist/bot/logic/mission/missions/defenceMission.js +62 -0
  19. package/dist/bot/logic/mission/missions/expansionMission.js +24 -0
  20. package/dist/bot/logic/mission/missions/oneTimeMission.js +26 -0
  21. package/dist/bot/logic/mission/missions/retreatMission.js +7 -0
  22. package/dist/bot/logic/mission/missions/scoutingMission.js +38 -0
  23. package/dist/bot/logic/squad/behaviours/attackSquad.js +82 -0
  24. package/dist/bot/logic/squad/behaviours/combatSquad.js +99 -0
  25. package/dist/bot/logic/squad/behaviours/common.js +37 -0
  26. package/dist/bot/logic/squad/behaviours/defenceSquad.js +48 -0
  27. package/dist/bot/logic/squad/behaviours/expansionSquad.js +42 -0
  28. package/dist/bot/logic/squad/behaviours/retreatSquad.js +32 -0
  29. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +38 -0
  30. package/dist/bot/logic/squad/behaviours/squadExpansion.js +26 -13
  31. package/dist/bot/logic/squad/squad.js +68 -15
  32. package/dist/bot/logic/squad/squadBehaviour.js +5 -5
  33. package/dist/bot/logic/squad/squadBehaviours.js +6 -0
  34. package/dist/bot/logic/squad/squadController.js +106 -15
  35. package/dist/exampleBot.js +22 -7
  36. package/package.json +29 -24
  37. package/src/bot/bot.ts +178 -378
  38. package/src/bot/logic/awareness.ts +220 -0
  39. package/src/bot/logic/building/ArtilleryUnit.ts +2 -2
  40. package/src/bot/logic/building/antiGroundStaticDefence.ts +2 -2
  41. package/src/bot/logic/building/basicAirUnit.ts +2 -2
  42. package/src/bot/logic/building/basicBuilding.ts +2 -2
  43. package/src/bot/logic/building/basicGroundUnit.ts +83 -78
  44. package/src/bot/logic/building/building.ts +125 -120
  45. package/src/bot/logic/building/harvester.ts +27 -27
  46. package/src/bot/logic/building/powerPlant.ts +1 -1
  47. package/src/bot/logic/building/queueController.ts +17 -38
  48. package/src/bot/logic/building/resourceCollectionBuilding.ts +1 -1
  49. package/src/bot/logic/common/scout.ts +12 -0
  50. package/src/bot/logic/map/map.ts +11 -3
  51. package/src/bot/logic/map/sector.ts +136 -130
  52. package/src/bot/logic/mission/mission.ts +83 -47
  53. package/src/bot/logic/mission/missionController.ts +103 -51
  54. package/src/bot/logic/mission/missionFactories.ts +46 -0
  55. package/src/bot/logic/mission/missions/attackMission.ts +152 -0
  56. package/src/bot/logic/mission/missions/defenceMission.ts +104 -0
  57. package/src/bot/logic/mission/missions/expansionMission.ts +49 -0
  58. package/src/bot/logic/mission/missions/oneTimeMission.ts +32 -0
  59. package/src/bot/logic/mission/missions/retreatMission.ts +9 -0
  60. package/src/bot/logic/mission/missions/scoutingMission.ts +59 -0
  61. package/src/bot/logic/squad/behaviours/combatSquad.ts +125 -0
  62. package/src/bot/logic/squad/behaviours/common.ts +37 -0
  63. package/src/bot/logic/squad/behaviours/expansionSquad.ts +59 -0
  64. package/src/bot/logic/squad/behaviours/retreatSquad.ts +46 -0
  65. package/src/bot/logic/squad/behaviours/scoutingSquad.ts +56 -0
  66. package/src/bot/logic/squad/squad.ts +163 -97
  67. package/src/bot/logic/squad/squadBehaviour.ts +61 -43
  68. package/src/bot/logic/squad/squadBehaviours.ts +8 -0
  69. package/src/bot/logic/squad/squadController.ts +190 -66
  70. package/src/exampleBot.ts +19 -4
  71. package/tsconfig.json +1 -1
  72. package/src/bot/logic/mission/basicMission.ts +0 -42
  73. package/src/bot/logic/mission/expansionMission.ts +0 -25
  74. 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
- getDesiredComposition(gameApi, playerData, squad, threatData) {
5
- // This squad desires an MCV.
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
- return {};
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, unitIds = [], liveness = SquadLiveness.SquadActive, lastLivenessUpdateTick = 0) {
30
+ constructor(name, behaviour, mission, killable = false) {
8
31
  this.name = name;
9
32
  this.behaviour = behaviour;
10
33
  this.mission = mission;
11
- this.unitIds = unitIds;
12
- this.liveness = liveness;
13
- this.lastLivenessUpdateTick = lastLivenessUpdateTick;
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
- onAiUpdate(gameApi, playerData, threatData) {
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(this);
23
- this.mission = undefined;
68
+ this.mission.removeSquad();
69
+ this.mission = null;
70
+ return disband();
24
71
  }
25
- let outcome = this.behaviour.onAiUpdate(gameApi, playerData, this, threatData);
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(this);
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
- getUnitsOfType(gameApi, f) {
96
+ getUnitsOfTypes(gameApi, ...names) {
47
97
  return this.unitIds
48
98
  .map((unitId) => gameApi.getUnitData(unitId))
49
- .filter(f)
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
- import { SquadExpansion } from "./behaviours/squadExpansion.js";
2
- export const allSquadBehaviours = [
3
- //new SquadScouters(),
4
- new SquadExpansion(),
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 });
@@ -0,0 +1,6 @@
1
+ export {};
2
+ /*
3
+ export const ALL_SQUAD_BEHAVIOURS: SquadBehaviour[] = [
4
+ //new SquadScouters(),
5
+ new SquadExpansion(),
6
+ ];*/
@@ -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(squads = [], unitIdToSquad = new Map()) {
5
- this.squads = squads;
6
- this.unitIdToSquad = unitIdToSquad;
5
+ constructor() {
6
+ this.squads = [];
7
+ this.unitIdToSquad = new Map();
7
8
  }
8
- onAiUpdate(gameApi, playerData, threatData) {
9
- // Remove dead squads.
10
- this.squads = this.squads.filter((squad) => squad.getLiveness() == SquadLiveness.SquadDead);
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
- console.log(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
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
- let squadActions = this.squads.map((squad) => {
25
+ const squadActions = this.squads.map((squad) => {
25
26
  return {
26
27
  squad,
27
- action: squad.onAiUpdate(gameApi, playerData, threatData),
28
+ action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
28
29
  };
29
30
  });
30
31
  // Handle disbands and merges.
31
- const isDisband = (a) => a.type == "disband";
32
- const isMerge = (a) => a.type == "mergeInto";
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
- console.log("Merging into a disbanded squad, cancelling.");
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
- // Form squads.
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
  }