@supalosa/chronodivide-bot 0.3.1 → 0.5.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 (149) hide show
  1. package/.env.template +5 -0
  2. package/README.md +57 -39
  3. package/dist/bot/bot.js +27 -37
  4. package/dist/bot/bot.js.map +1 -1
  5. package/dist/bot/logic/awareness.js +13 -8
  6. package/dist/bot/logic/awareness.js.map +1 -1
  7. package/dist/bot/logic/awarenessImpl.js +132 -0
  8. package/dist/bot/logic/awarenessImpl.js.map +1 -0
  9. package/dist/bot/logic/building/ArtilleryUnit.js +2 -29
  10. package/dist/bot/logic/building/ArtilleryUnit.js.map +1 -0
  11. package/dist/bot/logic/building/antiAirStaticDefence.js +43 -0
  12. package/dist/bot/logic/building/antiAirStaticDefence.js.map +1 -0
  13. package/dist/bot/logic/building/antiGroundStaticDefence.js +8 -5
  14. package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -1
  15. package/dist/bot/logic/building/basicAirUnit.js +2 -23
  16. package/dist/bot/logic/building/basicAirUnit.js.map +1 -1
  17. package/dist/bot/logic/building/basicBuilding.js +3 -2
  18. package/dist/bot/logic/building/basicBuilding.js.map +1 -1
  19. package/dist/bot/logic/building/basicGroundUnit.js +2 -43
  20. package/dist/bot/logic/building/basicGroundUnit.js.map +1 -1
  21. package/dist/bot/logic/building/building.js +55 -11
  22. package/dist/bot/logic/building/building.js.map +1 -0
  23. package/dist/bot/logic/building/buildingRules.js +62 -50
  24. package/dist/bot/logic/building/buildingRules.js.map +1 -1
  25. package/dist/bot/logic/building/common.js +19 -0
  26. package/dist/bot/logic/building/common.js.map +1 -0
  27. package/dist/bot/logic/building/harvester.js +2 -1
  28. package/dist/bot/logic/building/harvester.js.map +1 -1
  29. package/dist/bot/logic/building/queueController.js +73 -41
  30. package/dist/bot/logic/building/queueController.js.map +1 -1
  31. package/dist/bot/logic/common/utils.js +35 -0
  32. package/dist/bot/logic/common/utils.js.map +1 -1
  33. package/dist/bot/logic/composition/alliedCompositions.js +13 -0
  34. package/dist/bot/logic/composition/alliedCompositions.js.map +1 -0
  35. package/dist/bot/logic/composition/common.js +2 -0
  36. package/dist/bot/logic/composition/common.js.map +1 -0
  37. package/dist/bot/logic/composition/sovietCompositions.js +13 -0
  38. package/dist/bot/logic/composition/sovietCompositions.js.map +1 -0
  39. package/dist/bot/logic/mission/actionBatcher.js +92 -0
  40. package/dist/bot/logic/mission/actionBatcher.js.map +1 -0
  41. package/dist/bot/logic/mission/behaviours/combatSquad.js +124 -0
  42. package/dist/bot/logic/mission/behaviours/combatSquad.js.map +1 -0
  43. package/dist/bot/logic/mission/behaviours/common.js +58 -0
  44. package/dist/bot/logic/mission/behaviours/common.js.map +1 -0
  45. package/dist/bot/logic/mission/behaviours/engineerSquad.js +39 -0
  46. package/dist/bot/logic/mission/behaviours/engineerSquad.js.map +1 -0
  47. package/dist/bot/logic/mission/behaviours/expansionSquad.js +46 -0
  48. package/dist/bot/logic/mission/behaviours/expansionSquad.js.map +1 -0
  49. package/dist/bot/logic/mission/behaviours/retreatSquad.js +31 -0
  50. package/dist/bot/logic/mission/behaviours/retreatSquad.js.map +1 -0
  51. package/{src/bot/logic/squad/behaviours/scoutingSquad.ts → dist/bot/logic/mission/behaviours/scoutingSquad.js} +29 -47
  52. package/dist/bot/logic/mission/behaviours/scoutingSquad.js.map +1 -0
  53. package/dist/bot/logic/mission/mission.js +91 -19
  54. package/dist/bot/logic/mission/mission.js.map +1 -1
  55. package/dist/bot/logic/mission/missionController.js +262 -21
  56. package/dist/bot/logic/mission/missionController.js.map +1 -1
  57. package/dist/bot/logic/mission/missions/attackMission.js +159 -52
  58. package/dist/bot/logic/mission/missions/attackMission.js.map +1 -1
  59. package/dist/bot/logic/mission/missions/basicMission.js +13 -0
  60. package/dist/bot/logic/mission/missions/basicMission.js.map +1 -0
  61. package/dist/bot/logic/mission/missions/defenceMission.js +43 -28
  62. package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -1
  63. package/dist/bot/logic/mission/missions/engineerMission.js +37 -7
  64. package/dist/bot/logic/mission/missions/engineerMission.js.map +1 -1
  65. package/dist/bot/logic/mission/missions/expansionMission.js +42 -6
  66. package/dist/bot/logic/mission/missions/expansionMission.js.map +1 -1
  67. package/dist/bot/logic/mission/missions/missionBehaviour.js +2 -0
  68. package/dist/bot/logic/mission/missions/missionBehaviour.js.map +1 -0
  69. package/dist/bot/logic/mission/missions/retreatMission.js +31 -5
  70. package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -1
  71. package/dist/bot/logic/mission/missions/scoutingMission.js +103 -6
  72. package/dist/bot/logic/mission/missions/scoutingMission.js.map +1 -1
  73. package/dist/bot/logic/mission/missions/squads/combatSquad.js +116 -0
  74. package/dist/bot/logic/mission/missions/squads/combatSquad.js.map +1 -0
  75. package/dist/bot/logic/mission/missions/squads/common.js +58 -0
  76. package/dist/bot/logic/mission/missions/squads/common.js.map +1 -0
  77. package/dist/bot/logic/mission/missions/squads/squad.js +2 -0
  78. package/dist/bot/logic/mission/missions/squads/squad.js.map +1 -0
  79. package/dist/bot/logic/squad/behaviours/attackSquad.js +63 -56
  80. package/dist/bot/logic/squad/behaviours/combatSquad.js +19 -18
  81. package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -1
  82. package/dist/bot/logic/squad/behaviours/common.js +2 -19
  83. package/dist/bot/logic/squad/behaviours/common.js.map +1 -1
  84. package/dist/bot/logic/squad/behaviours/defenceSquad.js +15 -2
  85. package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +1 -1
  86. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +17 -21
  87. package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +1 -1
  88. package/dist/bot/logic/squad/squad.js +8 -5
  89. package/dist/bot/logic/squad/squad.js.map +1 -1
  90. package/dist/bot/logic/squad/squadBehaviour.js.map +1 -1
  91. package/dist/bot/logic/squad/squadController.js +3 -2
  92. package/dist/bot/logic/squad/squadController.js.map +1 -1
  93. package/dist/bot/logic/threat/threatCalculator.js +5 -5
  94. package/dist/bot/logic/threat/threatCalculator.js.map +1 -1
  95. package/dist/exampleBot.js +53 -16
  96. package/dist/exampleBot.js.map +1 -1
  97. package/package.json +5 -4
  98. package/src/bot/bot.ts +38 -53
  99. package/src/bot/logic/awareness.ts +34 -22
  100. package/src/bot/logic/building/antiAirStaticDefence.ts +64 -0
  101. package/src/bot/logic/building/antiGroundStaticDefence.ts +7 -20
  102. package/src/bot/logic/building/artilleryUnit.ts +2 -28
  103. package/src/bot/logic/building/basicAirUnit.ts +2 -33
  104. package/src/bot/logic/building/basicBuilding.ts +8 -6
  105. package/src/bot/logic/building/basicGroundUnit.ts +2 -46
  106. package/src/bot/logic/building/buildingRules.ts +73 -57
  107. package/src/bot/logic/building/common.ts +23 -0
  108. package/src/bot/logic/building/harvester.ts +2 -1
  109. package/src/bot/logic/building/queueController.ts +105 -42
  110. package/src/bot/logic/common/utils.ts +47 -0
  111. package/src/bot/logic/composition/alliedCompositions.ts +22 -0
  112. package/src/bot/logic/composition/common.ts +3 -0
  113. package/src/bot/logic/composition/sovietCompositions.ts +21 -0
  114. package/src/bot/logic/mission/actionBatcher.ts +124 -0
  115. package/src/bot/logic/mission/mission.ts +186 -37
  116. package/src/bot/logic/mission/missionController.ts +340 -31
  117. package/src/bot/logic/mission/missionFactories.ts +3 -3
  118. package/src/bot/logic/mission/missions/attackMission.ts +234 -56
  119. package/src/bot/logic/mission/missions/defenceMission.ts +72 -45
  120. package/src/bot/logic/mission/missions/engineerMission.ts +67 -15
  121. package/src/bot/logic/mission/missions/expansionMission.ts +67 -14
  122. package/src/bot/logic/mission/missions/retreatMission.ts +50 -6
  123. package/src/bot/logic/mission/missions/scoutingMission.ts +138 -14
  124. package/src/bot/logic/mission/missions/squads/combatSquad.ts +160 -0
  125. package/src/bot/logic/{squad/behaviours → mission/missions/squads}/common.ts +14 -20
  126. package/src/bot/logic/mission/missions/squads/squad.ts +19 -0
  127. package/src/bot/logic/threat/threat.ts +15 -15
  128. package/src/bot/logic/threat/threatCalculator.ts +10 -10
  129. package/src/exampleBot.ts +59 -19
  130. package/.prettierrc +0 -5
  131. package/TODO.md +0 -18
  132. package/dist/bot/logic/building/artilleryUnit.js.map +0 -1
  133. package/dist/bot/logic/building/massedAntiGroundUnit.js +0 -20
  134. package/dist/bot/logic/building/queues.js +0 -19
  135. package/dist/bot/logic/knowledge.js +0 -1
  136. package/dist/bot/logic/mission/basicMission.js +0 -26
  137. package/dist/bot/logic/mission/expansionMission.js +0 -32
  138. package/dist/bot/logic/squad/behaviours/squadExpansion.js +0 -31
  139. package/dist/bot/logic/squad/behaviours/squadScouters.js +0 -8
  140. package/rules.ini +0 -23126
  141. package/src/bot/logic/mission/missions/oneTimeMission.ts +0 -33
  142. package/src/bot/logic/squad/behaviours/combatSquad.ts +0 -127
  143. package/src/bot/logic/squad/behaviours/engineerSquad.ts +0 -53
  144. package/src/bot/logic/squad/behaviours/expansionSquad.ts +0 -59
  145. package/src/bot/logic/squad/behaviours/retreatSquad.ts +0 -44
  146. package/src/bot/logic/squad/squad.ts +0 -159
  147. package/src/bot/logic/squad/squadBehaviour.ts +0 -62
  148. package/src/bot/logic/squad/squadBehaviours.ts +0 -8
  149. package/src/bot/logic/squad/squadController.ts +0 -254
@@ -1,61 +1,196 @@
1
- import { GameApi, ObjectType, PlayerData, UnitData, Vector2 } from "@chronodivide/game-api";
2
- import { CombatSquad } from "../../squad/behaviours/combatSquad.js";
3
- import { Mission, MissionAction, disbandMission, noop } from "../mission.js";
4
- import { Squad } from "../../squad/squad.js";
1
+ import { ActionsApi, GameApi, ObjectType, PlayerData, SideType, UnitData, Vector2 } from "@chronodivide/game-api";
2
+ import { CombatSquad } from "./squads/combatSquad.js";
3
+ import { Mission, MissionAction, disbandMission, noop, requestUnits } from "../mission.js";
5
4
  import { MissionFactory } from "../missionFactories.js";
6
5
  import { MatchAwareness } from "../../awareness.js";
7
6
  import { MissionController } from "../missionController.js";
8
7
  import { RetreatMission } from "./retreatMission.js";
9
- import { DebugLogger, maxBy } from "../../common/utils.js";
8
+ import { DebugLogger, countBy, isOwnedByNeutral, maxBy } from "../../common/utils.js";
9
+ import { ActionBatcher } from "../actionBatcher.js";
10
+ import { getSovietComposition } from "../../composition/sovietCompositions.js";
11
+ import { getAlliedCompositions } from "../../composition/alliedCompositions.js";
12
+ import { UnitComposition } from "../../composition/common.js";
13
+ import { manageMoveMicro } from "./squads/common.js";
10
14
 
11
15
  export enum AttackFailReason {
12
16
  NoTargets = 0,
13
17
  DefenceTooStrong = 1,
14
18
  }
15
19
 
16
- const NO_TARGET_IDLE_TIMEOUT_TICKS = 60;
20
+ enum AttackMissionState {
21
+ Preparing = 0,
22
+ Attacking = 1,
23
+ Retreating = 2,
24
+ }
25
+
26
+ const NO_TARGET_RETARGET_TICKS = 450;
27
+ const NO_TARGET_IDLE_TIMEOUT_TICKS = 900;
28
+
29
+ function calculateTargetComposition(
30
+ gameApi: GameApi,
31
+ playerData: PlayerData,
32
+ matchAwareness: MatchAwareness,
33
+ ): UnitComposition {
34
+ if (!playerData.country) {
35
+ throw new Error(`player ${playerData.name} has no country`);
36
+ } else if (playerData.country.side === SideType.Nod) {
37
+ return getSovietComposition(gameApi, playerData, matchAwareness);
38
+ } else {
39
+ return getAlliedCompositions(gameApi, playerData, matchAwareness);
40
+ }
41
+ }
42
+
43
+ const ATTACK_MISSION_PRIORITY_RAMP = 1.01;
44
+ const ATTACK_MISSION_MAX_PRIORITY = 50;
17
45
 
18
46
  /**
19
47
  * A mission that tries to attack a certain area.
20
48
  */
21
49
  export class AttackMission extends Mission<AttackFailReason> {
50
+ private squad: CombatSquad;
51
+
22
52
  private lastTargetSeenAt = 0;
53
+ private hasPickedNewTarget: boolean = false;
54
+
55
+ private state: AttackMissionState = AttackMissionState.Preparing;
23
56
 
24
57
  constructor(
25
58
  uniqueName: string,
26
- priority: number,
27
- private rallyArea: Vector2,
59
+ private priority: number,
60
+ rallyArea: Vector2,
28
61
  private attackArea: Vector2,
29
62
  private radius: number,
63
+ private composition: UnitComposition,
30
64
  logger: DebugLogger,
31
65
  ) {
32
- super(uniqueName, priority, logger);
66
+ super(uniqueName, logger);
67
+ this.squad = new CombatSquad(rallyArea, attackArea, radius);
33
68
  }
34
69
 
35
- onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
36
- if (this.getSquad() === null) {
37
- return this.setSquad(
38
- new Squad(this.getUniqueName(), new CombatSquad(this.rallyArea, this.attackArea, this.radius), this),
70
+ _onAiUpdate(
71
+ gameApi: GameApi,
72
+ actionsApi: ActionsApi,
73
+ playerData: PlayerData,
74
+ matchAwareness: MatchAwareness,
75
+ actionBatcher: ActionBatcher,
76
+ ): MissionAction {
77
+ switch (this.state) {
78
+ case AttackMissionState.Preparing:
79
+ return this.handlePreparingState(gameApi, actionsApi, playerData, matchAwareness, actionBatcher);
80
+ case AttackMissionState.Attacking:
81
+ return this.handleAttackingState(gameApi, actionsApi, playerData, matchAwareness, actionBatcher);
82
+ case AttackMissionState.Retreating:
83
+ return this.handleRetreatingState(gameApi, actionsApi, playerData, matchAwareness, actionBatcher);
84
+ }
85
+ }
86
+
87
+ private handlePreparingState(
88
+ gameApi: GameApi,
89
+ actionsApi: ActionsApi,
90
+ playerData: PlayerData,
91
+ matchAwareness: MatchAwareness,
92
+ actionBatcher: ActionBatcher,
93
+ ) {
94
+ const currentComposition: UnitComposition = countBy(this.getUnits(gameApi), (unit) => unit.name);
95
+
96
+ const missingUnits = Object.entries(this.composition).filter(([unitType, targetAmount]) => {
97
+ return !currentComposition[unitType] || currentComposition[unitType] < targetAmount;
98
+ });
99
+
100
+ if (missingUnits.length > 0) {
101
+ this.priority = Math.min(this.priority * ATTACK_MISSION_PRIORITY_RAMP, ATTACK_MISSION_MAX_PRIORITY);
102
+ return requestUnits(
103
+ missingUnits.map(([unitName]) => unitName),
104
+ this.priority,
39
105
  );
40
106
  } else {
41
- // Dispatch missions.
42
- if (!matchAwareness.shouldAttack()) {
43
- return disbandMission(AttackFailReason.DefenceTooStrong);
44
- }
107
+ this.priority = ATTACK_MISSION_INITIAL_PRIORITY;
108
+ this.state = AttackMissionState.Attacking;
109
+ return noop();
110
+ }
111
+ }
45
112
 
46
- const foundTargets = matchAwareness.getHostilesNearPoint2d(this.attackArea, this.radius);
113
+ private handleAttackingState(
114
+ gameApi: GameApi,
115
+ actionsApi: ActionsApi,
116
+ playerData: PlayerData,
117
+ matchAwareness: MatchAwareness,
118
+ actionBatcher: ActionBatcher,
119
+ ) {
120
+ if (this.getUnitIds().length === 0) {
121
+ // TODO: disband directly (we no longer retreat when losing)
122
+ this.state = AttackMissionState.Retreating;
123
+ return noop();
124
+ }
125
+
126
+ const foundTargets = matchAwareness
127
+ .getHostilesNearPoint2d(this.attackArea, this.radius)
128
+ .map((unit) => gameApi.getUnitData(unit.unitId))
129
+ .filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];
130
+
131
+ const update = this.squad.onAiUpdate(
132
+ gameApi,
133
+ actionsApi,
134
+ actionBatcher,
135
+ playerData,
136
+ this,
137
+ matchAwareness,
138
+ this.logger,
139
+ );
140
+
141
+ if (update.type !== "noop") {
142
+ return update;
143
+ }
47
144
 
48
- if (foundTargets.length > 0) {
49
- this.lastTargetSeenAt = gameApi.getCurrentTick();
50
- } else if (gameApi.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_IDLE_TIMEOUT_TICKS) {
51
- return disbandMission(AttackFailReason.NoTargets);
145
+ if (foundTargets.length > 0) {
146
+ this.lastTargetSeenAt = gameApi.getCurrentTick();
147
+ this.hasPickedNewTarget = false;
148
+ } else if (gameApi.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_IDLE_TIMEOUT_TICKS) {
149
+ return disbandMission(AttackFailReason.NoTargets);
150
+ } else if (
151
+ !this.hasPickedNewTarget &&
152
+ gameApi.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_RETARGET_TICKS
153
+ ) {
154
+ const newTarget = generateTarget(gameApi, playerData, matchAwareness);
155
+ if (newTarget) {
156
+ this.squad.setAttackArea(newTarget);
157
+ this.hasPickedNewTarget = true;
52
158
  }
53
159
  }
160
+
54
161
  return noop();
55
162
  }
56
- }
57
163
 
58
- const ATTACK_COOLDOWN_TICKS = 120;
164
+ private handleRetreatingState(
165
+ gameApi: GameApi,
166
+ actionsApi: ActionsApi,
167
+ playerData: PlayerData,
168
+ matchAwareness: MatchAwareness,
169
+ actionBatcher: ActionBatcher,
170
+ ) {
171
+ this.getUnits(gameApi).forEach((unitId) => {
172
+ actionBatcher.push(manageMoveMicro(unitId, matchAwareness.getMainRallyPoint()));
173
+ });
174
+ return disbandMission();
175
+ }
176
+
177
+ public getGlobalDebugText(): string | undefined {
178
+ return this.squad.getGlobalDebugText() ?? "<none>";
179
+ }
180
+
181
+ public getState() {
182
+ return this.state;
183
+ }
184
+
185
+ // This mission can give up its units while preparing.
186
+ public isUnitsLocked(): boolean {
187
+ return this.state !== AttackMissionState.Preparing;
188
+ }
189
+
190
+ public getPriority() {
191
+ return this.priority;
192
+ }
193
+ }
59
194
 
60
195
  // Calculates the weight for initiating an attack on the position of a unit or building.
61
196
  // This is separate from unit micro; the squad will be ordered to attack in the vicinity of the point.
@@ -69,32 +204,64 @@ const getTargetWeight: (unitData: UnitData, tryFocusHarvester: boolean) => numbe
69
204
  }
70
205
  };
71
206
 
72
- export class AttackMissionFactory implements MissionFactory {
73
- constructor(private lastAttackAt: number = -ATTACK_COOLDOWN_TICKS) {}
207
+ function generateTarget(
208
+ gameApi: GameApi,
209
+ playerData: PlayerData,
210
+ matchAwareness: MatchAwareness,
211
+ includeBaseLocations: boolean = false,
212
+ ): Vector2 | null {
213
+ // Randomly decide between harvester and base.
214
+ try {
215
+ const tryFocusHarvester = gameApi.generateRandomInt(0, 1) === 0;
216
+ const enemyUnits = gameApi
217
+ .getVisibleUnits(playerData.name, "enemy")
218
+ .map((unitId) => gameApi.getUnitData(unitId))
219
+ .filter((u) => !!u && gameApi.getPlayerData(u.owner).isCombatant) as UnitData[];
74
220
 
75
- getName(): string {
76
- return "AttackMissionFactory";
77
- }
221
+ const maxUnit = maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));
222
+ if (maxUnit) {
223
+ return new Vector2(maxUnit.tile.rx, maxUnit.tile.ry);
224
+ }
225
+ if (includeBaseLocations) {
226
+ const mapApi = gameApi.mapApi;
227
+ const enemyPlayers = gameApi
228
+ .getPlayers()
229
+ .map(gameApi.getPlayerData)
230
+ .filter((otherPlayer) => !gameApi.areAlliedPlayers(playerData.name, otherPlayer.name));
78
231
 
79
- generateTarget(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): Vector2 | null {
80
- // Randomly decide between harvester and base.
81
- try {
82
- const tryFocusHarvester = gameApi.generateRandomInt(0, 1) === 0;
83
- const enemyUnits = gameApi
84
- .getVisibleUnits(playerData.name, "hostile")
85
- .map((unitId) => gameApi.getUnitData(unitId))
86
- .filter((u) => !!u && gameApi.getPlayerData(u.owner).isCombatant) as UnitData[];
87
-
88
- const maxUnit = maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));
89
- if (maxUnit) {
90
- return new Vector2(maxUnit.tile.rx, maxUnit.tile.ry);
232
+ const unexploredEnemyLocations = enemyPlayers.filter((otherPlayer) => {
233
+ const tile = mapApi.getTile(otherPlayer.startLocation.x, otherPlayer.startLocation.y);
234
+ if (!tile) {
235
+ return false;
236
+ }
237
+ return !mapApi.isVisibleTile(tile, playerData.name);
238
+ });
239
+ if (unexploredEnemyLocations.length > 0) {
240
+ const idx = gameApi.generateRandomInt(0, unexploredEnemyLocations.length - 1);
241
+ return unexploredEnemyLocations[idx].startLocation;
91
242
  }
92
- } catch (err) {
93
- // There's a crash here when accessing a building that got destroyed. Will catch and ignore or now.
94
- return null;
95
243
  }
244
+ } catch (err) {
245
+ // There's a crash here when accessing a building that got destroyed. Will catch and ignore or now.
96
246
  return null;
97
247
  }
248
+ return null;
249
+ }
250
+
251
+ // Number of ticks between attacking visible targets.
252
+ const VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS = 120;
253
+
254
+ // Number of ticks between attacking "bases" (enemy starting locations).
255
+ const BASE_ATTACK_COOLDOWN_TICKS = 1800;
256
+
257
+ const ATTACK_MISSION_INITIAL_PRIORITY = 1;
258
+
259
+ export class AttackMissionFactory implements MissionFactory {
260
+ constructor(private lastAttackAt: number = -VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS) {}
261
+
262
+ getName(): string {
263
+ return "AttackMissionFactory";
264
+ }
98
265
 
99
266
  maybeCreateMissions(
100
267
  gameApi: GameApi,
@@ -103,40 +270,51 @@ export class AttackMissionFactory implements MissionFactory {
103
270
  missionController: MissionController,
104
271
  logger: DebugLogger,
105
272
  ): void {
106
- if (!matchAwareness.shouldAttack()) {
273
+ if (gameApi.getCurrentTick() < this.lastAttackAt + VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS) {
107
274
  return;
108
275
  }
109
- if (gameApi.getCurrentTick() < this.lastAttackAt + ATTACK_COOLDOWN_TICKS) {
276
+
277
+ // can only have one attack 'preparing' at once.
278
+ if (
279
+ missionController
280
+ .getMissions()
281
+ .some(
282
+ (mission): mission is AttackMission =>
283
+ mission instanceof AttackMission && mission.getState() === AttackMissionState.Preparing,
284
+ )
285
+ ) {
110
286
  return;
111
287
  }
112
288
 
113
- const attackRadius = 15;
289
+ const attackRadius = 10;
114
290
 
115
- const attackArea = this.generateTarget(gameApi, playerData, matchAwareness);
291
+ const includeEnemyBases = gameApi.getCurrentTick() > this.lastAttackAt + BASE_ATTACK_COOLDOWN_TICKS;
292
+
293
+ const attackArea = generateTarget(gameApi, playerData, matchAwareness, includeEnemyBases);
116
294
 
117
295
  if (!attackArea) {
118
- // Nothing to attack.
119
296
  return;
120
297
  }
121
298
 
122
- // TODO: not using a fixed value here. But performance slows to a crawl when this is unique.
123
- const squadName = "globalAttack";
299
+ const squadName = "attack_" + gameApi.getCurrentTick();
300
+
301
+ const composition: UnitComposition = calculateTargetComposition(gameApi, playerData, matchAwareness);
124
302
 
125
303
  const tryAttack = missionController.addMission(
126
304
  new AttackMission(
127
305
  squadName,
128
- 100,
306
+ ATTACK_MISSION_INITIAL_PRIORITY,
129
307
  matchAwareness.getMainRallyPoint(),
130
308
  attackArea,
131
309
  attackRadius,
310
+ composition,
132
311
  logger,
133
- ).then((reason, squad) => {
312
+ ).then((unitIds, reason) => {
134
313
  missionController.addMission(
135
314
  new RetreatMission(
136
315
  "retreat-from-" + squadName + gameApi.getCurrentTick(),
137
- 100,
138
316
  matchAwareness.getMainRallyPoint(),
139
- squad?.getUnitIds() ?? [],
317
+ unitIds,
140
318
  logger,
141
319
  ),
142
320
  );
@@ -151,7 +329,7 @@ export class AttackMissionFactory implements MissionFactory {
151
329
  gameApi: GameApi,
152
330
  playerData: PlayerData,
153
331
  matchAwareness: MatchAwareness,
154
- failedMission: Mission,
332
+ failedMission: Mission<any>,
155
333
  failureReason: any,
156
334
  missionController: MissionController,
157
335
  ): void {}
@@ -1,55 +1,88 @@
1
- import { GameApi, PlayerData, Vector2 } from "@chronodivide/game-api";
1
+ import { ActionsApi, GameApi, PlayerData, UnitData, Vector2 } from "@chronodivide/game-api";
2
2
  import { MatchAwareness } from "../../awareness.js";
3
3
  import { MissionController } from "../missionController.js";
4
- import { Mission, MissionAction, disbandMission, noop } from "../mission.js";
4
+ import { Mission, MissionAction, grabCombatants, noop, releaseUnits, requestUnits } from "../mission.js";
5
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
- import { DebugLogger } from "../../common/utils.js";
6
+ import { CombatSquad } from "./squads/combatSquad.js";
7
+ import { DebugLogger, isOwnedByNeutral } from "../../common/utils.js";
8
+ import { ActionBatcher } from "../actionBatcher.js";
10
9
 
11
- export enum DefenceFailReason {
12
- NoTargets,
13
- }
10
+ export const MAX_PRIORITY = 100;
11
+ export const PRIORITY_INCREASE_PER_TICK_RATIO = 1.025;
14
12
 
15
13
  /**
16
14
  * A mission that tries to defend a certain area.
17
15
  */
18
- export class DefenceMission extends Mission<DefenceFailReason> {
19
- private combatSquad?: CombatSquad;
16
+ export class DefenceMission extends Mission<CombatSquad> {
17
+ private squad: CombatSquad;
20
18
 
21
19
  constructor(
22
20
  uniqueName: string,
23
- priority: number,
21
+ private priority: number,
22
+ rallyArea: Vector2,
24
23
  private defenceArea: Vector2,
25
24
  private radius: number,
26
25
  logger: DebugLogger,
27
26
  ) {
28
- super(uniqueName, priority, logger);
27
+ super(uniqueName, logger);
28
+ this.squad = new CombatSquad(rallyArea, defenceArea, radius);
29
29
  }
30
30
 
31
- onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
32
- if (this.getSquad() === null && !this.combatSquad) {
33
- this.combatSquad = new CombatSquad(matchAwareness.getMainRallyPoint(), this.defenceArea, this.radius);
34
- return this.setSquad(new Squad("defenceSquad-" + this.getUniqueName(), this.combatSquad, this));
35
- } else {
36
- // Dispatch missions.
37
- const foundTargets = matchAwareness.getHostilesNearPoint2d(this.defenceArea, this.radius);
31
+ _onAiUpdate(
32
+ gameApi: GameApi,
33
+ actionsApi: ActionsApi,
34
+ playerData: PlayerData,
35
+ matchAwareness: MatchAwareness,
36
+ actionBatcher: ActionBatcher,
37
+ ): MissionAction {
38
+ // Dispatch missions.
39
+ const foundTargets = matchAwareness
40
+ .getHostilesNearPoint2d(this.defenceArea, this.radius)
41
+ .map((unit) => gameApi.getUnitData(unit.unitId))
42
+ .filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];
43
+
44
+ const update = this.squad.onAiUpdate(
45
+ gameApi,
46
+ actionsApi,
47
+ actionBatcher,
48
+ playerData,
49
+ this,
50
+ matchAwareness,
51
+ this.logger,
52
+ );
53
+
54
+ if (update.type !== "noop") {
55
+ return update;
56
+ }
38
57
 
39
- if (foundTargets.length === 0) {
40
- this.logger(`(Defence Mission ${this.getUniqueName()}): No targets found, disbanding.`);
41
- return disbandMission(DefenceFailReason.NoTargets);
58
+ if (foundTargets.length === 0) {
59
+ this.priority = 0;
60
+ if (this.getUnitIds().length > 0) {
61
+ this.logger(`(Defence Mission ${this.getUniqueName()}): No targets found, releasing units.`);
62
+ return releaseUnits(this.getUnitIds());
42
63
  } else {
43
- const targetUnit = gameApi.getUnitData(foundTargets[0].unitId);
44
- this.logger(
45
- `(Defence Mission ${this.getUniqueName()}): Focused on target ${targetUnit?.name} (${
46
- foundTargets.length
47
- } found in area ${this.radius})`,
48
- );
49
- this.combatSquad?.setAttackArea(new Vector2(foundTargets[0].x, foundTargets[0].y));
64
+ return noop();
50
65
  }
66
+ } else {
67
+ const targetUnit = foundTargets[0];
68
+ this.logger(
69
+ `(Defence Mission ${this.getUniqueName()}): Focused on target ${targetUnit?.name} (${
70
+ foundTargets.length
71
+ } found in area ${this.radius})`,
72
+ );
73
+ this.squad.setAttackArea(new Vector2(foundTargets[0].tile.rx, foundTargets[0].tile.ry));
74
+ this.priority = MAX_PRIORITY; // Math.min(MAX_PRIORITY, this.priority * PRIORITY_INCREASE_PER_TICK_RATIO);
75
+ return grabCombatants(playerData.startLocation, this.priority);
51
76
  }
52
- return noop();
77
+ //return requestUnits(["E1", "E2", "FV", "HTK", "MTNK", "HTNK"], this.priority);
78
+ }
79
+
80
+ public getGlobalDebugText(): string | undefined {
81
+ return this.squad.getGlobalDebugText() ?? "<none>";
82
+ }
83
+
84
+ public getPriority() {
85
+ return this.priority;
53
86
  }
54
87
  }
55
88
 
@@ -83,7 +116,10 @@ export class DefenceMissionFactory implements MissionFactory {
83
116
 
84
117
  const defendableRadius =
85
118
  DEFENCE_STARTING_RADIUS + DEFENCE_RADIUS_INCREASE_PER_GAME_TICK * gameApi.getCurrentTick();
86
- const enemiesNearSpawn = matchAwareness.getHostilesNearPoint2d(playerData.startLocation, defendableRadius);
119
+ const enemiesNearSpawn = matchAwareness
120
+ .getHostilesNearPoint2d(playerData.startLocation, defendableRadius)
121
+ .map((unit) => gameApi.getUnitData(unit.unitId))
122
+ .filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];
87
123
 
88
124
  if (enemiesNearSpawn.length > 0) {
89
125
  logger(
@@ -94,21 +130,12 @@ export class DefenceMissionFactory implements MissionFactory {
94
130
  missionController.addMission(
95
131
  new DefenceMission(
96
132
  "globalDefence",
97
- 1000,
133
+ 10,
134
+ matchAwareness.getMainRallyPoint(),
98
135
  playerData.startLocation,
99
136
  defendableRadius * 1.2,
100
137
  logger,
101
- ).then((reason, squad) => {
102
- missionController.addMission(
103
- new RetreatMission(
104
- "retreat-from-globalDefence" + gameApi.getCurrentTick(),
105
- 100,
106
- matchAwareness.getMainRallyPoint(),
107
- squad?.getUnitIds() ?? [],
108
- logger,
109
- ),
110
- );
111
- }),
138
+ ),
112
139
  );
113
140
  }
114
141
  }
@@ -117,7 +144,7 @@ export class DefenceMissionFactory implements MissionFactory {
117
144
  gameApi: GameApi,
118
145
  playerData: PlayerData,
119
146
  matchAwareness: MatchAwareness,
120
- failedMission: Mission,
147
+ failedMission: Mission<any>,
121
148
  failureReason: undefined,
122
149
  missionController: MissionController,
123
150
  ): void {}
@@ -1,21 +1,70 @@
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";
1
+ import { ActionsApi, GameApi, OrderType, PlayerData } from "@chronodivide/game-api";
2
+ import { Mission, MissionAction, disbandMission, noop, requestUnits } from "../mission.js";
5
3
  import { MissionFactory } from "../missionFactories.js";
6
- import { OneTimeMission } from "./oneTimeMission.js";
7
4
  import { MatchAwareness } from "../../awareness.js";
8
5
  import { MissionController } from "../missionController.js";
9
6
  import { DebugLogger } from "../../common/utils.js";
10
- import { EngineerSquad } from "../../squad/behaviours/engineerSquad.js";
7
+ import { ActionBatcher } from "../actionBatcher.js";
8
+
9
+ const CAPTURE_COOLDOWN_TICKS = 30;
11
10
 
12
11
  /**
13
12
  * A mission that tries to send an engineer into a building (e.g. to capture tech building or repair bridge)
14
13
  */
15
- export class EngineerMission extends OneTimeMission {
16
- constructor(uniqueName: string, priority: number, selectedTechBuilding: number,
17
- logger: DebugLogger) {
18
- super(uniqueName, priority, () => new EngineerSquad(selectedTechBuilding), logger);
14
+ export class EngineerMission extends Mission {
15
+ private hasAttemptedCaptureWith: {
16
+ unitId: number;
17
+ gameTick: number;
18
+ } | null = null;
19
+
20
+ constructor(
21
+ uniqueName: string,
22
+ private priority: number,
23
+ private captureTargetId: number,
24
+ logger: DebugLogger,
25
+ ) {
26
+ super(uniqueName, logger);
27
+ }
28
+
29
+ public _onAiUpdate(
30
+ gameApi: GameApi,
31
+ actionsApi: ActionsApi,
32
+ playerData: PlayerData,
33
+ matchAwareness: MatchAwareness,
34
+ actionBatcher: ActionBatcher,
35
+ ): MissionAction {
36
+ const engineerTypes = ["ENGINEER", "SENGINEER"];
37
+ const engineers = this.getUnitsOfTypes(gameApi, ...engineerTypes);
38
+ if (engineers.length === 0) {
39
+ // Perhaps we deployed already (or the unit was destroyed), end the mission.
40
+ if (this.hasAttemptedCaptureWith !== null) {
41
+ return disbandMission();
42
+ }
43
+ return requestUnits(engineerTypes, this.priority);
44
+ } else if (
45
+ !this.hasAttemptedCaptureWith ||
46
+ gameApi.getCurrentTick() > this.hasAttemptedCaptureWith.gameTick + CAPTURE_COOLDOWN_TICKS
47
+ ) {
48
+ actionsApi.orderUnits(
49
+ engineers.map((engineer) => engineer.id),
50
+ OrderType.Capture,
51
+ this.captureTargetId,
52
+ );
53
+ // Add a cooldown to deploy attempts.
54
+ this.hasAttemptedCaptureWith = {
55
+ unitId: engineers[0].id,
56
+ gameTick: gameApi.getCurrentTick(),
57
+ };
58
+ }
59
+ return noop();
60
+ }
61
+
62
+ public getGlobalDebugText(): string | undefined {
63
+ return undefined;
64
+ }
65
+
66
+ public getPriority() {
67
+ return this.priority;
19
68
  }
20
69
  }
21
70
 
@@ -36,13 +85,17 @@ export class EngineerMissionFactory implements MissionFactory {
36
85
  playerData: PlayerData,
37
86
  matchAwareness: MatchAwareness,
38
87
  missionController: MissionController,
39
- logger: DebugLogger
88
+ logger: DebugLogger,
40
89
  ): void {
41
90
  if (!(gameApi.getCurrentTick() > this.lastCheckAt + TECH_CHECK_INTERVAL_TICKS)) {
42
91
  return;
43
92
  }
44
93
  this.lastCheckAt = gameApi.getCurrentTick();
45
- const eligibleTechBuildings = gameApi.getVisibleUnits(playerData.name, "hostile", (r) => r.capturable && r.produceCashAmount > 0);
94
+ const eligibleTechBuildings = gameApi.getVisibleUnits(
95
+ playerData.name,
96
+ "hostile",
97
+ (r) => r.capturable && r.produceCashAmount > 0,
98
+ );
46
99
 
47
100
  eligibleTechBuildings.forEach((techBuildingId) => {
48
101
  missionController.addMission(new EngineerMission("capture-" + techBuildingId, 100, techBuildingId, logger));
@@ -53,9 +106,8 @@ export class EngineerMissionFactory implements MissionFactory {
53
106
  gameApi: GameApi,
54
107
  playerData: PlayerData,
55
108
  matchAwareness: MatchAwareness,
56
- failedMission: Mission,
109
+ failedMission: Mission<any>,
57
110
  failureReason: undefined,
58
111
  missionController: MissionController,
59
- ): void {
60
- }
112
+ ): void {}
61
113
  }