@supalosa/chronodivide-bot 0.1.0 → 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.
- package/README.md +71 -46
- package/dist/bot/bot.js +27 -183
- package/dist/bot/logic/awareness.js +122 -0
- package/dist/bot/logic/building/basicGroundUnit.js +8 -6
- package/dist/bot/logic/building/building.js +6 -3
- package/dist/bot/logic/building/harvester.js +1 -1
- package/dist/bot/logic/building/queueController.js +4 -21
- package/dist/bot/logic/common/scout.js +10 -0
- package/dist/bot/logic/knowledge.js +1 -0
- package/dist/bot/logic/map/map.js +6 -0
- package/dist/bot/logic/map/sector.js +6 -1
- package/dist/bot/logic/mission/basicMission.js +1 -5
- package/dist/bot/logic/mission/expansionMission.js +22 -4
- package/dist/bot/logic/mission/mission.js +49 -2
- package/dist/bot/logic/mission/missionController.js +67 -34
- package/dist/bot/logic/mission/missionFactories.js +10 -0
- package/dist/bot/logic/mission/missions/attackMission.js +109 -0
- package/dist/bot/logic/mission/missions/defenceMission.js +62 -0
- package/dist/bot/logic/mission/missions/expansionMission.js +24 -0
- package/dist/bot/logic/mission/missions/oneTimeMission.js +26 -0
- package/dist/bot/logic/mission/missions/retreatMission.js +7 -0
- package/dist/bot/logic/mission/missions/scoutingMission.js +38 -0
- package/dist/bot/logic/squad/behaviours/attackSquad.js +82 -0
- package/dist/bot/logic/squad/behaviours/combatSquad.js +99 -0
- package/dist/bot/logic/squad/behaviours/common.js +37 -0
- package/dist/bot/logic/squad/behaviours/defenceSquad.js +48 -0
- package/dist/bot/logic/squad/behaviours/expansionSquad.js +42 -0
- package/dist/bot/logic/squad/behaviours/retreatSquad.js +32 -0
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js +38 -0
- package/dist/bot/logic/squad/behaviours/squadExpansion.js +26 -13
- package/dist/bot/logic/squad/squad.js +68 -15
- package/dist/bot/logic/squad/squadBehaviour.js +5 -5
- package/dist/bot/logic/squad/squadBehaviours.js +6 -0
- package/dist/bot/logic/squad/squadController.js +106 -15
- package/dist/exampleBot.js +22 -7
- package/package.json +29 -24
- package/src/bot/bot.ts +178 -378
- package/src/bot/logic/awareness.ts +220 -0
- package/src/bot/logic/building/ArtilleryUnit.ts +2 -2
- package/src/bot/logic/building/antiGroundStaticDefence.ts +2 -2
- package/src/bot/logic/building/basicAirUnit.ts +2 -2
- package/src/bot/logic/building/basicBuilding.ts +2 -2
- package/src/bot/logic/building/basicGroundUnit.ts +83 -78
- package/src/bot/logic/building/building.ts +125 -120
- package/src/bot/logic/building/harvester.ts +27 -27
- package/src/bot/logic/building/powerPlant.ts +1 -1
- package/src/bot/logic/building/queueController.ts +17 -38
- package/src/bot/logic/building/resourceCollectionBuilding.ts +1 -1
- package/src/bot/logic/common/scout.ts +12 -0
- package/src/bot/logic/map/map.ts +11 -3
- package/src/bot/logic/map/sector.ts +136 -130
- package/src/bot/logic/mission/mission.ts +83 -47
- package/src/bot/logic/mission/missionController.ts +103 -51
- package/src/bot/logic/mission/missionFactories.ts +46 -0
- package/src/bot/logic/mission/missions/attackMission.ts +152 -0
- package/src/bot/logic/mission/missions/defenceMission.ts +104 -0
- package/src/bot/logic/mission/missions/expansionMission.ts +49 -0
- package/src/bot/logic/mission/missions/oneTimeMission.ts +32 -0
- package/src/bot/logic/mission/missions/retreatMission.ts +9 -0
- package/src/bot/logic/mission/missions/scoutingMission.ts +59 -0
- package/src/bot/logic/squad/behaviours/combatSquad.ts +125 -0
- package/src/bot/logic/squad/behaviours/common.ts +37 -0
- package/src/bot/logic/squad/behaviours/expansionSquad.ts +59 -0
- package/src/bot/logic/squad/behaviours/retreatSquad.ts +46 -0
- package/src/bot/logic/squad/behaviours/scoutingSquad.ts +56 -0
- package/src/bot/logic/squad/squad.ts +163 -97
- package/src/bot/logic/squad/squadBehaviour.ts +61 -43
- package/src/bot/logic/squad/squadBehaviours.ts +8 -0
- package/src/bot/logic/squad/squadController.ts +190 -66
- package/src/exampleBot.ts +19 -4
- package/tsconfig.json +1 -1
- package/src/bot/logic/mission/basicMission.ts +0 -42
- package/src/bot/logic/mission/expansionMission.ts +0 -25
- 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
|
-
|
|
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
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
// No auto-expansion missions.
|
|
30
|
+
return null;
|
|
13
31
|
}
|
|
14
32
|
}
|
|
@@ -1,2 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 {
|
|
2
|
+
import { createMissionFactories } from "./missionFactories.js";
|
|
3
3
|
export class MissionController {
|
|
4
|
-
constructor(
|
|
5
|
-
this.
|
|
4
|
+
constructor(logger) {
|
|
5
|
+
this.logger = logger;
|
|
6
|
+
this.missions = [];
|
|
7
|
+
this.forceDisbandedMissions = [];
|
|
8
|
+
this.missionFactories = createMissionFactories();
|
|
6
9
|
}
|
|
7
|
-
onAiUpdate(gameApi, playerData,
|
|
8
|
-
// Remove
|
|
10
|
+
onAiUpdate(gameApi, playerData, matchAwareness, squadController) {
|
|
11
|
+
// Remove inactive missions.
|
|
9
12
|
this.missions = this.missions.filter((missions) => missions.isActive());
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
//
|
|
28
|
-
this.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|