@supalosa/chronodivide-bot 0.5.3 → 0.6.4

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 (134) hide show
  1. package/.env.template +4 -4
  2. package/.github/workflows/npm-publish.yml +24 -0
  3. package/README.md +108 -97
  4. package/dist/bot/bot.js +105 -105
  5. package/dist/bot/bot.js.map +1 -1
  6. package/dist/bot/logic/awareness.js +136 -136
  7. package/dist/bot/logic/building/antiAirStaticDefence.js +42 -42
  8. package/dist/bot/logic/building/antiGroundStaticDefence.js +34 -30
  9. package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -1
  10. package/dist/bot/logic/building/{ArtilleryUnit.js → artilleryUnit.js} +18 -18
  11. package/dist/bot/logic/building/basicAirUnit.js +19 -19
  12. package/dist/bot/logic/building/basicBuilding.js +26 -26
  13. package/dist/bot/logic/building/basicGroundUnit.js +19 -19
  14. package/dist/bot/logic/building/buildingRules.js +175 -174
  15. package/dist/bot/logic/building/buildingRules.js.map +1 -1
  16. package/dist/bot/logic/building/common.js +19 -18
  17. package/dist/bot/logic/building/common.js.map +1 -1
  18. package/dist/bot/logic/building/harvester.js +16 -16
  19. package/dist/bot/logic/building/powerPlant.js +20 -20
  20. package/dist/bot/logic/building/queueController.js +183 -183
  21. package/dist/bot/logic/building/resourceCollectionBuilding.js +36 -36
  22. package/dist/bot/logic/common/scout.js +126 -126
  23. package/dist/bot/logic/common/utils.js +95 -85
  24. package/dist/bot/logic/common/utils.js.map +1 -1
  25. package/dist/bot/logic/composition/alliedCompositions.js +12 -12
  26. package/dist/bot/logic/composition/common.js +1 -1
  27. package/dist/bot/logic/composition/sovietCompositions.js +12 -12
  28. package/dist/bot/logic/map/map.js +44 -44
  29. package/dist/bot/logic/map/sector.js +137 -137
  30. package/dist/bot/logic/mission/actionBatcher.js +91 -91
  31. package/dist/bot/logic/mission/mission.js +122 -122
  32. package/dist/bot/logic/mission/missionController.js +321 -321
  33. package/dist/bot/logic/mission/missionFactories.js +12 -12
  34. package/dist/bot/logic/mission/missions/attackMission.js +214 -214
  35. package/dist/bot/logic/mission/missions/defenceMission.js +82 -82
  36. package/dist/bot/logic/mission/missions/engineerMission.js +63 -63
  37. package/dist/bot/logic/mission/missions/expansionMission.js +60 -60
  38. package/dist/bot/logic/mission/missions/retreatMission.js +33 -33
  39. package/dist/bot/logic/mission/missions/scoutingMission.js +133 -133
  40. package/dist/bot/logic/mission/missions/squads/combatSquad.js +115 -115
  41. package/dist/bot/logic/mission/missions/squads/common.js +57 -57
  42. package/dist/bot/logic/mission/missions/squads/squad.js +1 -1
  43. package/dist/bot/logic/threat/threat.js +22 -22
  44. package/dist/bot/logic/threat/threatCalculator.js +73 -73
  45. package/dist/exampleBot.js +100 -112
  46. package/dist/exampleBot.js.map +1 -1
  47. package/package.json +32 -29
  48. package/src/bot/bot.ts +161 -161
  49. package/src/bot/logic/awareness.ts +245 -245
  50. package/src/bot/logic/building/antiAirStaticDefence.ts +64 -64
  51. package/src/bot/logic/building/antiGroundStaticDefence.ts +55 -51
  52. package/src/bot/logic/building/artilleryUnit.ts +39 -39
  53. package/src/bot/logic/building/basicAirUnit.ts +39 -39
  54. package/src/bot/logic/building/basicBuilding.ts +49 -49
  55. package/src/bot/logic/building/basicGroundUnit.ts +39 -39
  56. package/src/bot/logic/building/buildingRules.ts +250 -247
  57. package/src/bot/logic/building/common.ts +21 -23
  58. package/src/bot/logic/building/harvester.ts +31 -31
  59. package/src/bot/logic/building/powerPlant.ts +32 -32
  60. package/src/bot/logic/building/queueController.ts +297 -297
  61. package/src/bot/logic/building/resourceCollectionBuilding.ts +52 -52
  62. package/src/bot/logic/common/scout.ts +183 -183
  63. package/src/bot/logic/common/utils.ts +120 -112
  64. package/src/bot/logic/composition/alliedCompositions.ts +22 -22
  65. package/src/bot/logic/composition/common.ts +3 -3
  66. package/src/bot/logic/composition/sovietCompositions.ts +21 -21
  67. package/src/bot/logic/map/map.ts +66 -66
  68. package/src/bot/logic/map/sector.ts +174 -174
  69. package/src/bot/logic/mission/actionBatcher.ts +124 -124
  70. package/src/bot/logic/mission/mission.ts +232 -232
  71. package/src/bot/logic/mission/missionController.ts +413 -413
  72. package/src/bot/logic/mission/missionFactories.ts +51 -51
  73. package/src/bot/logic/mission/missions/attackMission.ts +336 -336
  74. package/src/bot/logic/mission/missions/defenceMission.ts +151 -151
  75. package/src/bot/logic/mission/missions/engineerMission.ts +113 -113
  76. package/src/bot/logic/mission/missions/expansionMission.ts +104 -104
  77. package/src/bot/logic/mission/missions/retreatMission.ts +54 -54
  78. package/src/bot/logic/mission/missions/scoutingMission.ts +186 -186
  79. package/src/bot/logic/mission/missions/squads/combatSquad.ts +160 -160
  80. package/src/bot/logic/mission/missions/squads/common.ts +63 -63
  81. package/src/bot/logic/mission/missions/squads/squad.ts +19 -19
  82. package/src/bot/logic/threat/threatCalculator.ts +100 -100
  83. package/src/exampleBot.ts +111 -124
  84. package/tsconfig.json +73 -73
  85. package/dist/bot/logic/building/building.js +0 -82
  86. package/dist/bot/logic/building/massedAntiGroundUnit.js +0 -20
  87. package/dist/bot/logic/building/queues.js +0 -19
  88. package/dist/bot/logic/knowledge.js +0 -1
  89. package/dist/bot/logic/mission/basicMission.js +0 -26
  90. package/dist/bot/logic/mission/behaviours/combatSquad.js +0 -124
  91. package/dist/bot/logic/mission/behaviours/combatSquad.js.map +0 -1
  92. package/dist/bot/logic/mission/behaviours/common.js +0 -56
  93. package/dist/bot/logic/mission/behaviours/common.js.map +0 -1
  94. package/dist/bot/logic/mission/behaviours/engineerSquad.js +0 -39
  95. package/dist/bot/logic/mission/behaviours/engineerSquad.js.map +0 -1
  96. package/dist/bot/logic/mission/behaviours/expansionSquad.js +0 -46
  97. package/dist/bot/logic/mission/behaviours/expansionSquad.js.map +0 -1
  98. package/dist/bot/logic/mission/behaviours/retreatSquad.js +0 -31
  99. package/dist/bot/logic/mission/behaviours/retreatSquad.js.map +0 -1
  100. package/dist/bot/logic/mission/behaviours/scoutingSquad.js +0 -94
  101. package/dist/bot/logic/mission/behaviours/scoutingSquad.js.map +0 -1
  102. package/dist/bot/logic/mission/expansionMission.js +0 -32
  103. package/dist/bot/logic/mission/missions/basicMission.js +0 -13
  104. package/dist/bot/logic/mission/missions/basicMission.js.map +0 -1
  105. package/dist/bot/logic/mission/missions/missionBehaviour.js +0 -2
  106. package/dist/bot/logic/mission/missions/missionBehaviour.js.map +0 -1
  107. package/dist/bot/logic/mission/missions/oneTimeMission.js +0 -27
  108. package/dist/bot/logic/mission/missions/oneTimeMission.js.map +0 -1
  109. package/dist/bot/logic/squad/behaviours/actionBatcher.js +0 -36
  110. package/dist/bot/logic/squad/behaviours/actionBatcher.js.map +0 -1
  111. package/dist/bot/logic/squad/behaviours/attackSquad.js +0 -82
  112. package/dist/bot/logic/squad/behaviours/combatSquad.js +0 -106
  113. package/dist/bot/logic/squad/behaviours/combatSquad.js.map +0 -1
  114. package/dist/bot/logic/squad/behaviours/common.js +0 -55
  115. package/dist/bot/logic/squad/behaviours/common.js.map +0 -1
  116. package/dist/bot/logic/squad/behaviours/defenceSquad.js +0 -48
  117. package/dist/bot/logic/squad/behaviours/engineerSquad.js +0 -38
  118. package/dist/bot/logic/squad/behaviours/engineerSquad.js.map +0 -1
  119. package/dist/bot/logic/squad/behaviours/expansionSquad.js +0 -45
  120. package/dist/bot/logic/squad/behaviours/expansionSquad.js.map +0 -1
  121. package/dist/bot/logic/squad/behaviours/retreatSquad.js +0 -31
  122. package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +0 -1
  123. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +0 -93
  124. package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +0 -1
  125. package/dist/bot/logic/squad/behaviours/squadExpansion.js +0 -31
  126. package/dist/bot/logic/squad/behaviours/squadScouters.js +0 -8
  127. package/dist/bot/logic/squad/squad.js +0 -126
  128. package/dist/bot/logic/squad/squad.js.map +0 -1
  129. package/dist/bot/logic/squad/squadBehaviour.js +0 -6
  130. package/dist/bot/logic/squad/squadBehaviour.js.map +0 -1
  131. package/dist/bot/logic/squad/squadBehaviours.js +0 -7
  132. package/dist/bot/logic/squad/squadBehaviours.js.map +0 -1
  133. package/dist/bot/logic/squad/squadController.js +0 -215
  134. package/dist/bot/logic/squad/squadController.js.map +0 -1
@@ -1,322 +1,322 @@
1
- // Meta-controller for forming and controlling missions.
2
- // Missions are groups of zero or more units that aim to accomplish a particular goal.
3
- import { Vector2 } from "@chronodivide/game-api";
4
- import { isDisbandMission, isGrabCombatants, isReleaseUnits, isRequestSpecificUnits, isRequestUnits, } from "./mission.js";
5
- import { createMissionFactories } from "./missionFactories.js";
6
- import { ActionBatcher } from "./actionBatcher.js";
7
- import { countBy, isSelectableCombatant } from "../common/utils.js";
8
- // `missingUnitTypes` priority decays by this much every update loop.
9
- const MISSING_UNIT_TYPE_REQUEST_DECAY_MULT_RATE = 0.75;
10
- const MISSING_UNIT_TYPE_REQUEST_DECAY_FLAT_RATE = 1;
11
- export class MissionController {
12
- constructor(logger) {
13
- this.logger = logger;
14
- this.missions = [];
15
- // A mapping of unit IDs to the missions they are assigned to. This may contain units that are dead, but
16
- // is periodically cleaned in the update loop.
17
- this.unitIdToMission = new Map();
18
- // A mapping of unit types to the highest priority requested for a mission.
19
- // This decays over time if requests are not 'refreshed' by mission.
20
- this.requestedUnitTypes = new Map();
21
- // Tracks missions to be externally disbanded the next time the mission update loop occurs.
22
- this.forceDisbandedMissions = [];
23
- this.missionFactories = createMissionFactories();
24
- }
25
- updateUnitIds(gameApi) {
26
- // Check for units in multiple missions, this shouldn't happen.
27
- this.unitIdToMission = new Map();
28
- this.missions.forEach((mission) => {
29
- const toRemove = [];
30
- mission.getUnitIds().forEach((unitId) => {
31
- if (this.unitIdToMission.has(unitId)) {
32
- this.logger(`WARNING: unit ${unitId} is in multiple missions, please debug.`);
33
- }
34
- else if (!gameApi.getGameObjectData(unitId)) {
35
- // say, if a unit was killed
36
- toRemove.push(unitId);
37
- }
38
- else {
39
- this.unitIdToMission.set(unitId, mission);
40
- }
41
- });
42
- toRemove.forEach((unitId) => mission.removeUnit(unitId));
43
- });
44
- }
45
- onAiUpdate(gameApi, actionsApi, playerData, matchAwareness) {
46
- // Remove inactive missions.
47
- this.missions = this.missions.filter((missions) => missions.isActive());
48
- this.updateUnitIds(gameApi);
49
- // Batch actions to reduce spamming of actions for larger armies.
50
- const actionBatcher = new ActionBatcher();
51
- // Poll missions for requested actions.
52
- const missionActions = this.missions.map((mission) => ({
53
- mission,
54
- action: mission.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness, actionBatcher),
55
- }));
56
- // Handle disbands and merges.
57
- const disbandedMissions = new Map();
58
- const disbandedMissionsArray = [];
59
- this.forceDisbandedMissions.forEach((name) => disbandedMissions.set(name, null));
60
- this.forceDisbandedMissions = [];
61
- missionActions.filter(isDisbandMission).forEach((a) => {
62
- this.logger(`Mission ${a.mission.getUniqueName()} disbanding as requested.`);
63
- a.mission.getUnitIds().forEach((unitId) => {
64
- this.unitIdToMission.delete(unitId);
65
- actionsApi.setUnitDebugText(unitId, undefined);
66
- });
67
- disbandedMissions.set(a.mission.getUniqueName(), a.action.reason);
68
- });
69
- // Handle unit requests.
70
- // Release units
71
- missionActions.filter(isReleaseUnits).forEach((a) => {
72
- a.action.unitIds.forEach((unitId) => {
73
- if (this.unitIdToMission.get(unitId)?.getUniqueName() === a.mission.getUniqueName()) {
74
- this.removeUnitFromMission(a.mission, unitId, actionsApi);
75
- }
76
- });
77
- });
78
- // Request specific units by ID
79
- const unitIdToHighestRequest = missionActions.filter(isRequestSpecificUnits).reduce((prev, missionWithAction) => {
80
- const { unitIds } = missionWithAction.action;
81
- unitIds.forEach((unitId) => {
82
- if (prev.hasOwnProperty(unitId)) {
83
- if (prev[unitId].action.priority > prev[unitId].action.priority) {
84
- prev[unitId] = missionWithAction;
85
- }
86
- }
87
- else {
88
- prev[unitId] = missionWithAction;
89
- }
90
- });
91
- return prev;
92
- }, {});
93
- // Map of Mission ID to Unit Type to Count.
94
- const newMissionAssignments = Object.entries(unitIdToHighestRequest)
95
- .flatMap(([id, request]) => {
96
- const unitId = Number.parseInt(id);
97
- const unit = gameApi.getGameObjectData(unitId);
98
- const { mission: requestingMission } = request;
99
- const missionName = requestingMission.getUniqueName();
100
- if (!unit) {
101
- this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
102
- return [];
103
- }
104
- if (!this.unitIdToMission.has(unitId)) {
105
- this.addUnitToMission(requestingMission, unit, actionsApi);
106
- return [{ unitName: unit?.name, mission: requestingMission.getUniqueName() }];
107
- }
108
- return [];
109
- })
110
- .reduce((acc, curr) => {
111
- if (!acc[curr.mission]) {
112
- acc[curr.mission] = {};
113
- }
114
- if (!acc[curr.mission][curr.unitName]) {
115
- acc[curr.mission][curr.unitName] = 0;
116
- }
117
- acc[curr.mission][curr.unitName] = acc[curr.mission][curr.unitName] + 1;
118
- return acc;
119
- }, {});
120
- Object.entries(newMissionAssignments).forEach(([mission, assignments]) => {
121
- this.logger(`Mission ${mission} received: ${Object.entries(assignments)
122
- .map(([unitType, count]) => unitType + " x " + count)
123
- .join(", ")}`);
124
- });
125
- // Request units by type - store the highest priority mission for each unit type.
126
- const unitTypeToHighestRequest = missionActions.filter(isRequestUnits).reduce((prev, missionWithAction) => {
127
- const { unitNames } = missionWithAction.action;
128
- unitNames.forEach((unitName) => {
129
- if (prev.hasOwnProperty(unitName)) {
130
- if (prev[unitName].action.priority > prev[unitName].action.priority) {
131
- prev[unitName] = missionWithAction;
132
- }
133
- }
134
- else {
135
- prev[unitName] = missionWithAction;
136
- }
137
- });
138
- return prev;
139
- }, {});
140
- // Request combat-capable units in an area
141
- const grabRequests = missionActions.filter(isGrabCombatants);
142
- // Find un-assigned units and distribute them among all the requesting missions.
143
- const unitIds = gameApi.getVisibleUnits(playerData.name, "self");
144
- // List of units that are unassigned or not in a locked mission.
145
- const freeUnits = unitIds
146
- .map((unitId) => gameApi.getGameObjectData(unitId))
147
- .filter((unit) => !!unit)
148
- .map((unit) => ({
149
- unit,
150
- mission: this.unitIdToMission.get(unit.id),
151
- }))
152
- .filter((unitWithMission) => !unitWithMission.mission || unitWithMission.mission.isUnitsLocked() === false);
153
- // Sort free units so that unassigned units get chosen before assigned (but unlocked) units.
154
- freeUnits.sort((u1, u2) => (u1.mission?.getPriority() ?? 0) - (u2.mission?.getPriority() ?? 0));
155
- const newAssignmentsByType = freeUnits
156
- .flatMap(({ unit: freeUnit, mission: donatingMission }) => {
157
- if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
158
- const { mission: requestingMission } = unitTypeToHighestRequest[freeUnit.name];
159
- if (donatingMission) {
160
- if (donatingMission === requestingMission ||
161
- donatingMission.getPriority() > requestingMission.getPriority()) {
162
- return [];
163
- }
164
- this.removeUnitFromMission(donatingMission, freeUnit.id, actionsApi);
165
- }
166
- this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to mission ${requestingMission.getUniqueName()}`);
167
- this.addUnitToMission(requestingMission, freeUnit, actionsApi);
168
- delete unitTypeToHighestRequest[freeUnit.name];
169
- return [
170
- { unitName: freeUnit.name, missionName: requestingMission.getUniqueName(), method: "type" },
171
- ];
172
- }
173
- else if (grabRequests.length > 0) {
174
- const grantedMission = grabRequests.find((request) => {
175
- const canGrabUnit = isSelectableCombatant(freeUnit);
176
- return (canGrabUnit &&
177
- request.action.point.distanceTo(new Vector2(freeUnit.tile.rx, freeUnit.tile.ry)) <=
178
- request.action.radius);
179
- });
180
- if (grantedMission) {
181
- if (donatingMission) {
182
- if (donatingMission === grantedMission.mission ||
183
- donatingMission.getPriority() > grantedMission.mission.getPriority()) {
184
- return [];
185
- }
186
- this.removeUnitFromMission(donatingMission, freeUnit.id, actionsApi);
187
- }
188
- this.addUnitToMission(grantedMission.mission, freeUnit, actionsApi);
189
- return [
190
- {
191
- unitName: freeUnit.name,
192
- missionName: grantedMission.mission.getUniqueName(),
193
- method: "grab",
194
- },
195
- ];
196
- }
197
- }
198
- return [];
199
- })
200
- .reduce((acc, curr) => {
201
- if (!acc[curr.missionName]) {
202
- acc[curr.missionName] = {};
203
- }
204
- if (!acc[curr.missionName][curr.unitName]) {
205
- acc[curr.missionName][curr.unitName] = { grab: 0, type: 0 };
206
- }
207
- acc[curr.missionName][curr.unitName][curr.method] =
208
- acc[curr.missionName][curr.unitName][curr.method] + 1;
209
- return acc;
210
- }, {});
211
- Object.entries(newAssignmentsByType).forEach(([mission, assignments]) => {
212
- this.logger(`Mission ${mission} received: ${Object.entries(assignments)
213
- .flatMap(([unitType, methodToCount]) => Object.entries(methodToCount)
214
- .filter(([, count]) => count > 0)
215
- .map(([method, count]) => unitType + " x " + count + " (by " + method + ")"))
216
- .join(", ")}`);
217
- });
218
- this.updateRequestedUnitTypes(unitTypeToHighestRequest);
219
- // Send all actions that can be batched together.
220
- actionBatcher.resolve(actionsApi);
221
- // Remove disbanded and merged missions.
222
- this.missions
223
- .filter((missions) => disbandedMissions.has(missions.getUniqueName()))
224
- .forEach((disbandedMission) => {
225
- const reason = disbandedMissions.get(disbandedMission.getUniqueName());
226
- this.logger(`mission disbanded: ${disbandedMission.getUniqueName()}, reason: ${reason}`);
227
- disbandedMissionsArray.push({ mission: disbandedMission, reason });
228
- disbandedMission.endMission(disbandedMissions.get(disbandedMission.getUniqueName()));
229
- });
230
- this.missions = this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
231
- // Create dynamic missions.
232
- this.missionFactories.forEach((missionFactory) => {
233
- missionFactory.maybeCreateMissions(gameApi, playerData, matchAwareness, this, this.logger);
234
- disbandedMissionsArray.forEach(({ reason, mission }) => {
235
- missionFactory.onMissionFailed(gameApi, playerData, matchAwareness, mission, reason, this, this.logger);
236
- });
237
- });
238
- }
239
- updateRequestedUnitTypes(missingUnitTypeToHighestRequest) {
240
- // Decay the priority over time.
241
- const currentUnitTypes = Array.from(this.requestedUnitTypes.keys());
242
- for (const unitType of currentUnitTypes) {
243
- const newPriority = this.requestedUnitTypes.get(unitType) * MISSING_UNIT_TYPE_REQUEST_DECAY_MULT_RATE -
244
- MISSING_UNIT_TYPE_REQUEST_DECAY_FLAT_RATE;
245
- if (newPriority > 0.5) {
246
- this.requestedUnitTypes.set(unitType, newPriority);
247
- }
248
- else {
249
- this.requestedUnitTypes.delete(unitType);
250
- }
251
- }
252
- // Add the new missing units to the priority set, if the request is higher than the existing value.
253
- Object.entries(missingUnitTypeToHighestRequest).forEach(([unitType, request]) => {
254
- const currentPriority = this.requestedUnitTypes.get(unitType);
255
- this.requestedUnitTypes.set(unitType, currentPriority ? Math.max(currentPriority, request.action.priority) : request.action.priority);
256
- });
257
- }
258
- /**
259
- * Returns the set of units that have been requested for production by the missions.
260
- *
261
- * @returns A map of unit type to the highest priority for that unit type.
262
- */
263
- getRequestedUnitTypes() {
264
- return this.requestedUnitTypes;
265
- }
266
- addUnitToMission(mission, unit, actionsApi) {
267
- mission.addUnit(unit.id);
268
- this.unitIdToMission.set(unit.id, mission);
269
- actionsApi.setUnitDebugText(unit.id, mission.getUniqueName() + "_" + unit.id);
270
- }
271
- removeUnitFromMission(mission, unitId, actionsApi) {
272
- mission.removeUnit(unitId);
273
- this.unitIdToMission.delete(unitId);
274
- actionsApi.setUnitDebugText(unitId, undefined);
275
- }
276
- /**
277
- * Attempts to add a mission to the active set.
278
- * @param mission
279
- * @returns The mission if it was accepted, or null if it was not.
280
- */
281
- addMission(mission) {
282
- if (this.missions.some((m) => m.getUniqueName() === mission.getUniqueName())) {
283
- // reject non-unique mission names
284
- return null;
285
- }
286
- this.logger(`Added mission: ${mission.getUniqueName()}`);
287
- this.missions.push(mission);
288
- return mission;
289
- }
290
- /**
291
- * Disband the provided mission on the next possible opportunity.
292
- */
293
- disbandMission(missionName) {
294
- this.forceDisbandedMissions.push(missionName);
295
- }
296
- // return text to display for global debug
297
- getGlobalDebugText(gameApi) {
298
- const unitsInMission = (unitIds) => countBy(unitIds, (unitId) => gameApi.getGameObjectData(unitId)?.name);
299
- let globalDebugText = "";
300
- this.missions.forEach((mission) => {
301
- this.logger(`Mission ${mission.getUniqueName()}: ${Object.entries(unitsInMission(mission.getUnitIds()))
302
- .map(([unitName, count]) => `${unitName} x ${count}`)
303
- .join(", ")}`);
304
- const missionDebugText = mission.getGlobalDebugText();
305
- if (missionDebugText) {
306
- globalDebugText += mission.getUniqueName() + ": " + missionDebugText + "\n";
307
- }
308
- });
309
- return globalDebugText;
310
- }
311
- updateDebugText(actionsApi) {
312
- this.missions.forEach((mission) => {
313
- mission
314
- .getUnitIds()
315
- .forEach((unitId) => actionsApi.setUnitDebugText(unitId, `${unitId}: ${mission.getUniqueName()}`));
316
- });
317
- }
318
- getMissions() {
319
- return this.missions;
320
- }
321
- }
1
+ // Meta-controller for forming and controlling missions.
2
+ // Missions are groups of zero or more units that aim to accomplish a particular goal.
3
+ import { Vector2 } from "@chronodivide/game-api";
4
+ import { isDisbandMission, isGrabCombatants, isReleaseUnits, isRequestSpecificUnits, isRequestUnits, } from "./mission.js";
5
+ import { createMissionFactories } from "./missionFactories.js";
6
+ import { ActionBatcher } from "./actionBatcher.js";
7
+ import { countBy, isSelectableCombatant } from "../common/utils.js";
8
+ // `missingUnitTypes` priority decays by this much every update loop.
9
+ const MISSING_UNIT_TYPE_REQUEST_DECAY_MULT_RATE = 0.75;
10
+ const MISSING_UNIT_TYPE_REQUEST_DECAY_FLAT_RATE = 1;
11
+ export class MissionController {
12
+ constructor(logger) {
13
+ this.logger = logger;
14
+ this.missions = [];
15
+ // A mapping of unit IDs to the missions they are assigned to. This may contain units that are dead, but
16
+ // is periodically cleaned in the update loop.
17
+ this.unitIdToMission = new Map();
18
+ // A mapping of unit types to the highest priority requested for a mission.
19
+ // This decays over time if requests are not 'refreshed' by mission.
20
+ this.requestedUnitTypes = new Map();
21
+ // Tracks missions to be externally disbanded the next time the mission update loop occurs.
22
+ this.forceDisbandedMissions = [];
23
+ this.missionFactories = createMissionFactories();
24
+ }
25
+ updateUnitIds(gameApi) {
26
+ // Check for units in multiple missions, this shouldn't happen.
27
+ this.unitIdToMission = new Map();
28
+ this.missions.forEach((mission) => {
29
+ const toRemove = [];
30
+ mission.getUnitIds().forEach((unitId) => {
31
+ if (this.unitIdToMission.has(unitId)) {
32
+ this.logger(`WARNING: unit ${unitId} is in multiple missions, please debug.`);
33
+ }
34
+ else if (!gameApi.getGameObjectData(unitId)) {
35
+ // say, if a unit was killed
36
+ toRemove.push(unitId);
37
+ }
38
+ else {
39
+ this.unitIdToMission.set(unitId, mission);
40
+ }
41
+ });
42
+ toRemove.forEach((unitId) => mission.removeUnit(unitId));
43
+ });
44
+ }
45
+ onAiUpdate(gameApi, actionsApi, playerData, matchAwareness) {
46
+ // Remove inactive missions.
47
+ this.missions = this.missions.filter((missions) => missions.isActive());
48
+ this.updateUnitIds(gameApi);
49
+ // Batch actions to reduce spamming of actions for larger armies.
50
+ const actionBatcher = new ActionBatcher();
51
+ // Poll missions for requested actions.
52
+ const missionActions = this.missions.map((mission) => ({
53
+ mission,
54
+ action: mission.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness, actionBatcher),
55
+ }));
56
+ // Handle disbands and merges.
57
+ const disbandedMissions = new Map();
58
+ const disbandedMissionsArray = [];
59
+ this.forceDisbandedMissions.forEach((name) => disbandedMissions.set(name, null));
60
+ this.forceDisbandedMissions = [];
61
+ missionActions.filter(isDisbandMission).forEach((a) => {
62
+ this.logger(`Mission ${a.mission.getUniqueName()} disbanding as requested.`);
63
+ a.mission.getUnitIds().forEach((unitId) => {
64
+ this.unitIdToMission.delete(unitId);
65
+ actionsApi.setUnitDebugText(unitId, undefined);
66
+ });
67
+ disbandedMissions.set(a.mission.getUniqueName(), a.action.reason);
68
+ });
69
+ // Handle unit requests.
70
+ // Release units
71
+ missionActions.filter(isReleaseUnits).forEach((a) => {
72
+ a.action.unitIds.forEach((unitId) => {
73
+ if (this.unitIdToMission.get(unitId)?.getUniqueName() === a.mission.getUniqueName()) {
74
+ this.removeUnitFromMission(a.mission, unitId, actionsApi);
75
+ }
76
+ });
77
+ });
78
+ // Request specific units by ID
79
+ const unitIdToHighestRequest = missionActions.filter(isRequestSpecificUnits).reduce((prev, missionWithAction) => {
80
+ const { unitIds } = missionWithAction.action;
81
+ unitIds.forEach((unitId) => {
82
+ if (prev.hasOwnProperty(unitId)) {
83
+ if (prev[unitId].action.priority > prev[unitId].action.priority) {
84
+ prev[unitId] = missionWithAction;
85
+ }
86
+ }
87
+ else {
88
+ prev[unitId] = missionWithAction;
89
+ }
90
+ });
91
+ return prev;
92
+ }, {});
93
+ // Map of Mission ID to Unit Type to Count.
94
+ const newMissionAssignments = Object.entries(unitIdToHighestRequest)
95
+ .flatMap(([id, request]) => {
96
+ const unitId = Number.parseInt(id);
97
+ const unit = gameApi.getGameObjectData(unitId);
98
+ const { mission: requestingMission } = request;
99
+ const missionName = requestingMission.getUniqueName();
100
+ if (!unit) {
101
+ this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
102
+ return [];
103
+ }
104
+ if (!this.unitIdToMission.has(unitId)) {
105
+ this.addUnitToMission(requestingMission, unit, actionsApi);
106
+ return [{ unitName: unit?.name, mission: requestingMission.getUniqueName() }];
107
+ }
108
+ return [];
109
+ })
110
+ .reduce((acc, curr) => {
111
+ if (!acc[curr.mission]) {
112
+ acc[curr.mission] = {};
113
+ }
114
+ if (!acc[curr.mission][curr.unitName]) {
115
+ acc[curr.mission][curr.unitName] = 0;
116
+ }
117
+ acc[curr.mission][curr.unitName] = acc[curr.mission][curr.unitName] + 1;
118
+ return acc;
119
+ }, {});
120
+ Object.entries(newMissionAssignments).forEach(([mission, assignments]) => {
121
+ this.logger(`Mission ${mission} received: ${Object.entries(assignments)
122
+ .map(([unitType, count]) => unitType + " x " + count)
123
+ .join(", ")}`);
124
+ });
125
+ // Request units by type - store the highest priority mission for each unit type.
126
+ const unitTypeToHighestRequest = missionActions.filter(isRequestUnits).reduce((prev, missionWithAction) => {
127
+ const { unitNames } = missionWithAction.action;
128
+ unitNames.forEach((unitName) => {
129
+ if (prev.hasOwnProperty(unitName)) {
130
+ if (prev[unitName].action.priority > prev[unitName].action.priority) {
131
+ prev[unitName] = missionWithAction;
132
+ }
133
+ }
134
+ else {
135
+ prev[unitName] = missionWithAction;
136
+ }
137
+ });
138
+ return prev;
139
+ }, {});
140
+ // Request combat-capable units in an area
141
+ const grabRequests = missionActions.filter(isGrabCombatants);
142
+ // Find un-assigned units and distribute them among all the requesting missions.
143
+ const unitIds = gameApi.getVisibleUnits(playerData.name, "self");
144
+ // List of units that are unassigned or not in a locked mission.
145
+ const freeUnits = unitIds
146
+ .map((unitId) => gameApi.getGameObjectData(unitId))
147
+ .filter((unit) => !!unit)
148
+ .map((unit) => ({
149
+ unit,
150
+ mission: this.unitIdToMission.get(unit.id),
151
+ }))
152
+ .filter((unitWithMission) => !unitWithMission.mission || unitWithMission.mission.isUnitsLocked() === false);
153
+ // Sort free units so that unassigned units get chosen before assigned (but unlocked) units.
154
+ freeUnits.sort((u1, u2) => (u1.mission?.getPriority() ?? 0) - (u2.mission?.getPriority() ?? 0));
155
+ const newAssignmentsByType = freeUnits
156
+ .flatMap(({ unit: freeUnit, mission: donatingMission }) => {
157
+ if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
158
+ const { mission: requestingMission } = unitTypeToHighestRequest[freeUnit.name];
159
+ if (donatingMission) {
160
+ if (donatingMission === requestingMission ||
161
+ donatingMission.getPriority() > requestingMission.getPriority()) {
162
+ return [];
163
+ }
164
+ this.removeUnitFromMission(donatingMission, freeUnit.id, actionsApi);
165
+ }
166
+ this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to mission ${requestingMission.getUniqueName()}`);
167
+ this.addUnitToMission(requestingMission, freeUnit, actionsApi);
168
+ delete unitTypeToHighestRequest[freeUnit.name];
169
+ return [
170
+ { unitName: freeUnit.name, missionName: requestingMission.getUniqueName(), method: "type" },
171
+ ];
172
+ }
173
+ else if (grabRequests.length > 0) {
174
+ const grantedMission = grabRequests.find((request) => {
175
+ const canGrabUnit = isSelectableCombatant(freeUnit);
176
+ return (canGrabUnit &&
177
+ request.action.point.distanceTo(new Vector2(freeUnit.tile.rx, freeUnit.tile.ry)) <=
178
+ request.action.radius);
179
+ });
180
+ if (grantedMission) {
181
+ if (donatingMission) {
182
+ if (donatingMission === grantedMission.mission ||
183
+ donatingMission.getPriority() > grantedMission.mission.getPriority()) {
184
+ return [];
185
+ }
186
+ this.removeUnitFromMission(donatingMission, freeUnit.id, actionsApi);
187
+ }
188
+ this.addUnitToMission(grantedMission.mission, freeUnit, actionsApi);
189
+ return [
190
+ {
191
+ unitName: freeUnit.name,
192
+ missionName: grantedMission.mission.getUniqueName(),
193
+ method: "grab",
194
+ },
195
+ ];
196
+ }
197
+ }
198
+ return [];
199
+ })
200
+ .reduce((acc, curr) => {
201
+ if (!acc[curr.missionName]) {
202
+ acc[curr.missionName] = {};
203
+ }
204
+ if (!acc[curr.missionName][curr.unitName]) {
205
+ acc[curr.missionName][curr.unitName] = { grab: 0, type: 0 };
206
+ }
207
+ acc[curr.missionName][curr.unitName][curr.method] =
208
+ acc[curr.missionName][curr.unitName][curr.method] + 1;
209
+ return acc;
210
+ }, {});
211
+ Object.entries(newAssignmentsByType).forEach(([mission, assignments]) => {
212
+ this.logger(`Mission ${mission} received: ${Object.entries(assignments)
213
+ .flatMap(([unitType, methodToCount]) => Object.entries(methodToCount)
214
+ .filter(([, count]) => count > 0)
215
+ .map(([method, count]) => unitType + " x " + count + " (by " + method + ")"))
216
+ .join(", ")}`);
217
+ });
218
+ this.updateRequestedUnitTypes(unitTypeToHighestRequest);
219
+ // Send all actions that can be batched together.
220
+ actionBatcher.resolve(actionsApi);
221
+ // Remove disbanded and merged missions.
222
+ this.missions
223
+ .filter((missions) => disbandedMissions.has(missions.getUniqueName()))
224
+ .forEach((disbandedMission) => {
225
+ const reason = disbandedMissions.get(disbandedMission.getUniqueName());
226
+ this.logger(`mission disbanded: ${disbandedMission.getUniqueName()}, reason: ${reason}`);
227
+ disbandedMissionsArray.push({ mission: disbandedMission, reason });
228
+ disbandedMission.endMission(disbandedMissions.get(disbandedMission.getUniqueName()));
229
+ });
230
+ this.missions = this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
231
+ // Create dynamic missions.
232
+ this.missionFactories.forEach((missionFactory) => {
233
+ missionFactory.maybeCreateMissions(gameApi, playerData, matchAwareness, this, this.logger);
234
+ disbandedMissionsArray.forEach(({ reason, mission }) => {
235
+ missionFactory.onMissionFailed(gameApi, playerData, matchAwareness, mission, reason, this, this.logger);
236
+ });
237
+ });
238
+ }
239
+ updateRequestedUnitTypes(missingUnitTypeToHighestRequest) {
240
+ // Decay the priority over time.
241
+ const currentUnitTypes = Array.from(this.requestedUnitTypes.keys());
242
+ for (const unitType of currentUnitTypes) {
243
+ const newPriority = this.requestedUnitTypes.get(unitType) * MISSING_UNIT_TYPE_REQUEST_DECAY_MULT_RATE -
244
+ MISSING_UNIT_TYPE_REQUEST_DECAY_FLAT_RATE;
245
+ if (newPriority > 0.5) {
246
+ this.requestedUnitTypes.set(unitType, newPriority);
247
+ }
248
+ else {
249
+ this.requestedUnitTypes.delete(unitType);
250
+ }
251
+ }
252
+ // Add the new missing units to the priority set, if the request is higher than the existing value.
253
+ Object.entries(missingUnitTypeToHighestRequest).forEach(([unitType, request]) => {
254
+ const currentPriority = this.requestedUnitTypes.get(unitType);
255
+ this.requestedUnitTypes.set(unitType, currentPriority ? Math.max(currentPriority, request.action.priority) : request.action.priority);
256
+ });
257
+ }
258
+ /**
259
+ * Returns the set of units that have been requested for production by the missions.
260
+ *
261
+ * @returns A map of unit type to the highest priority for that unit type.
262
+ */
263
+ getRequestedUnitTypes() {
264
+ return this.requestedUnitTypes;
265
+ }
266
+ addUnitToMission(mission, unit, actionsApi) {
267
+ mission.addUnit(unit.id);
268
+ this.unitIdToMission.set(unit.id, mission);
269
+ actionsApi.setUnitDebugText(unit.id, mission.getUniqueName() + "_" + unit.id);
270
+ }
271
+ removeUnitFromMission(mission, unitId, actionsApi) {
272
+ mission.removeUnit(unitId);
273
+ this.unitIdToMission.delete(unitId);
274
+ actionsApi.setUnitDebugText(unitId, undefined);
275
+ }
276
+ /**
277
+ * Attempts to add a mission to the active set.
278
+ * @param mission
279
+ * @returns The mission if it was accepted, or null if it was not.
280
+ */
281
+ addMission(mission) {
282
+ if (this.missions.some((m) => m.getUniqueName() === mission.getUniqueName())) {
283
+ // reject non-unique mission names
284
+ return null;
285
+ }
286
+ this.logger(`Added mission: ${mission.getUniqueName()}`);
287
+ this.missions.push(mission);
288
+ return mission;
289
+ }
290
+ /**
291
+ * Disband the provided mission on the next possible opportunity.
292
+ */
293
+ disbandMission(missionName) {
294
+ this.forceDisbandedMissions.push(missionName);
295
+ }
296
+ // return text to display for global debug
297
+ getGlobalDebugText(gameApi) {
298
+ const unitsInMission = (unitIds) => countBy(unitIds, (unitId) => gameApi.getGameObjectData(unitId)?.name);
299
+ let globalDebugText = "";
300
+ this.missions.forEach((mission) => {
301
+ this.logger(`Mission ${mission.getUniqueName()}: ${Object.entries(unitsInMission(mission.getUnitIds()))
302
+ .map(([unitName, count]) => `${unitName} x ${count}`)
303
+ .join(", ")}`);
304
+ const missionDebugText = mission.getGlobalDebugText();
305
+ if (missionDebugText) {
306
+ globalDebugText += mission.getUniqueName() + ": " + missionDebugText + "\n";
307
+ }
308
+ });
309
+ return globalDebugText;
310
+ }
311
+ updateDebugText(actionsApi) {
312
+ this.missions.forEach((mission) => {
313
+ mission
314
+ .getUnitIds()
315
+ .forEach((unitId) => actionsApi.setUnitDebugText(unitId, `${unitId}: ${mission.getUniqueName()}`));
316
+ });
317
+ }
318
+ getMissions() {
319
+ return this.missions;
320
+ }
321
+ }
322
322
  //# sourceMappingURL=missionController.js.map