@supalosa/chronodivide-bot 0.4.0 → 0.5.2

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 (123) hide show
  1. package/.env.template +5 -0
  2. package/README.md +54 -47
  3. package/dist/bot/bot.js +14 -35
  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/building/ArtilleryUnit.js +2 -29
  8. package/dist/bot/logic/building/antiAirStaticDefence.js +43 -0
  9. package/dist/bot/logic/building/antiAirStaticDefence.js.map +1 -0
  10. package/dist/bot/logic/building/antiGroundStaticDefence.js +10 -20
  11. package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -1
  12. package/dist/bot/logic/building/artilleryUnit.js.map +1 -1
  13. package/dist/bot/logic/building/basicAirUnit.js +2 -23
  14. package/dist/bot/logic/building/basicAirUnit.js.map +1 -1
  15. package/dist/bot/logic/building/basicBuilding.js +3 -2
  16. package/dist/bot/logic/building/basicBuilding.js.map +1 -1
  17. package/dist/bot/logic/building/basicGroundUnit.js +2 -43
  18. package/dist/bot/logic/building/basicGroundUnit.js.map +1 -1
  19. package/dist/bot/logic/building/buildingRules.js +15 -9
  20. package/dist/bot/logic/building/buildingRules.js.map +1 -1
  21. package/dist/bot/logic/building/common.js +19 -0
  22. package/dist/bot/logic/building/common.js.map +1 -0
  23. package/dist/bot/logic/building/harvester.js +2 -1
  24. package/dist/bot/logic/building/harvester.js.map +1 -1
  25. package/dist/bot/logic/building/queueController.js +69 -42
  26. package/dist/bot/logic/building/queueController.js.map +1 -1
  27. package/dist/bot/logic/common/utils.js +21 -0
  28. package/dist/bot/logic/common/utils.js.map +1 -1
  29. package/dist/bot/logic/composition/alliedCompositions.js +13 -0
  30. package/dist/bot/logic/composition/alliedCompositions.js.map +1 -0
  31. package/dist/bot/logic/composition/common.js +2 -0
  32. package/dist/bot/logic/composition/common.js.map +1 -0
  33. package/dist/bot/logic/composition/sovietCompositions.js +13 -0
  34. package/dist/bot/logic/composition/sovietCompositions.js.map +1 -0
  35. package/dist/bot/logic/mission/actionBatcher.js +92 -0
  36. package/dist/bot/logic/mission/actionBatcher.js.map +1 -0
  37. package/dist/bot/logic/mission/behaviours/combatSquad.js +124 -0
  38. package/dist/bot/logic/mission/behaviours/combatSquad.js.map +1 -0
  39. package/dist/bot/logic/mission/behaviours/common.js +56 -0
  40. package/dist/bot/logic/mission/behaviours/common.js.map +1 -0
  41. package/dist/bot/logic/mission/behaviours/engineerSquad.js +39 -0
  42. package/dist/bot/logic/mission/behaviours/engineerSquad.js.map +1 -0
  43. package/dist/bot/logic/mission/behaviours/expansionSquad.js +46 -0
  44. package/dist/bot/logic/mission/behaviours/expansionSquad.js.map +1 -0
  45. package/dist/bot/logic/mission/behaviours/retreatSquad.js +31 -0
  46. package/dist/bot/logic/mission/behaviours/retreatSquad.js.map +1 -0
  47. package/{src/bot/logic/squad/behaviours/scoutingSquad.ts → dist/bot/logic/mission/behaviours/scoutingSquad.js} +27 -51
  48. package/dist/bot/logic/mission/behaviours/scoutingSquad.js.map +1 -0
  49. package/dist/bot/logic/mission/mission.js +91 -19
  50. package/dist/bot/logic/mission/mission.js.map +1 -1
  51. package/dist/bot/logic/mission/missionController.js +262 -21
  52. package/dist/bot/logic/mission/missionController.js.map +1 -1
  53. package/dist/bot/logic/mission/missions/attackMission.js +113 -39
  54. package/dist/bot/logic/mission/missions/attackMission.js.map +1 -1
  55. package/dist/bot/logic/mission/missions/basicMission.js +13 -0
  56. package/dist/bot/logic/mission/missions/basicMission.js.map +1 -0
  57. package/dist/bot/logic/mission/missions/defenceMission.js +43 -28
  58. package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -1
  59. package/dist/bot/logic/mission/missions/engineerMission.js +37 -7
  60. package/dist/bot/logic/mission/missions/engineerMission.js.map +1 -1
  61. package/dist/bot/logic/mission/missions/expansionMission.js +42 -6
  62. package/dist/bot/logic/mission/missions/expansionMission.js.map +1 -1
  63. package/dist/bot/logic/mission/missions/missionBehaviour.js +2 -0
  64. package/dist/bot/logic/mission/missions/missionBehaviour.js.map +1 -0
  65. package/dist/bot/logic/mission/missions/retreatMission.js +31 -5
  66. package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -1
  67. package/dist/bot/logic/mission/missions/scoutingMission.js +103 -6
  68. package/dist/bot/logic/mission/missions/scoutingMission.js.map +1 -1
  69. package/dist/bot/logic/mission/missions/squads/combatSquad.js +116 -0
  70. package/dist/bot/logic/mission/missions/squads/combatSquad.js.map +1 -0
  71. package/dist/bot/logic/mission/missions/squads/common.js +58 -0
  72. package/dist/bot/logic/mission/missions/squads/common.js.map +1 -0
  73. package/dist/bot/logic/mission/missions/squads/squad.js +2 -0
  74. package/dist/bot/logic/mission/missions/squads/squad.js.map +1 -0
  75. package/dist/bot/logic/squad/squadController.js +6 -2
  76. package/dist/bot/logic/squad/squadController.js.map +1 -1
  77. package/dist/bot/logic/threat/threatCalculator.js +9 -9
  78. package/dist/bot/logic/threat/threatCalculator.js.map +1 -1
  79. package/dist/exampleBot.js +50 -18
  80. package/dist/exampleBot.js.map +1 -1
  81. package/package.json +5 -4
  82. package/src/bot/bot.ts +19 -51
  83. package/src/bot/logic/awareness.ts +34 -22
  84. package/src/bot/logic/building/antiAirStaticDefence.ts +64 -0
  85. package/src/bot/logic/building/antiGroundStaticDefence.ts +7 -20
  86. package/src/bot/logic/building/artilleryUnit.ts +2 -28
  87. package/src/bot/logic/building/basicAirUnit.ts +2 -33
  88. package/src/bot/logic/building/basicBuilding.ts +8 -6
  89. package/src/bot/logic/building/basicGroundUnit.ts +2 -46
  90. package/src/bot/logic/building/buildingRules.ts +15 -9
  91. package/src/bot/logic/building/common.ts +23 -0
  92. package/src/bot/logic/building/harvester.ts +2 -1
  93. package/src/bot/logic/building/queueController.ts +98 -43
  94. package/src/bot/logic/common/utils.ts +28 -0
  95. package/src/bot/logic/composition/alliedCompositions.ts +22 -0
  96. package/src/bot/logic/composition/common.ts +3 -0
  97. package/src/bot/logic/composition/sovietCompositions.ts +21 -0
  98. package/src/bot/logic/{squad/behaviours → mission}/actionBatcher.ts +66 -7
  99. package/src/bot/logic/mission/mission.ts +186 -37
  100. package/src/bot/logic/mission/missionController.ts +340 -31
  101. package/src/bot/logic/mission/missionFactories.ts +3 -3
  102. package/src/bot/logic/mission/missions/attackMission.ts +181 -44
  103. package/src/bot/logic/mission/missions/defenceMission.ts +72 -45
  104. package/src/bot/logic/mission/missions/engineerMission.ts +67 -15
  105. package/src/bot/logic/mission/missions/expansionMission.ts +67 -14
  106. package/src/bot/logic/mission/missions/retreatMission.ts +50 -6
  107. package/src/bot/logic/mission/missions/scoutingMission.ts +138 -14
  108. package/src/bot/logic/{squad/behaviours → mission/missions/squads}/combatSquad.ts +56 -33
  109. package/src/bot/logic/{squad/behaviours → mission/missions/squads}/common.ts +11 -17
  110. package/src/bot/logic/mission/missions/squads/squad.ts +19 -0
  111. package/src/bot/logic/threat/threatCalculator.ts +10 -10
  112. package/src/exampleBot.ts +56 -24
  113. package/.prettierrc +0 -5
  114. package/TODO.md +0 -15
  115. package/rules.ini +0 -23126
  116. package/src/bot/logic/mission/missions/oneTimeMission.ts +0 -33
  117. package/src/bot/logic/squad/behaviours/engineerSquad.ts +0 -58
  118. package/src/bot/logic/squad/behaviours/expansionSquad.ts +0 -64
  119. package/src/bot/logic/squad/behaviours/retreatSquad.ts +0 -50
  120. package/src/bot/logic/squad/squad.ts +0 -165
  121. package/src/bot/logic/squad/squadBehaviour.ts +0 -66
  122. package/src/bot/logic/squad/squadBehaviours.ts +0 -8
  123. package/src/bot/logic/squad/squadController.ts +0 -271
@@ -1,70 +1,195 @@
1
- import { GameApi, GameMath, MapApi, 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
 
20
+ enum AttackMissionState {
21
+ Preparing = 0,
22
+ Attacking = 1,
23
+ Retreating = 2,
24
+ }
25
+
16
26
  const NO_TARGET_RETARGET_TICKS = 450;
17
27
  const NO_TARGET_IDLE_TIMEOUT_TICKS = 900;
18
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;
45
+
19
46
  /**
20
47
  * A mission that tries to attack a certain area.
21
48
  */
22
49
  export class AttackMission extends Mission<AttackFailReason> {
50
+ private squad: CombatSquad;
51
+
23
52
  private lastTargetSeenAt = 0;
24
- private behaviour: CombatSquad | undefined;
25
53
  private hasPickedNewTarget: boolean = false;
26
54
 
55
+ private state: AttackMissionState = AttackMissionState.Preparing;
56
+
27
57
  constructor(
28
58
  uniqueName: string,
29
- priority: number,
30
- private rallyArea: Vector2,
59
+ private priority: number,
60
+ rallyArea: Vector2,
31
61
  private attackArea: Vector2,
32
62
  private radius: number,
63
+ private composition: UnitComposition,
33
64
  logger: DebugLogger,
34
65
  ) {
35
- super(uniqueName, priority, logger);
66
+ super(uniqueName, logger);
67
+ this.squad = new CombatSquad(rallyArea, attackArea, radius);
68
+ }
69
+
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
+ }
36
85
  }
37
86
 
38
- onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
39
- if (this.getSquad() === null) {
40
- this.behaviour = new CombatSquad(this.rallyArea, this.attackArea, this.radius);
41
- return this.setSquad(new Squad(this.getUniqueName(), this.behaviour, this));
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,
105
+ );
42
106
  } else {
43
- // Dispatch missions.
44
- if (!matchAwareness.shouldAttack()) {
45
- return disbandMission(AttackFailReason.DefenceTooStrong);
46
- }
107
+ this.priority = ATTACK_MISSION_INITIAL_PRIORITY;
108
+ this.state = AttackMissionState.Attacking;
109
+ return noop();
110
+ }
111
+ }
47
112
 
48
- const foundTargets = matchAwareness.getHostilesNearPoint2d(this.attackArea, this.radius);
49
-
50
- if (foundTargets.length > 0) {
51
- this.lastTargetSeenAt = gameApi.getCurrentTick();
52
- this.hasPickedNewTarget = false;
53
- } else if (gameApi.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_IDLE_TIMEOUT_TICKS) {
54
- return disbandMission(AttackFailReason.NoTargets);
55
- } else if (
56
- !this.hasPickedNewTarget &&
57
- gameApi.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_RETARGET_TICKS
58
- ) {
59
- const newTarget = generateTarget(gameApi, playerData, matchAwareness);
60
- if (newTarget) {
61
- this.behaviour?.setAttackArea(newTarget);
62
- this.hasPickedNewTarget = true;
63
- }
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
+ }
144
+
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;
64
158
  }
65
159
  }
160
+
66
161
  return noop();
67
162
  }
163
+
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
+ }
68
193
  }
69
194
 
70
195
  // Calculates the weight for initiating an attack on the position of a unit or building.
@@ -89,7 +214,7 @@ function generateTarget(
89
214
  try {
90
215
  const tryFocusHarvester = gameApi.generateRandomInt(0, 1) === 0;
91
216
  const enemyUnits = gameApi
92
- .getVisibleUnits(playerData.name, "hostile")
217
+ .getVisibleUnits(playerData.name, "enemy")
93
218
  .map((unitId) => gameApi.getUnitData(unitId))
94
219
  .filter((u) => !!u && gameApi.getPlayerData(u.owner).isCombatant) as UnitData[];
95
220
 
@@ -129,6 +254,8 @@ const VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS = 120;
129
254
  // Number of ticks between attacking "bases" (enemy starting locations).
130
255
  const BASE_ATTACK_COOLDOWN_TICKS = 1800;
131
256
 
257
+ const ATTACK_MISSION_INITIAL_PRIORITY = 1;
258
+
132
259
  export class AttackMissionFactory implements MissionFactory {
133
260
  constructor(private lastAttackAt: number = -VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS) {}
134
261
 
@@ -143,14 +270,23 @@ export class AttackMissionFactory implements MissionFactory {
143
270
  missionController: MissionController,
144
271
  logger: DebugLogger,
145
272
  ): void {
146
- if (!matchAwareness.shouldAttack()) {
273
+ if (gameApi.getCurrentTick() < this.lastAttackAt + VISIBLE_TARGET_ATTACK_COOLDOWN_TICKS) {
147
274
  return;
148
275
  }
149
- if (gameApi.getCurrentTick() < this.lastAttackAt + VISIBLE_TARGET_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
+ ) {
150
286
  return;
151
287
  }
152
288
 
153
- const attackRadius = 15;
289
+ const attackRadius = 10;
154
290
 
155
291
  const includeEnemyBases = gameApi.getCurrentTick() > this.lastAttackAt + BASE_ATTACK_COOLDOWN_TICKS;
156
292
 
@@ -160,24 +296,25 @@ export class AttackMissionFactory implements MissionFactory {
160
296
  return;
161
297
  }
162
298
 
163
- // TODO: not using a fixed value here. But performance slows to a crawl when this is unique.
164
- const squadName = "globalAttack";
299
+ const squadName = "attack_" + gameApi.getCurrentTick();
300
+
301
+ const composition: UnitComposition = calculateTargetComposition(gameApi, playerData, matchAwareness);
165
302
 
166
303
  const tryAttack = missionController.addMission(
167
304
  new AttackMission(
168
305
  squadName,
169
- 100,
306
+ ATTACK_MISSION_INITIAL_PRIORITY,
170
307
  matchAwareness.getMainRallyPoint(),
171
308
  attackArea,
172
309
  attackRadius,
310
+ composition,
173
311
  logger,
174
- ).then((reason, squad) => {
312
+ ).then((unitIds, reason) => {
175
313
  missionController.addMission(
176
314
  new RetreatMission(
177
315
  "retreat-from-" + squadName + gameApi.getCurrentTick(),
178
- 100,
179
316
  matchAwareness.getMainRallyPoint(),
180
- squad?.getUnitIds() ?? [],
317
+ unitIds,
181
318
  logger,
182
319
  ),
183
320
  );
@@ -192,7 +329,7 @@ export class AttackMissionFactory implements MissionFactory {
192
329
  gameApi: GameApi,
193
330
  playerData: PlayerData,
194
331
  matchAwareness: MatchAwareness,
195
- failedMission: Mission,
332
+ failedMission: Mission<any>,
196
333
  failureReason: any,
197
334
  missionController: MissionController,
198
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
  }