@supalosa/chronodivide-bot 0.1.1 → 0.2.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 (74) hide show
  1. package/README.md +71 -46
  2. package/dist/bot/bot.js +27 -183
  3. package/dist/bot/logic/awareness.js +122 -0
  4. package/dist/bot/logic/building/basicGroundUnit.js +8 -6
  5. package/dist/bot/logic/building/building.js +6 -3
  6. package/dist/bot/logic/building/harvester.js +1 -1
  7. package/dist/bot/logic/building/queueController.js +4 -21
  8. package/dist/bot/logic/common/scout.js +10 -0
  9. package/dist/bot/logic/knowledge.js +1 -0
  10. package/dist/bot/logic/map/map.js +6 -0
  11. package/dist/bot/logic/map/sector.js +6 -1
  12. package/dist/bot/logic/mission/basicMission.js +1 -5
  13. package/dist/bot/logic/mission/expansionMission.js +22 -4
  14. package/dist/bot/logic/mission/mission.js +49 -2
  15. package/dist/bot/logic/mission/missionController.js +67 -34
  16. package/dist/bot/logic/mission/missionFactories.js +10 -0
  17. package/dist/bot/logic/mission/missions/attackMission.js +109 -0
  18. package/dist/bot/logic/mission/missions/defenceMission.js +62 -0
  19. package/dist/bot/logic/mission/missions/expansionMission.js +24 -0
  20. package/dist/bot/logic/mission/missions/oneTimeMission.js +26 -0
  21. package/dist/bot/logic/mission/missions/retreatMission.js +7 -0
  22. package/dist/bot/logic/mission/missions/scoutingMission.js +38 -0
  23. package/dist/bot/logic/squad/behaviours/attackSquad.js +82 -0
  24. package/dist/bot/logic/squad/behaviours/combatSquad.js +99 -0
  25. package/dist/bot/logic/squad/behaviours/common.js +37 -0
  26. package/dist/bot/logic/squad/behaviours/defenceSquad.js +48 -0
  27. package/dist/bot/logic/squad/behaviours/expansionSquad.js +42 -0
  28. package/dist/bot/logic/squad/behaviours/retreatSquad.js +32 -0
  29. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +38 -0
  30. package/dist/bot/logic/squad/behaviours/squadExpansion.js +26 -13
  31. package/dist/bot/logic/squad/squad.js +68 -15
  32. package/dist/bot/logic/squad/squadBehaviour.js +5 -5
  33. package/dist/bot/logic/squad/squadBehaviours.js +6 -0
  34. package/dist/bot/logic/squad/squadController.js +106 -15
  35. package/dist/exampleBot.js +22 -7
  36. package/package.json +29 -24
  37. package/src/bot/bot.ts +178 -378
  38. package/src/bot/logic/awareness.ts +220 -0
  39. package/src/bot/logic/building/ArtilleryUnit.ts +2 -2
  40. package/src/bot/logic/building/antiGroundStaticDefence.ts +2 -2
  41. package/src/bot/logic/building/basicAirUnit.ts +2 -2
  42. package/src/bot/logic/building/basicBuilding.ts +2 -2
  43. package/src/bot/logic/building/basicGroundUnit.ts +83 -78
  44. package/src/bot/logic/building/building.ts +125 -120
  45. package/src/bot/logic/building/harvester.ts +27 -27
  46. package/src/bot/logic/building/powerPlant.ts +1 -1
  47. package/src/bot/logic/building/queueController.ts +17 -38
  48. package/src/bot/logic/building/resourceCollectionBuilding.ts +1 -1
  49. package/src/bot/logic/common/scout.ts +12 -0
  50. package/src/bot/logic/map/map.ts +11 -3
  51. package/src/bot/logic/map/sector.ts +136 -130
  52. package/src/bot/logic/mission/mission.ts +83 -47
  53. package/src/bot/logic/mission/missionController.ts +103 -51
  54. package/src/bot/logic/mission/missionFactories.ts +46 -0
  55. package/src/bot/logic/mission/missions/attackMission.ts +152 -0
  56. package/src/bot/logic/mission/missions/defenceMission.ts +104 -0
  57. package/src/bot/logic/mission/missions/expansionMission.ts +49 -0
  58. package/src/bot/logic/mission/missions/oneTimeMission.ts +32 -0
  59. package/src/bot/logic/mission/missions/retreatMission.ts +9 -0
  60. package/src/bot/logic/mission/missions/scoutingMission.ts +59 -0
  61. package/src/bot/logic/squad/behaviours/combatSquad.ts +125 -0
  62. package/src/bot/logic/squad/behaviours/common.ts +37 -0
  63. package/src/bot/logic/squad/behaviours/expansionSquad.ts +59 -0
  64. package/src/bot/logic/squad/behaviours/retreatSquad.ts +46 -0
  65. package/src/bot/logic/squad/behaviours/scoutingSquad.ts +56 -0
  66. package/src/bot/logic/squad/squad.ts +163 -97
  67. package/src/bot/logic/squad/squadBehaviour.ts +61 -43
  68. package/src/bot/logic/squad/squadBehaviours.ts +8 -0
  69. package/src/bot/logic/squad/squadController.ts +190 -66
  70. package/src/exampleBot.ts +19 -4
  71. package/tsconfig.json +1 -1
  72. package/src/bot/logic/mission/basicMission.ts +0 -42
  73. package/src/bot/logic/mission/expansionMission.ts +0 -25
  74. package/src/bot/logic/squad/behaviours/squadExpansion.ts +0 -33
@@ -0,0 +1,10 @@
1
+ export const getUnseenStartingLocations = (gameApi, playerData) => {
2
+ const unseenStartingLocations = gameApi.mapApi.getStartingLocations().filter((startingLocation) => {
3
+ if (startingLocation == playerData.startLocation) {
4
+ return false;
5
+ }
6
+ let tile = gameApi.mapApi.getTile(startingLocation.x, startingLocation.y);
7
+ return tile ? !gameApi.mapApi.isVisibleTile(tile, playerData.name) : false;
8
+ });
9
+ return unseenStartingLocations;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -55,3 +55,9 @@ export function getPointTowardsOtherPoint(gameApi, startLocation, endLocation, m
55
55
  export function getDistanceBetweenPoints(startLocation, endLocation) {
56
56
  return Math.sqrt((startLocation.x - endLocation.x) ** 2 + (startLocation.y - endLocation.y) ** 2);
57
57
  }
58
+ export function getDistanceBetweenUnits(unit1, unit2) {
59
+ return getDistanceBetweenPoints({ x: unit1.tile.rx, y: unit1.tile.ry }, { x: unit2.tile.rx, y: unit2.tile.ry });
60
+ }
61
+ export function getDistanceBetween(unit, point) {
62
+ return getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, point);
63
+ }
@@ -23,6 +23,9 @@ export class SectorCache {
23
23
  }
24
24
  }
25
25
  }
26
+ getMapBounds() {
27
+ return this.mapBounds;
28
+ }
26
29
  updateSectors(currentGameTick, maxSectorsToUpdate, mapApi, playerData) {
27
30
  let nextSectorX = this.lastUpdatedSectorX ? this.lastUpdatedSectorX + 1 : 0;
28
31
  let nextSectorY = this.lastUpdatedSectorY ? this.lastUpdatedSectorY : 0;
@@ -74,7 +77,9 @@ export class SectorCache {
74
77
  }
75
78
  return updated / total;
76
79
  }
77
- // Return % of tiles that are visible. Returns undefined if we haven't scanned the whole map yet.
80
+ /**
81
+ * Return the ratio (0-1) of tiles that are visible. Returns undefined if we haven't scanned the whole map yet.
82
+ */
78
83
  getOverallVisibility() {
79
84
  let visible = 0, total = 0;
80
85
  for (let xx = 0; xx < this.sectorsX; ++xx) {
@@ -1,5 +1,4 @@
1
- // A basic mission requests specific units and does nothing with them. It is not recommended
2
- // to actually create this in a game as they'll just sit around idle.
1
+ // A basic mission requests specific units.
3
2
  export class BasicMission {
4
3
  constructor(uniqueName, priority = 1, squads = []) {
5
4
  this.uniqueName = uniqueName;
@@ -23,8 +22,5 @@ export class BasicMission {
23
22
  getSquads() {
24
23
  return this.squads;
25
24
  }
26
- onAiUpdate(gameApi, playerData, threatData) {
27
- return {};
28
- }
29
25
  onSquadAdded(gameApi, playerData, threatData) { }
30
26
  }
@@ -1,14 +1,32 @@
1
- import { BasicMission } from "./basicMission.js";
2
- export class ExpansionMission extends BasicMission {
1
+ import { Mission, disbandMission, noop } from "./mission.js";
2
+ import { SquadExpansion } from "../squad/behaviours/expansionSquad.js";
3
+ import { Squad } from "../squad/squad.js";
4
+ /**
5
+ * A mission that tries to create an MCV (if it doesn't exist) and deploy it somewhere it can be deployed.
6
+ */
7
+ export class ExpansionMission extends Mission {
3
8
  constructor(uniqueName, priority) {
4
9
  super(uniqueName, priority);
10
+ this.hadSquad = false;
5
11
  }
6
12
  onAiUpdate(gameApi, playerData, threatData) {
7
- return {};
13
+ if (this.getSquad() === null) {
14
+ if (!this.hadSquad) {
15
+ this.hadSquad = true;
16
+ return this.setSquad(new Squad(this.getUniqueName(), new SquadExpansion(), this));
17
+ }
18
+ else {
19
+ return disbandMission();
20
+ }
21
+ }
22
+ else {
23
+ return noop();
24
+ }
8
25
  }
9
26
  }
10
27
  export class ExpansionMissionFactory {
11
28
  maybeCreateMission(gameApi, playerData, threatData, existingMissions) {
12
- return new ExpansionMission("expansion", 10);
29
+ // No auto-expansion missions.
30
+ return null;
13
31
  }
14
32
  }
@@ -1,2 +1,49 @@
1
- import { ExpansionMissionFactory } from "./expansionMission.js";
2
- export const missionFactories = [new ExpansionMissionFactory()];
1
+ // AI starts Missions based on heuristics, which have one or more squads.
2
+ // Missions can create squads (but squads will disband themselves).
3
+ export class Mission {
4
+ constructor(uniqueName, priority = 1) {
5
+ this.uniqueName = uniqueName;
6
+ this.priority = priority;
7
+ this.squad = null;
8
+ this.active = true;
9
+ this.onFinish = () => { };
10
+ }
11
+ isActive() {
12
+ return this.active;
13
+ }
14
+ setSquad(squad) {
15
+ this.squad = squad;
16
+ return registerSquad(squad);
17
+ }
18
+ getSquad() {
19
+ return this.squad;
20
+ }
21
+ removeSquad() {
22
+ // The squad was removed from this mission.
23
+ this.squad = null;
24
+ }
25
+ getUniqueName() {
26
+ return this.uniqueName;
27
+ }
28
+ // Don't call this from the mission itself
29
+ endMission(reason) {
30
+ this.onFinish(reason, this.squad);
31
+ this.squad = null;
32
+ this.active = false;
33
+ }
34
+ /**
35
+ * Declare a callback that is executed when the mission is disbanded for whatever reason.
36
+ */
37
+ then(onFinish) {
38
+ this.onFinish = onFinish;
39
+ return this;
40
+ }
41
+ }
42
+ export const noop = () => ({
43
+ type: "noop",
44
+ });
45
+ export const registerSquad = (squad) => ({
46
+ type: "registerSquad",
47
+ squad,
48
+ });
49
+ export const disbandMission = (reason) => ({ type: "disband", reason });
@@ -1,47 +1,80 @@
1
1
  // Meta-controller for forming and controlling squads.
2
- import { missionFactories } from "./mission.js";
2
+ import { createMissionFactories } from "./missionFactories.js";
3
3
  export class MissionController {
4
- constructor(missions = []) {
5
- this.missions = missions;
4
+ constructor(logger) {
5
+ this.logger = logger;
6
+ this.missions = [];
7
+ this.forceDisbandedMissions = [];
8
+ this.missionFactories = createMissionFactories();
6
9
  }
7
- onAiUpdate(gameApi, playerData, threatData) {
8
- // Remove disbanded missions.
10
+ onAiUpdate(gameApi, playerData, matchAwareness, squadController) {
11
+ // Remove inactive missions.
9
12
  this.missions = this.missions.filter((missions) => missions.isActive());
10
- let missionActions = this.missions.map((mission) => {
11
- return {
12
- mission,
13
- action: mission.onAiUpdate(gameApi, playerData, threatData),
14
- };
15
- });
13
+ // Poll missions for requested actions.
14
+ const missionActions = this.missions.map((mission) => ({
15
+ mission,
16
+ action: mission.onAiUpdate(gameApi, playerData, matchAwareness),
17
+ }));
16
18
  // Handle disbands and merges.
17
19
  const isDisband = (a) => a.type == "disband";
18
- let disbandedMissions = new Set();
20
+ const disbandedMissions = new Map();
21
+ const disbandedMissionsArray = [];
22
+ this.forceDisbandedMissions.forEach((name) => disbandedMissions.set(name, null));
23
+ this.forceDisbandedMissions = [];
19
24
  missionActions
20
25
  .filter((a) => isDisband(a.action))
21
26
  .forEach((a) => {
22
- a.mission.getSquads().forEach((squad) => {
23
- squad.setMission(undefined);
24
- });
25
- disbandedMissions.add(a.mission.getUniqueName());
27
+ disbandedMissions.set(a.mission.getUniqueName(), a.action.reason);
28
+ });
29
+ // Remove disbanded and merged squads.
30
+ this.missions
31
+ .filter((missions) => disbandedMissions.has(missions.getUniqueName()))
32
+ .forEach((disbandedMission) => {
33
+ this.logger(`mission disbanded: ${disbandedMission.getUniqueName()}`);
34
+ const reason = disbandedMissions.get(disbandedMission.getUniqueName());
35
+ disbandedMissionsArray.push({ mission: disbandedMission, reason });
36
+ disbandedMission.getSquad()?.setMission(null);
37
+ disbandedMission.endMission(disbandedMissions.get(disbandedMission.getUniqueName()));
38
+ });
39
+ this.missions = this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
40
+ // Register new squads
41
+ const isNewSquad = (a) => a.type == "registerSquad";
42
+ missionActions
43
+ .filter((a) => isNewSquad(a.action))
44
+ .forEach((a) => {
45
+ const action = a.action;
46
+ squadController.registerSquad(action.squad);
47
+ this.logger(`registered a squad: ${action.squad.getName()}`);
26
48
  });
27
- // remove disbanded and merged squads.
28
- this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
29
- // Create missions.
30
- let newMissions;
31
- let missionNames = new Set();
32
- this.missions.forEach((mission) => missionNames.add(mission.getUniqueName()));
33
- missionFactories.forEach((missionFactory) => {
34
- let maybeMission = missionFactory.maybeCreateMission(gameApi, playerData, threatData, this.missions);
35
- if (maybeMission) {
36
- if (missionNames.has(maybeMission.getUniqueName())) {
37
- //console.log(`Rejecting new mission ${maybeMission.getUniqueName()} as another mission exists.`);
38
- }
39
- else {
40
- console.log(`Starting new mission ${maybeMission.getUniqueName()}.`);
41
- this.missions.push(maybeMission);
42
- missionNames.add(maybeMission.getUniqueName());
43
- }
44
- }
49
+ // Create dynamic missions.
50
+ this.missionFactories.forEach((missionFactory) => {
51
+ missionFactory.maybeCreateMissions(gameApi, playerData, matchAwareness, this);
52
+ disbandedMissionsArray.forEach(({ reason, mission }) => {
53
+ missionFactory.onMissionFailed(gameApi, playerData, matchAwareness, mission, reason, this);
54
+ });
45
55
  });
46
56
  }
57
+ /**
58
+ * Attempts to add a mission to the active set.
59
+ * @param mission
60
+ * @returns The mission if it was accepted, or null if it was not.
61
+ */
62
+ addMission(mission) {
63
+ if (this.missions.some((m) => m.getUniqueName() === mission.getUniqueName())) {
64
+ // reject non-unique mission names
65
+ return null;
66
+ }
67
+ this.logger(`Added mission: ${mission.getUniqueName()}`);
68
+ this.missions.push(mission);
69
+ return mission;
70
+ }
71
+ /**
72
+ * Disband the provided mission on the next possible opportunity.
73
+ */
74
+ disbandMission(missionName) {
75
+ this.forceDisbandedMissions.push(missionName);
76
+ }
77
+ logDebugOutput() {
78
+ this.logger(`Missions (${this.missions.length}): ${this.missions.map((m) => m.getUniqueName()).join(", ")}`);
79
+ }
47
80
  }
@@ -0,0 +1,10 @@
1
+ import { ExpansionMissionFactory } from "./missions/expansionMission.js";
2
+ import { ScoutingMissionFactory } from "./missions/scoutingMission.js";
3
+ import { AttackMissionFactory } from "./missions/attackMission.js";
4
+ import { DefenceMissionFactory } from "./missions/defenceMission.js";
5
+ export const createMissionFactories = () => [
6
+ new ExpansionMissionFactory(),
7
+ new ScoutingMissionFactory(),
8
+ new AttackMissionFactory(),
9
+ new DefenceMissionFactory(),
10
+ ];
@@ -0,0 +1,109 @@
1
+ import { ObjectType } from "@chronodivide/game-api";
2
+ import { CombatSquad } from "../../squad/behaviours/combatSquad.js";
3
+ import { Mission, disbandMission, noop } from "../mission.js";
4
+ import { Squad } from "../../squad/squad.js";
5
+ import { RetreatMission } from "./retreatMission.js";
6
+ import _ from "lodash";
7
+ export var AttackFailReason;
8
+ (function (AttackFailReason) {
9
+ AttackFailReason[AttackFailReason["NoTargets"] = 0] = "NoTargets";
10
+ AttackFailReason[AttackFailReason["DefenceTooStrong"] = 1] = "DefenceTooStrong";
11
+ })(AttackFailReason || (AttackFailReason = {}));
12
+ const NO_TARGET_IDLE_TIMEOUT_TICKS = 60;
13
+ /**
14
+ * A mission that tries to attack a certain area.
15
+ */
16
+ export class AttackMission extends Mission {
17
+ constructor(uniqueName, priority, rallyArea, attackArea, radius) {
18
+ super(uniqueName, priority);
19
+ this.rallyArea = rallyArea;
20
+ this.attackArea = attackArea;
21
+ this.radius = radius;
22
+ this.lastTargetSeenAt = 0;
23
+ }
24
+ onAiUpdate(gameApi, playerData, matchAwareness) {
25
+ if (this.getSquad() === null) {
26
+ return this.setSquad(new Squad(this.getUniqueName(), new CombatSquad(this.rallyArea, this.attackArea, this.radius), this));
27
+ }
28
+ else {
29
+ // Dispatch missions.
30
+ if (!matchAwareness.shouldAttack()) {
31
+ return disbandMission(AttackFailReason.DefenceTooStrong);
32
+ }
33
+ const foundTargets = matchAwareness.getHostilesNearPoint2d(this.attackArea, this.radius);
34
+ if (foundTargets.length > 0) {
35
+ this.lastTargetSeenAt = gameApi.getCurrentTick();
36
+ }
37
+ else if (gameApi.getCurrentTick() > this.lastTargetSeenAt + NO_TARGET_IDLE_TIMEOUT_TICKS) {
38
+ return disbandMission(AttackFailReason.NoTargets);
39
+ }
40
+ }
41
+ return noop();
42
+ }
43
+ }
44
+ const ATTACK_COOLDOWN_TICKS = 120;
45
+ // Calculates the weight for initiating an attack on the position of a unit or building.
46
+ // This is separate from unit micro; the squad will be ordered to attack in the vicinity of the point.
47
+ const getTargetWeight = (unitData, tryFocusHarvester) => {
48
+ if (tryFocusHarvester && unitData.rules.harvester) {
49
+ return 100000;
50
+ }
51
+ else if (unitData.type === ObjectType.Building) {
52
+ return unitData.maxHitPoints * 10;
53
+ }
54
+ else {
55
+ return unitData.maxHitPoints;
56
+ }
57
+ };
58
+ export class AttackMissionFactory {
59
+ constructor(lastAttackAt = -ATTACK_COOLDOWN_TICKS) {
60
+ this.lastAttackAt = lastAttackAt;
61
+ }
62
+ getName() {
63
+ return "AttackMissionFactory";
64
+ }
65
+ generateTarget(gameApi, playerData, matchAwareness) {
66
+ // Randomly decide between harvester and base.
67
+ try {
68
+ const tryFocusHarvester = gameApi.generateRandomInt(0, 1) === 0;
69
+ const enemyUnits = gameApi
70
+ .getVisibleUnits(playerData.name, "hostile")
71
+ .map((unitId) => gameApi.getUnitData(unitId))
72
+ .filter((u) => !!u && gameApi.getPlayerData(u.owner).isCombatant);
73
+ const maxUnit = _.maxBy(enemyUnits, (u) => getTargetWeight(u, tryFocusHarvester));
74
+ if (maxUnit) {
75
+ return { x: maxUnit.tile.rx, y: maxUnit.tile.ry };
76
+ }
77
+ }
78
+ catch (err) {
79
+ // There's a crash here when accessing a building that got destroyed. Will catch and ignore or now.
80
+ return null;
81
+ }
82
+ return null;
83
+ }
84
+ maybeCreateMissions(gameApi, playerData, matchAwareness, missionController) {
85
+ if (!matchAwareness.shouldAttack()) {
86
+ return;
87
+ }
88
+ if (gameApi.getCurrentTick() < this.lastAttackAt + ATTACK_COOLDOWN_TICKS) {
89
+ return;
90
+ }
91
+ const attackRadius = 15;
92
+ const attackArea = this.generateTarget(gameApi, playerData, matchAwareness);
93
+ if (!attackArea) {
94
+ // Nothing to attack.
95
+ return;
96
+ }
97
+ // TODO: not using a fixed value here. But performance slows to a crawl when this is unique.
98
+ const squadName = "globalAttack";
99
+ const tryAttack = missionController
100
+ .addMission(new AttackMission(squadName, 100, matchAwareness.getMainRallyPoint(), attackArea, attackRadius))
101
+ ?.then((reason, squad) => {
102
+ missionController.addMission(new RetreatMission("retreat-from-" + squadName + gameApi.getCurrentTick(), 100, matchAwareness.getMainRallyPoint(), squad?.getUnitIds() ?? []));
103
+ });
104
+ if (tryAttack) {
105
+ this.lastAttackAt = gameApi.getCurrentTick();
106
+ }
107
+ }
108
+ onMissionFailed(gameApi, playerData, matchAwareness, failedMission, failureReason, missionController) { }
109
+ }
@@ -0,0 +1,62 @@
1
+ import { Mission, disbandMission, noop } from "../mission.js";
2
+ import { Squad } from "../../squad/squad.js";
3
+ import { CombatSquad } from "../../squad/behaviours/combatSquad.js";
4
+ import { RetreatMission } from "./retreatMission.js";
5
+ export var DefenceFailReason;
6
+ (function (DefenceFailReason) {
7
+ DefenceFailReason[DefenceFailReason["NoTargets"] = 0] = "NoTargets";
8
+ })(DefenceFailReason || (DefenceFailReason = {}));
9
+ /**
10
+ * A mission that tries to defend a certain area.
11
+ */
12
+ export class DefenceMission extends Mission {
13
+ constructor(uniqueName, priority, defenceArea, radius) {
14
+ super(uniqueName, priority);
15
+ this.defenceArea = defenceArea;
16
+ this.radius = radius;
17
+ }
18
+ onAiUpdate(gameApi, playerData, matchAwareness) {
19
+ if (this.getSquad() === null && !this.combatSquad) {
20
+ this.combatSquad = new CombatSquad(matchAwareness.getMainRallyPoint(), this.defenceArea, this.radius);
21
+ return this.setSquad(new Squad("defenceSquad-" + this.getUniqueName(), this.combatSquad, this));
22
+ }
23
+ else {
24
+ // Dispatch missions.
25
+ const foundTargets = matchAwareness.getHostilesNearPoint2d(this.defenceArea, this.radius);
26
+ if (foundTargets.length === 0) {
27
+ return disbandMission(DefenceFailReason.NoTargets);
28
+ }
29
+ else {
30
+ this.combatSquad?.setAttackArea({ x: foundTargets[0].x, y: foundTargets[0].y });
31
+ }
32
+ }
33
+ return noop();
34
+ }
35
+ }
36
+ const DEFENCE_CHECK_TICKS = 30;
37
+ // Starting radius around the player's base to trigger defense.
38
+ const DEFENCE_STARTING_RADIUS = 20;
39
+ // Every game tick, we increase the defendable area by this amount.
40
+ const DEFENCE_RADIUS_INCREASE_PER_GAME_TICK = 0.005;
41
+ export class DefenceMissionFactory {
42
+ constructor() {
43
+ this.lastDefenceCheckAt = 0;
44
+ }
45
+ getName() {
46
+ return "DefenceMissionFactory";
47
+ }
48
+ maybeCreateMissions(gameApi, playerData, matchAwareness, missionController) {
49
+ if (gameApi.getCurrentTick() < this.lastDefenceCheckAt + DEFENCE_CHECK_TICKS) {
50
+ return;
51
+ }
52
+ this.lastDefenceCheckAt = gameApi.getCurrentTick();
53
+ const defendableRadius = DEFENCE_STARTING_RADIUS + DEFENCE_RADIUS_INCREASE_PER_GAME_TICK * gameApi.getCurrentTick();
54
+ const enemiesNearSpawn = matchAwareness.getHostilesNearPoint2d(playerData.startLocation, defendableRadius);
55
+ if (enemiesNearSpawn.length > 0) {
56
+ missionController.addMission(new DefenceMission("globalDefence", 1000, playerData.startLocation, defendableRadius * 1.2)?.then((reason, squad) => {
57
+ missionController.addMission(new RetreatMission("retreat-from-globalDefence" + gameApi.getCurrentTick(), 100, matchAwareness.getMainRallyPoint(), squad?.getUnitIds() ?? []));
58
+ }));
59
+ }
60
+ }
61
+ onMissionFailed(gameApi, playerData, matchAwareness, failedMission, failureReason, missionController) { }
62
+ }
@@ -0,0 +1,24 @@
1
+ import { ExpansionSquad } from "../../squad/behaviours/expansionSquad.js";
2
+ import { OneTimeMission } from "./oneTimeMission.js";
3
+ /**
4
+ * A mission that tries to create an MCV (if it doesn't exist) and deploy it somewhere it can be deployed.
5
+ */
6
+ export class ExpansionMission extends OneTimeMission {
7
+ constructor(uniqueName, priority, selectedMcv) {
8
+ super(uniqueName, priority, () => new ExpansionSquad(selectedMcv));
9
+ }
10
+ }
11
+ export class ExpansionMissionFactory {
12
+ getName() {
13
+ return "ExpansionMissionFactory";
14
+ }
15
+ maybeCreateMissions(gameApi, playerData, matchAwareness, missionController) {
16
+ // At this point, only expand if we have a loose MCV.
17
+ const mcvs = gameApi.getVisibleUnits(playerData.name, "self", (r) => gameApi.getGeneralRules().baseUnit.includes(r.name));
18
+ mcvs.forEach((mcv) => {
19
+ missionController.addMission(new ExpansionMission("expand-with-" + mcv, 100, mcv));
20
+ });
21
+ }
22
+ onMissionFailed(gameApi, playerData, matchAwareness, failedMission, failureReason, missionController) {
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ import { Mission, disbandMission, noop } from "../mission.js";
2
+ import { Squad } from "../../squad/squad.js";
3
+ /**
4
+ * A mission that gets dispatched once, and once the squad decides to disband, the mission is disbanded.
5
+ */
6
+ export class OneTimeMission extends Mission {
7
+ constructor(uniqueName, priority, behaviourFactory) {
8
+ super(uniqueName, priority);
9
+ this.behaviourFactory = behaviourFactory;
10
+ this.hadSquad = false;
11
+ }
12
+ onAiUpdate(gameApi, playerData, matchAwareness) {
13
+ if (this.getSquad() === null) {
14
+ if (!this.hadSquad) {
15
+ this.hadSquad = true;
16
+ return this.setSquad(new Squad(this.getUniqueName(), this.behaviourFactory(), this));
17
+ }
18
+ else {
19
+ return disbandMission();
20
+ }
21
+ }
22
+ else {
23
+ return noop();
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,7 @@
1
+ import { OneTimeMission } from "./oneTimeMission.js";
2
+ import { RetreatSquad } from "../../squad/behaviours/retreatSquad.js";
3
+ export class RetreatMission extends OneTimeMission {
4
+ constructor(uniqueName, priority, retreatToPoint, unitIds) {
5
+ super(uniqueName, priority, () => new RetreatSquad(unitIds, retreatToPoint));
6
+ }
7
+ }
@@ -0,0 +1,38 @@
1
+ import { ScoutingSquad } from "../../squad/behaviours/scoutingSquad.js";
2
+ import { OneTimeMission } from "./oneTimeMission.js";
3
+ import { AttackMission } from "./attackMission.js";
4
+ import { getUnseenStartingLocations } from "../../common/scout.js";
5
+ /**
6
+ * A mission that tries to scout around the map with a cheap, fast unit (usually attack dogs)
7
+ */
8
+ export class ScoutingMission extends OneTimeMission {
9
+ constructor(uniqueName, priority) {
10
+ super(uniqueName, priority, () => new ScoutingSquad());
11
+ }
12
+ }
13
+ const SCOUT_COOLDOWN_TICKS = 300;
14
+ export class ScoutingMissionFactory {
15
+ constructor(lastScoutAt = -SCOUT_COOLDOWN_TICKS) {
16
+ this.lastScoutAt = lastScoutAt;
17
+ }
18
+ getName() {
19
+ return "ScoutingMissionFactory";
20
+ }
21
+ maybeCreateMissions(gameApi, playerData, matchAwareness, missionController) {
22
+ if (gameApi.getCurrentTick() < this.lastScoutAt + SCOUT_COOLDOWN_TICKS) {
23
+ return;
24
+ }
25
+ const candidatePoints = getUnseenStartingLocations(gameApi, playerData);
26
+ if (candidatePoints.length === 0) {
27
+ return;
28
+ }
29
+ if (!missionController.addMission(new ScoutingMission("globalScout", 100))) {
30
+ this.lastScoutAt = gameApi.getCurrentTick();
31
+ }
32
+ }
33
+ onMissionFailed(gameApi, playerData, matchAwareness, failedMission, failureReason, missionController) {
34
+ if (failedMission instanceof AttackMission) {
35
+ missionController.addMission(new ScoutingMission("globalScout", 100));
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,82 @@
1
+ import _ from "lodash";
2
+ import { MovementZone } from "@chronodivide/game-api";
3
+ import { grabCombatants, noop } from "../squadBehaviour.js";
4
+ import { getDistanceBetweenPoints } from "../../map/map.js";
5
+ import { manageAttackMicro, manageMoveMicro } from "./common.js";
6
+ const TARGET_UPDATE_INTERVAL_TICKS = 10;
7
+ const GRAB_INTERVAL_TICKS = 10;
8
+ const GRAB_RADIUS = 30;
9
+ // Units must be in a certain radius of the center of mass before attacking.
10
+ // This scales for number of units in the squad though.
11
+ const MIN_GATHER_RADIUS = 5;
12
+ const GATHER_RATIO = 10;
13
+ var SquadState;
14
+ (function (SquadState) {
15
+ SquadState[SquadState["Gathering"] = 0] = "Gathering";
16
+ SquadState[SquadState["Attacking"] = 1] = "Attacking";
17
+ })(SquadState || (SquadState = {}));
18
+ export class AttackOrDefenceSquad {
19
+ constructor(rallyArea, targetArea, radius) {
20
+ this.rallyArea = rallyArea;
21
+ this.targetArea = targetArea;
22
+ this.radius = radius;
23
+ this.lastGrab = null;
24
+ this.lastCommand = null;
25
+ this.state = SquadState.Gathering;
26
+ }
27
+ setAttackArea(targetArea) {
28
+ this.targetArea = targetArea;
29
+ }
30
+ onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
31
+ if (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS) {
32
+ this.lastCommand = gameApi.getCurrentTick();
33
+ const centerOfMass = squad.getCenterOfMass();
34
+ const maxDistance = squad.getMaxDistanceToCenterOfMass();
35
+ const units = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant);
36
+ if (this.state === SquadState.Gathering) {
37
+ // Only use ground units for center of mass.
38
+ const groundUnits = squad.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant &&
39
+ (r.rules.movementZone === MovementZone.Infantry ||
40
+ r.rules.movementZone === MovementZone.Normal ||
41
+ r.rules.movementZone === MovementZone.InfantryDestroyer));
42
+ const requiredGatherRadius = Math.sqrt(groundUnits.length) * GATHER_RATIO + MIN_GATHER_RADIUS;
43
+ if (centerOfMass &&
44
+ maxDistance &&
45
+ gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
46
+ maxDistance > requiredGatherRadius) {
47
+ units.forEach((unit) => {
48
+ manageMoveMicro(actionsApi, unit, centerOfMass);
49
+ });
50
+ }
51
+ else {
52
+ this.state = SquadState.Attacking;
53
+ }
54
+ }
55
+ else {
56
+ const targetPoint = this.targetArea || playerData.startLocation;
57
+ for (const unit of units) {
58
+ if (unit.isIdle) {
59
+ const { rx: x, ry: y } = unit.tile;
60
+ const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
61
+ const nearbyHostiles = matchAwareness.getHostilesNearPoint(x, y, range * 2);
62
+ const closest = _.minBy(nearbyHostiles, ({ x: hX, y: hY }) => getDistanceBetweenPoints({ x, y }, { x: hX, y: hY }));
63
+ const closestUnit = closest ? gameApi.getUnitData(closest.unitId) ?? null : null;
64
+ if (closestUnit) {
65
+ manageAttackMicro(actionsApi, unit, closestUnit);
66
+ }
67
+ else {
68
+ manageMoveMicro(actionsApi, unit, targetPoint);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ if (!this.lastGrab || gameApi.getCurrentTick() > this.lastGrab + GRAB_INTERVAL_TICKS) {
75
+ this.lastGrab = gameApi.getCurrentTick();
76
+ return grabCombatants(this.rallyArea, this.radius * GRAB_RADIUS);
77
+ }
78
+ else {
79
+ return noop();
80
+ }
81
+ }
82
+ }