@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,152 @@
1
+ import { AttackState, GameApi, ObjectType, PlayerData, Point2D, UnitData } from "@chronodivide/game-api";
2
+ import { OneTimeMission } from "./oneTimeMission.js";
3
+ import { CombatSquad } from "../../squad/behaviours/combatSquad.js";
4
+ import { Mission, MissionAction, disbandMission, noop } from "../mission.js";
5
+ import { GlobalThreat } from "../../threat/threat.js";
6
+ import { Squad } from "../../squad/squad.js";
7
+ import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../map/map.js";
8
+ import { MissionFactory } from "../missionFactories.js";
9
+ import { MatchAwareness } from "../../awareness.js";
10
+ import { MissionController } from "../missionController.js";
11
+ import { match } from "assert";
12
+ import { RetreatMission } from "./retreatMission.js";
13
+ import _ from "lodash";
14
+
15
+ export enum AttackFailReason {
16
+ NoTargets = 0,
17
+ DefenceTooStrong = 1,
18
+ }
19
+
20
+ const NO_TARGET_IDLE_TIMEOUT_TICKS = 60;
21
+
22
+ /**
23
+ * A mission that tries to attack a certain area.
24
+ */
25
+ export class AttackMission extends Mission<AttackFailReason> {
26
+ private lastTargetSeenAt = 0;
27
+
28
+ constructor(
29
+ uniqueName: string,
30
+ priority: number,
31
+ private rallyArea: Point2D,
32
+ private attackArea: Point2D,
33
+ private radius: number,
34
+ ) {
35
+ super(uniqueName, priority);
36
+ }
37
+
38
+ onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
39
+ if (this.getSquad() === null) {
40
+ return this.setSquad(
41
+ new Squad(this.getUniqueName(), new CombatSquad(this.rallyArea, this.attackArea, this.radius), this),
42
+ );
43
+ } else {
44
+ // Dispatch missions.
45
+ if (!matchAwareness.shouldAttack()) {
46
+ return disbandMission(AttackFailReason.DefenceTooStrong);
47
+ }
48
+
49
+ const foundTargets = matchAwareness.getHostilesNearPoint2d(this.attackArea, this.radius);
50
+
51
+ if (foundTargets.length > 0) {
52
+ this.lastTargetSeenAt = gameApi.getCurrentTick();
53
+ } else if (gameApi.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_IDLE_TIMEOUT_TICKS) {
54
+ return disbandMission(AttackFailReason.NoTargets);
55
+ }
56
+ }
57
+ return noop();
58
+ }
59
+ }
60
+
61
+ const ATTACK_COOLDOWN_TICKS = 120;
62
+
63
+ // Calculates the weight for initiating an attack on the position of a unit or building.
64
+ // This is separate from unit micro; the squad will be ordered to attack in the vicinity of the point.
65
+ const getTargetWeight: (unitData: UnitData, tryFocusHarvester: boolean) => number = (unitData, tryFocusHarvester) => {
66
+ if (tryFocusHarvester && unitData.rules.harvester) {
67
+ return 100000;
68
+ } else if (unitData.type === ObjectType.Building) {
69
+ return unitData.maxHitPoints * 10;
70
+ } else {
71
+ return unitData.maxHitPoints;
72
+ }
73
+ };
74
+
75
+ export class AttackMissionFactory implements MissionFactory {
76
+ constructor(private lastAttackAt: number = -ATTACK_COOLDOWN_TICKS) {}
77
+
78
+ getName(): string {
79
+ return "AttackMissionFactory";
80
+ }
81
+
82
+ generateTarget(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): Point2D | null {
83
+ // Randomly decide between harvester and base.
84
+ try {
85
+ const tryFocusHarvester = gameApi.generateRandomInt(0, 1) === 0;
86
+ const enemyUnits = gameApi
87
+ .getVisibleUnits(playerData.name, "hostile")
88
+ .map((unitId) => gameApi.getUnitData(unitId))
89
+ .filter((u) => !!u && gameApi.getPlayerData(u.owner).isCombatant) as UnitData[];
90
+
91
+ const maxUnit = _.maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));
92
+ if (maxUnit) {
93
+ return { x: maxUnit.tile.rx, y: maxUnit.tile.ry };
94
+ }
95
+ } catch (err) {
96
+ // There's a crash here when accessing a building that got destroyed. Will catch and ignore or now.
97
+ return null;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ maybeCreateMissions(
103
+ gameApi: GameApi,
104
+ playerData: PlayerData,
105
+ matchAwareness: MatchAwareness,
106
+ missionController: MissionController,
107
+ ): void {
108
+ if (!matchAwareness.shouldAttack()) {
109
+ return;
110
+ }
111
+ if (gameApi.getCurrentTick() < this.lastAttackAt + ATTACK_COOLDOWN_TICKS) {
112
+ return;
113
+ }
114
+
115
+ const attackRadius = 15;
116
+
117
+ const attackArea = this.generateTarget(gameApi, playerData, matchAwareness);
118
+
119
+ if (!attackArea) {
120
+ // Nothing to attack.
121
+ return;
122
+ }
123
+
124
+ // TODO: not using a fixed value here. But performance slows to a crawl when this is unique.
125
+ const squadName = "globalAttack";
126
+
127
+ const tryAttack = missionController
128
+ .addMission(new AttackMission(squadName, 100, matchAwareness.getMainRallyPoint(), attackArea, attackRadius))
129
+ ?.then((reason, squad) => {
130
+ missionController.addMission(
131
+ new RetreatMission(
132
+ "retreat-from-" + squadName + gameApi.getCurrentTick(),
133
+ 100,
134
+ matchAwareness.getMainRallyPoint(),
135
+ squad?.getUnitIds() ?? [],
136
+ ),
137
+ );
138
+ });
139
+ if (tryAttack) {
140
+ this.lastAttackAt = gameApi.getCurrentTick();
141
+ }
142
+ }
143
+
144
+ onMissionFailed(
145
+ gameApi: GameApi,
146
+ playerData: PlayerData,
147
+ matchAwareness: MatchAwareness,
148
+ failedMission: Mission,
149
+ failureReason: any,
150
+ missionController: MissionController,
151
+ ): void {}
152
+ }
@@ -0,0 +1,104 @@
1
+ import { GameApi, PlayerData, Point2D } from "@chronodivide/game-api";
2
+ import { MatchAwareness } from "../../awareness.js";
3
+ import { MissionController } from "../missionController.js";
4
+ import { Mission, MissionAction, disbandMission, noop } from "../mission.js";
5
+ import { MissionFactory } from "../missionFactories.js";
6
+ import { Squad } from "../../squad/squad.js";
7
+ import { CombatSquad } from "../../squad/behaviours/combatSquad.js";
8
+ import { RetreatMission } from "./retreatMission.js";
9
+
10
+ export enum DefenceFailReason {
11
+ NoTargets,
12
+ }
13
+
14
+ /**
15
+ * A mission that tries to defend a certain area.
16
+ */
17
+ export class DefenceMission extends Mission<DefenceFailReason> {
18
+ private combatSquad?: CombatSquad;
19
+
20
+ constructor(
21
+ uniqueName: string,
22
+ priority: number,
23
+ private defenceArea: Point2D,
24
+ private radius: number,
25
+ ) {
26
+ super(uniqueName, priority);
27
+ }
28
+
29
+ onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
30
+ if (this.getSquad() === null && !this.combatSquad) {
31
+ this.combatSquad = new CombatSquad(matchAwareness.getMainRallyPoint(), this.defenceArea, this.radius);
32
+ return this.setSquad(new Squad("defenceSquad-" + this.getUniqueName(), this.combatSquad, this));
33
+ } else {
34
+ // Dispatch missions.
35
+ const foundTargets = matchAwareness.getHostilesNearPoint2d(this.defenceArea, this.radius);
36
+
37
+ if (foundTargets.length === 0) {
38
+ return disbandMission(DefenceFailReason.NoTargets);
39
+ } else {
40
+ this.combatSquad?.setAttackArea({ x: foundTargets[0].x, y: foundTargets[0].y });
41
+ }
42
+ }
43
+ return noop();
44
+ }
45
+ }
46
+
47
+ const DEFENCE_CHECK_TICKS = 30;
48
+
49
+ // Starting radius around the player's base to trigger defense.
50
+ const DEFENCE_STARTING_RADIUS = 20;
51
+ // Every game tick, we increase the defendable area by this amount.
52
+ const DEFENCE_RADIUS_INCREASE_PER_GAME_TICK = 0.005;
53
+
54
+ export class DefenceMissionFactory implements MissionFactory {
55
+ private lastDefenceCheckAt = 0;
56
+
57
+ constructor() {}
58
+
59
+ getName(): string {
60
+ return "DefenceMissionFactory";
61
+ }
62
+
63
+ maybeCreateMissions(
64
+ gameApi: GameApi,
65
+ playerData: PlayerData,
66
+ matchAwareness: MatchAwareness,
67
+ missionController: MissionController,
68
+ ): void {
69
+ if (gameApi.getCurrentTick() < this.lastDefenceCheckAt + DEFENCE_CHECK_TICKS) {
70
+ return;
71
+ }
72
+ this.lastDefenceCheckAt = gameApi.getCurrentTick();
73
+
74
+ const defendableRadius =
75
+ DEFENCE_STARTING_RADIUS + DEFENCE_RADIUS_INCREASE_PER_GAME_TICK * gameApi.getCurrentTick();
76
+ const enemiesNearSpawn = matchAwareness.getHostilesNearPoint2d(playerData.startLocation, defendableRadius);
77
+
78
+ if (enemiesNearSpawn.length > 0) {
79
+ missionController.addMission(
80
+ new DefenceMission("globalDefence", 1000, playerData.startLocation, defendableRadius * 1.2)?.then(
81
+ (reason, squad) => {
82
+ missionController.addMission(
83
+ new RetreatMission(
84
+ "retreat-from-globalDefence" + gameApi.getCurrentTick(),
85
+ 100,
86
+ matchAwareness.getMainRallyPoint(),
87
+ squad?.getUnitIds() ?? [],
88
+ ),
89
+ );
90
+ },
91
+ ),
92
+ );
93
+ }
94
+ }
95
+
96
+ onMissionFailed(
97
+ gameApi: GameApi,
98
+ playerData: PlayerData,
99
+ matchAwareness: MatchAwareness,
100
+ failedMission: Mission,
101
+ failureReason: undefined,
102
+ missionController: MissionController,
103
+ ): void {}
104
+ }
@@ -0,0 +1,49 @@
1
+ import { GameApi, PlayerData } from "@chronodivide/game-api";
2
+ import { GlobalThreat } from "../../threat/threat.js";
3
+ import { Mission } from "../mission.js";
4
+ import { ExpansionSquad } from "../../squad/behaviours/expansionSquad.js";
5
+ import { MissionFactory } from "../missionFactories.js";
6
+ import { OneTimeMission } from "./oneTimeMission.js";
7
+ import { MatchAwareness } from "../../awareness.js";
8
+ import { AttackFailReason, AttackMission } from "./attackMission.js";
9
+ import { MissionController } from "../missionController.js";
10
+
11
+ /**
12
+ * A mission that tries to create an MCV (if it doesn't exist) and deploy it somewhere it can be deployed.
13
+ */
14
+ export class ExpansionMission extends OneTimeMission {
15
+ constructor(uniqueName: string, priority: number, selectedMcv: number | null) {
16
+ super(uniqueName, priority, () => new ExpansionSquad(selectedMcv));
17
+ }
18
+ }
19
+
20
+ export class ExpansionMissionFactory implements MissionFactory {
21
+ getName(): string {
22
+ return "ExpansionMissionFactory";
23
+ }
24
+
25
+ maybeCreateMissions(
26
+ gameApi: GameApi,
27
+ playerData: PlayerData,
28
+ matchAwareness: MatchAwareness,
29
+ missionController: MissionController,
30
+ ): void {
31
+ // At this point, only expand if we have a loose MCV.
32
+ const mcvs = gameApi.getVisibleUnits(playerData.name, "self", (r) =>
33
+ gameApi.getGeneralRules().baseUnit.includes(r.name)
34
+ );
35
+ mcvs.forEach((mcv) => {
36
+ missionController.addMission(new ExpansionMission("expand-with-" + mcv, 100, mcv));
37
+ });
38
+ }
39
+
40
+ onMissionFailed(
41
+ gameApi: GameApi,
42
+ playerData: PlayerData,
43
+ matchAwareness: MatchAwareness,
44
+ failedMission: Mission,
45
+ failureReason: undefined,
46
+ missionController: MissionController,
47
+ ): void {
48
+ }
49
+ }
@@ -0,0 +1,32 @@
1
+ import { GameApi, PlayerData } from "@chronodivide/game-api";
2
+ import { GlobalThreat } from "../../threat/threat.js";
3
+ import { Mission, MissionAction, disbandMission, noop } from "../mission.js";
4
+ import { ExpansionSquad } from "../../squad/behaviours/expansionSquad.js";
5
+ import { Squad } from "../../squad/squad.js";
6
+ import { MissionFactory } from "../missionFactories.js";
7
+ import { SquadBehaviour } from "../../squad/squadBehaviour.js";
8
+ import { MatchAwareness } from "../../awareness.js";
9
+
10
+ /**
11
+ * A mission that gets dispatched once, and once the squad decides to disband, the mission is disbanded.
12
+ */
13
+ export abstract class OneTimeMission<T = undefined> extends Mission<T> {
14
+ private hadSquad = false;
15
+
16
+ constructor(uniqueName: string, priority: number, private behaviourFactory: () => SquadBehaviour) {
17
+ super(uniqueName, priority);
18
+ }
19
+
20
+ onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
21
+ if (this.getSquad() === null) {
22
+ if (!this.hadSquad) {
23
+ this.hadSquad = true;
24
+ return this.setSquad(new Squad(this.getUniqueName(), this.behaviourFactory(), this));
25
+ } else {
26
+ return disbandMission();
27
+ }
28
+ } else {
29
+ return noop();
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ import { Point2D } from "@chronodivide/game-api";
2
+ import { OneTimeMission } from "./oneTimeMission.js";
3
+ import { RetreatSquad } from "../../squad/behaviours/retreatSquad.js";
4
+
5
+ export class RetreatMission extends OneTimeMission {
6
+ constructor(uniqueName: string, priority: number, retreatToPoint: Point2D, unitIds: number[]) {
7
+ super(uniqueName, priority, () => new RetreatSquad(unitIds, retreatToPoint));
8
+ }
9
+ }
@@ -0,0 +1,59 @@
1
+ import { GameApi, PlayerData } from "@chronodivide/game-api";
2
+ import { ScoutingSquad } from "../../squad/behaviours/scoutingSquad.js";
3
+ import { MissionFactory } from "../missionFactories.js";
4
+ import { OneTimeMission } from "./oneTimeMission.js";
5
+ import { MatchAwareness } from "../../awareness.js";
6
+ import { Mission } from "../mission.js";
7
+ import { AttackMission } from "./attackMission.js";
8
+ import { MissionController } from "../missionController.js";
9
+ import { getUnseenStartingLocations } from "../../common/scout.js";
10
+
11
+ /**
12
+ * A mission that tries to scout around the map with a cheap, fast unit (usually attack dogs)
13
+ */
14
+ export class ScoutingMission extends OneTimeMission {
15
+ constructor(uniqueName: string, priority: number) {
16
+ super(uniqueName, priority, () => new ScoutingSquad());
17
+ }
18
+ }
19
+
20
+ const SCOUT_COOLDOWN_TICKS = 300;
21
+
22
+ export class ScoutingMissionFactory implements MissionFactory {
23
+ constructor(private lastScoutAt: number = -SCOUT_COOLDOWN_TICKS) {}
24
+
25
+ getName(): string {
26
+ return "ScoutingMissionFactory";
27
+ }
28
+
29
+ maybeCreateMissions(
30
+ gameApi: GameApi,
31
+ playerData: PlayerData,
32
+ matchAwareness: MatchAwareness,
33
+ missionController: MissionController,
34
+ ): void {
35
+ if (gameApi.getCurrentTick() < this.lastScoutAt + SCOUT_COOLDOWN_TICKS) {
36
+ return;
37
+ }
38
+ const candidatePoints = getUnseenStartingLocations(gameApi, playerData);
39
+ if (candidatePoints.length === 0) {
40
+ return;
41
+ }
42
+ if (!missionController.addMission(new ScoutingMission("globalScout", 100))) {
43
+ this.lastScoutAt = gameApi.getCurrentTick();
44
+ }
45
+ }
46
+
47
+ onMissionFailed(
48
+ gameApi: GameApi,
49
+ playerData: PlayerData,
50
+ matchAwareness: MatchAwareness,
51
+ failedMission: Mission,
52
+ failureReason: undefined,
53
+ missionController: MissionController,
54
+ ): void {
55
+ if (failedMission instanceof AttackMission) {
56
+ missionController.addMission(new ScoutingMission("globalScout", 100));
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,125 @@
1
+ import _ from "lodash";
2
+ import { ActionsApi, GameApi, MovementZone, PlayerData, Point2D } from "@chronodivide/game-api";
3
+ import { Squad } from "../squad.js";
4
+ import { SquadAction, SquadBehaviour, grabCombatants, noop } from "../squadBehaviour.js";
5
+ import { MatchAwareness } from "../../awareness.js";
6
+ import { getDistanceBetweenPoints } from "../../map/map.js";
7
+ import { manageAttackMicro, manageMoveMicro } from "./common.js";
8
+
9
+ const TARGET_UPDATE_INTERVAL_TICKS = 10;
10
+ const GRAB_INTERVAL_TICKS = 10;
11
+
12
+ const GRAB_RADIUS = 20;
13
+
14
+ // Units must be in a certain radius of the center of mass before attacking.
15
+ // This scales for number of units in the squad though.
16
+ const MIN_GATHER_RADIUS = 5;
17
+
18
+ // If the radius expands beyond this amount then we should switch back to gathering mode.
19
+ const MAX_GATHER_RADIUS = 15;
20
+
21
+ const GATHER_RATIO = 10;
22
+
23
+ enum SquadState {
24
+ Gathering,
25
+ Attacking,
26
+ }
27
+
28
+ export class CombatSquad implements SquadBehaviour {
29
+ private lastGrab: number | null = null;
30
+ private lastCommand: number | null = null;
31
+ private state = SquadState.Gathering;
32
+
33
+ /**
34
+ *
35
+ * @param rallyArea the initial location to grab combatants
36
+ * @param targetArea
37
+ * @param radius
38
+ */
39
+ constructor(
40
+ private rallyArea: Point2D,
41
+ private targetArea: Point2D,
42
+ private radius: number,
43
+ ) {}
44
+
45
+ public setAttackArea(targetArea: Point2D) {
46
+ this.targetArea = targetArea;
47
+ }
48
+
49
+ public onAiUpdate(
50
+ gameApi: GameApi,
51
+ actionsApi: ActionsApi,
52
+ playerData: PlayerData,
53
+ squad: Squad,
54
+ matchAwareness: MatchAwareness,
55
+ ): SquadAction {
56
+ if (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS) {
57
+ this.lastCommand = gameApi.getCurrentTick();
58
+ const centerOfMass = squad.getCenterOfMass();
59
+ const maxDistance = squad.getMaxDistanceToCenterOfMass();
60
+ const units = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant);
61
+
62
+ // Only use ground units for center of mass.
63
+ const groundUnits = squad.getUnitsMatching(
64
+ gameApi,
65
+ (r) =>
66
+ r.rules.isSelectableCombatant &&
67
+ (r.rules.movementZone === MovementZone.Infantry ||
68
+ r.rules.movementZone === MovementZone.Normal ||
69
+ r.rules.movementZone === MovementZone.InfantryDestroyer),
70
+ );
71
+
72
+ if (this.state === SquadState.Gathering) {
73
+ const requiredGatherRadius = Math.sqrt(groundUnits.length) * GATHER_RATIO + MIN_GATHER_RADIUS;
74
+ if (
75
+ centerOfMass &&
76
+ maxDistance &&
77
+ gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
78
+ maxDistance > requiredGatherRadius
79
+ ) {
80
+ units.forEach((unit) => {
81
+ manageMoveMicro(actionsApi, unit, centerOfMass);
82
+ });
83
+ } else {
84
+ this.state = SquadState.Attacking;
85
+ }
86
+ } else {
87
+ const targetPoint = this.targetArea || playerData.startLocation;
88
+ const requiredGatherRadius = Math.sqrt(groundUnits.length) * GATHER_RATIO + MAX_GATHER_RADIUS;
89
+ if (
90
+ centerOfMass &&
91
+ maxDistance &&
92
+ gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
93
+ maxDistance > requiredGatherRadius
94
+ ) {
95
+ // Switch back to gather mode.
96
+ this.state = SquadState.Gathering;
97
+ return noop();
98
+ }
99
+ for (const unit of units) {
100
+ if (unit.isIdle) {
101
+ const { rx: x, ry: y } = unit.tile;
102
+ const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
103
+ const nearbyHostiles = matchAwareness.getHostilesNearPoint(x, y, range * 2);
104
+ const closest = _.minBy(nearbyHostiles, ({ x: hX, y: hY }) =>
105
+ getDistanceBetweenPoints({ x, y }, { x: hX, y: hY }),
106
+ );
107
+ const closestUnit = closest ? gameApi.getUnitData(closest.unitId) ?? null : null;
108
+ if (closestUnit) {
109
+ manageAttackMicro(actionsApi, unit, closestUnit);
110
+ } else {
111
+ manageMoveMicro(actionsApi, unit, targetPoint);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ if (!this.lastGrab || gameApi.getCurrentTick() > this.lastGrab + GRAB_INTERVAL_TICKS) {
119
+ this.lastGrab = gameApi.getCurrentTick();
120
+ return grabCombatants(squad.getCenterOfMass() ?? this.rallyArea, GRAB_RADIUS);
121
+ } else {
122
+ return noop();
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,37 @@
1
+ import { ActionsApi, ObjectType, OrderType, Point2D, UnitData } from "@chronodivide/game-api";
2
+ import { getDistanceBetweenUnits } from "../../map/map.js";
3
+
4
+ // Micro methods
5
+ export function manageMoveMicro(actionsApi: ActionsApi, attacker: UnitData, attackPoint: Point2D) {
6
+ if (attacker.name === "E1") {
7
+ if (!attacker.canMove) {
8
+ actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
9
+ }
10
+ }
11
+ actionsApi.orderUnits([attacker.id], OrderType.Move, attackPoint.x, attackPoint.y);
12
+ }
13
+
14
+ export function manageAttackMicro(actionsApi: ActionsApi, attacker: UnitData, target: UnitData) {
15
+ const distance = getDistanceBetweenUnits(attacker, target);
16
+ if (attacker.name === "E1") {
17
+ // Para (deployed weapon) range is 5.
18
+ const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;
19
+ if (attacker.canMove && distance <= deployedWeaponRange * 0.8) {
20
+ actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
21
+ return;
22
+ } else if (!attacker.canMove && distance > deployedWeaponRange) {
23
+ actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
24
+ return;
25
+ }
26
+ }
27
+ let targetData = target;
28
+ let orderType: OrderType = OrderType.Attack;
29
+ const primaryWeaponRange = attacker.primaryWeapon?.maxRange || 5;
30
+ if (targetData?.type == ObjectType.Building && distance < primaryWeaponRange * 0.8) {
31
+ orderType = OrderType.Attack;
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,59 @@
1
+ import { ActionsApi, GameApi, OrderType, PlayerData, SideType } from "@chronodivide/game-api";
2
+ import { GlobalThreat } from "../../threat/threat.js";
3
+ import { Squad } from "../squad.js";
4
+ import { SquadAction, SquadBehaviour, disband, noop, requestSpecificUnits, requestUnits } from "../squadBehaviour.js";
5
+ import { MatchAwareness } from "../../awareness.js";
6
+
7
+ const DEPLOY_COOLDOWN_TICKS = 30;
8
+
9
+ // Expansion or initial base.
10
+ export class ExpansionSquad implements SquadBehaviour {
11
+ private hasAttemptedDeployWith: {
12
+ unitId: number;
13
+ gameTick: number;
14
+ } | null = null;
15
+
16
+ /**
17
+ * @param selectedMcv ID of the MCV to try to expand with. If that unit dies, the squad will disband. If no value is provided,
18
+ * the mission requests an MCV.
19
+ */
20
+ constructor(private selectedMcv: number | null) {
21
+ };
22
+
23
+ public onAiUpdate(
24
+ gameApi: GameApi,
25
+ actionsApi: ActionsApi,
26
+ playerData: PlayerData,
27
+ squad: Squad,
28
+ matchAwareness: MatchAwareness
29
+ ): SquadAction {
30
+ const mcvTypes = ["AMCV", "SMCV"];
31
+ const mcvs = squad.getUnitsOfTypes(gameApi, ...mcvTypes);
32
+ if (mcvs.length === 0) {
33
+ // Perhaps we deployed already (or the unit was destroyed), end the mission.
34
+ if (this.hasAttemptedDeployWith !== null) {
35
+ return disband();
36
+ }
37
+ // We need an mcv!
38
+ if (this.selectedMcv) {
39
+ return requestSpecificUnits([this.selectedMcv], 100);
40
+ } else {
41
+ return requestUnits(mcvTypes, 100);
42
+ }
43
+ } else if (
44
+ !this.hasAttemptedDeployWith ||
45
+ gameApi.getCurrentTick() > this.hasAttemptedDeployWith.gameTick + DEPLOY_COOLDOWN_TICKS
46
+ ) {
47
+ actionsApi.orderUnits(
48
+ mcvs.map((mcv) => mcv.id),
49
+ OrderType.DeploySelected
50
+ );
51
+ // Add a cooldown to deploy attempts.
52
+ this.hasAttemptedDeployWith = {
53
+ unitId: mcvs[0].id,
54
+ gameTick: gameApi.getCurrentTick(),
55
+ };
56
+ }
57
+ return noop();
58
+ }
59
+ }
@@ -0,0 +1,46 @@
1
+ import { ActionsApi, GameApi, OrderType, PlayerData, Point2D, SideType } from "@chronodivide/game-api";
2
+ import { GlobalThreat } from "../../threat/threat.js";
3
+ import { Squad } from "../squad.js";
4
+ import { SquadAction, SquadBehaviour, disband, noop, requestSpecificUnits, requestUnits } from "../squadBehaviour.js";
5
+ import { MatchAwareness } from "../../awareness.js";
6
+
7
+ const SCOUT_MOVE_COOLDOWN_TICKS = 30;
8
+
9
+ export class RetreatSquad implements SquadBehaviour {
10
+ private hasRequestedUnits: boolean = false;
11
+ private moveOrderSentAt: number | null = null;
12
+ private createdAt: number | null = null;
13
+
14
+ constructor(
15
+ private unitIds: number[],
16
+ private retreatToPoint: Point2D,
17
+ ) {}
18
+
19
+ public onAiUpdate(
20
+ gameApi: GameApi,
21
+ actionsApi: ActionsApi,
22
+ playerData: PlayerData,
23
+ squad: Squad,
24
+ matchAwareness: MatchAwareness,
25
+ ): SquadAction {
26
+ if (!this.createdAt) {
27
+ this.createdAt = gameApi.getCurrentTick();
28
+ }
29
+ if (squad.getUnitIds().length > 0) {
30
+ // Only send the order once we have managed to claim some units.
31
+ console.log(`Retreat squad ordered ${squad.getUnitIds()} to retreat`);
32
+ actionsApi.orderUnits(squad.getUnitIds(), OrderType.Move, this.retreatToPoint.x, this.retreatToPoint.y);
33
+ if (!this.moveOrderSentAt) {
34
+ this.moveOrderSentAt = gameApi.getCurrentTick();
35
+ }
36
+ }
37
+ if (
38
+ (this.moveOrderSentAt && gameApi.getCurrentTick() > this.moveOrderSentAt + 60) ||
39
+ (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240)
40
+ ) {
41
+ return disband();
42
+ } else {
43
+ return requestSpecificUnits(this.unitIds, 100);
44
+ }
45
+ }
46
+ }