@supalosa/chronodivide-bot 0.1.1 → 0.2.1

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 +79 -46
  2. package/dist/bot/bot.js +33 -185
  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 +8 -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 +107 -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 +39 -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 +27 -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 +116 -15
  35. package/dist/exampleBot.js +37 -6
  36. package/package.json +29 -24
  37. package/src/bot/bot.ts +180 -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 +127 -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 +105 -51
  54. package/src/bot/logic/mission/missionFactories.ts +46 -0
  55. package/src/bot/logic/mission/missions/attackMission.ts +154 -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 +39 -0
  63. package/src/bot/logic/squad/behaviours/expansionSquad.ts +59 -0
  64. package/src/bot/logic/squad/behaviours/retreatSquad.ts +44 -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 +210 -66
  70. package/src/exampleBot.ts +41 -7
  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,82 @@
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 = 30;
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
+ const GATHER_RATIO = 10;
13
+ var SquadState;
14
+ (function (SquadState) {
15
+ SquadState[SquadState["Gathering"] = 0] = "Gathering";
16
+ SquadState[SquadState["Attacking"] = 1] = "Attacking";
17
+ })(SquadState || (SquadState = {}));
18
+ export class AttackOrDefenceSquad {
19
+ constructor(rallyArea, targetArea, radius) {
20
+ this.rallyArea = rallyArea;
21
+ this.targetArea = targetArea;
22
+ this.radius = radius;
23
+ this.lastGrab = null;
24
+ this.lastCommand = null;
25
+ this.state = SquadState.Gathering;
26
+ }
27
+ setAttackArea(targetArea) {
28
+ this.targetArea = targetArea;
29
+ }
30
+ onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
31
+ if (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS) {
32
+ this.lastCommand = gameApi.getCurrentTick();
33
+ const centerOfMass = squad.getCenterOfMass();
34
+ const maxDistance = squad.getMaxDistanceToCenterOfMass();
35
+ const units = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant);
36
+ if (this.state === SquadState.Gathering) {
37
+ // Only use ground units for center of mass.
38
+ const groundUnits = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant &&
39
+ (r.rules.movementZone === MovementZone.Infantry ||
40
+ r.rules.movementZone === MovementZone.Normal ||
41
+ r.rules.movementZone === MovementZone.InfantryDestroyer));
42
+ const requiredGatherRadius = Math.sqrt(groundUnits.length) * GATHER_RATIO + MIN_GATHER_RADIUS;
43
+ if (centerOfMass &&
44
+ maxDistance &&
45
+ gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
46
+ maxDistance > requiredGatherRadius) {
47
+ units.forEach((unit) => {
48
+ manageMoveMicro(actionsApi, unit, centerOfMass);
49
+ });
50
+ }
51
+ else {
52
+ this.state = SquadState.Attacking;
53
+ }
54
+ }
55
+ else {
56
+ const targetPoint = this.targetArea || playerData.startLocation;
57
+ for (const unit of units) {
58
+ if (unit.isIdle) {
59
+ const { rx: x, ry: y } = unit.tile;
60
+ const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
61
+ const nearbyHostiles = matchAwareness.getHostilesNearPoint(x, y, range * 2);
62
+ const closest = _.minBy(nearbyHostiles, ({ x: hX, y: hY }) => getDistanceBetweenPoints({ x, y }, { x: hX, y: hY }));
63
+ const closestUnit = closest ? gameApi.getUnitData(closest.unitId) ?? null : null;
64
+ if (closestUnit) {
65
+ manageAttackMicro(actionsApi, unit, closestUnit);
66
+ }
67
+ else {
68
+ manageMoveMicro(actionsApi, unit, targetPoint);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ if (!this.lastGrab || gameApi.getCurrentTick() > this.lastGrab + GRAB_INTERVAL_TICKS) {
75
+ this.lastGrab = gameApi.getCurrentTick();
76
+ return grabCombatants(this.rallyArea, this.radius * GRAB_RADIUS);
77
+ }
78
+ else {
79
+ return noop();
80
+ }
81
+ }
82
+ }
@@ -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,39 @@
1
+ import { AttackState, ObjectType, OrderType, StanceType } 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
+ const isDeployed = attacker.stance === StanceType.Deployed;
7
+ if (isDeployed) {
8
+ actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
9
+ }
10
+ }
11
+ actionsApi.orderUnits([attacker.id], OrderType.Move, attackPoint.x, attackPoint.y);
12
+ }
13
+ export function manageAttackMicro(actionsApi, attacker, target) {
14
+ const distance = getDistanceBetweenUnits(attacker, target);
15
+ if (attacker.name === "E1") {
16
+ // Para (deployed weapon) range is 5.
17
+ const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;
18
+ const isDeployed = attacker.stance === StanceType.Deployed;
19
+ if (!isDeployed && (distance <= deployedWeaponRange || attacker.attackState === AttackState.JustFired)) {
20
+ actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
21
+ return;
22
+ }
23
+ else if (isDeployed && distance > deployedWeaponRange) {
24
+ actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
25
+ return;
26
+ }
27
+ }
28
+ let targetData = target;
29
+ let orderType = OrderType.Attack;
30
+ const primaryWeaponRange = attacker.primaryWeapon?.maxRange || 5;
31
+ if (targetData?.type == ObjectType.Building && distance < primaryWeaponRange * 0.8) {
32
+ orderType = OrderType.Attack;
33
+ }
34
+ else if (targetData?.rules.canDisguise) {
35
+ // Special case for mirage tank/spy as otherwise they just sit next to it.
36
+ orderType = OrderType.Attack;
37
+ }
38
+ actionsApi.orderUnits([attacker.id], orderType, target.id);
39
+ }
@@ -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,27 @@
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.createdAt = null;
9
+ }
10
+ onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
11
+ if (!this.createdAt) {
12
+ this.createdAt = gameApi.getCurrentTick();
13
+ }
14
+ if (squad.getUnitIds().length > 0) {
15
+ // Only send the order once we have managed to claim some units.
16
+ actionsApi.orderUnits(squad.getUnitIds(), OrderType.AttackMove, this.retreatToPoint.x, this.retreatToPoint.y);
17
+ return disband();
18
+ }
19
+ if (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240) {
20
+ // Disband automatically after 240 ticks in case we couldn't actually claim any units.
21
+ return disband();
22
+ }
23
+ else {
24
+ return requestSpecificUnits(this.unitIds, 1000);
25
+ }
26
+ }
27
+ }
@@ -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
+ ];*/