@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.
Files changed (108) hide show
  1. package/.prettierrc +5 -5
  2. package/TODO.md +18 -0
  3. package/dist/bot/bot.js +4 -4
  4. package/dist/bot/bot.js.map +1 -1
  5. package/dist/bot/logic/awareness.js +8 -8
  6. package/dist/bot/logic/awareness.js.map +1 -1
  7. package/dist/bot/logic/building/ArtilleryUnit.js +30 -9
  8. package/dist/bot/logic/building/antiGroundStaticDefence.js +2 -2
  9. package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -1
  10. package/dist/bot/logic/building/artilleryUnit.js.map +1 -0
  11. package/dist/bot/logic/building/basicAirUnit.js +3 -2
  12. package/dist/bot/logic/building/basicAirUnit.js.map +1 -1
  13. package/dist/bot/logic/building/basicBuilding.js +1 -1
  14. package/dist/bot/logic/building/basicBuilding.js.map +1 -1
  15. package/dist/bot/logic/building/basicGroundUnit.js +4 -3
  16. package/dist/bot/logic/building/basicGroundUnit.js.map +1 -1
  17. package/dist/bot/logic/building/building.js +11 -55
  18. package/dist/bot/logic/building/buildingRules.js +162 -0
  19. package/dist/bot/logic/building/buildingRules.js.map +1 -0
  20. package/dist/bot/logic/building/harvester.js.map +1 -1
  21. package/dist/bot/logic/building/massedAntiGroundUnit.js +20 -0
  22. package/dist/bot/logic/building/powerPlant.js +1 -1
  23. package/dist/bot/logic/building/powerPlant.js.map +1 -1
  24. package/dist/bot/logic/building/queueController.js +1 -1
  25. package/dist/bot/logic/building/queueController.js.map +1 -1
  26. package/dist/bot/logic/building/queues.js +19 -0
  27. package/dist/bot/logic/building/resourceCollectionBuilding.js +5 -3
  28. package/dist/bot/logic/building/resourceCollectionBuilding.js.map +1 -1
  29. package/dist/bot/logic/common/scout.js +49 -32
  30. package/dist/bot/logic/common/scout.js.map +1 -1
  31. package/dist/bot/logic/common/utils.js +50 -1
  32. package/dist/bot/logic/common/utils.js.map +1 -1
  33. package/dist/bot/logic/knowledge.js +1 -0
  34. package/dist/bot/logic/map/map.js +17 -19
  35. package/dist/bot/logic/map/map.js.map +1 -1
  36. package/dist/bot/logic/map/sector.js +10 -13
  37. package/dist/bot/logic/map/sector.js.map +1 -1
  38. package/dist/bot/logic/mission/basicMission.js +26 -0
  39. package/dist/bot/logic/mission/expansionMission.js +32 -0
  40. package/dist/bot/logic/mission/missionFactories.js +2 -0
  41. package/dist/bot/logic/mission/missionFactories.js.map +1 -1
  42. package/dist/bot/logic/mission/missions/attackMission.js +4 -4
  43. package/dist/bot/logic/mission/missions/attackMission.js.map +1 -1
  44. package/dist/bot/logic/mission/missions/defenceMission.js +2 -1
  45. package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -1
  46. package/dist/bot/logic/mission/missions/engineerMission.js +34 -0
  47. package/dist/bot/logic/mission/missions/engineerMission.js.map +1 -0
  48. package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -1
  49. package/dist/bot/logic/squad/behaviours/attackSquad.js +56 -63
  50. package/dist/bot/logic/squad/behaviours/combatSquad.js +18 -19
  51. package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -1
  52. package/dist/bot/logic/squad/behaviours/common.js +19 -2
  53. package/dist/bot/logic/squad/behaviours/common.js.map +1 -1
  54. package/dist/bot/logic/squad/behaviours/defenceSquad.js +2 -15
  55. package/dist/bot/logic/squad/behaviours/engineerSquad.js +36 -0
  56. package/dist/bot/logic/squad/behaviours/engineerSquad.js.map +1 -0
  57. package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +1 -1
  58. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +22 -18
  59. package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +1 -1
  60. package/dist/bot/logic/squad/behaviours/squadExpansion.js +31 -0
  61. package/dist/bot/logic/squad/behaviours/squadScouters.js +8 -0
  62. package/dist/bot/logic/squad/squad.js +5 -8
  63. package/dist/bot/logic/squad/squad.js.map +1 -1
  64. package/dist/bot/logic/squad/squadBehaviour.js.map +1 -1
  65. package/dist/bot/logic/squad/squadController.js +37 -25
  66. package/dist/bot/logic/squad/squadController.js.map +1 -1
  67. package/dist/bot/logic/threat/threatCalculator.js +4 -3
  68. package/dist/bot/logic/threat/threatCalculator.js.map +1 -1
  69. package/dist/exampleBot.js +6 -6
  70. package/dist/exampleBot.js.map +1 -1
  71. package/package.json +5 -9
  72. package/src/bot/bot.ts +8 -10
  73. package/src/bot/logic/awareness.ts +13 -17
  74. package/src/bot/logic/building/antiGroundStaticDefence.ts +13 -9
  75. package/src/bot/logic/building/artilleryUnit.ts +65 -0
  76. package/src/bot/logic/building/basicAirUnit.ts +10 -8
  77. package/src/bot/logic/building/basicBuilding.ts +1 -1
  78. package/src/bot/logic/building/basicGroundUnit.ts +4 -4
  79. package/src/bot/logic/building/{building.ts → buildingRules.ts} +94 -48
  80. package/src/bot/logic/building/harvester.ts +7 -4
  81. package/src/bot/logic/building/powerPlant.ts +1 -1
  82. package/src/bot/logic/building/queueController.ts +1 -1
  83. package/src/bot/logic/building/resourceCollectionBuilding.ts +8 -12
  84. package/src/bot/logic/common/scout.ts +83 -38
  85. package/src/bot/logic/common/utils.ts +65 -1
  86. package/src/bot/logic/map/map.ts +27 -31
  87. package/src/bot/logic/map/sector.ts +17 -21
  88. package/src/bot/logic/mission/missionFactories.ts +2 -0
  89. package/src/bot/logic/mission/missions/attackMission.ts +27 -27
  90. package/src/bot/logic/mission/missions/defenceMission.ts +3 -3
  91. package/src/bot/logic/mission/missions/engineerMission.ts +61 -0
  92. package/src/bot/logic/mission/missions/retreatMission.ts +2 -2
  93. package/src/bot/logic/squad/behaviours/combatSquad.ts +24 -26
  94. package/src/bot/logic/squad/behaviours/common.ts +33 -3
  95. package/src/bot/logic/squad/behaviours/engineerSquad.ts +53 -0
  96. package/src/bot/logic/squad/behaviours/retreatSquad.ts +2 -2
  97. package/src/bot/logic/squad/behaviours/scoutingSquad.ts +26 -28
  98. package/src/bot/logic/squad/squad.ts +8 -13
  99. package/src/bot/logic/squad/squadBehaviour.ts +9 -10
  100. package/src/bot/logic/squad/squadController.ts +2 -5
  101. package/src/bot/logic/threat/threat.ts +15 -15
  102. package/src/bot/logic/threat/threatCalculator.ts +4 -3
  103. package/src/exampleBot.ts +6 -6
  104. package/dist/bot/logic/awarenessImpl.js +0 -132
  105. package/dist/bot/logic/awarenessImpl.js.map +0 -1
  106. package/dist/bot/logic/building/ArtilleryUnit.js.map +0 -1
  107. package/dist/bot/logic/building/building.js.map +0 -1
  108. package/src/bot/logic/building/ArtilleryUnit.ts +0 -43
@@ -1,4 +1,4 @@
1
- import { GameApi, PlayerData, Point2D } from "@chronodivide/game-api";
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 _targetPoint2D?: Point2D;
17
+ export class PrioritisedScoutTarget {
18
+ private _targetPoint?: Vector2;
19
19
  private _targetSector?: Sector;
20
20
  private _priority: number;
21
21
 
22
- constructor(priority: number, target: Point2D | Sector) {
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._targetPoint2D = target as Point2D;
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
- asPoint2D() {
38
- return this._targetPoint2D ?? this._targetSector?.sectorStartPoint ?? null;
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 NEARBY_SECTOR_RADIUS = 2;
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((a: PrioritisedScoutTarget, b: PrioritisedScoutTarget) => b.priority - a.priority);
70
+ this.scoutingQueue = new PriorityQueue(
71
+ (a: PrioritisedScoutTarget, b: PrioritisedScoutTarget) => b.priority - a.priority,
72
+ );
58
73
  }
59
74
 
60
- onGameStart(gameApi: GameApi, playerData: PlayerData, sectorCache: SectorCache) {
61
- // Queue hostile starting locations with high priority.
62
- gameApi.mapApi
63
- .getStartingLocations()
64
- .filter((startingLocation) => {
65
- if (startingLocation == playerData.startLocation) {
66
- return false;
67
- }
68
- let tile = gameApi.mapApi.getTile(startingLocation.x, startingLocation.y);
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 - NEARBY_SECTOR_RADIUS);
88
- x <= Math.min(sectorsX, startingSector.sectorX + NEARBY_SECTOR_RADIUS);
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 - NEARBY_SECTOR_RADIUS);
93
- y <= Math.min(sectorsY, startingSector.sectorY + NEARBY_SECTOR_RADIUS);
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 = Math.pow(x - startingSector.sectorX, 2) + Math.pow(y - startingSector.sectorY, 2);
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(NEARBY_SECTOR_BASE_PRIORITY - distanceFactor, sector);
104
- const maybePoint = maybeTarget.asPoint2D();
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
- onAiUpdate(gameApi: GameApi, playerData: PlayerData) {
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.asPoint2D();
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
+ }
@@ -1,27 +1,15 @@
1
- import { GameApi, MapApi, PlayerData, Point2D, Tile, UnitData } from "@chronodivide/game-api";
2
- import _ from "lodash";
1
+ import { GameApi, GameMath, MapApi, PlayerData, Size, Tile, UnitData, Vector2 } from "@chronodivide/game-api";
2
+ import { maxBy } from "../common/utils.js";
3
3
 
4
- const MAX_WIDTH_AND_HEIGHT = 500;
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: Point2D,
24
- endPoint: Point2D,
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: Point2D,
46
- endLocation: Point2D,
33
+ startLocation: Vector2,
34
+ endLocation: Vector2,
47
35
  minRadius: number,
48
36
  maxRadius: number,
49
37
  randomAngle: number,
50
- ): Point2D {
38
+ ): Vector2 {
39
+ // TODO: Use proper vector maths here.
51
40
  let radius = minRadius + Math.round(gameApi.generateRandom() * (maxRadius - minRadius));
52
- let directionToSpawn = Math.atan2(endLocation.y - startLocation.y, endLocation.x - startLocation.x);
41
+ let directionToEndLocation = GameMath.atan2(endLocation.y - startLocation.y, endLocation.x - startLocation.x);
53
42
  let randomisedDirection =
54
- directionToSpawn - (randomAngle * (Math.PI / 12) + 2 * randomAngle * gameApi.generateRandom() * (Math.PI / 12));
55
- let candidatePointX = Math.round(startLocation.x + Math.cos(randomisedDirection) * radius);
56
- let candidatePointY = Math.round(startLocation.y + Math.sin(randomisedDirection) * radius);
57
- return { x: candidatePointX, y: candidatePointY };
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 getDistanceBetweenPoints(startLocation: Point2D, endLocation: Point2D): number {
61
- return Math.sqrt((startLocation.x - endLocation.x) ** 2 + (startLocation.y - endLocation.y) ** 2);
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 getDistanceBetweenPoints({ x: unit1.tile.rx, y: unit1.tile.ry }, { x: unit2.tile.rx, y: unit2.tile.ry });
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: Point2D): number {
69
- return getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, point);
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, Point2D, Tile } from "@chronodivide/game-api";
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: Point2D,
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: Point2D;
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: Point2D) {
49
+ constructor(mapApi: MapApi, mapBounds: Size) {
50
50
  this.mapBounds = mapBounds;
51
- this.sectorsX = Math.ceil(mapBounds.x / SECTOR_SIZE);
52
- this.sectorsY = Math.ceil(mapBounds.y / SECTOR_SIZE);
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
- { x: xx * SECTOR_SIZE, y: yy * SECTOR_SIZE },
59
- mapApi.getTile(xx * SECTOR_SIZE, yy * SECTOR_SIZE),
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(): Point2D {
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(): Point2D {
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.x || y < 0 || y >= this.mapBounds.y) {
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 { AttackState, GameApi, ObjectType, PlayerData, Point2D, UnitData } from "@chronodivide/game-api";
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 _ from "lodash";
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: Point2D,
33
- private attackArea: Point2D,
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): Point2D | null {
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 = _.maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));
88
+ const maxUnit = maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));
94
89
  if (maxUnit) {
95
- return { x: maxUnit.tile.rx, y: maxUnit.tile.ry };
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(squadName, 100, matchAwareness.getMainRallyPoint(), attackArea, attackRadius, logger).then(
132
- (reason, squad) => {
133
- missionController.addMission(
134
- new RetreatMission(
135
- "retreat-from-" + squadName + gameApi.getCurrentTick(),
136
- 100,
137
- matchAwareness.getMainRallyPoint(),
138
- squad?.getUnitIds() ?? [],
139
- logger,
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, Point2D } from "@chronodivide/game-api";
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: Point2D,
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({ x: foundTargets[0].x, y: foundTargets[0].y });
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: Point2D, unitIds: number[], logger: DebugLogger) {
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
  }