@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.
- package/.env.template +5 -0
- package/README.md +57 -39
- package/dist/bot/bot.js +27 -37
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/logic/awareness.js +13 -8
- package/dist/bot/logic/awareness.js.map +1 -1
- package/dist/bot/logic/awarenessImpl.js +132 -0
- package/dist/bot/logic/awarenessImpl.js.map +1 -0
- package/dist/bot/logic/building/ArtilleryUnit.js +2 -29
- package/dist/bot/logic/building/ArtilleryUnit.js.map +1 -0
- package/dist/bot/logic/building/antiAirStaticDefence.js +43 -0
- package/dist/bot/logic/building/antiAirStaticDefence.js.map +1 -0
- package/dist/bot/logic/building/antiGroundStaticDefence.js +8 -5
- package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -1
- package/dist/bot/logic/building/basicAirUnit.js +2 -23
- package/dist/bot/logic/building/basicAirUnit.js.map +1 -1
- package/dist/bot/logic/building/basicBuilding.js +3 -2
- package/dist/bot/logic/building/basicBuilding.js.map +1 -1
- package/dist/bot/logic/building/basicGroundUnit.js +2 -43
- package/dist/bot/logic/building/basicGroundUnit.js.map +1 -1
- package/dist/bot/logic/building/building.js +55 -11
- package/dist/bot/logic/building/building.js.map +1 -0
- package/dist/bot/logic/building/buildingRules.js +62 -50
- package/dist/bot/logic/building/buildingRules.js.map +1 -1
- package/dist/bot/logic/building/common.js +19 -0
- package/dist/bot/logic/building/common.js.map +1 -0
- package/dist/bot/logic/building/harvester.js +2 -1
- package/dist/bot/logic/building/harvester.js.map +1 -1
- package/dist/bot/logic/building/queueController.js +73 -41
- package/dist/bot/logic/building/queueController.js.map +1 -1
- package/dist/bot/logic/common/utils.js +35 -0
- package/dist/bot/logic/common/utils.js.map +1 -1
- package/dist/bot/logic/composition/alliedCompositions.js +13 -0
- package/dist/bot/logic/composition/alliedCompositions.js.map +1 -0
- package/dist/bot/logic/composition/common.js +2 -0
- package/dist/bot/logic/composition/common.js.map +1 -0
- package/dist/bot/logic/composition/sovietCompositions.js +13 -0
- package/dist/bot/logic/composition/sovietCompositions.js.map +1 -0
- package/dist/bot/logic/mission/actionBatcher.js +92 -0
- package/dist/bot/logic/mission/actionBatcher.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/combatSquad.js +124 -0
- package/dist/bot/logic/mission/behaviours/combatSquad.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/common.js +58 -0
- package/dist/bot/logic/mission/behaviours/common.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/engineerSquad.js +39 -0
- package/dist/bot/logic/mission/behaviours/engineerSquad.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/expansionSquad.js +46 -0
- package/dist/bot/logic/mission/behaviours/expansionSquad.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/retreatSquad.js +31 -0
- package/dist/bot/logic/mission/behaviours/retreatSquad.js.map +1 -0
- package/{src/bot/logic/squad/behaviours/scoutingSquad.ts → dist/bot/logic/mission/behaviours/scoutingSquad.js} +29 -47
- package/dist/bot/logic/mission/behaviours/scoutingSquad.js.map +1 -0
- package/dist/bot/logic/mission/mission.js +91 -19
- package/dist/bot/logic/mission/mission.js.map +1 -1
- package/dist/bot/logic/mission/missionController.js +262 -21
- package/dist/bot/logic/mission/missionController.js.map +1 -1
- package/dist/bot/logic/mission/missions/attackMission.js +159 -52
- package/dist/bot/logic/mission/missions/attackMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/basicMission.js +13 -0
- package/dist/bot/logic/mission/missions/basicMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/defenceMission.js +43 -28
- package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/engineerMission.js +37 -7
- package/dist/bot/logic/mission/missions/engineerMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/expansionMission.js +42 -6
- package/dist/bot/logic/mission/missions/expansionMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/missionBehaviour.js +2 -0
- package/dist/bot/logic/mission/missions/missionBehaviour.js.map +1 -0
- package/dist/bot/logic/mission/missions/retreatMission.js +31 -5
- package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/scoutingMission.js +103 -6
- package/dist/bot/logic/mission/missions/scoutingMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/squads/combatSquad.js +116 -0
- package/dist/bot/logic/mission/missions/squads/combatSquad.js.map +1 -0
- package/dist/bot/logic/mission/missions/squads/common.js +58 -0
- package/dist/bot/logic/mission/missions/squads/common.js.map +1 -0
- package/dist/bot/logic/mission/missions/squads/squad.js +2 -0
- package/dist/bot/logic/mission/missions/squads/squad.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/attackSquad.js +63 -56
- package/dist/bot/logic/squad/behaviours/combatSquad.js +19 -18
- package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/common.js +2 -19
- package/dist/bot/logic/squad/behaviours/common.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/defenceSquad.js +15 -2
- package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js +17 -21
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +1 -1
- package/dist/bot/logic/squad/squad.js +8 -5
- package/dist/bot/logic/squad/squad.js.map +1 -1
- package/dist/bot/logic/squad/squadBehaviour.js.map +1 -1
- package/dist/bot/logic/squad/squadController.js +3 -2
- package/dist/bot/logic/squad/squadController.js.map +1 -1
- package/dist/bot/logic/threat/threatCalculator.js +5 -5
- package/dist/bot/logic/threat/threatCalculator.js.map +1 -1
- package/dist/exampleBot.js +53 -16
- package/dist/exampleBot.js.map +1 -1
- package/package.json +5 -4
- package/src/bot/bot.ts +38 -53
- package/src/bot/logic/awareness.ts +34 -22
- package/src/bot/logic/building/antiAirStaticDefence.ts +64 -0
- package/src/bot/logic/building/antiGroundStaticDefence.ts +7 -20
- package/src/bot/logic/building/artilleryUnit.ts +2 -28
- package/src/bot/logic/building/basicAirUnit.ts +2 -33
- package/src/bot/logic/building/basicBuilding.ts +8 -6
- package/src/bot/logic/building/basicGroundUnit.ts +2 -46
- package/src/bot/logic/building/buildingRules.ts +73 -57
- package/src/bot/logic/building/common.ts +23 -0
- package/src/bot/logic/building/harvester.ts +2 -1
- package/src/bot/logic/building/queueController.ts +105 -42
- package/src/bot/logic/common/utils.ts +47 -0
- package/src/bot/logic/composition/alliedCompositions.ts +22 -0
- package/src/bot/logic/composition/common.ts +3 -0
- package/src/bot/logic/composition/sovietCompositions.ts +21 -0
- package/src/bot/logic/mission/actionBatcher.ts +124 -0
- package/src/bot/logic/mission/mission.ts +186 -37
- package/src/bot/logic/mission/missionController.ts +340 -31
- package/src/bot/logic/mission/missionFactories.ts +3 -3
- package/src/bot/logic/mission/missions/attackMission.ts +234 -56
- package/src/bot/logic/mission/missions/defenceMission.ts +72 -45
- package/src/bot/logic/mission/missions/engineerMission.ts +67 -15
- package/src/bot/logic/mission/missions/expansionMission.ts +67 -14
- package/src/bot/logic/mission/missions/retreatMission.ts +50 -6
- package/src/bot/logic/mission/missions/scoutingMission.ts +138 -14
- package/src/bot/logic/mission/missions/squads/combatSquad.ts +160 -0
- package/src/bot/logic/{squad/behaviours → mission/missions/squads}/common.ts +14 -20
- package/src/bot/logic/mission/missions/squads/squad.ts +19 -0
- package/src/bot/logic/threat/threat.ts +15 -15
- package/src/bot/logic/threat/threatCalculator.ts +10 -10
- package/src/exampleBot.ts +59 -19
- package/.prettierrc +0 -5
- package/TODO.md +0 -18
- package/dist/bot/logic/building/artilleryUnit.js.map +0 -1
- package/dist/bot/logic/building/massedAntiGroundUnit.js +0 -20
- package/dist/bot/logic/building/queues.js +0 -19
- package/dist/bot/logic/knowledge.js +0 -1
- package/dist/bot/logic/mission/basicMission.js +0 -26
- package/dist/bot/logic/mission/expansionMission.js +0 -32
- package/dist/bot/logic/squad/behaviours/squadExpansion.js +0 -31
- package/dist/bot/logic/squad/behaviours/squadScouters.js +0 -8
- package/rules.ini +0 -23126
- package/src/bot/logic/mission/missions/oneTimeMission.ts +0 -33
- package/src/bot/logic/squad/behaviours/combatSquad.ts +0 -127
- package/src/bot/logic/squad/behaviours/engineerSquad.ts +0 -53
- package/src/bot/logic/squad/behaviours/expansionSquad.ts +0 -59
- package/src/bot/logic/squad/behaviours/retreatSquad.ts +0 -44
- package/src/bot/logic/squad/squad.ts +0 -159
- package/src/bot/logic/squad/squadBehaviour.ts +0 -62
- package/src/bot/logic/squad/squadBehaviours.ts +0 -8
- package/src/bot/logic/squad/squadController.ts +0 -254
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DEFAULT_BUILDING_PRIORITY,
|
|
15
15
|
getDefaultPlacementLocation,
|
|
16
16
|
} from "./buildingRules.js";
|
|
17
|
+
import { DebugLogger } from "../common/utils";
|
|
17
18
|
|
|
18
19
|
export const QUEUES = [
|
|
19
20
|
QueueType.Structures,
|
|
@@ -43,9 +44,16 @@ export const queueTypeToName = (queue: QueueType) => {
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
type QueueState = {
|
|
48
|
+
queue: QueueType;
|
|
49
|
+
/** sorted in ascending order (last item is the topItem) */
|
|
50
|
+
items: TechnoRulesWithPriority[];
|
|
51
|
+
topItem: TechnoRulesWithPriority | undefined;
|
|
52
|
+
};
|
|
47
53
|
|
|
48
54
|
export class QueueController {
|
|
55
|
+
private queueStates: QueueState[] = [];
|
|
56
|
+
|
|
49
57
|
constructor() {}
|
|
50
58
|
|
|
51
59
|
public onAiUpdate(
|
|
@@ -54,23 +62,35 @@ export class QueueController {
|
|
|
54
62
|
actionsApi: ActionsApi,
|
|
55
63
|
playerData: PlayerData,
|
|
56
64
|
threatCache: GlobalThreat | null,
|
|
65
|
+
unitTypeRequests: Map<string, number>,
|
|
57
66
|
logger: (message: string) => void,
|
|
58
67
|
) {
|
|
59
|
-
|
|
68
|
+
this.queueStates = QUEUES.map((queueType) => {
|
|
60
69
|
const options = productionApi.getAvailableObjects(queueType);
|
|
70
|
+
const items = this.getPrioritiesForBuildingOptions(
|
|
71
|
+
game,
|
|
72
|
+
options,
|
|
73
|
+
threatCache,
|
|
74
|
+
playerData,
|
|
75
|
+
unitTypeRequests,
|
|
76
|
+
logger,
|
|
77
|
+
);
|
|
78
|
+
const topItem = items.length > 0 ? items[items.length - 1] : undefined;
|
|
61
79
|
return {
|
|
62
80
|
queue: queueType,
|
|
63
|
-
|
|
81
|
+
items,
|
|
82
|
+
// only if the top item has a priority above zero
|
|
83
|
+
topItem: topItem && topItem.priority > 0 ? topItem : undefined,
|
|
64
84
|
};
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
.map((decision) => decision.
|
|
85
|
+
});
|
|
86
|
+
const totalWeightAcrossQueues = this.queueStates
|
|
87
|
+
.map((decision) => decision.topItem?.priority!)
|
|
68
88
|
.reduce((pV, cV) => pV + cV, 0);
|
|
69
|
-
|
|
70
|
-
.map((decision) => decision.
|
|
89
|
+
const totalCostAcrossQueues = this.queueStates
|
|
90
|
+
.map((decision) => decision.topItem?.unit.cost!)
|
|
71
91
|
.reduce((pV, cV) => pV + cV, 0);
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
this.queueStates.forEach((decision) => {
|
|
74
94
|
this.updateBuildQueue(
|
|
75
95
|
game,
|
|
76
96
|
productionApi,
|
|
@@ -78,7 +98,7 @@ export class QueueController {
|
|
|
78
98
|
playerData,
|
|
79
99
|
threatCache,
|
|
80
100
|
decision.queue,
|
|
81
|
-
decision.
|
|
101
|
+
decision.topItem,
|
|
82
102
|
totalWeightAcrossQueues,
|
|
83
103
|
totalCostAcrossQueues,
|
|
84
104
|
logger,
|
|
@@ -86,13 +106,17 @@ export class QueueController {
|
|
|
86
106
|
});
|
|
87
107
|
|
|
88
108
|
// Repair is simple - just repair everything that's damaged.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
if (playerData.credits > 0) {
|
|
110
|
+
game.getVisibleUnits(playerData.name, "self", (r) => r.repairable).forEach((unitId) => {
|
|
111
|
+
const unit = game.getUnitData(unitId);
|
|
112
|
+
if (!unit || !unit.hitPoints || !unit.maxHitPoints || unit.hasWrenchRepair) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (unit.hitPoints < unit.maxHitPoints) {
|
|
116
|
+
actionsApi.toggleRepairWrench(unitId);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
private updateBuildQueue(
|
|
@@ -120,31 +144,41 @@ export class QueueController {
|
|
|
120
144
|
// Consider placing it.
|
|
121
145
|
const objectReady: TechnoRules = queueData.items[0].rules;
|
|
122
146
|
if (queueType == QueueType.Structures || queueType == QueueType.Armory) {
|
|
123
|
-
logger(`Complete ${queueTypeToName(queueType)}: ${objectReady.name}`);
|
|
124
147
|
let location: { rx: number; ry: number } | undefined = this.getBestLocationForStructure(
|
|
125
148
|
game,
|
|
126
149
|
playerData,
|
|
127
150
|
objectReady,
|
|
128
151
|
);
|
|
129
152
|
if (location !== undefined) {
|
|
153
|
+
logger(
|
|
154
|
+
`Completed: ${queueTypeToName(queueType)}: ${objectReady.name}, placing at ${location.rx},${
|
|
155
|
+
location.ry
|
|
156
|
+
}`,
|
|
157
|
+
);
|
|
130
158
|
actionsApi.placeBuilding(objectReady.name, location.rx, location.ry);
|
|
159
|
+
} else {
|
|
160
|
+
logger(`Completed: ${queueTypeToName(queueType)}: ${objectReady.name} but nowhere to place it`);
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
} else if (queueData.status == QueueStatus.Active && queueData.items.length > 0 && decision != null) {
|
|
134
|
-
// Consider cancelling if something else is significantly higher priority.
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (decision.unit != current) {
|
|
164
|
+
// Consider cancelling if something else is significantly higher priority than what is currently being produced.
|
|
165
|
+
const currentProduction = queueData.items[0].rules;
|
|
166
|
+
if (decision.unit != currentProduction) {
|
|
138
167
|
// Changing our mind.
|
|
139
|
-
let currentItemPriority = this.getPriorityForBuildingOption(
|
|
168
|
+
let currentItemPriority = this.getPriorityForBuildingOption(
|
|
169
|
+
currentProduction,
|
|
170
|
+
game,
|
|
171
|
+
playerData,
|
|
172
|
+
threatCache,
|
|
173
|
+
);
|
|
140
174
|
let newItemPriority = decision.priority;
|
|
141
175
|
if (newItemPriority > currentItemPriority * 2) {
|
|
142
176
|
logger(
|
|
143
|
-
`Dequeueing queue ${queueTypeToName(queueData.type)} unit ${
|
|
177
|
+
`Dequeueing queue ${queueTypeToName(queueData.type)} unit ${currentProduction.name} because ${
|
|
144
178
|
decision.unit.name
|
|
145
179
|
} has 2x higher priority.`,
|
|
146
180
|
);
|
|
147
|
-
actionsApi.unqueueFromProduction(queueData.type,
|
|
181
|
+
actionsApi.unqueueFromProduction(queueData.type, currentProduction.name, currentProduction.type, 1);
|
|
148
182
|
}
|
|
149
183
|
} else {
|
|
150
184
|
// Not changing our mind, but maybe other queues are more important for now.
|
|
@@ -173,32 +207,29 @@ export class QueueController {
|
|
|
173
207
|
}
|
|
174
208
|
}
|
|
175
209
|
|
|
176
|
-
private
|
|
210
|
+
private getPrioritiesForBuildingOptions(
|
|
177
211
|
game: GameApi,
|
|
178
212
|
options: TechnoRules[],
|
|
179
213
|
threatCache: GlobalThreat | null,
|
|
180
214
|
playerData: PlayerData,
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
unitTypeRequests: Map<string, number>,
|
|
216
|
+
logger: DebugLogger,
|
|
217
|
+
): TechnoRulesWithPriority[] {
|
|
183
218
|
let priorityQueue: TechnoRulesWithPriority[] = [];
|
|
184
219
|
options.forEach((option) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
220
|
+
const calculatedPriority = this.getPriorityForBuildingOption(option, game, playerData, threatCache);
|
|
221
|
+
// Get the higher of the dynamic and the mission priority for the unit.
|
|
222
|
+
const actualPriority = Math.max(
|
|
223
|
+
calculatedPriority,
|
|
224
|
+
unitTypeRequests.get(option.name) ?? calculatedPriority,
|
|
225
|
+
);
|
|
226
|
+
if (actualPriority > 0) {
|
|
227
|
+
priorityQueue.push({ unit: option, priority: actualPriority });
|
|
188
228
|
}
|
|
189
229
|
});
|
|
190
230
|
|
|
191
|
-
priorityQueue = priorityQueue.sort((a, b) =>
|
|
192
|
-
|
|
193
|
-
});
|
|
194
|
-
if (priorityQueue.length > 0) {
|
|
195
|
-
if (DEBUG_BUILD_QUEUES && game.getCurrentTick() % 100 === 0) {
|
|
196
|
-
let queueString = priorityQueue.map((item) => item.unit.name + "(" + item.priority + ")").join(", ");
|
|
197
|
-
logger(`Build priority currently: ${queueString}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return priorityQueue.pop();
|
|
231
|
+
priorityQueue = priorityQueue.sort((a, b) => a.priority - b.priority);
|
|
232
|
+
return priorityQueue;
|
|
202
233
|
}
|
|
203
234
|
|
|
204
235
|
private getPriorityForBuildingOption(
|
|
@@ -231,4 +262,36 @@ export class QueueController {
|
|
|
231
262
|
return getDefaultPlacementLocation(game, playerData, playerData.startLocation, objectReady);
|
|
232
263
|
}
|
|
233
264
|
}
|
|
265
|
+
|
|
266
|
+
public getGlobalDebugText(gameApi: GameApi, productionApi: ProductionApi) {
|
|
267
|
+
const productionState = QUEUES.reduce((prev, queueType) => {
|
|
268
|
+
if (productionApi.getQueueData(queueType).size === 0) {
|
|
269
|
+
return prev;
|
|
270
|
+
}
|
|
271
|
+
const paused = productionApi.getQueueData(queueType).status === QueueStatus.OnHold;
|
|
272
|
+
return (
|
|
273
|
+
prev +
|
|
274
|
+
" [" +
|
|
275
|
+
queueTypeToName(queueType) +
|
|
276
|
+
(paused ? " PAUSED" : "") +
|
|
277
|
+
": " +
|
|
278
|
+
productionApi
|
|
279
|
+
.getQueueData(queueType)
|
|
280
|
+
.items.map((item) => item.rules.name + (item.quantity > 1 ? "x" + item.quantity : "")) +
|
|
281
|
+
"]"
|
|
282
|
+
);
|
|
283
|
+
}, "");
|
|
284
|
+
|
|
285
|
+
const queueStates = this.queueStates
|
|
286
|
+
.filter((queueState) => queueState.items.length > 0)
|
|
287
|
+
.map((queueState) => {
|
|
288
|
+
const queueString = queueState.items
|
|
289
|
+
.map((item) => item.unit.name + "(" + Math.round(item.priority * 10) / 10 + ")")
|
|
290
|
+
.join(", ");
|
|
291
|
+
return `${queueTypeToName(queueState.queue)} Prios: ${queueString}\n`;
|
|
292
|
+
})
|
|
293
|
+
.join("");
|
|
294
|
+
|
|
295
|
+
return `Production: ${productionState}\n${queueStates}`;
|
|
296
|
+
}
|
|
234
297
|
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { GameObjectData, TechnoRules, UnitData } from "@chronodivide/game-api";
|
|
2
|
+
|
|
1
3
|
export type DebugLogger = (message: string, sayInGame?: boolean) => void;
|
|
2
4
|
|
|
5
|
+
const SOVIET_COUNTRY_NAMES = ["Africans", "Arabs", "Confederation", "Russians"];
|
|
6
|
+
|
|
7
|
+
export const isSoviet = (countryName: string) => SOVIET_COUNTRY_NAMES.includes(countryName);
|
|
8
|
+
|
|
9
|
+
export const isOwnedByNeutral = (unitData: UnitData | undefined) => unitData?.owner === "@@NEUTRAL@@";
|
|
10
|
+
|
|
11
|
+
// Return if the given unit would have .isSelectableCombatant = true.
|
|
12
|
+
// Usable on GameObjectData (which is faster to get than TechnoRules)
|
|
13
|
+
export const isSelectableCombatant = (rules: GameObjectData | undefined) =>
|
|
14
|
+
!!(rules?.rules as any)?.isSelectableCombatant;
|
|
15
|
+
|
|
3
16
|
// Thanks use-strict!
|
|
4
17
|
export function formatTimeDuration(timeSeconds: number, skipZeroHours = false) {
|
|
5
18
|
let h = Math.floor(timeSeconds / 3600);
|
|
@@ -16,6 +29,23 @@ export function pad(n: any, format = "0000") {
|
|
|
16
29
|
return format.substring(0, format.length - str.length) + str;
|
|
17
30
|
}
|
|
18
31
|
|
|
32
|
+
// So we don't need lodash
|
|
33
|
+
export function minBy<T>(array: T[], predicate: (arg: T) => number | null): T | null {
|
|
34
|
+
if (array.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
let minIdx = 0;
|
|
38
|
+
let minVal = predicate(array[0]);
|
|
39
|
+
for (let i = 1; i < array.length; ++i) {
|
|
40
|
+
const newVal = predicate(array[i]);
|
|
41
|
+
if (minVal === null || (newVal !== null && newVal < minVal)) {
|
|
42
|
+
minIdx = i;
|
|
43
|
+
minVal = newVal;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return array[minIdx];
|
|
47
|
+
}
|
|
48
|
+
|
|
19
49
|
export function maxBy<T>(array: T[], predicate: (arg: T) => number | null): T | null {
|
|
20
50
|
if (array.length === 0) {
|
|
21
51
|
return null;
|
|
@@ -63,3 +93,20 @@ export function countBy<T>(array: T[], predicate: (arg: T) => string | undefined
|
|
|
63
93
|
{} as Record<string, number>,
|
|
64
94
|
);
|
|
65
95
|
}
|
|
96
|
+
|
|
97
|
+
export function groupBy<K extends string, V>(array: V[], predicate: (arg: V) => K): { [key in K]: V[] } {
|
|
98
|
+
return array.reduce(
|
|
99
|
+
(prev, newVal) => {
|
|
100
|
+
const val = predicate(newVal);
|
|
101
|
+
if (val === undefined) {
|
|
102
|
+
return prev;
|
|
103
|
+
}
|
|
104
|
+
if (!prev.hasOwnProperty(val)) {
|
|
105
|
+
prev[val] = [];
|
|
106
|
+
}
|
|
107
|
+
prev[val].push(newVal);
|
|
108
|
+
return prev;
|
|
109
|
+
},
|
|
110
|
+
{} as Record<K, V[]>,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { MatchAwareness } from "../awareness";
|
|
3
|
+
import { UnitComposition } from "./common";
|
|
4
|
+
|
|
5
|
+
export const getAlliedCompositions = (
|
|
6
|
+
gameApi: GameApi,
|
|
7
|
+
playerData: PlayerData,
|
|
8
|
+
matchAwareness: MatchAwareness,
|
|
9
|
+
): UnitComposition => {
|
|
10
|
+
const hasWarFactory = gameApi.getVisibleUnits(playerData.name, "self", (r) => r.name === "GAWEAP").length > 0;
|
|
11
|
+
const hasAirforce =
|
|
12
|
+
gameApi.getVisibleUnits(playerData.name, "self", (r) => r.name === "GAAIRC" || r.name === "AMRADR").length > 0;
|
|
13
|
+
const hasBattleLab = gameApi.getVisibleUnits(playerData.name, "self", (r) => r.name === "GATECH").length > 0;
|
|
14
|
+
|
|
15
|
+
const includeInfantry = !hasAirforce && !hasBattleLab;
|
|
16
|
+
return {
|
|
17
|
+
...(includeInfantry && { E1: 5 }),
|
|
18
|
+
...(hasWarFactory && { MTNK: 3, FV: 2 }),
|
|
19
|
+
...(hasAirforce && { JUMPJET: 6 }),
|
|
20
|
+
...(hasBattleLab && { SREF: 2, MGTK: 3 }),
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { MatchAwareness } from "../awareness";
|
|
3
|
+
import { UnitComposition } from "./common";
|
|
4
|
+
|
|
5
|
+
export const getSovietComposition = (
|
|
6
|
+
gameApi: GameApi,
|
|
7
|
+
playerData: PlayerData,
|
|
8
|
+
matchAwareness: MatchAwareness,
|
|
9
|
+
): UnitComposition => {
|
|
10
|
+
const hasWarFactory = gameApi.getVisibleUnits(playerData.name, "self", (r) => r.name === "NAWEAP").length > 0;
|
|
11
|
+
const hasRadar = gameApi.getVisibleUnits(playerData.name, "self", (r) => r.name === "NARADR").length > 0;
|
|
12
|
+
const hasBattleLab = gameApi.getVisibleUnits(playerData.name, "self", (r) => r.name === "NATECH").length > 0;
|
|
13
|
+
|
|
14
|
+
const includeInfantry = !hasBattleLab;
|
|
15
|
+
return {
|
|
16
|
+
...(includeInfantry && { E2: 10 }),
|
|
17
|
+
...(hasWarFactory && { HTNK: 3, HTK: 2 }),
|
|
18
|
+
...(hasRadar && { V3: 1 }),
|
|
19
|
+
...(hasBattleLab && { APOC: 2 }),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Used to group related actions together to minimise actionApi calls. For example, if multiple units
|
|
2
|
+
|
|
3
|
+
import { ActionsApi, OrderType, Vector2 } from "@chronodivide/game-api";
|
|
4
|
+
import { groupBy } from "../common/utils.js";
|
|
5
|
+
|
|
6
|
+
// are ordered to move to the same location, all of them will be ordered to move in a single action.
|
|
7
|
+
export class BatchableAction {
|
|
8
|
+
private constructor(
|
|
9
|
+
private _unitId: number,
|
|
10
|
+
private _orderType: OrderType,
|
|
11
|
+
private _point?: Vector2,
|
|
12
|
+
private _targetId?: number,
|
|
13
|
+
// If you don't want this action to be swallowed by dedupe, provide a unique nonce
|
|
14
|
+
private _nonce: number = 0,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
static noTarget(unitId: number, orderType: OrderType, nonce: number = 0) {
|
|
18
|
+
return new BatchableAction(unitId, orderType, undefined, undefined, nonce);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static toPoint(unitId: number, orderType: OrderType, point: Vector2, nonce: number = 0) {
|
|
22
|
+
return new BatchableAction(unitId, orderType, point, undefined);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static toTargetId(unitId: number, orderType: OrderType, targetId: number, nonce: number = 0) {
|
|
26
|
+
return new BatchableAction(unitId, orderType, undefined, targetId, nonce);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public get unitId() {
|
|
30
|
+
return this._unitId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public get orderType() {
|
|
34
|
+
return this._orderType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public get point() {
|
|
38
|
+
return this._point;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public get targetId() {
|
|
42
|
+
return this._targetId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public isSameAs(other: BatchableAction) {
|
|
46
|
+
if (this._unitId !== other._unitId) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (this._orderType !== other._orderType) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (this._point !== other._point) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (this._targetId !== other._targetId) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (this._nonce !== other._nonce) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ActionBatcher {
|
|
66
|
+
private actions: BatchableAction[];
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
this.actions = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
push(action: BatchableAction) {
|
|
73
|
+
this.actions.push(action);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resolve(actionsApi: ActionsApi) {
|
|
77
|
+
const groupedCommands = groupBy(this.actions, (action) => action.orderType.valueOf().toString());
|
|
78
|
+
const vectorToStr = (v: Vector2) => v.x + "," + v.y;
|
|
79
|
+
const strToVector = (str: string) => {
|
|
80
|
+
const [x, y] = str.split(",");
|
|
81
|
+
return new Vector2(parseInt(x), parseInt(y));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Group by command type.
|
|
85
|
+
Object.entries(groupedCommands).forEach(([commandValue, commands]) => {
|
|
86
|
+
// i hate this
|
|
87
|
+
const commandType: OrderType = parseInt(commandValue) as OrderType;
|
|
88
|
+
// Group by command target ID.
|
|
89
|
+
const byTarget = groupBy(
|
|
90
|
+
commands.filter((command) => !!command.targetId),
|
|
91
|
+
(command) => command.targetId?.toString()!,
|
|
92
|
+
);
|
|
93
|
+
Object.entries(byTarget).forEach(([targetId, unitCommands]) => {
|
|
94
|
+
actionsApi.orderUnits(
|
|
95
|
+
unitCommands.map((command) => command.unitId),
|
|
96
|
+
commandType,
|
|
97
|
+
parseInt(targetId),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
// Group by position (the vector is encoded as a string of the form "x,y")
|
|
101
|
+
const byPosition = groupBy(
|
|
102
|
+
commands.filter((command) => !!command.point),
|
|
103
|
+
(command) => vectorToStr(command.point!),
|
|
104
|
+
);
|
|
105
|
+
Object.entries(byPosition).forEach(([point, unitCommands]) => {
|
|
106
|
+
const vector = strToVector(point);
|
|
107
|
+
actionsApi.orderUnits(
|
|
108
|
+
unitCommands.map((command) => command.unitId),
|
|
109
|
+
commandType,
|
|
110
|
+
vector.x,
|
|
111
|
+
vector.y,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
// Actions with no targets
|
|
115
|
+
const noTargets = commands.filter((command) => !command.targetId && !command.point);
|
|
116
|
+
if (noTargets.length > 0) {
|
|
117
|
+
actionsApi.orderUnits(
|
|
118
|
+
noTargets.map((action) => action.unitId),
|
|
119
|
+
commandType,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|