@supalosa/chronodivide-bot 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +5 -5
- package/TODO.md +18 -0
- package/dist/bot/bot.js +4 -4
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/logic/awareness.js +8 -8
- package/dist/bot/logic/awareness.js.map +1 -1
- package/dist/bot/logic/building/ArtilleryUnit.js +30 -9
- package/dist/bot/logic/building/antiGroundStaticDefence.js +2 -2
- package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -1
- package/dist/bot/logic/building/artilleryUnit.js.map +1 -0
- package/dist/bot/logic/building/basicAirUnit.js +3 -2
- package/dist/bot/logic/building/basicAirUnit.js.map +1 -1
- package/dist/bot/logic/building/basicBuilding.js +1 -1
- package/dist/bot/logic/building/basicBuilding.js.map +1 -1
- package/dist/bot/logic/building/basicGroundUnit.js +4 -3
- package/dist/bot/logic/building/basicGroundUnit.js.map +1 -1
- package/dist/bot/logic/building/building.js +11 -55
- package/dist/bot/logic/building/buildingRules.js +162 -0
- package/dist/bot/logic/building/buildingRules.js.map +1 -0
- package/dist/bot/logic/building/harvester.js.map +1 -1
- package/dist/bot/logic/building/massedAntiGroundUnit.js +20 -0
- package/dist/bot/logic/building/powerPlant.js +1 -1
- package/dist/bot/logic/building/powerPlant.js.map +1 -1
- package/dist/bot/logic/building/queueController.js +1 -1
- package/dist/bot/logic/building/queueController.js.map +1 -1
- package/dist/bot/logic/building/queues.js +19 -0
- package/dist/bot/logic/building/resourceCollectionBuilding.js +5 -3
- package/dist/bot/logic/building/resourceCollectionBuilding.js.map +1 -1
- package/dist/bot/logic/common/scout.js +49 -32
- package/dist/bot/logic/common/scout.js.map +1 -1
- package/dist/bot/logic/common/utils.js +50 -1
- package/dist/bot/logic/common/utils.js.map +1 -1
- package/dist/bot/logic/knowledge.js +1 -0
- package/dist/bot/logic/map/map.js +17 -19
- package/dist/bot/logic/map/map.js.map +1 -1
- package/dist/bot/logic/map/sector.js +10 -13
- package/dist/bot/logic/map/sector.js.map +1 -1
- package/dist/bot/logic/mission/basicMission.js +26 -0
- package/dist/bot/logic/mission/expansionMission.js +32 -0
- package/dist/bot/logic/mission/missionFactories.js +2 -0
- package/dist/bot/logic/mission/missionFactories.js.map +1 -1
- package/dist/bot/logic/mission/missions/attackMission.js +4 -4
- package/dist/bot/logic/mission/missions/attackMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/defenceMission.js +2 -1
- package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/engineerMission.js +34 -0
- package/dist/bot/logic/mission/missions/engineerMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/attackSquad.js +56 -63
- package/dist/bot/logic/squad/behaviours/combatSquad.js +18 -19
- package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/common.js +19 -2
- package/dist/bot/logic/squad/behaviours/common.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/defenceSquad.js +2 -15
- package/dist/bot/logic/squad/behaviours/engineerSquad.js +36 -0
- package/dist/bot/logic/squad/behaviours/engineerSquad.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js +22 -18
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +1 -1
- package/dist/bot/logic/squad/behaviours/squadExpansion.js +31 -0
- package/dist/bot/logic/squad/behaviours/squadScouters.js +8 -0
- package/dist/bot/logic/squad/squad.js +5 -8
- 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 +37 -25
- package/dist/bot/logic/squad/squadController.js.map +1 -1
- package/dist/bot/logic/threat/threatCalculator.js +4 -3
- package/dist/bot/logic/threat/threatCalculator.js.map +1 -1
- package/dist/exampleBot.js +6 -6
- package/dist/exampleBot.js.map +1 -1
- package/package.json +5 -9
- package/src/bot/bot.ts +8 -10
- package/src/bot/logic/awareness.ts +13 -17
- package/src/bot/logic/building/antiGroundStaticDefence.ts +13 -9
- package/src/bot/logic/building/artilleryUnit.ts +65 -0
- package/src/bot/logic/building/basicAirUnit.ts +10 -8
- package/src/bot/logic/building/basicBuilding.ts +1 -1
- package/src/bot/logic/building/basicGroundUnit.ts +4 -4
- package/src/bot/logic/building/{building.ts → buildingRules.ts} +94 -48
- package/src/bot/logic/building/harvester.ts +7 -4
- package/src/bot/logic/building/powerPlant.ts +1 -1
- package/src/bot/logic/building/queueController.ts +1 -1
- package/src/bot/logic/building/resourceCollectionBuilding.ts +8 -12
- package/src/bot/logic/common/scout.ts +83 -38
- package/src/bot/logic/common/utils.ts +65 -1
- package/src/bot/logic/map/map.ts +27 -31
- package/src/bot/logic/map/sector.ts +17 -21
- package/src/bot/logic/mission/missionFactories.ts +2 -0
- package/src/bot/logic/mission/missions/attackMission.ts +27 -27
- package/src/bot/logic/mission/missions/defenceMission.ts +3 -3
- package/src/bot/logic/mission/missions/engineerMission.ts +61 -0
- package/src/bot/logic/mission/missions/retreatMission.ts +2 -2
- package/src/bot/logic/squad/behaviours/combatSquad.ts +24 -26
- package/src/bot/logic/squad/behaviours/common.ts +33 -3
- package/src/bot/logic/squad/behaviours/engineerSquad.ts +53 -0
- package/src/bot/logic/squad/behaviours/retreatSquad.ts +2 -2
- package/src/bot/logic/squad/behaviours/scoutingSquad.ts +26 -28
- package/src/bot/logic/squad/squad.ts +8 -13
- package/src/bot/logic/squad/squadBehaviour.ts +9 -10
- package/src/bot/logic/squad/squadController.ts +2 -5
- package/src/bot/logic/threat/threat.ts +15 -15
- package/src/bot/logic/threat/threatCalculator.ts +4 -3
- package/src/exampleBot.ts +6 -6
- package/dist/bot/logic/awarenessImpl.js +0 -132
- package/dist/bot/logic/awarenessImpl.js.map +0 -1
- package/dist/bot/logic/building/ArtilleryUnit.js.map +0 -1
- package/dist/bot/logic/building/building.js.map +0 -1
- package/src/bot/logic/building/ArtilleryUnit.ts +0 -43
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GameApi, PlayerData,
|
|
1
|
+
import { GameApi, GameMath, PlayerData, Vector2 } from "@chronodivide/game-api";
|
|
2
2
|
import { Sector, SectorCache } from "../map/sector";
|
|
3
3
|
import { DebugLogger } from "./utils";
|
|
4
4
|
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
|
@@ -14,14 +14,18 @@ export const getUnseenStartingLocations = (gameApi: GameApi, playerData: PlayerD
|
|
|
14
14
|
return unseenStartingLocations;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
class PrioritisedScoutTarget {
|
|
18
|
-
private
|
|
17
|
+
export class PrioritisedScoutTarget {
|
|
18
|
+
private _targetPoint?: Vector2;
|
|
19
19
|
private _targetSector?: Sector;
|
|
20
20
|
private _priority: number;
|
|
21
21
|
|
|
22
|
-
constructor(
|
|
22
|
+
constructor(
|
|
23
|
+
priority: number,
|
|
24
|
+
target: Vector2 | Sector,
|
|
25
|
+
private permanent: boolean = false,
|
|
26
|
+
) {
|
|
23
27
|
if (target.hasOwnProperty("x") && target.hasOwnProperty("y")) {
|
|
24
|
-
this.
|
|
28
|
+
this._targetPoint = target as Vector2;
|
|
25
29
|
} else if (target.hasOwnProperty("sectorStartPoint")) {
|
|
26
30
|
this._targetSector = target as Sector;
|
|
27
31
|
} else {
|
|
@@ -34,49 +38,49 @@ class PrioritisedScoutTarget {
|
|
|
34
38
|
return this._priority;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
return this.
|
|
41
|
+
asVector2() {
|
|
42
|
+
return this._targetPoint ?? this._targetSector?.sectorStartPoint ?? null;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
get targetSector() {
|
|
42
46
|
return this._targetSector;
|
|
43
47
|
}
|
|
48
|
+
|
|
49
|
+
get isPermanent() {
|
|
50
|
+
return this.permanent;
|
|
51
|
+
}
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
const ENEMY_SPAWN_POINT_PRIORITY = 100;
|
|
47
55
|
|
|
48
56
|
// Amount of sectors around the starting sector to try to scout.
|
|
49
|
-
const
|
|
57
|
+
const NEARBY_SECTOR_STARTING_RADIUS = 2;
|
|
50
58
|
const NEARBY_SECTOR_BASE_PRIORITY = 1000;
|
|
51
59
|
|
|
60
|
+
// Amount of ticks per 'radius' to expand for scouting.
|
|
61
|
+
const SCOUTING_RADIUS_EXPANSION_TICKS = 9000; // 10 minutes
|
|
62
|
+
|
|
52
63
|
export class ScoutingManager {
|
|
53
64
|
private scoutingQueue: PriorityQueue<PrioritisedScoutTarget>;
|
|
54
65
|
|
|
66
|
+
private queuedRadius = NEARBY_SECTOR_STARTING_RADIUS;
|
|
67
|
+
|
|
55
68
|
constructor(private logger: DebugLogger) {
|
|
56
69
|
// Order by descending priority.
|
|
57
|
-
this.scoutingQueue = new PriorityQueue(
|
|
70
|
+
this.scoutingQueue = new PriorityQueue(
|
|
71
|
+
(a: PrioritisedScoutTarget, b: PrioritisedScoutTarget) => b.priority - a.priority,
|
|
72
|
+
);
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return tile ? !gameApi.mapApi.isVisibleTile(tile, playerData.name) : false;
|
|
70
|
-
})
|
|
71
|
-
.map((tile) => new PrioritisedScoutTarget(ENEMY_SPAWN_POINT_PRIORITY, tile))
|
|
72
|
-
.forEach((target) => {
|
|
73
|
-
this.logger(`Adding ${target.asPoint2D()?.x},${target.asPoint2D()?.y} to initial scouting queue`);
|
|
74
|
-
this.scoutingQueue.enqueue(target);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Queue nearby sectors.
|
|
78
|
-
const { x: startX, y: startY } = playerData.startLocation;
|
|
79
|
-
const { x: sectorsX, y: sectorsY } = sectorCache.getSectorBounds();
|
|
75
|
+
addRadiusToScout(
|
|
76
|
+
gameApi: GameApi,
|
|
77
|
+
centerPoint: Vector2,
|
|
78
|
+
sectorCache: SectorCache,
|
|
79
|
+
radius: number,
|
|
80
|
+
startingPriority: number,
|
|
81
|
+
) {
|
|
82
|
+
const { x: startX, y: startY } = centerPoint;
|
|
83
|
+
const { width: sectorsX, height: sectorsY } = sectorCache.getSectorBounds();
|
|
80
84
|
const startingSector = sectorCache.getSectorCoordinatesForWorldPosition(startX, startY);
|
|
81
85
|
|
|
82
86
|
if (!startingSector) {
|
|
@@ -84,24 +88,25 @@ export class ScoutingManager {
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
for (
|
|
87
|
-
let x: number = Math.max(0, startingSector.sectorX -
|
|
88
|
-
x
|
|
91
|
+
let x: number = Math.max(0, startingSector.sectorX - radius);
|
|
92
|
+
x < Math.min(sectorsX, startingSector.sectorX + radius);
|
|
89
93
|
++x
|
|
90
94
|
) {
|
|
91
95
|
for (
|
|
92
|
-
let y: number = Math.max(0, startingSector.sectorY -
|
|
93
|
-
y
|
|
96
|
+
let y: number = Math.max(0, startingSector.sectorY - radius);
|
|
97
|
+
y < Math.min(sectorsY, startingSector.sectorY + radius);
|
|
94
98
|
++y
|
|
95
99
|
) {
|
|
96
100
|
if (x === startingSector?.sectorX && y === startingSector?.sectorY) {
|
|
97
101
|
continue;
|
|
98
102
|
}
|
|
99
103
|
// Make it scout closer sectors first.
|
|
100
|
-
const distanceFactor =
|
|
104
|
+
const distanceFactor =
|
|
105
|
+
GameMath.pow(x - startingSector.sectorX, 2) + GameMath.pow(y - startingSector.sectorY, 2);
|
|
101
106
|
const sector = sectorCache.getSector(x, y);
|
|
102
107
|
if (sector) {
|
|
103
|
-
const maybeTarget = new PrioritisedScoutTarget(
|
|
104
|
-
const maybePoint = maybeTarget.
|
|
108
|
+
const maybeTarget = new PrioritisedScoutTarget(startingPriority - distanceFactor, sector);
|
|
109
|
+
const maybePoint = maybeTarget.asVector2();
|
|
105
110
|
if (maybePoint && gameApi.mapApi.getTile(maybePoint.x, maybePoint.y)) {
|
|
106
111
|
this.scoutingQueue.enqueue(maybeTarget);
|
|
107
112
|
}
|
|
@@ -110,12 +115,39 @@ export class ScoutingManager {
|
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
|
|
118
|
+
onGameStart(gameApi: GameApi, playerData: PlayerData, sectorCache: SectorCache) {
|
|
119
|
+
// Queue hostile starting locations with high priority and as permanent scouting candidates.
|
|
120
|
+
gameApi.mapApi
|
|
121
|
+
.getStartingLocations()
|
|
122
|
+
.filter((startingLocation) => {
|
|
123
|
+
if (startingLocation == playerData.startLocation) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
let tile = gameApi.mapApi.getTile(startingLocation.x, startingLocation.y);
|
|
127
|
+
return tile ? !gameApi.mapApi.isVisibleTile(tile, playerData.name) : false;
|
|
128
|
+
})
|
|
129
|
+
.map((tile) => new PrioritisedScoutTarget(ENEMY_SPAWN_POINT_PRIORITY, tile, true))
|
|
130
|
+
.forEach((target) => {
|
|
131
|
+
this.logger(`Adding ${target.asVector2()?.x},${target.asVector2()?.y} to initial scouting queue`);
|
|
132
|
+
this.scoutingQueue.enqueue(target);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Queue sectors near the spawn point.
|
|
136
|
+
this.addRadiusToScout(
|
|
137
|
+
gameApi,
|
|
138
|
+
playerData.startLocation,
|
|
139
|
+
sectorCache,
|
|
140
|
+
NEARBY_SECTOR_STARTING_RADIUS,
|
|
141
|
+
NEARBY_SECTOR_BASE_PRIORITY,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onAiUpdate(gameApi: GameApi, playerData: PlayerData, sectorCache: SectorCache) {
|
|
114
146
|
const currentHead = this.scoutingQueue.front();
|
|
115
147
|
if (!currentHead) {
|
|
116
148
|
return;
|
|
117
149
|
}
|
|
118
|
-
const head = currentHead.
|
|
150
|
+
const head = currentHead.asVector2();
|
|
119
151
|
if (!head) {
|
|
120
152
|
this.scoutingQueue.dequeue();
|
|
121
153
|
return;
|
|
@@ -126,6 +158,19 @@ export class ScoutingManager {
|
|
|
126
158
|
this.logger(`head point is visible, dequeueing`);
|
|
127
159
|
this.scoutingQueue.dequeue();
|
|
128
160
|
}
|
|
161
|
+
|
|
162
|
+
const requiredRadius = Math.floor(gameApi.getCurrentTick() / SCOUTING_RADIUS_EXPANSION_TICKS);
|
|
163
|
+
if (requiredRadius > this.queuedRadius) {
|
|
164
|
+
this.logger(`expanding scouting radius from ${this.queuedRadius} to ${requiredRadius}`);
|
|
165
|
+
this.addRadiusToScout(
|
|
166
|
+
gameApi,
|
|
167
|
+
playerData.startLocation,
|
|
168
|
+
sectorCache,
|
|
169
|
+
requiredRadius,
|
|
170
|
+
NEARBY_SECTOR_BASE_PRIORITY,
|
|
171
|
+
);
|
|
172
|
+
this.queuedRadius = requiredRadius;
|
|
173
|
+
}
|
|
129
174
|
}
|
|
130
175
|
|
|
131
176
|
getNewScoutTarget() {
|
|
@@ -1 +1,65 @@
|
|
|
1
|
-
export type DebugLogger = (message: string, sayInGame?: boolean) => void;
|
|
1
|
+
export type DebugLogger = (message: string, sayInGame?: boolean) => void;
|
|
2
|
+
|
|
3
|
+
// Thanks use-strict!
|
|
4
|
+
export function formatTimeDuration(timeSeconds: number, skipZeroHours = false) {
|
|
5
|
+
let h = Math.floor(timeSeconds / 3600);
|
|
6
|
+
timeSeconds -= h * 3600;
|
|
7
|
+
let m = Math.floor(timeSeconds / 60);
|
|
8
|
+
timeSeconds -= m * 60;
|
|
9
|
+
let s = Math.floor(timeSeconds);
|
|
10
|
+
|
|
11
|
+
return [...(h || !skipZeroHours ? [h] : []), pad(m, "00"), pad(s, "00")].join(":");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function pad(n: any, format = "0000") {
|
|
15
|
+
let str = "" + n;
|
|
16
|
+
return format.substring(0, format.length - str.length) + str;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function maxBy<T>(array: T[], predicate: (arg: T) => number | null): T | null {
|
|
20
|
+
if (array.length === 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
let maxIdx = 0;
|
|
24
|
+
let maxVal = predicate(array[0]);
|
|
25
|
+
for (let i = 1; i < array.length; ++i) {
|
|
26
|
+
const newVal = predicate(array[i]);
|
|
27
|
+
if (maxVal === null || (newVal !== null && newVal > maxVal)) {
|
|
28
|
+
maxIdx = i;
|
|
29
|
+
maxVal = newVal;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return array[maxIdx];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function uniqBy<T>(array: T[], predicate: (arg: T) => string | number): T[] {
|
|
36
|
+
return Object.values(
|
|
37
|
+
array.reduce(
|
|
38
|
+
(prev, newVal) => {
|
|
39
|
+
const val = predicate(newVal);
|
|
40
|
+
if (!prev[val]) {
|
|
41
|
+
prev[val] = newVal;
|
|
42
|
+
}
|
|
43
|
+
return prev;
|
|
44
|
+
},
|
|
45
|
+
{} as Record<string, T>,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function countBy<T>(array: T[], predicate: (arg: T) => string | undefined): { [key: string]: number } {
|
|
51
|
+
return array.reduce(
|
|
52
|
+
(prev, newVal) => {
|
|
53
|
+
const val = predicate(newVal);
|
|
54
|
+
if (val === undefined) {
|
|
55
|
+
return prev;
|
|
56
|
+
}
|
|
57
|
+
if (!prev[val]) {
|
|
58
|
+
prev[val] = 0;
|
|
59
|
+
}
|
|
60
|
+
prev[val] = prev[val] + 1;
|
|
61
|
+
return prev;
|
|
62
|
+
},
|
|
63
|
+
{} as Record<string, number>,
|
|
64
|
+
);
|
|
65
|
+
}
|
package/src/bot/logic/map/map.ts
CHANGED
|
@@ -1,27 +1,15 @@
|
|
|
1
|
-
import { GameApi, MapApi, PlayerData,
|
|
2
|
-
import
|
|
1
|
+
import { GameApi, GameMath, MapApi, PlayerData, Size, Tile, UnitData, Vector2 } from "@chronodivide/game-api";
|
|
2
|
+
import { maxBy } from "../common/utils.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// Expensive one-time call to determine the size of the map.
|
|
7
|
-
// The result is a point just outside the bounds of the map.
|
|
8
|
-
export function determineMapBounds(mapApi: MapApi): Point2D {
|
|
9
|
-
// Probably want to ask for an API change to get this.
|
|
10
|
-
// Note that the maps is not always a rectangle!
|
|
11
|
-
const zeroTile = { rx: 0, ry: 0 } as Tile;
|
|
12
|
-
const allTiles = mapApi.getTilesInRect(zeroTile, { width: MAX_WIDTH_AND_HEIGHT, height: MAX_WIDTH_AND_HEIGHT });
|
|
13
|
-
|
|
14
|
-
const maxX = _.maxBy(allTiles, (tile) => tile.rx)?.rx!;
|
|
15
|
-
const maxY = _.maxBy(allTiles, (tile) => tile.ry)?.ry!;
|
|
16
|
-
|
|
17
|
-
return { x: maxX, y: maxY };
|
|
4
|
+
export function determineMapBounds(mapApi: MapApi): Size {
|
|
5
|
+
return mapApi.getRealMapSize();
|
|
18
6
|
}
|
|
19
7
|
|
|
20
8
|
export function calculateAreaVisibility(
|
|
21
9
|
mapApi: MapApi,
|
|
22
10
|
playerData: PlayerData,
|
|
23
|
-
startPoint:
|
|
24
|
-
endPoint:
|
|
11
|
+
startPoint: Vector2,
|
|
12
|
+
endPoint: Vector2,
|
|
25
13
|
): { visibleTiles: number; validTiles: number } {
|
|
26
14
|
let validTiles: number = 0,
|
|
27
15
|
visibleTiles: number = 0;
|
|
@@ -42,29 +30,37 @@ export function calculateAreaVisibility(
|
|
|
42
30
|
|
|
43
31
|
export function getPointTowardsOtherPoint(
|
|
44
32
|
gameApi: GameApi,
|
|
45
|
-
startLocation:
|
|
46
|
-
endLocation:
|
|
33
|
+
startLocation: Vector2,
|
|
34
|
+
endLocation: Vector2,
|
|
47
35
|
minRadius: number,
|
|
48
36
|
maxRadius: number,
|
|
49
37
|
randomAngle: number,
|
|
50
|
-
):
|
|
38
|
+
): Vector2 {
|
|
39
|
+
// TODO: Use proper vector maths here.
|
|
51
40
|
let radius = minRadius + Math.round(gameApi.generateRandom() * (maxRadius - minRadius));
|
|
52
|
-
let
|
|
41
|
+
let directionToEndLocation = GameMath.atan2(endLocation.y - startLocation.y, endLocation.x - startLocation.x);
|
|
53
42
|
let randomisedDirection =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
43
|
+
directionToEndLocation -
|
|
44
|
+
(randomAngle * (Math.PI / 12) + 2 * randomAngle * gameApi.generateRandom() * (Math.PI / 12));
|
|
45
|
+
let candidatePointX = Math.round(startLocation.x + GameMath.cos(randomisedDirection) * radius);
|
|
46
|
+
let candidatePointY = Math.round(startLocation.y + GameMath.sin(randomisedDirection) * radius);
|
|
47
|
+
return new Vector2(candidatePointX, candidatePointY);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getDistanceBetweenPoints(startLocation: Vector2, endLocation: Vector2): number {
|
|
51
|
+
// TODO: Remove this now we have Vector2s.
|
|
52
|
+
return startLocation.distanceTo(endLocation);
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
export function
|
|
61
|
-
|
|
55
|
+
export function getDistanceBetweenTileAndPoint(tile: Tile, vector: Vector2): number {
|
|
56
|
+
// TODO: Remove this now we have Vector2s.
|
|
57
|
+
return new Vector2(tile.rx, tile.ry).distanceTo(vector);
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
export function getDistanceBetweenUnits(unit1: UnitData, unit2: UnitData): number {
|
|
65
|
-
return
|
|
61
|
+
return new Vector2(unit1.tile.rx, unit1.tile.ry).distanceTo(new Vector2(unit2.tile.rx, unit2.tile.ry));
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
export function getDistanceBetween(unit: UnitData, point:
|
|
69
|
-
return getDistanceBetweenPoints(
|
|
64
|
+
export function getDistanceBetween(unit: UnitData, point: Vector2): number {
|
|
65
|
+
return getDistanceBetweenPoints(new Vector2(unit.tile.rx, unit.tile.ry), point);
|
|
70
66
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A sector is a uniform-sized segment of the map.
|
|
2
2
|
|
|
3
|
-
import { MapApi, PlayerData,
|
|
3
|
+
import { MapApi, PlayerData, Size, Tile, Vector2 } from "@chronodivide/game-api";
|
|
4
4
|
import { calculateAreaVisibility } from "./map.js";
|
|
5
5
|
|
|
6
6
|
export const SECTOR_SIZE = 8;
|
|
@@ -11,7 +11,7 @@ export class Sector {
|
|
|
11
11
|
private sectorLastExploredAt: number | undefined;
|
|
12
12
|
|
|
13
13
|
constructor(
|
|
14
|
-
public sectorStartPoint:
|
|
14
|
+
public sectorStartPoint: Vector2,
|
|
15
15
|
public sectorStartTile: Tile | undefined,
|
|
16
16
|
public sectorVisibilityPct: number | undefined,
|
|
17
17
|
public sectorVisibilityLastCheckTick: number | undefined,
|
|
@@ -40,23 +40,25 @@ export class Sector {
|
|
|
40
40
|
|
|
41
41
|
export class SectorCache {
|
|
42
42
|
private sectors: Sector[][] = [];
|
|
43
|
-
private mapBounds:
|
|
43
|
+
private mapBounds: Size;
|
|
44
44
|
private sectorsX: number;
|
|
45
45
|
private sectorsY: number;
|
|
46
46
|
private lastUpdatedSectorX: number | undefined;
|
|
47
47
|
private lastUpdatedSectorY: number | undefined;
|
|
48
48
|
|
|
49
|
-
constructor(mapApi: MapApi, mapBounds:
|
|
49
|
+
constructor(mapApi: MapApi, mapBounds: Size) {
|
|
50
50
|
this.mapBounds = mapBounds;
|
|
51
|
-
this.sectorsX = Math.ceil(mapBounds.
|
|
52
|
-
this.sectorsY = Math.ceil(mapBounds.
|
|
51
|
+
this.sectorsX = Math.ceil(mapBounds.width / SECTOR_SIZE);
|
|
52
|
+
this.sectorsY = Math.ceil(mapBounds.height / SECTOR_SIZE);
|
|
53
53
|
this.sectors = new Array(this.sectorsX);
|
|
54
54
|
for (let xx = 0; xx < this.sectorsX; ++xx) {
|
|
55
55
|
this.sectors[xx] = new Array(this.sectorsY);
|
|
56
56
|
for (let yy = 0; yy < this.sectorsY; ++yy) {
|
|
57
|
+
const tileX = xx * SECTOR_SIZE;
|
|
58
|
+
const tileY = yy * SECTOR_SIZE;
|
|
57
59
|
this.sectors[xx][yy] = new Sector(
|
|
58
|
-
|
|
59
|
-
mapApi.getTile(
|
|
60
|
+
new Vector2(tileX, tileY),
|
|
61
|
+
mapApi.getTile(tileX, tileY),
|
|
60
62
|
undefined,
|
|
61
63
|
undefined,
|
|
62
64
|
);
|
|
@@ -64,7 +66,7 @@ export class SectorCache {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
public getMapBounds():
|
|
69
|
+
public getMapBounds(): Size {
|
|
68
70
|
return this.mapBounds;
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -86,10 +88,7 @@ export class SectorCache {
|
|
|
86
88
|
if (sector) {
|
|
87
89
|
sector.sectorVisibilityLastCheckTick = currentGameTick;
|
|
88
90
|
let sp = sector.sectorStartPoint;
|
|
89
|
-
let ep =
|
|
90
|
-
x: sector.sectorStartPoint.x + SECTOR_SIZE,
|
|
91
|
-
y: sector.sectorStartPoint.y + SECTOR_SIZE,
|
|
92
|
-
};
|
|
91
|
+
let ep = new Vector2(sp.x + SECTOR_SIZE, sp.y + SECTOR_SIZE);
|
|
93
92
|
let visibility = calculateAreaVisibility(mapApi, playerData, sp, ep);
|
|
94
93
|
if (visibility.validTiles > 0) {
|
|
95
94
|
sector.sectorVisibilityPct = visibility.visibleTiles / visibility.validTiles;
|
|
@@ -151,21 +150,18 @@ export class SectorCache {
|
|
|
151
150
|
return this.sectors[sectorX][sectorY];
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
public getSectorBounds():
|
|
155
|
-
return {
|
|
156
|
-
x: this.sectorsX,
|
|
157
|
-
y: this.sectorsY,
|
|
158
|
-
}
|
|
153
|
+
public getSectorBounds(): Size {
|
|
154
|
+
return { width: this.sectorsX, height: this.sectorsY };
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
public getSectorCoordinatesForWorldPosition(x: number, y: number) {
|
|
162
|
-
if (x < 0 || x >= this.mapBounds.
|
|
158
|
+
if (x < 0 || x >= this.mapBounds.width || y < 0 || y >= this.mapBounds.height) {
|
|
163
159
|
return undefined;
|
|
164
160
|
}
|
|
165
161
|
return {
|
|
166
162
|
sectorX: Math.floor(x / SECTOR_SIZE),
|
|
167
|
-
sectorY: Math.floor(y / SECTOR_SIZE)
|
|
168
|
-
}
|
|
163
|
+
sectorY: Math.floor(y / SECTOR_SIZE),
|
|
164
|
+
};
|
|
169
165
|
}
|
|
170
166
|
|
|
171
167
|
public getSectorForWorldPosition(x: number, y: number): Sector | undefined {
|
|
@@ -7,6 +7,7 @@ import { AttackMissionFactory } from "./missions/attackMission.js";
|
|
|
7
7
|
import { MissionController } from "./missionController.js";
|
|
8
8
|
import { DefenceMissionFactory } from "./missions/defenceMission.js";
|
|
9
9
|
import { DebugLogger } from "../common/utils.js";
|
|
10
|
+
import { EngineerMissionFactory } from "./missions/engineerMission.js";
|
|
10
11
|
|
|
11
12
|
export interface MissionFactory {
|
|
12
13
|
getName(): string;
|
|
@@ -46,4 +47,5 @@ export const createMissionFactories = () => [
|
|
|
46
47
|
new ScoutingMissionFactory(),
|
|
47
48
|
new AttackMissionFactory(),
|
|
48
49
|
new DefenceMissionFactory(),
|
|
50
|
+
new EngineerMissionFactory(),
|
|
49
51
|
];
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OneTimeMission } from "./oneTimeMission.js";
|
|
1
|
+
import { GameApi, ObjectType, PlayerData, UnitData, Vector2 } from "@chronodivide/game-api";
|
|
3
2
|
import { CombatSquad } from "../../squad/behaviours/combatSquad.js";
|
|
4
3
|
import { Mission, MissionAction, disbandMission, noop } from "../mission.js";
|
|
5
|
-
import { GlobalThreat } from "../../threat/threat.js";
|
|
6
4
|
import { Squad } from "../../squad/squad.js";
|
|
7
|
-
import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../map/map.js";
|
|
8
5
|
import { MissionFactory } from "../missionFactories.js";
|
|
9
6
|
import { MatchAwareness } from "../../awareness.js";
|
|
10
7
|
import { MissionController } from "../missionController.js";
|
|
11
|
-
import { match } from "assert";
|
|
12
8
|
import { RetreatMission } from "./retreatMission.js";
|
|
13
|
-
import
|
|
14
|
-
import { DebugLogger } from "../../common/utils.js";
|
|
9
|
+
import { DebugLogger, maxBy } from "../../common/utils.js";
|
|
15
10
|
|
|
16
11
|
export enum AttackFailReason {
|
|
17
12
|
NoTargets = 0,
|
|
@@ -29,10 +24,10 @@ export class AttackMission extends Mission<AttackFailReason> {
|
|
|
29
24
|
constructor(
|
|
30
25
|
uniqueName: string,
|
|
31
26
|
priority: number,
|
|
32
|
-
private rallyArea:
|
|
33
|
-
private attackArea:
|
|
27
|
+
private rallyArea: Vector2,
|
|
28
|
+
private attackArea: Vector2,
|
|
34
29
|
private radius: number,
|
|
35
|
-
logger: DebugLogger
|
|
30
|
+
logger: DebugLogger,
|
|
36
31
|
) {
|
|
37
32
|
super(uniqueName, priority, logger);
|
|
38
33
|
}
|
|
@@ -81,7 +76,7 @@ export class AttackMissionFactory implements MissionFactory {
|
|
|
81
76
|
return "AttackMissionFactory";
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
generateTarget(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness):
|
|
79
|
+
generateTarget(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): Vector2 | null {
|
|
85
80
|
// Randomly decide between harvester and base.
|
|
86
81
|
try {
|
|
87
82
|
const tryFocusHarvester = gameApi.generateRandomInt(0, 1) === 0;
|
|
@@ -90,9 +85,9 @@ export class AttackMissionFactory implements MissionFactory {
|
|
|
90
85
|
.map((unitId) => gameApi.getUnitData(unitId))
|
|
91
86
|
.filter((u) => !!u && gameApi.getPlayerData(u.owner).isCombatant) as UnitData[];
|
|
92
87
|
|
|
93
|
-
const maxUnit =
|
|
88
|
+
const maxUnit = maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));
|
|
94
89
|
if (maxUnit) {
|
|
95
|
-
return
|
|
90
|
+
return new Vector2(maxUnit.tile.rx, maxUnit.tile.ry);
|
|
96
91
|
}
|
|
97
92
|
} catch (err) {
|
|
98
93
|
// There's a crash here when accessing a building that got destroyed. Will catch and ignore or now.
|
|
@@ -106,7 +101,7 @@ export class AttackMissionFactory implements MissionFactory {
|
|
|
106
101
|
playerData: PlayerData,
|
|
107
102
|
matchAwareness: MatchAwareness,
|
|
108
103
|
missionController: MissionController,
|
|
109
|
-
logger: DebugLogger
|
|
104
|
+
logger: DebugLogger,
|
|
110
105
|
): void {
|
|
111
106
|
if (!matchAwareness.shouldAttack()) {
|
|
112
107
|
return;
|
|
@@ -128,19 +123,24 @@ export class AttackMissionFactory implements MissionFactory {
|
|
|
128
123
|
const squadName = "globalAttack";
|
|
129
124
|
|
|
130
125
|
const tryAttack = missionController.addMission(
|
|
131
|
-
new AttackMission(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
126
|
+
new AttackMission(
|
|
127
|
+
squadName,
|
|
128
|
+
100,
|
|
129
|
+
matchAwareness.getMainRallyPoint(),
|
|
130
|
+
attackArea,
|
|
131
|
+
attackRadius,
|
|
132
|
+
logger,
|
|
133
|
+
).then((reason, squad) => {
|
|
134
|
+
missionController.addMission(
|
|
135
|
+
new RetreatMission(
|
|
136
|
+
"retreat-from-" + squadName + gameApi.getCurrentTick(),
|
|
137
|
+
100,
|
|
138
|
+
matchAwareness.getMainRallyPoint(),
|
|
139
|
+
squad?.getUnitIds() ?? [],
|
|
140
|
+
logger,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}),
|
|
144
144
|
);
|
|
145
145
|
if (tryAttack) {
|
|
146
146
|
this.lastAttackAt = gameApi.getCurrentTick();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GameApi, PlayerData,
|
|
1
|
+
import { GameApi, PlayerData, Vector2 } from "@chronodivide/game-api";
|
|
2
2
|
import { MatchAwareness } from "../../awareness.js";
|
|
3
3
|
import { MissionController } from "../missionController.js";
|
|
4
4
|
import { Mission, MissionAction, disbandMission, noop } from "../mission.js";
|
|
@@ -21,7 +21,7 @@ export class DefenceMission extends Mission<DefenceFailReason> {
|
|
|
21
21
|
constructor(
|
|
22
22
|
uniqueName: string,
|
|
23
23
|
priority: number,
|
|
24
|
-
private defenceArea:
|
|
24
|
+
private defenceArea: Vector2,
|
|
25
25
|
private radius: number,
|
|
26
26
|
logger: DebugLogger,
|
|
27
27
|
) {
|
|
@@ -46,7 +46,7 @@ export class DefenceMission extends Mission<DefenceFailReason> {
|
|
|
46
46
|
foundTargets.length
|
|
47
47
|
} found in area ${this.radius})`,
|
|
48
48
|
);
|
|
49
|
-
this.combatSquad?.setAttackArea(
|
|
49
|
+
this.combatSquad?.setAttackArea(new Vector2(foundTargets[0].x, foundTargets[0].y));
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
return noop();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { GlobalThreat } from "../../threat/threat.js";
|
|
3
|
+
import { Mission } from "../mission.js";
|
|
4
|
+
import { ExpansionSquad } from "../../squad/behaviours/expansionSquad.js";
|
|
5
|
+
import { MissionFactory } from "../missionFactories.js";
|
|
6
|
+
import { OneTimeMission } from "./oneTimeMission.js";
|
|
7
|
+
import { MatchAwareness } from "../../awareness.js";
|
|
8
|
+
import { MissionController } from "../missionController.js";
|
|
9
|
+
import { DebugLogger } from "../../common/utils.js";
|
|
10
|
+
import { EngineerSquad } from "../../squad/behaviours/engineerSquad.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A mission that tries to send an engineer into a building (e.g. to capture tech building or repair bridge)
|
|
14
|
+
*/
|
|
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);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Only try to capture tech buildings within this radius of the starting point.
|
|
23
|
+
const MAX_TECH_CAPTURE_RADIUS = 50;
|
|
24
|
+
|
|
25
|
+
const TECH_CHECK_INTERVAL_TICKS = 300;
|
|
26
|
+
|
|
27
|
+
export class EngineerMissionFactory implements MissionFactory {
|
|
28
|
+
private lastCheckAt = 0;
|
|
29
|
+
|
|
30
|
+
getName(): string {
|
|
31
|
+
return "EngineerMissionFactory";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
maybeCreateMissions(
|
|
35
|
+
gameApi: GameApi,
|
|
36
|
+
playerData: PlayerData,
|
|
37
|
+
matchAwareness: MatchAwareness,
|
|
38
|
+
missionController: MissionController,
|
|
39
|
+
logger: DebugLogger
|
|
40
|
+
): void {
|
|
41
|
+
if (!(gameApi.getCurrentTick() > this.lastCheckAt + TECH_CHECK_INTERVAL_TICKS)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.lastCheckAt = gameApi.getCurrentTick();
|
|
45
|
+
const eligibleTechBuildings = gameApi.getVisibleUnits(playerData.name, "hostile", (r) => r.capturable && r.produceCashAmount > 0);
|
|
46
|
+
|
|
47
|
+
eligibleTechBuildings.forEach((techBuildingId) => {
|
|
48
|
+
missionController.addMission(new EngineerMission("capture-" + techBuildingId, 100, techBuildingId, logger));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onMissionFailed(
|
|
53
|
+
gameApi: GameApi,
|
|
54
|
+
playerData: PlayerData,
|
|
55
|
+
matchAwareness: MatchAwareness,
|
|
56
|
+
failedMission: Mission,
|
|
57
|
+
failureReason: undefined,
|
|
58
|
+
missionController: MissionController,
|
|
59
|
+
): void {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Point2D } from "@chronodivide/game-api";
|
|
2
1
|
import { OneTimeMission } from "./oneTimeMission.js";
|
|
3
2
|
import { RetreatSquad } from "../../squad/behaviours/retreatSquad.js";
|
|
4
3
|
import { DebugLogger } from "../../common/utils.js";
|
|
4
|
+
import { Vector2 } from "@chronodivide/game-api";
|
|
5
5
|
|
|
6
6
|
export class RetreatMission extends OneTimeMission {
|
|
7
|
-
constructor(uniqueName: string, priority: number, retreatToPoint:
|
|
7
|
+
constructor(uniqueName: string, priority: number, retreatToPoint: Vector2, unitIds: number[], logger: DebugLogger) {
|
|
8
8
|
super(uniqueName, priority, () => new RetreatSquad(unitIds, retreatToPoint), logger);
|
|
9
9
|
}
|
|
10
10
|
}
|