Use JS classes for the AI

Fixes: #6285
This commit is contained in:
phosit 2026-07-13 19:47:21 +02:00
parent 09671b49a2
commit 8b1e78ae1e
No known key found for this signature in database
GPG key ID: C9430B600671C268
27 changed files with 16452 additions and 16378 deletions

View file

@ -1,29 +1,31 @@
globalThis.PlayerID = -1;
export function BaseAI(settings)
export class BaseAI
{
constructor(settings)
{
if (!settings)
return;
this.player = settings.player;
}
}
/** Return a simple object (using no classes etc) that will be serialized into saved games */
BaseAI.prototype.Serialize = function()
{
/** Return a simple object (using no classes etc) that will be serialized into saved games */
Serialize()
{
return {};
};
}
/**
/**
* Called after the constructor when loading a saved game, with 'data' being
* whatever Serialize() returned
*/
BaseAI.prototype.Deserialize = function(data, sharedScript)
{
};
Deserialize(data, sharedScript)
{
}
BaseAI.prototype.Init = function(playerID, sharedAI)
{
Init(playerID, sharedAI)
{
PlayerID = playerID;
this.territoryMap = sharedAI.territoryMap;
@ -35,26 +37,27 @@ BaseAI.prototype.Init = function(playerID, sharedAI)
this.timeElapsed = sharedAI.timeElapsed;
this.CustomInit(this.gameState);
};
}
/** AIs override this function */
BaseAI.prototype.CustomInit = function()
{
};
/** AIs override this function */
CustomInit()
{
}
BaseAI.prototype.HandleMessage = function(state, playerID, sharedAI)
{
HandleMessage(state, playerID, sharedAI)
{
PlayerID = playerID;
this.territoryMap = sharedAI.territoryMap;
this.OnUpdate(sharedAI);
};
}
/** AIs override this function */
BaseAI.prototype.OnUpdate = function()
{
};
/** AIs override this function */
OnUpdate()
{
}
BaseAI.prototype.chat = function(message)
{
chat(message)
{
Engine.PostCommand(PlayerID, { "type": "aichat", "message": message });
};
}
}

View file

@ -1,7 +1,9 @@
import { SquareVectorDistance } from "simulation/ai/common-api/utils.js";
export function EntityCollection(sharedAI, entities = new Map(), filters = [])
export class EntityCollection
{
constructor(sharedAI, entities = new Map(), filters = [])
{
this._ai = sharedAI;
this._entities = entities;
this._filters = filters;
@ -12,10 +14,10 @@ export function EntityCollection(sharedAI, entities = new Map(), filters = [])
Object.defineProperty(this, "length", { "get": () => this._entities.size });
this.frozen = false;
}
}
EntityCollection.prototype.Serialize = function()
{
Serialize()
{
const filters = [];
for (const f of this._filters)
filters.push(uneval(f));
@ -24,10 +26,10 @@ EntityCollection.prototype.Serialize = function()
"frozen": this.frozen,
"filters": filters
};
};
}
EntityCollection.prototype.Deserialize = function(data, sharedAI)
{
Deserialize(data, sharedAI)
{
this._ai = sharedAI;
for (const id of data.ents)
this._entities.set(id, sharedAI._entities.get(id));
@ -39,46 +41,46 @@ EntityCollection.prototype.Deserialize = function(data, sharedAI)
this.freeze();
else
this.defreeze();
};
}
/**
/**
* If an entitycollection is frozen, it will never automatically add a unit.
* But can remove one.
* this makes it easy to create entity collection that will auto-remove dead units
* but never add new ones.
*/
EntityCollection.prototype.freeze = function()
{
freeze()
{
this.frozen = true;
};
}
EntityCollection.prototype.defreeze = function()
{
defreeze()
{
this.frozen = false;
};
}
EntityCollection.prototype.toIdArray = function()
{
toIdArray()
{
return Array.from(this._entities.keys());
};
}
EntityCollection.prototype.toEntityArray = function()
{
toEntityArray()
{
return Array.from(this._entities.values());
};
}
EntityCollection.prototype.values = function()
{
values()
{
return this._entities.values();
};
}
EntityCollection.prototype.toString = function()
{
toString()
{
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
};
}
EntityCollection.prototype.filter = function(filter, thisp)
{
filter(filter, thisp)
{
if (typeof filter === "function")
filter = { "func": filter, "dynamicProperties": [] };
@ -88,13 +90,13 @@ EntityCollection.prototype.filter = function(filter, thisp)
ret.set(id, ent);
return new EntityCollection(this._ai, ret, this._filters.concat([filter]));
};
}
/**
/**
* Returns the (at most) n entities nearest to targetPos.
*/
EntityCollection.prototype.filterNearest = function(targetPos, n)
{
filterNearest(targetPos, n)
{
// Compute the distance of each entity
const data = []; // [ [id, ent, distance], ... ]
for (const [id, ent] of this._entities)
@ -115,10 +117,10 @@ EntityCollection.prototype.filterNearest = function(targetPos, n)
ret.set(data[i][0], data[i][1]);
return new EntityCollection(this._ai, ret);
};
}
EntityCollection.prototype.filter_raw = function(callback, thisp)
{
filter_raw(callback, thisp)
{
const ret = new Map();
for (const [id, ent] of this._entities)
{
@ -127,22 +129,22 @@ EntityCollection.prototype.filter_raw = function(callback, thisp)
ret.set(id, ent);
}
return new EntityCollection(this._ai, ret);
};
}
EntityCollection.prototype.forEach = function(callback)
{
forEach(callback)
{
for (const ent of this._entities.values())
callback(ent);
return this;
};
}
EntityCollection.prototype.hasEntities = function()
{
hasEntities()
{
return this._entities.size !== 0;
};
}
EntityCollection.prototype.move = function(x, z, queued = false, pushFront = false)
{
move(x, z, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "walk",
"entities": this.toIdArray(),
@ -152,10 +154,10 @@ EntityCollection.prototype.move = function(x, z, queued = false, pushFront = fal
"pushFront": pushFront
});
return this;
};
}
EntityCollection.prototype.moveToRange = function(x, z, min, max, queued = false, pushFront = false)
{
moveToRange(x, z, min, max, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "walk-to-range",
"entities": this.toIdArray(),
@ -167,10 +169,10 @@ EntityCollection.prototype.moveToRange = function(x, z, min, max, queued = false
"pushFront": pushFront
});
return this;
};
}
EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
{
attackMove(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "attack-walk",
"entities": this.toIdArray(),
@ -182,10 +184,10 @@ EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCaptu
"pushFront": pushFront
});
return this;
};
}
EntityCollection.prototype.moveIndiv = function(x, z, queued = false, pushFront = false)
{
moveIndiv(x, z, queued = false, pushFront = false)
{
for (const id of this._entities.keys())
Engine.PostCommand(PlayerID, {
"type": "walk",
@ -196,10 +198,10 @@ EntityCollection.prototype.moveIndiv = function(x, z, queued = false, pushFront
"pushFront": pushFront
});
return this;
};
}
EntityCollection.prototype.garrison = function(target, queued = false, pushFront = false)
{
garrison(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "garrison",
"entities": this.toIdArray(),
@ -208,10 +210,10 @@ EntityCollection.prototype.garrison = function(target, queued = false, pushFront
"pushFront": pushFront
});
return this;
};
}
EntityCollection.prototype.occupyTurret = function(target, queued = false, pushFront = false)
{
occupyTurret(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "occupy-turret",
"entities": this.toIdArray(),
@ -220,16 +222,16 @@ EntityCollection.prototype.occupyTurret = function(target, queued = false, pushF
"pushFront": pushFront
});
return this;
};
}
EntityCollection.prototype.destroy = function()
{
destroy()
{
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() });
return this;
};
}
EntityCollection.prototype.attack = function(unitId, queued = false, pushFront = false)
{
attack(unitId, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "attack",
"entities": this.toIdArray(),
@ -238,22 +240,22 @@ EntityCollection.prototype.attack = function(unitId, queued = false, pushFront =
"pushFront": pushFront
});
return this;
};
}
/** violent, aggressive, defensive, passive, standground */
EntityCollection.prototype.setStance = function(stance)
{
/** violent, aggressive, defensive, passive, standground */
setStance(stance)
{
Engine.PostCommand(PlayerID, {
"type": "stance",
"entities": this.toIdArray(),
"name": stance
});
return this;
};
}
/** Returns the average position of all units */
EntityCollection.prototype.getCentrePosition = function()
{
/** Returns the average position of all units */
getCentrePosition()
{
const sumPos = [0, 0];
let count = 0;
for (const ent of this._entities.values())
@ -266,15 +268,15 @@ EntityCollection.prototype.getCentrePosition = function()
}
return count ? [sumPos[0]/count, sumPos[1]/count] : undefined;
};
}
/**
/**
* returns the average position from the sample first units.
* This might be faster for huge collections, but there's
* always a risk that it'll be unprecise.
*/
EntityCollection.prototype.getApproximatePosition = function(sample)
{
getApproximatePosition(sample)
{
const sumPos = [0, 0];
let i = 0;
for (const ent of this._entities.values())
@ -289,25 +291,25 @@ EntityCollection.prototype.getApproximatePosition = function(sample)
}
return i ? [sumPos[0]/i, sumPos[1]/i] : undefined;
};
}
EntityCollection.prototype.hasEntId = function(id)
{
hasEntId(id)
{
return this._entities.has(id);
};
}
/** Removes an entity from the collection, returns true if the entity was a member, false otherwise */
EntityCollection.prototype.removeEnt = function(ent)
{
/** Removes an entity from the collection, returns true if the entity was a member, false otherwise */
removeEnt(ent)
{
if (!this._entities.has(ent.id()))
return false;
this._entities.delete(ent.id());
return true;
};
}
/** Adds an entity to the collection, returns true if the entity was not member, false otherwise */
EntityCollection.prototype.addEnt = function(ent)
{
/** Adds an entity to the collection, returns true if the entity was not member, false otherwise */
addEnt(ent)
{
if (this._entities.has(ent.id()))
return false;
this._entities.set(ent.id(), ent);
@ -317,17 +319,17 @@ EntityCollection.prototype.addEnt = function(ent)
for (const e of temp)
this._entities.set(e.id(), e);
return true;
};
}
/**
/**
* Checks the entity against the filters, and adds or removes it appropriately, returns true if the
* entity collection was modified.
* Force can add a unit despite a freezing.
* If an entitycollection is frozen, it will never automatically add a unit.
* But can remove one.
*/
EntityCollection.prototype.updateEnt = function(ent, force)
{
updateEnt(ent, force)
{
let passesFilters = true;
for (const filter of this._filters)
passesFilters = passesFilters && filter.func(ent);
@ -340,29 +342,30 @@ EntityCollection.prototype.updateEnt = function(ent, force)
}
return this.removeEnt(ent);
};
}
EntityCollection.prototype.registerUpdates = function()
{
registerUpdates()
{
this._ai.registerUpdatingEntityCollection(this);
};
}
EntityCollection.prototype.unregister = function()
{
unregister()
{
this._ai.removeUpdatingEntityCollection(this);
};
}
EntityCollection.prototype.dynamicProperties = function()
{
dynamicProperties()
{
return this.dynamicProp;
};
}
EntityCollection.prototype.setUID = function(id)
{
setUID(id)
{
this._UID = id;
};
}
EntityCollection.prototype.getUID = function()
{
getUID()
{
return this._UID;
};
}
}

View file

@ -1,60 +1,63 @@
Resources = new Resources();
export function ResourcesManager(amounts = {}, population = 0)
export class ResourcesManager
{
constructor(amounts = {}, population = 0)
{
for (const key of Resources.GetCodes())
this[key] = amounts[key] || 0;
this.population = population > 0 ? population : 0;
}
}
ResourcesManager.prototype.reset = function()
{
reset()
{
for (const key of Resources.GetCodes())
this[key] = 0;
this.population = 0;
};
}
ResourcesManager.prototype.canAfford = function(that)
{
canAfford(that)
{
for (const key of Resources.GetCodes())
if (this[key] < that[key])
return false;
return true;
};
}
ResourcesManager.prototype.add = function(that)
{
add(that)
{
for (const key of Resources.GetCodes())
this[key] += that[key];
this.population += that.population;
};
}
ResourcesManager.prototype.subtract = function(that)
{
subtract(that)
{
for (const key of Resources.GetCodes())
this[key] -= that[key];
this.population += that.population;
};
}
ResourcesManager.prototype.multiply = function(n)
{
multiply(n)
{
for (const key of Resources.GetCodes())
this[key] *= n;
this.population *= n;
};
}
ResourcesManager.prototype.Serialize = function()
{
Serialize()
{
const amounts = {};
for (const key of Resources.GetCodes())
amounts[key] = this[key];
return { "amounts": amounts, "population": this.population };
};
}
ResourcesManager.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data.amounts)
this[key] = data.amounts[key];
this.population = data.population;
};
}
}

View file

@ -5,8 +5,10 @@ import { InfoMap } from "simulation/ai/common-api/map-module.js";
import { Accessibility, TerrainAnalysis } from "simulation/ai/common-api/terrain-analysis.js";
/** Shared script handling templates and basic terrain analysis */
export function SharedScript(settings)
export class SharedScript
{
constructor(settings)
{
if (!settings)
return;
@ -25,48 +27,48 @@ export function SharedScript(settings)
this._entityCollectionsName = new Map();
this._entityCollectionsByDynProp = {};
this._entityCollectionsUID = 0;
}
}
/** Return a simple object (using no classes etc) that will be serialized into saved games */
SharedScript.prototype.Serialize = function()
{
/** Return a simple object (using no classes etc) that will be serialized into saved games */
Serialize()
{
return {
"players": this._players,
"templatesModifications": this._templatesModifications,
"entitiesModifications": this._entitiesModifications,
"metadata": this._entityMetadata
};
};
}
/**
/**
* Called after the constructor when loading a saved game, with 'data' being
* whatever Serialize() returned
*/
SharedScript.prototype.Deserialize = function(data)
{
Deserialize(data)
{
this._players = data.players;
this._templatesModifications = data.templatesModifications;
this._entitiesModifications = data.entitiesModifications;
this._entityMetadata = data.metadata;
this.isDeserialized = true;
};
}
SharedScript.prototype.GetTemplate = function(name)
{
GetTemplate(name)
{
if (this._templates[name] === undefined)
this._templates[name] = Engine.GetTemplate(name) || null;
return this._templates[name];
};
}
/**
/**
* Initialize the shared component.
* We need to know the initial state of the game for this, as we will use it.
* This is called right at the end of the map generation.
*/
SharedScript.prototype.init = function(state, deserialization)
{
init(state, deserialization)
{
if (!deserialization)
this._entitiesModifications = new Map();
@ -103,7 +105,7 @@ SharedScript.prototype.init = function(state, deserialization)
}
Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
*/
*/
this._entities = new Map();
if (state.entities)
@ -136,14 +138,14 @@ SharedScript.prototype.init = function(state, deserialization)
this.gameState[player] = new GameState();
this.gameState[player].init(this, state, player);
}
};
}
/**
/**
* General update of the shared script, before each AI's update
* applies entity deltas, and each gamestate.
*/
SharedScript.prototype.onUpdate = function(state)
{
onUpdate(state)
{
if (this.isDeserialized)
{
this.init(state, true);
@ -177,10 +179,10 @@ SharedScript.prototype.onUpdate = function(state)
this.updateResourceMaps(this.events);
Engine.ProfileStop();
};
}
SharedScript.prototype.ApplyEntitiesDelta = function(state)
{
ApplyEntitiesDelta(state)
{
Engine.ProfileStart("Shared ApplyEntitiesDelta");
const foundationFinished = {};
@ -285,10 +287,10 @@ SharedScript.prototype.ApplyEntitiesDelta = function(state)
modif.set(change.variable, change.value);
}
Engine.ProfileStop();
};
}
SharedScript.prototype.ApplyTemplatesDelta = function(state)
{
ApplyTemplatesDelta(state)
{
Engine.ProfileStart("Shared ApplyTemplatesDelta");
for (const player in state.changedTemplateInfo)
@ -309,10 +311,10 @@ SharedScript.prototype.ApplyTemplatesDelta = function(state)
this._templatesModifications =
Object.fromEntries(Object.entries(this._templatesModifications).sort());
Engine.ProfileStop();
};
}
SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection)
{
registerUpdatingEntityCollection(entCollection)
{
entCollection.setUID(this._entityCollectionsUID);
this._entityCollections.set(this._entityCollectionsUID, entCollection);
for (const prop of entCollection.dynamicProperties())
@ -322,10 +324,10 @@ SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection
this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection);
}
this._entityCollectionsUID++;
};
}
SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
{
removeUpdatingEntityCollection(entCollection)
{
const uid = entCollection.getUID();
if (this._entityCollections.has(uid))
@ -334,19 +336,19 @@ SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
for (const prop of entCollection.dynamicProperties())
if (this._entityCollectionsByDynProp[prop].has(uid))
this._entityCollectionsByDynProp[prop].delete(uid);
};
}
SharedScript.prototype.updateEntityCollections = function(property, ent)
{
updateEntityCollections(property, ent)
{
if (this._entityCollectionsByDynProp[property] === undefined)
return;
for (const entCol of this._entityCollectionsByDynProp[property].values())
entCol.updateEnt(ent);
};
}
SharedScript.prototype.setMetadata = function(player, ent, key, value)
{
setMetadata(player, ent, key, value)
{
let metadata = this._entityMetadata[player][ent.id()];
if (!metadata)
{
@ -357,15 +359,15 @@ SharedScript.prototype.setMetadata = function(player, ent, key, value)
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
};
}
SharedScript.prototype.getMetadata = function(player, ent, key)
{
getMetadata(player, ent, key)
{
return this._entityMetadata[player][ent.id()]?.[key];
};
}
SharedScript.prototype.deleteMetadata = function(player, ent, key)
{
deleteMetadata(player, ent, key)
{
const metadata = this._entityMetadata[player][ent.id()];
if (!metadata || !(key in metadata))
@ -375,11 +377,11 @@ SharedScript.prototype.deleteMetadata = function(player, ent, key)
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
return true;
};
}
/** creates a map of resource density */
SharedScript.prototype.createResourceMaps = function()
{
/** creates a map of resource density */
createResourceMaps()
{
for (const resource of Resources.GetCodes())
{
if (this.resourceMaps[resource] ||
@ -392,13 +394,13 @@ SharedScript.prototype.createResourceMaps = function()
}
for (const ent of this._entities.values())
this.addEntityToResourceMap(ent);
};
}
/**
/**
* @param {Object} events - The events from a turn.
*/
SharedScript.prototype.updateResourceMaps = function(events)
{
updateResourceMaps(events)
{
if (events.Destroy.some(e => this.resources.includes(e.entity)))
{
this.resources = [];
@ -409,30 +411,30 @@ SharedScript.prototype.updateResourceMaps = function(events)
for (const e of events.Create)
if (e.entity && this._entities.has(e.entity))
this.addEntityToResourceMap(this._entities.get(e.entity));
};
}
/**
/**
* @param {entity} entity - The entity to add to the resource map.
*/
SharedScript.prototype.addEntityToResourceMap = function(entity)
{
addEntityToResourceMap(entity)
{
this.changeEntityInResourceMapHelper(entity, 1);
this.resources.push(entity.id());
};
}
/**
/**
* @param {entity} entity - The entity to remove from the resource map.
*/
SharedScript.prototype.removeEntityFromResourceMap = function(entity)
{
removeEntityFromResourceMap(entity)
{
this.changeEntityInResourceMapHelper(entity, -1);
};
}
/**
/**
* @param {entity} ent - The entity to add to the resource map.
*/
SharedScript.prototype.changeEntityInResourceMapHelper = function(ent, multiplication = 1)
{
changeEntityInResourceMapHelper(ent, multiplication = 1)
{
if (!ent)
return;
const entPos = ent.position();
@ -449,4 +451,5 @@ SharedScript.prototype.changeEntityInResourceMapHelper = function(ent, multiplic
this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2, "constant");
this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2);
this.ccResourceMaps[resource].addInfluence(x, y, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
};
}
}

View file

@ -1,8 +1,10 @@
LoadModificationTemplates();
/** Wrapper around a technology template */
export function Technology(templateName)
export class Technology
{
constructor(templateName)
{
this._templateName = templateName;
const template = TechnologyTemplates.Get(templateName);
@ -16,47 +18,47 @@ export function Technology(templateName)
// check if it only defines a pair:
this._definesPair = !!template.pair;
this._template = template;
}
}
/** returns generic, or specific if civ provided. */
Technology.prototype.name = function(civ)
{
/** returns generic, or specific if civ provided. */
name(civ)
{
if (civ === undefined)
return this._template.genericName;
if (this._template.specificName === undefined || this._template.specificName[civ] === undefined)
return undefined;
return this._template.specificName[civ];
};
}
Technology.prototype.pairDef = function()
{
pairDef()
{
return this._definesPair;
};
}
/** in case this defines a pair only, returns the two paired technologies. */
Technology.prototype.getPairedTechs = function()
{
/** in case this defines a pair only, returns the two paired technologies. */
getPairedTechs()
{
if (!this._definesPair)
return undefined;
return this._template.pair.map(name => new Technology(name));
};
}
Technology.prototype.pair = function()
{
pair()
{
return this._template.partOfPair;
};
}
Technology.prototype.pairedWith = function()
{
pairedWith()
{
if (!this._template.partOfPair)
return undefined;
return this._pairedWith;
};
}
Technology.prototype.cost = function(researcher)
{
cost(researcher)
{
if (!this._template.cost)
return undefined;
const cost = {};
@ -67,10 +69,10 @@ Technology.prototype.cost = function(researcher)
cost[type] *= researcher.techCostMultiplier(type);
}
return cost;
};
}
Technology.prototype.costSum = function(researcher)
{
costSum(researcher)
{
const cost = this.cost(researcher);
if (!cost)
return 0;
@ -78,47 +80,48 @@ Technology.prototype.costSum = function(researcher)
for (const type in cost)
ret += cost[type];
return ret;
};
}
Technology.prototype.researchTime = function()
{
researchTime()
{
return this._template.researchTime || 0;
};
}
Technology.prototype.requirements = function(civ)
{
requirements(civ)
{
return DeriveTechnologyRequirements(this._template, civ);
};
}
Technology.prototype.autoResearch = function()
{
autoResearch()
{
if (!this._template.autoResearch)
return undefined;
return this._template.autoResearch;
};
}
Technology.prototype.supersedes = function()
{
supersedes()
{
if (!this._template.supersedes)
return undefined;
return this._template.supersedes;
};
}
Technology.prototype.modifications = function()
{
modifications()
{
if (!this._template.modifications)
return undefined;
return this._template.modifications;
};
}
Technology.prototype.affects = function()
{
affects()
{
if (!this._template.affects)
return undefined;
return this._template.affects;
};
}
Technology.prototype.isAffected = function(classes)
{
isAffected(classes)
{
return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect));
};
}
}

View file

@ -6,9 +6,11 @@ import { Queue } from "simulation/ai/petra/queue.js";
import { QueueManager } from "simulation/ai/petra/queueManager.js";
import { gameAnalysis } from "simulation/ai/petra/startingStrategy.js";
export function PetraBot(settings)
export class PetraBot extends BaseAI
{
BaseAI.call(this, settings);
constructor(settings)
{
super(settings);
// played turn, because Petra doesn't play every turn.
this.turn = 0;
@ -25,12 +27,10 @@ export function PetraBot(settings)
this.Config = new Config(settings.difficulty, settings.behavior);
this.savedEvents = {};
}
}
PetraBot.prototype = Object.create(BaseAI.prototype);
PetraBot.prototype.CustomInit = function(gameState)
{
CustomInit(gameState)
{
if (this.isDeserialized)
{
// WARNING: the deserializations should not modify the metadatas infos inside their init functions
@ -88,10 +88,10 @@ PetraBot.prototype.CustomInit = function(gameState)
// Try to analyze our starting position and set a strategy.
this.canPlay = gameAnalysis(this.HQ, gameState);
}
};
}
PetraBot.prototype.OnUpdate = function(sharedScript)
{
OnUpdate(sharedScript)
{
if (this.isDeserialized)
this.Init(PlayerID, sharedScript);
@ -133,10 +133,10 @@ PetraBot.prototype.OnUpdate = function(sharedScript)
}
this.turn++;
};
}
PetraBot.prototype.Serialize = function()
{
Serialize()
{
if (this.isDeserialized)
return this.data;
@ -167,10 +167,12 @@ PetraBot.prototype.Serialize = function()
"queueManager": this.queueManager.Serialize(),
"HQ": this.HQ.Serialize()
};
};
}
PetraBot.prototype.Deserialize = function(data, sharedScript)
{
Deserialize(data, sharedScript)
{
this.isDeserialized = true;
this.data = data;
};
}
}

View file

@ -7,43 +7,45 @@ import * as difficulty from "simulation/ai/petra/difficultyLevel.js";
import { allowCapture, getLandAccess } from "simulation/ai/petra/entityExtend.js";
import { Worker } from "simulation/ai/petra/worker.js";
export function AttackManager(config)
export class AttackManager
{
totalNumber = 0;
attackNumber = 0;
rushNumber = 0;
raidNumber = 0;
upcomingAttacks = {
[AttackPlan.TYPE_RUSH]: [],
[AttackPlan.TYPE_RAID]: [],
[AttackPlan.TYPE_DEFAULT]: [],
[AttackPlan.TYPE_HUGE_ATTACK]: []
};
startedAttacks = {
[AttackPlan.TYPE_RUSH]: [],
[AttackPlan.TYPE_RAID]: [],
[AttackPlan.TYPE_DEFAULT]: [],
[AttackPlan.TYPE_HUGE_ATTACK]: []
};
bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start
debugTime = 0;
maxRushes = 0;
rushSize = [];
currentEnemyPlayer = undefined; // enemy player we are currently targeting
defeated = {};
constructor(config)
{
this.Config = config;
}
this.totalNumber = 0;
this.attackNumber = 0;
this.rushNumber = 0;
this.raidNumber = 0;
this.upcomingAttacks = {
[AttackPlan.TYPE_RUSH]: [],
[AttackPlan.TYPE_RAID]: [],
[AttackPlan.TYPE_DEFAULT]: [],
[AttackPlan.TYPE_HUGE_ATTACK]: []
};
this.startedAttacks = {
[AttackPlan.TYPE_RUSH]: [],
[AttackPlan.TYPE_RAID]: [],
[AttackPlan.TYPE_DEFAULT]: [],
[AttackPlan.TYPE_HUGE_ATTACK]: []
};
this.bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start
this.debugTime = 0;
this.maxRushes = 0;
this.rushSize = [];
this.currentEnemyPlayer = undefined; // enemy player we are currently targeting
this.defeated = {};
}
/** More initialisation for stuff that needs the gameState */
AttackManager.prototype.init = function(gameState)
{
/** More initialisation for stuff that needs the gameState */
init(gameState)
{
this.outOfPlan = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "plan", -1));
this.outOfPlan.registerUpdates();
};
}
AttackManager.prototype.setRushes = function(allowed)
{
setRushes(allowed)
{
if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2)
{
this.maxRushes = 3;
@ -59,10 +61,10 @@ AttackManager.prototype.setRushes = function(allowed)
this.maxRushes = 1;
this.rushSize = [ 20 ];
}
};
}
AttackManager.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
for (const evt of events.PlayerDefeated)
this.defeated[evt.playerId] = true;
@ -134,13 +136,13 @@ AttackManager.prototype.checkEvents = function(gameState, events)
}
}
}
};
}
/**
/**
* Check for any structure in range from within our territory, and bomb it
*/
AttackManager.prototype.assignBombers = function(gameState)
{
assignBombers(gameState)
{
// First some cleaning of current bombing attacks
for (const [targetId, unitIds] of this.bombingAttacks)
{
@ -247,14 +249,14 @@ AttackManager.prototype.assignBombers = function(gameState)
break;
}
}
};
}
/**
/**
* Some functions are run every turn
* Others once in a while
*/
AttackManager.prototype.update = function(gameState, queues, events)
{
update(gameState, queues, events)
{
if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60)
{
this.debugTime = gameState.ai.elapsedTime;
@ -439,10 +441,10 @@ AttackManager.prototype.update = function(gameState, queues, events)
// Check if we have some unused ranged siege unit which could do something useful while waiting
if (this.Config.difficulty > difficulty.VERY_EASY && gameState.ai.playedTurn % 5 == 0)
this.assignBombers(gameState);
};
}
AttackManager.prototype.getPlan = function(planName)
{
getPlan(planName)
{
for (const attackType in this.upcomingAttacks)
{
for (const attack of this.upcomingAttacks[attackType])
@ -456,24 +458,24 @@ AttackManager.prototype.getPlan = function(planName)
return attack;
}
return undefined;
};
}
AttackManager.prototype.pausePlan = function(planName)
{
pausePlan(planName)
{
const attack = this.getPlan(planName);
if (attack)
attack.setPaused(true);
};
}
AttackManager.prototype.unpausePlan = function(planName)
{
unpausePlan(planName)
{
const attack = this.getPlan(planName);
if (attack)
attack.setPaused(false);
};
}
AttackManager.prototype.pauseAllPlans = function()
{
pauseAllPlans()
{
for (const attackType in this.upcomingAttacks)
for (const attack of this.upcomingAttacks[attackType])
attack.setPaused(true);
@ -481,10 +483,10 @@ AttackManager.prototype.pauseAllPlans = function()
for (const attackType in this.startedAttacks)
for (const attack of this.startedAttacks[attackType])
attack.setPaused(true);
};
}
AttackManager.prototype.unpauseAllPlans = function()
{
unpauseAllPlans()
{
for (const attackType in this.upcomingAttacks)
for (const attack of this.upcomingAttacks[attackType])
attack.setPaused(false);
@ -492,20 +494,20 @@ AttackManager.prototype.unpauseAllPlans = function()
for (const attackType in this.startedAttacks)
for (const attack of this.startedAttacks[attackType])
attack.setPaused(false);
};
}
AttackManager.prototype.getAttackInPreparation = function(type)
{
getAttackInPreparation(type)
{
return this.upcomingAttacks[type].length ? this.upcomingAttacks[type][0] : undefined;
};
}
/**
/**
* Determine which player should be attacked: when called when starting the attack,
* attack.targetPlayer is undefined and in that case, we keep track of the chosen target
* for future attacks.
*/
AttackManager.prototype.getEnemyPlayer = function(gameState, attack)
{
getEnemyPlayer(gameState, attack)
{
let enemyPlayer;
// First check if there is a preferred enemy based on our victory conditions.
@ -612,14 +614,14 @@ AttackManager.prototype.getEnemyPlayer = function(gameState, attack)
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
return enemyPlayer;
};
}
/**
/**
* Target the player with the most advanced wonder.
* TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead.
*/
AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack)
{
getWonderEnemyPlayer(gameState, attack)
{
let enemyPlayer;
let enemyWonder;
let moreAdvanced;
@ -645,13 +647,13 @@ AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack)
this.currentEnemyPlayer = enemyPlayer;
}
return enemyPlayer;
};
}
/**
/**
* Target the player with the most relics (including gaia).
*/
AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack)
{
getRelicEnemyPlayer(gameState, attack)
{
let enemyPlayer;
const allRelics = gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic"));
let maxRelicsOwned = 0;
@ -675,11 +677,11 @@ AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack)
gameState.ai.HQ.victoryManager.resetCaptureGaiaRelic(gameState);
}
return enemyPlayer;
};
}
/** f.e. if we have changed diplomacy with another player. */
AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player)
{
/** f.e. if we have changed diplomacy with another player. */
cancelAttacksAgainstPlayer(gameState, player)
{
for (const attackType in this.upcomingAttacks)
for (const attack of this.upcomingAttacks[attackType])
if (attack.targetPlayer === player)
@ -695,10 +697,10 @@ AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player)
this.startedAttacks[attackType].splice(i--, 1);
}
}
};
}
AttackManager.prototype.raidTargetEntity = function(gameState, ent)
{
raidTargetEntity(gameState, ent)
{
const data = { "target": ent };
const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber,
AttackPlan.TYPE_RAID, data, false);
@ -711,13 +713,13 @@ AttackManager.prototype.raidTargetEntity = function(gameState, ent)
attackPlan.init(gameState);
this.upcomingAttacks[AttackPlan.TYPE_RAID].push(attackPlan);
return attackPlan;
};
}
/**
/**
* Return the number of units from any of our attacking armies around this position
*/
AttackManager.prototype.numAttackingUnitsAround = function(pos, dist)
{
numAttackingUnitsAround(pos, dist)
{
let num = 0;
for (const attackType in this.startedAttacks)
for (const attack of this.startedAttacks[attackType])
@ -728,16 +730,16 @@ AttackManager.prototype.numAttackingUnitsAround = function(pos, dist)
num += attack.unitCollection.length;
}
return num;
};
}
/**
/**
* Switch defense armies into an attack one against the given target
* data.range: transform all defense armies inside range of the target into a new attack
* data.armyID: transform only the defense army ID into a new attack
* data.uniqueTarget: the attack will stop when the target is destroyed or captured
*/
AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data)
{
switchDefenseToAttack(gameState, target, data)
{
if (!target || !target.position())
return false;
if (!data.range && !data.armyID)
@ -797,10 +799,10 @@ AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data
attackPlan.target = target;
attackPlan.state = AttackPlan.STATE_ARRIVED;
return true;
};
}
AttackManager.prototype.Serialize = function()
{
Serialize()
{
const properties = {
"totalNumber": this.totalNumber,
"attackNumber": this.attackNumber,
@ -830,10 +832,10 @@ AttackManager.prototype.Serialize = function()
}
return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks };
};
}
AttackManager.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data.properties)
this[key] = data.properties[key];
@ -864,4 +866,5 @@ AttackManager.prototype.Deserialize = function(gameState, data)
this.startedAttacks[key].push(attack);
}
}
};
}
}

View file

@ -16,8 +16,10 @@ import { Worker } from "simulation/ai/petra/worker.js";
* When @c deserialized is true, do not call any random function inside constructor
* as that would cause oos.
*/
export function AttackPlan(gameState, config, uniqueID, type = AttackPlan.TYPE_DEFAULT, data, deserialized)
export class AttackPlan
{
constructor(gameState, config, uniqueID, type = AttackPlan.TYPE_DEFAULT, data, deserialized)
{
this.Config = config;
this.name = uniqueID;
this.type = type;
@ -239,31 +241,31 @@ export function AttackPlan(gameState, config, uniqueID, type = AttackPlan.TYPE_D
this.isBlocked = false; // true when this attack faces walls
return true;
}
}
AttackPlan.PREPARATION_FAILED = 0;
AttackPlan.PREPARATION_KEEP_GOING = 1;
AttackPlan.PREPARATION_START = 2;
static PREPARATION_FAILED = 0;
static PREPARATION_KEEP_GOING = 1;
static PREPARATION_START = 2;
AttackPlan.SIEGE_NOT_TESTED = 0;
AttackPlan.SIEGE_NO_TRAINER = 1;
static SIEGE_NOT_TESTED = 0;
static SIEGE_NO_TRAINER = 1;
/**
/**
* Siege added in build orders
*/
AttackPlan.SIEGE_ADDED = 2;
static SIEGE_ADDED = 2;
AttackPlan.STATE_UNEXECUTED = "unexecuted";
AttackPlan.STATE_COMPLETING = "completing";
AttackPlan.STATE_ARRIVED = "arrived";
static STATE_UNEXECUTED = "unexecuted";
static STATE_COMPLETING = "completing";
static STATE_ARRIVED = "arrived";
AttackPlan.TYPE_DEFAULT = "Attack";
AttackPlan.TYPE_HUGE_ATTACK = "HugeAttack";
AttackPlan.TYPE_RAID = "Raid";
AttackPlan.TYPE_RUSH = "Rush";
static TYPE_DEFAULT = "Attack";
static TYPE_HUGE_ATTACK = "HugeAttack";
static TYPE_RAID = "Raid";
static TYPE_RUSH = "Rush";
AttackPlan.prototype.init = function(gameState)
{
init(gameState)
{
this.queue = gameState.ai.queues["plan_" + this.name];
this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"];
this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"];
@ -284,39 +286,39 @@ AttackPlan.prototype.init = function(gameState)
if (this.canBuildUnits)
this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]);
}
};
}
AttackPlan.prototype.getName = function()
{
getName()
{
return this.name;
};
}
AttackPlan.prototype.getType = function()
{
getType()
{
return this.type;
};
}
AttackPlan.prototype.isStarted = function()
{
isStarted()
{
return this.state !== AttackPlan.STATE_UNEXECUTED && this.state !== AttackPlan.STATE_COMPLETING;
};
}
AttackPlan.prototype.isPaused = function()
{
isPaused()
{
return this.paused;
};
}
AttackPlan.prototype.setPaused = function(boolValue)
{
setPaused(boolValue)
{
this.paused = boolValue;
};
}
/**
/**
* Returns true if the attack can be executed at the current time
* Basically it checks we have enough units.
*/
AttackPlan.prototype.canStart = function()
{
canStart()
{
if (!this.canBuildUnits)
return true;
@ -325,10 +327,10 @@ AttackPlan.prototype.canStart = function()
return false;
return true;
};
}
AttackPlan.prototype.mustStart = function()
{
mustStart()
{
if (this.isPaused())
return false;
@ -355,10 +357,10 @@ AttackPlan.prototype.mustStart = function()
return this.type === AttackPlan.TYPE_RAID && this.target && this.target.foundationProgress() &&
this.target.foundationProgress() > 50;
return false;
};
}
AttackPlan.prototype.forceStart = function()
{
forceStart()
{
for (const unitCat in this.unitStat)
{
const Unit = this.unitStat[unitCat];
@ -366,25 +368,25 @@ AttackPlan.prototype.forceStart = function()
Unit.minSize = 0;
}
this.forced = true;
};
}
AttackPlan.prototype.emptyQueues = function()
{
emptyQueues()
{
this.queue.empty();
this.queueChamp.empty();
this.queueSiege.empty();
};
}
AttackPlan.prototype.removeQueues = function(gameState)
{
removeQueues(gameState)
{
gameState.ai.queueManager.removeQueue("plan_" + this.name);
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ");
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
};
}
/** Adds a build order. If resetQueue is true, this will reset the queue. */
AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue)
{
/** Adds a build order. If resetQueue is true, this will reset the queue. */
addBuildOrder(gameState, name, unitStats, resetQueue)
{
if (!this.isStarted())
{
// no minsize as we don't want the plan to fail at the last minute though.
@ -396,10 +398,10 @@ AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQ
if (resetQueue)
this.emptyQueues();
}
};
}
AttackPlan.prototype.addSiegeUnits = function(gameState)
{
addSiegeUnits(gameState)
{
if (this.siegeState === AttackPlan.SIEGE_ADDED || this.state !== AttackPlan.STATE_UNEXECUTED)
return false;
@ -450,11 +452,11 @@ AttackPlan.prototype.addSiegeUnits = function(gameState)
"classes": classes[i], "interests": [ ["siegeStrength", 3] ] };
this.addBuildOrder(gameState, "Siege", stat, true);
return true;
};
}
/** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */
AttackPlan.prototype.updatePreparation = function(gameState)
{
/** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */
updatePreparation(gameState)
{
// the completing step is used to return resources and regroup the units
// so we check that we have no more forced order before starting the attack
if (this.state === AttackPlan.STATE_COMPLETING)
@ -618,10 +620,10 @@ AttackPlan.prototype.updatePreparation = function(gameState)
// reset all queued units
this.removeQueues(gameState);
return AttackPlan.PREPARATION_KEEP_GOING;
};
}
AttackPlan.prototype.trainMoreUnits = function(gameState)
{
trainMoreUnits(gameState)
{
// let's sort by training advancement, ie 'current size / target size'
// count the number of queued units too.
// substract priority.
@ -716,10 +718,10 @@ AttackPlan.prototype.trainMoreUnits = function(gameState)
}
}
}
};
}
AttackPlan.prototype.assignUnits = function(gameState)
{
assignUnits(gameState)
{
const plan = this.name;
let added = false;
// If we can not build units, assign all available except those affected to allied defense to the current attack.
@ -807,10 +809,10 @@ AttackPlan.prototype.assignUnits = function(gameState)
added = true;
}
return added;
};
}
AttackPlan.prototype.isAvailableUnit = function(gameState, ent)
{
isAvailableUnit(gameState, ent)
{
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 ||
@ -819,11 +821,11 @@ AttackPlan.prototype.isAvailableUnit = function(gameState, ent)
if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8))
return false;
return true;
};
}
/** Reassign one (at each turn) FastMoving unit to fasten raid preparation. */
AttackPlan.prototype.reassignFastUnit = function(gameState)
{
/** Reassign one (at each turn) FastMoving unit to fasten raid preparation. */
reassignFastUnit(gameState)
{
for (const ent of this.unitCollection.values())
{
if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined)
@ -836,10 +838,10 @@ AttackPlan.prototype.reassignFastUnit = function(gameState)
raid.unitCollection.updateEnt(ent);
return;
}
};
}
AttackPlan.prototype.chooseTarget = function(gameState)
{
chooseTarget(gameState)
{
if (this.targetPlayer === undefined)
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
@ -921,12 +923,12 @@ AttackPlan.prototype.chooseTarget = function(gameState)
this.overseas = 0;
return true;
};
/**
}
/**
* sameLand true means that we look for a target for which we do not need to take a transport
*/
AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand)
{
getNearestTarget(gameState, position, sameLand)
{
this.isBlocked = false;
// Temporary variables needed by isValidTarget
this.gameState = gameState;
@ -991,14 +993,14 @@ AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand)
// Obstruction also can change the enemy target
this.targetPlayer = target.owner();
return target;
};
}
/**
/**
* Default target finder aims for conquest critical targets
* We must apply the *same* selection (isValidTarget) as done in getNearestTarget
*/
AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy)
{
defaultTargetFinder(gameState, playerEnemy)
{
let targets = new EntityCollection(gameState.sharedScript);
if (gameState.getVictoryConditions().has("wonder"))
{
@ -1045,20 +1047,20 @@ AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy)
.filter(filters.not(filters.byClass("Ship")));
}
return targets;
};
}
AttackPlan.prototype.isValidTarget = function(ent)
{
isValidTarget(ent)
{
if (!ent.position())
return false;
if (this.sameLand && getLandAccess(this.gameState, ent) != this.sameLand)
return false;
return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length;
};
}
/** Rush target finder aims at isolated non-defended buildings */
AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
{
/** Rush target finder aims at isolated non-defended buildings */
rushTargetFinder(gameState, playerEnemy)
{
let targets = new EntityCollection(gameState.sharedScript);
let buildings;
if (playerEnemy !== undefined)
@ -1110,11 +1112,11 @@ AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
targets = this.rushTargetFinder(gameState);
return targets;
};
}
/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */
AttackPlan.prototype.raidTargetFinder = function(gameState)
{
/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */
raidTargetFinder(gameState)
{
const targets = new EntityCollection(gameState.sharedScript);
for (const targetId of gameState.ai.HQ.defenseManager.targetList)
{
@ -1123,15 +1125,15 @@ AttackPlan.prototype.raidTargetFinder = function(gameState)
targets.addEnt(target);
}
return targets;
};
}
/**
/**
* Check that we can have a path to this target
* otherwise we may be blocked by walls and try to react accordingly
* This is done only when attacker and target are on the same land
*/
AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position)
{
checkTargetObstruction(gameState, target, position)
{
if (getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position))
return target;
@ -1234,10 +1236,10 @@ AttackPlan.prototype.checkTargetObstruction = function(gameState, target, positi
}
return target;
};
}
AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false)
{
getPathToTarget(gameState, fixedRallyPoint = false)
{
const startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
const endAccess = getLandAccess(gameState, this.target);
if (startAccess != endAccess)
@ -1259,11 +1261,11 @@ AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = fal
Engine.ProfileStop();
return true;
};
}
/** Set rally point at the border of our territory */
AttackPlan.prototype.setRallyPoint = function(gameState)
{
/** Set rally point at the border of our territory */
setRallyPoint(gameState)
{
for (let i = 0; i < this.path.length; ++i)
{
if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID)
@ -1283,14 +1285,14 @@ AttackPlan.prototype.setRallyPoint = function(gameState)
}
break;
}
};
}
/**
/**
* Executes the attack plan, after this is executed the update function will be run every turn
* If we're here, it's because we have enough units.
*/
AttackPlan.prototype.StartAttack = function(gameState)
{
StartAttack(gameState)
{
if (this.Config.debug > 1)
aiWarn("start attack " + this.name + " with type " + this.type);
@ -1334,11 +1336,11 @@ AttackPlan.prototype.StartAttack = function(gameState)
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos);
}
return true;
};
}
/** Runs every turn after the attack is executed */
AttackPlan.prototype.update = function(gameState, events)
{
/** Runs every turn after the attack is executed */
update(gameState, events)
{
if (!this.unitCollection.hasEntities())
return 0;
@ -1841,10 +1843,10 @@ AttackPlan.prototype.update = function(gameState, events)
Engine.ProfileStop();
return this.unitCollection.length;
};
}
AttackPlan.prototype.UpdateTransporting = function(gameState, events)
{
UpdateTransporting(gameState, events)
{
let done = true;
for (const ent of this.unitCollection.values())
{
@ -1884,10 +1886,10 @@ AttackPlan.prototype.UpdateTransporting = function(gameState, events)
}
break;
}
};
}
AttackPlan.prototype.UpdateWalking = function(gameState, events)
{
UpdateWalking(gameState, events)
{
// we're marching towards the target
// Let's check if any of our unit has been attacked.
// In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building
@ -1999,10 +2001,10 @@ AttackPlan.prototype.UpdateWalking = function(gameState, events)
}
return true;
};
}
AttackPlan.prototype.UpdateTarget = function(gameState)
{
UpdateTarget(gameState)
{
// First update the target position in case it's a unit (and check if it has garrisoned)
if (this.target && this.target.hasClass("Unit"))
{
@ -2092,11 +2094,11 @@ AttackPlan.prototype.UpdateTarget = function(gameState)
this.targetPos = this.target.position();
}
return true;
};
}
/** reset any units */
AttackPlan.prototype.Abort = function(gameState)
{
/** reset any units */
Abort(gameState)
{
this.unitCollection.unregister();
if (this.unitCollection.hasEntities())
{
@ -2140,10 +2142,10 @@ AttackPlan.prototype.Abort = function(gameState)
this.unit[unitCat].unregister();
this.removeQueues(gameState);
};
}
AttackPlan.prototype.removeUnit = function(ent, update)
{
removeUnit(ent, update)
{
if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_ATTACK)
{
if (ent.hasClass("CitizenSoldier"))
@ -2155,10 +2157,10 @@ AttackPlan.prototype.removeUnit = function(ent, update)
ent.setMetadata(PlayerID, "plan", -1);
if (update)
this.unitCollection.updateEnt(ent);
};
}
AttackPlan.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
for (const evt of events.EntityRenamed)
{
if (!this.target || this.target.id() != evt.entity)
@ -2208,26 +2210,26 @@ AttackPlan.prototype.checkEvents = function(gameState, events)
this.rallyPoint = base.anchor.position();
}
}
};
}
AttackPlan.prototype.waitingForTransport = function()
{
waitingForTransport()
{
for (const ent of this.unitCollection.values())
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return true;
return false;
};
}
AttackPlan.prototype.hasSiegeUnits = function()
{
hasSiegeUnits()
{
for (const ent of this.unitCollection.values())
if (isSiegeUnit(ent))
return true;
return false;
};
}
AttackPlan.prototype.hasForceOrder = function(data, value)
{
hasForceOrder(data, value)
{
for (const ent of this.unitCollection.values())
{
if (data && +ent.getMetadata(PlayerID, data) !== value)
@ -2238,22 +2240,22 @@ AttackPlan.prototype.hasForceOrder = function(data, value)
return true;
}
return false;
};
}
/**
/**
* The center position of this attack may be in an inaccessible area. So we use the access
* of the unit nearest to this center position.
*/
AttackPlan.prototype.getAttackAccess = function(gameState)
{
getAttackAccess(gameState)
{
for (const ent of this.unitCollection.filterNearest(this.position, 1).values())
return getLandAccess(gameState, ent);
return 0;
};
}
AttackPlan.prototype.debugAttack = function()
{
debugAttack()
{
aiWarn("---------- attack " + this.name);
for (const unitCat in this.unitStat)
{
@ -2262,10 +2264,10 @@ AttackPlan.prototype.debugAttack = function()
Unit.targetSize);
}
aiWarn("------------------------------");
};
}
AttackPlan.prototype.Serialize = function()
{
Serialize()
{
const properties = {
"name": this.name,
"type": this.type,
@ -2291,10 +2293,10 @@ AttackPlan.prototype.Serialize = function()
};
return { "properties": properties };
};
}
AttackPlan.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data.properties)
this[key] = data.properties[key];
@ -2302,4 +2304,5 @@ AttackPlan.prototype.Deserialize = function(gameState, data)
this.target = gameState.getEntityById(this.target);
this.failed = undefined;
};
}
}

View file

@ -20,8 +20,10 @@ import { Worker } from "simulation/ai/petra/worker.js";
* -updating whatever needs updating, keeping track of stuffs (rebuilding needs)
*/
export function BaseManager(gameState, basesManager)
export class BaseManager
{
constructor(gameState, basesManager)
{
this.Config = basesManager.Config;
this.ID = gameState.ai.uniqueIDs.bases++;
this.basesManager = basesManager;
@ -43,28 +45,28 @@ export function BaseManager(gameState, basesManager)
this.territoryIndices = [];
this.timeNextIdleCheck = 0;
}
}
BaseManager.STATE_WITH_ANCHOR = "anchored";
static STATE_WITH_ANCHOR = "anchored";
/**
/**
* New base with a foundation anchor.
*/
BaseManager.STATE_UNCONSTRUCTED = "unconstructed";
static STATE_UNCONSTRUCTED = "unconstructed";
/**
/**
* Captured base with an anchor.
*/
BaseManager.STATE_CAPTURED = "captured";
static STATE_CAPTURED = "captured";
/**
/**
* Anchorless base, currently with dock.
*/
BaseManager.STATE_ANCHORLESS = "anchorless";
static STATE_ANCHORLESS = "anchorless";
BaseManager.prototype.init = function(gameState, state)
{
init(gameState, state)
{
if (state === BaseManager.STATE_UNCONSTRUCTED)
this.constructing = true;
else if (state !== BaseManager.STATE_CAPTURED)
@ -90,10 +92,10 @@ BaseManager.prototype.init = function(gameState, state)
this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] };
this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 };
}
};
}
BaseManager.prototype.reset = function(gameState, state)
{
reset(gameState, state)
{
if (state === BaseManager.STATE_UNCONSTRUCTED)
this.constructing = true;
else
@ -102,20 +104,20 @@ BaseManager.prototype.reset = function(gameState, state)
this.neededDefenders = 0;
else
this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3);
};
}
BaseManager.prototype.assignEntity = function(gameState, ent)
{
assignEntity(gameState, ent)
{
ent.setMetadata(PlayerID, "base", this.ID);
this.units.updateEnt(ent);
this.workers.updateEnt(ent);
this.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit"))
this.assignResourceToDropsite(gameState, ent, false);
};
}
BaseManager.prototype.setAnchor = function(gameState, anchorEntity, deserialize = false)
{
setAnchor(gameState, anchorEntity, deserialize = false)
{
if (!anchorEntity.hasClass("CivCentre"))
{
aiWarn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() +
@ -134,20 +136,20 @@ BaseManager.prototype.setAnchor = function(gameState, anchorEntity, deserialize
this.buildings.updateEnt(anchorEntity);
this.accessIndex = getLandAccess(gameState, anchorEntity);
return true;
};
}
/* we lost our anchor. Let's reassign our units and buildings */
BaseManager.prototype.anchorLost = function(gameState)
{
/* we lost our anchor. Let's reassign our units and buildings */
anchorLost(gameState)
{
this.anchor = undefined;
this.anchorId = undefined;
this.neededDefenders = 0;
this.basesManager.resetBaseCache();
};
}
/** Set a building of an anchorless base */
BaseManager.prototype.setAnchorlessEntity = function(gameState, ent, deserialize = false)
{
/** Set a building of an anchorless base */
setAnchorlessEntity(gameState, ent, deserialize = false)
{
if (!this.buildings.hasEntities())
{
if (!getBuiltEntity(gameState, ent).resourceDropsiteTypes())
@ -168,14 +170,14 @@ BaseManager.prototype.setAnchorlessEntity = function(gameState, ent, deserialize
ent.setMetadata(PlayerID, "base", this.ID);
this.buildings.updateEnt(ent);
return true;
};
}
/**
/**
* Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
* Moving resources (animals) and buildable resources (fields) are treated elsewhere.
*/
BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite, deserialized)
{
assignResourceToDropsite(gameState, dropsite, deserialized)
{
if (this.dropsites[dropsite.id()])
{
if (this.Config.debug > 0)
@ -256,10 +258,10 @@ BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite, d
"entities": [dropsiteId],
"shared": dropsiteId != this.anchorId
});
};
}
BaseManager.prototype.removeFromAssignedDropsite = function(entityID)
{
removeFromAssignedDropsite(entityID)
{
for (const type in this.dropsiteSupplies)
for (const proxim in this.dropsiteSupplies[type])
{
@ -270,11 +272,11 @@ BaseManager.prototype.removeFromAssignedDropsite = function(entityID)
resourcesList.splice(i--, 1);
}
}
};
}
// completely remove the dropsite resources from our list.
BaseManager.prototype.removeDropsite = function(gameState, entityID)
{
// completely remove the dropsite resources from our list.
removeDropsite(gameState, entityID)
{
if (!entityID)
return;
@ -299,14 +301,14 @@ BaseManager.prototype.removeDropsite = function(gameState, entityID)
}
this.dropsites[entityID] = undefined;
};
}
/**
/**
* @return {Object} - The position of the best place to build a new dropsite for the specified resource,
* its quality and its template name.
*/
BaseManager.prototype.findBestDropsiteAndLocation = function(gameState, resource)
{
findBestDropsiteAndLocation(gameState, resource)
{
let bestResult = {
"quality": 0,
"pos": [0, 0]
@ -321,13 +323,13 @@ BaseManager.prototype.findBestDropsiteAndLocation = function(gameState, resource
bestResult.templateName = templateName;
}
return bestResult;
};
}
/**
/**
* Returns the position of the best place to build a new dropsite for the specified resource and dropsite template.
*/
BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource, templateName)
{
findBestDropsiteLocation(gameState, resource, templateName)
{
const template = gameState.getTemplate(gameState.applyCiv(templateName));
// CCs and Docks are handled elsewhere.
@ -414,10 +416,10 @@ BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource, t
const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return { "quality": bestVal, "pos": [x, z] };
};
}
BaseManager.prototype.getResourceLevel = function(gameState, type, distances = ["nearby", "medium", "faraway"])
{
getResourceLevel(gameState, type, distances = ["nearby", "medium", "faraway"])
{
let count = 0;
const check = {};
for (const proxim of distances)
@ -431,11 +433,11 @@ BaseManager.prototype.getResourceLevel = function(gameState, type, distances = [
count += supplyEntity.resourceSupplyAmount();
}
return count;
};
}
/** check our resource levels and react accordingly */
BaseManager.prototype.checkResourceLevels = function(gameState, queues)
{
/** check our resource levels and react accordingly */
checkResourceLevels(gameState, queues)
{
for (const type of Resources.GetCodes())
{
if (type == "food")
@ -550,12 +552,11 @@ BaseManager.prototype.checkResourceLevels = function(gameState, queues)
else if (total == 0)
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
}
}
};
/** Adds the estimated gather rates from this base to the currentRates */
BaseManager.prototype.addGatherRates = function(gameState, currentRates)
{
/** Adds the estimated gather rates from this base to the currentRates */
addGatherRates(gameState, currentRates)
{
for (const res in currentRates)
{
// I calculate the exact gathering rate for each unit.
@ -592,10 +593,10 @@ BaseManager.prototype.addGatherRates = function(gameState, currentRates)
});
}
}
};
}
BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
{
assignRolelessUnits(gameState, roleless)
{
if (!roleless)
roleless = this.units.filter(filters.not(filters.byHasMetadata(PlayerID, "role"))).values();
@ -606,15 +607,15 @@ BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
else if (ent.hasClass("Support") && ent.hasClass("Elephant"))
ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER);
}
};
}
/**
/**
* If the numbers of workers on the resources is unbalanced then set some of workers to idle so
* they can be reassigned by reassignIdleWorkers.
* TODO: actually this probably should be in the HQ.
*/
BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
{
setWorkersIdleByPriority(gameState)
{
this.timeNextIdleCheck = gameState.ai.elapsedTime + 8;
// change resource only towards one which is more needed, and if changing will not change this order
let nb = 1; // no more than 1 change per turn (otherwise we should update the rates)
@ -662,15 +663,15 @@ BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
}
}
}
};
}
/**
/**
* Switch some gatherers (limited to number) from resource "from" to resource "to"
* and return remaining number of possible switches.
* Prefer Civilian for food and CitizenSoldier for other resources.
*/
BaseManager.prototype.switchGatherer = function(gameState, from, to, number)
{
switchGatherer(gameState, from, to, number)
{
let num = number;
let only;
const gatherers = this.gatherersByType(gameState, from);
@ -693,10 +694,10 @@ BaseManager.prototype.switchGatherer = function(gameState, from, to, number)
this.basesManager.AddTCResGatherer(to);
}
return num;
};
}
BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
{
reassignIdleWorkers(gameState, idleWorkers)
{
// Search for idle workers, and tell them to gather resources based on demand
if (!idleWorkers)
{
@ -746,27 +747,27 @@ BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
else if (ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_FISHER);
}
};
}
BaseManager.prototype.workersBySubrole = function(gameState, subrole)
{
workersBySubrole(gameState, subrole)
{
return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID,
filters.byMetadata(PlayerID, "subrole", subrole), this.workers);
};
}
BaseManager.prototype.gatherersByType = function(gameState, type)
{
gatherersByType(gameState, type)
{
return gameState.updatingCollection("workers-gathering-" + type +"-base-" +
this.ID, filters.byMetadata(PlayerID, "gather-type", type),
this.workersBySubrole(gameState, Worker.SUBROLE_GATHERER));
};
}
/**
/**
* returns an entity collection of workers.
* They are idled immediatly and their subrole set to idle.
*/
BaseManager.prototype.pickBuilders = function(gameState, workers, number)
{
pickBuilders(gameState, workers, number)
{
const availableWorkers = this.workers.filter(ent =>
{
if (!ent.position() || !ent.isBuilder())
@ -803,15 +804,15 @@ BaseManager.prototype.pickBuilders = function(gameState, workers, number)
workers.addEnt(availableWorkers[i]);
}
return;
};
}
/**
/**
* If we have some foundations, and we don't have enough builder-workers,
* try reassigning some other workers who are nearby
* AI tries to use builders sensibly, not completely stopping its econ.
*/
BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
{
assignToFoundations(gameState, noRepair)
{
let foundations = this.buildings.filter(filters.and(filters.isFoundation(),
filters.not(filters.byClass("Field"))));
@ -1050,11 +1051,11 @@ BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
}
};
}
/** Return false when the base is not active (no workers on it) */
BaseManager.prototype.update = function(gameState, queues, events)
{
/** Return false when the base is not active (no workers on it) */
update(gameState, queues, events)
{
if (this.ID == this.basesManager.baselessBase().ID)
{
// if some active base, reassigns the workers/buildings
@ -1190,25 +1191,25 @@ BaseManager.prototype.update = function(gameState, queues, events)
Engine.ProfileStop();
return true;
};
}
BaseManager.prototype.AddTCGatherer = function(supplyID)
{
AddTCGatherer(supplyID)
{
return this.basesManager.AddTCGatherer(supplyID);
};
}
BaseManager.prototype.RemoveTCGatherer = function(supplyID)
{
RemoveTCGatherer(supplyID)
{
this.basesManager.RemoveTCGatherer(supplyID);
};
}
BaseManager.prototype.GetTCGatherer = function(supplyID)
{
GetTCGatherer(supplyID)
{
return this.basesManager.GetTCGatherer(supplyID);
};
}
BaseManager.prototype.Serialize = function()
{
Serialize()
{
return {
"ID": this.ID,
"anchorId": this.anchorId,
@ -1221,12 +1222,13 @@ BaseManager.prototype.Serialize = function()
"timeNextIdleCheck": this.timeNextIdleCheck,
"dropsiteSupplies": this.dropsiteSupplies
};
};
}
BaseManager.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data)
this[key] = data[key];
this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined;
};
}
}

View file

@ -12,23 +12,25 @@ import { Worker } from "simulation/ai/petra/worker.js";
* Only one base is run every turn.
*/
export function BasesManager(Config)
export class BasesManager
{
this.Config = Config;
this.currentBase = 0;
currentBase = 0;
// Cache some quantities for performance.
this.turnCache = {};
turnCache = {};
// Deals with unit/structure without base.
this.noBase = undefined;
noBase = undefined;
this.baseManagers = [];
}
baseManagers = [];
BasesManager.prototype.init = function(gameState, deserialize = false)
{
constructor(Config)
{
this.Config = Config;
}
init(gameState, deserialize = false)
{
// Initialize base map. Each pixel is a base ID, or 0 if not or not accessible.
this.basesMap = new InfoMap(gameState.sharedScript, "territory");
@ -41,13 +43,13 @@ BasesManager.prototype.init = function(gameState, deserialize = false)
this.createBase(gameState, cc, BaseManager.STATE_WITH_ANCHOR, deserialize);
else
this.createBase(gameState, cc, BaseManager.STATE_UNCONSTRUCTED, deserialize);
};
}
/**
/**
* Initialization needed after deserialization (only called when deserialising).
*/
BasesManager.prototype.postinit = function(gameState)
{
postinit(gameState)
{
// Rebuild the base maps from the territory indices of each base.
this.basesMap = new InfoMap(gameState.sharedScript, "territory");
for (const base of this.baseManagers)
@ -65,17 +67,16 @@ BasesManager.prototype.postinit = function(gameState)
continue;
const base = this.getBaseByID(baseID);
}
};
}
/**
/**
* Create a new base in the baseManager:
* If an existing one without anchor already exist, use it.
* Otherwise create a new one.
* TODO when buildings, criteria should depend on distance
*/
BasesManager.prototype.createBase = function(gameState, ent, type = BaseManager.STATE_WITH_ANCHOR,
deserialize = false)
{
createBase(gameState, ent, type = BaseManager.STATE_WITH_ANCHOR, deserialize = false)
{
const access = getLandAccess(gameState, ent);
let newbase;
for (const base of this.baseManagers)
@ -126,11 +127,11 @@ BasesManager.prototype.createBase = function(gameState, ent, type = BaseManager.
newbase.setAnchorlessEntity(gameState, ent, deserialize);
return newbase;
};
}
/** TODO check if the new anchorless bases should be added to addBase */
BasesManager.prototype.checkEvents = function(gameState, events)
{
/** TODO check if the new anchorless bases should be added to addBase */
checkEvents(gameState, events)
{
this.turnCache = {};
let addBase = false;
@ -346,14 +347,14 @@ BasesManager.prototype.checkEvents = function(gameState, events)
if (addBase)
gameState.ai.HQ.handleNewBase(gameState);
};
}
/**
/**
* returns an entity collection of workers through BaseManager.pickBuilders
* TODO: when same accessIndex, sort by distance
*/
BasesManager.prototype.bulkPickWorkers = function(gameState, baseRef, number)
{
bulkPickWorkers(gameState, baseRef, number)
{
const accessIndex = baseRef.accessIndex;
if (!accessIndex)
return false;
@ -383,13 +384,13 @@ BasesManager.prototype.bulkPickWorkers = function(gameState, baseRef, number)
if (!workers.length)
return false;
return workers;
};
}
/**
/**
* @return {Object} - Resources (estimation) still gatherable in our territory.
*/
BasesManager.prototype.getTotalResourceLevel = function(gameState, resources = Resources.GetCodes(), proximity = ["nearby", "medium"])
{
getTotalResourceLevel(gameState, resources = Resources.GetCodes(), proximity = ["nearby", "medium"])
{
const total = {};
for (const res of resources)
total[res] = 0;
@ -398,14 +399,14 @@ BasesManager.prototype.getTotalResourceLevel = function(gameState, resources = R
total[res] += base.getResourceLevel(gameState, res, proximity);
return total;
};
}
/**
/**
* Returns the current gather rate
* This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that.
*/
BasesManager.prototype.GetCurrentGatherRates = function(gameState)
{
GetCurrentGatherRates(gameState)
{
if (!this.turnCache.currentRates)
{
const currentRates = {};
@ -421,13 +422,13 @@ BasesManager.prototype.GetCurrentGatherRates = function(gameState)
}
return this.turnCache.currentRates;
};
}
/** Some functions that register that we assigned a gatherer to a resource this turn */
/** Some functions that register that we assigned a gatherer to a resource this turn */
/** Add a gatherer to the turn cache for this supply. */
BasesManager.prototype.AddTCGatherer = function(supplyID)
{
/** Add a gatherer to the turn cache for this supply. */
AddTCGatherer(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined)
++this.turnCache.resourceGatherer[supplyID];
else
@ -436,11 +437,11 @@ BasesManager.prototype.AddTCGatherer = function(supplyID)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = 1;
}
};
}
/** Remove a gatherer from the turn cache for this supply. */
BasesManager.prototype.RemoveTCGatherer = function(supplyID)
{
/** Remove a gatherer from the turn cache for this supply. */
RemoveTCGatherer(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
--this.turnCache.resourceGatherer[supplyID];
else
@ -449,19 +450,19 @@ BasesManager.prototype.RemoveTCGatherer = function(supplyID)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = -1;
}
};
}
BasesManager.prototype.GetTCGatherer = function(supplyID)
{
GetTCGatherer(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
return this.turnCache.resourceGatherer[supplyID];
return 0;
};
}
/** The next two are to register that we assigned a gatherer to a resource this turn. */
BasesManager.prototype.AddTCResGatherer = function(resource)
{
/** The next two are to register that we assigned a gatherer to a resource this turn. */
AddTCResGatherer(resource)
{
const check = "resourceGatherer-" + resource;
if (this.turnCache[check])
++this.turnCache[check];
@ -470,53 +471,53 @@ BasesManager.prototype.AddTCResGatherer = function(resource)
if (this.turnCache.currentRates)
this.turnCache.currentRates[resource] += 0.5;
};
}
BasesManager.prototype.GetTCResGatherer = function(resource)
{
GetTCResGatherer(resource)
{
const check = "resourceGatherer-" + resource;
if (this.turnCache[check])
return this.turnCache[check];
return 0;
};
}
/**
/**
* returns the number of bases with a cc
* ActiveBases includes only those with a built cc
* PotentialBases includes also those with a cc in construction
*/
BasesManager.prototype.numActiveBases = function()
{
numActiveBases()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.active;
};
}
BasesManager.prototype.hasActiveBase = function()
{
hasActiveBase()
{
return !!this.numActiveBases();
};
}
BasesManager.prototype.numPotentialBases = function()
{
numPotentialBases()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.potential;
};
}
BasesManager.prototype.hasPotentialBase = function()
{
hasPotentialBase()
{
return !!this.numPotentialBases();
};
}
/**
/**
* Updates the number of active and potential bases.
* .potential {number} - Bases that may or may not still be a foundation.
* .active {number} - Usable bases.
*/
BasesManager.prototype.updateBaseCache = function()
{
updateBaseCache()
{
this.turnCache.base = { "active": 0, "potential": 0 };
for (const base of this.baseManagers)
{
@ -526,46 +527,46 @@ BasesManager.prototype.updateBaseCache = function()
if (base.anchor.foundationProgress() === undefined)
++this.turnCache.base.active;
}
};
}
BasesManager.prototype.resetBaseCache = function()
{
resetBaseCache()
{
this.turnCache.base = undefined;
};
}
BasesManager.prototype.baselessBase = function()
{
baselessBase()
{
return this.noBase;
};
}
/**
/**
* @param {number} baseID
* @return {Object} - The base corresponding to baseID.
*/
BasesManager.prototype.getBaseByID = function(baseID)
{
getBaseByID(baseID)
{
if (this.noBase.ID === baseID)
return this.noBase;
return this.baseManagers.find(base => base.ID === baseID);
};
}
/**
/**
* flag a resource as exhausted
*/
BasesManager.prototype.isResourceExhausted = function(resource)
{
isResourceExhausted(resource)
{
return this.baseManagers.every(base =>
!base.dropsiteSupplies[resource].nearby.length &&
!base.dropsiteSupplies[resource].medium.length &&
!base.dropsiteSupplies[resource].faraway.length);
};
}
/**
/**
* Count gatherers returning resources in the number of gatherers of resourceSupplies
* to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps).
*/
BasesManager.prototype.assignGatherers = function()
{
assignGatherers()
{
for (const base of this.baseManagers)
for (const worker of base.workers.values())
{
@ -576,14 +577,14 @@ BasesManager.prototype.assignGatherers = function()
continue;
this.AddTCGatherer(orders[1].target);
}
};
}
/**
/**
* Assign an entity to the closest base.
* Used by the starting strategy.
*/
BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex)
{
assignEntity(gameState, ent, territoryIndex)
{
let bestbase;
for (const base of this.baseManagers)
{
@ -616,33 +617,33 @@ BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex)
bestbase.workerObject.update(gameState, ent);
}
}
};
}
/**
/**
* Adds the gather rates of individual bases to a shared object.
* @param {Object} gameState
* @param {Object} rates - The rates to add the gather rates to.
*/
BasesManager.prototype.addGatherRates = function(gameState, rates)
{
addGatherRates(gameState, rates)
{
for (const base of this.baseManagers)
base.addGatherRates(gameState, rates);
};
}
/**
/**
* @param {number} territoryIndex
* @return {number} - The ID of the base at the given territory index.
*/
BasesManager.prototype.baseAtIndex = function(territoryIndex)
{
baseAtIndex(territoryIndex)
{
return this.basesMap.map[territoryIndex];
};
}
/**
/**
* @param {number} territoryIndex
*/
BasesManager.prototype.removeBaseFromTerritoryIndex = function(territoryIndex)
{
removeBaseFromTerritoryIndex(territoryIndex)
{
const baseID = this.basesMap.map[territoryIndex];
if (baseID == 0)
return;
@ -658,13 +659,13 @@ BasesManager.prototype.removeBaseFromTerritoryIndex = function(territoryIndex)
else
aiWarn(" problem in headquarters::updateTerritories without base " + baseID);
this.basesMap.map[territoryIndex] = 0;
};
}
/**
/**
* @return {boolean} - Whether the index was added to a base.
*/
BasesManager.prototype.addTerritoryIndexToBase = function(gameState, territoryIndex, passabilityMap)
{
addTerritoryIndexToBase(gameState, territoryIndex, passabilityMap)
{
if (this.baseAtIndex(territoryIndex) != 0)
return false;
let landPassable = false;
@ -700,11 +701,11 @@ BasesManager.prototype.addTerritoryIndexToBase = function(gameState, territoryIn
this.getBaseByID(baseID).territoryIndices.push(territoryIndex);
this.basesMap.map[territoryIndex] = baseID;
return true;
};
}
/** Reassign territories when a base is going to be deleted */
BasesManager.prototype.reassignTerritories = function(deletedBase, territoryMap)
{
/** Reassign territories when a base is going to be deleted */
reassignTerritories(deletedBase, territoryMap)
{
const cellSize = territoryMap.cellSize;
const width = territoryMap.width;
for (let j = 0; j < territoryMap.length; ++j)
@ -741,13 +742,13 @@ BasesManager.prototype.reassignTerritories = function(deletedBase, territoryMap)
else
this.basesMap.map[j] = 0;
}
};
}
/**
/**
* We will loop only on one active base per turn.
*/
BasesManager.prototype.update = function(gameState, queues, events)
{
update(gameState, queues, events)
{
Engine.ProfileStart("BasesManager update");
this.assignGatherers();
@ -763,10 +764,10 @@ BasesManager.prototype.update = function(gameState, queues, events)
}
Engine.ProfileStop();
};
}
BasesManager.prototype.Serialize = function()
{
Serialize()
{
const properties = {
"currentBase": this.currentBase
};
@ -781,10 +782,10 @@ BasesManager.prototype.Serialize = function()
"noBase": this.noBase?.Serialize(),
"baseManagers": baseManagers
};
};
}
BasesManager.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data.properties)
this[key] = data.properties[key];
@ -806,4 +807,5 @@ BasesManager.prototype.Deserialize = function(gameState, data)
newbase.Deserialize(gameState, basedata);
this.baseManagers.push(newbase);
}
};
}
}

View file

@ -1,21 +1,6 @@
import * as filters from "simulation/ai/common-api/filters.js";
import { aiWarn } from "simulation/ai/common-api/utils.js";
/**
* One task of this manager is to cache the list of structures we have builders for,
* to avoid having to loop on all entities each time.
* It also takes care of the structures we can't currently build and should not try to build endlessly.
*/
export function BuildManager()
{
// List of buildings we have builders for, with number of possible builders.
this.builders = new Map();
// List of buildings we can't currently build (because no room, no builder or whatever),
// with time we should wait before trying again to build it.
this.unbuildables = new Map();
}
function addBuilder(builders, civ, entity)
{
for (const buildable of entity.buildableEntities(civ))
@ -36,17 +21,31 @@ function removeBuilder(builders, entityId)
}
}
/** Initialization at start of game */
BuildManager.prototype.init = function(gameState)
/**
* One task of this manager is to cache the list of structures we have builders for,
* to avoid having to loop on all entities each time.
* It also takes care of the structures we can't currently build and should not try to build endlessly.
*/
export class BuildManager
{
// List of buildings we have builders for, with number of possible builders.
builders = new Map();
// List of buildings we can't currently build (because no room, no builder or whatever),
// with time we should wait before trying again to build it.
unbuildables = new Map();
/** Initialization at start of game */
init(gameState)
{
const civ = gameState.getPlayerCiv();
for (const ent of gameState.getOwnUnits().values())
addBuilder(this.builders, civ, ent);
};
}
/** Update the builders counters */
BuildManager.prototype.checkEvents = function(gameState, events)
{
/** Update the builders counters */
checkEvents(gameState, events)
{
this.elapsedTime = gameState.ai.elapsedTime;
const civ = gameState.getPlayerCiv();
@ -90,14 +89,14 @@ BuildManager.prototype.checkEvents = function(gameState, events)
for (const ent of gameState.getOwnUnits().values())
addBuilder(this.builders, civ, ent);
}
};
}
/**
/**
* Get the buildable structures passing a filter.
*/
BuildManager.prototype.findStructuresByFilter = function(gameState, filter)
{
findStructuresByFilter(gameState, filter)
{
const result = [];
for (const [templateName, entities] of this.builders)
{
@ -110,39 +109,43 @@ BuildManager.prototype.findStructuresByFilter = function(gameState, filter)
result.push(templateName);
}
return result;
};
}
/**
/**
* Get the first buildable structure with a given class
* TODO when several available, choose the best one
*/
BuildManager.prototype.findStructureWithClass = function(gameState, classes)
{
findStructureWithClass(gameState, classes)
{
return this.findStructuresByFilter(gameState, filters.byClasses(classes))[0];
};
}
BuildManager.prototype.hasBuilder = function(template)
{
hasBuilder(template)
{
const numBuilders = this.builders.get(template);
return numBuilders && numBuilders.length > 0;
};
}
BuildManager.prototype.isUnbuildable = function(gameState, template)
{
return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime;
};
isUnbuildable(gameState, template)
{
return this.unbuildables.has(template) &&
this.unbuildables.get(template).time > gameState.ai.elapsedTime;
}
BuildManager.prototype.setBuildable = function(template)
{
setBuildable(template)
{
if (this.unbuildables.has(template))
this.unbuildables.delete(template);
};
}
/** Time is the duration in second that we will wait before checking again if it is buildable */
BuildManager.prototype.setUnbuildable = function(gameState, template, time = 90, reason = "room")
{
/** Time is the duration in second that we will wait before checking again if it is buildable */
setUnbuildable(gameState, template, time = 90, reason = "room")
{
if (!this.unbuildables.has(template))
this.unbuildables.set(template, { "reason": reason, "time": gameState.ai.elapsedTime + time });
{
this.unbuildables.set(template,
{ "reason": reason, "time": gameState.ai.elapsedTime + time });
}
else
{
const unbuildable = this.unbuildables.get(template);
@ -152,37 +155,37 @@ BuildManager.prototype.setUnbuildable = function(gameState, template, time = 90,
unbuildable.time = gameState.ai.elapsedTime + time;
}
}
};
}
/** Return the number of unbuildables due to missing room */
BuildManager.prototype.numberMissingRoom = function(gameState)
{
/** Return the number of unbuildables due to missing room */
numberMissingRoom(gameState)
{
let num = 0;
for (const unbuildable of this.unbuildables.values())
if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime)
++num;
return num;
};
}
/** Reset the unbuildables due to missing room */
BuildManager.prototype.resetMissingRoom = function(gameState)
{
/** Reset the unbuildables due to missing room */
resetMissingRoom(gameState)
{
for (const [key, unbuildable] of this.unbuildables)
if (unbuildable.reason == "room")
this.unbuildables.delete(key);
};
}
BuildManager.prototype.Serialize = function()
{
Serialize()
{
return {
"builders": this.builders,
"unbuildables": this.unbuildables
};
};
}
BuildManager.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -1,21 +1,16 @@
import { aiWarn } from "simulation/ai/common-api/utils.js";
import * as difficultyLevel from "simulation/ai/petra/difficultyLevel.js";
export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
export class Config
{
this.difficulty = difficulty;
// for instance "balanced", "aggressive" or "defensive"
this.behavior = behavior || "random";
// debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug
this.debug = 0;
debug = 0;
this.chat = true; // false to prevent AI's chats
chat = true; // false to prevent AI's chats
this.popScaling = 1; // scale factor depending on the max population
popScaling = 1; // scale factor depending on the max population
this.Military = {
Military = {
"towerLapseTime": 360, // Time to wait between building 2 towers
"fortressLapseTime": 390, // Time to wait between building 2 fortresses
"popForBarracks1": 25,
@ -24,14 +19,14 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
"numSentryTowers": 1
};
this.DamageTypeImportance = {
DamageTypeImportance = {
"Hack": 0.075,
"Pierce": 0.085,
"Crush": 0.045,
"Fire": 0.001
};
this.Economy = {
Economy = {
"popPhase2": 150, // How many units we want before aging to phase2.
"workPhase3": 180, // How many workers we want before aging to phase3.
"workPhase4": 200, // How many workers we want before aging to phase4 or higher.
@ -45,7 +40,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
// Note: attack settings are set directly in attack_plan.js
// defense
this.Defense =
Defense =
{
"defenseRatio": { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers.
"armyCompactSize": 2000, // squared. Half-diameter of an army.
@ -55,7 +50,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
// Additional buildings that the AI does not yet know when to build
// and that it will try to build on phase 3 when enough resources.
this.buildings =
buildings =
{
"default": [],
"achae": [
@ -112,7 +107,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
]
};
this.priorities =
priorities =
{
"villager": 300, // should be slightly lower than the citizen soldier one to not get all the food
"citizenSoldier": 600,
@ -135,7 +130,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
};
// Default personality (will be updated in setConfig)
this.personality =
personality =
{
"aggressive": 0.5,
"cooperative": 0.5,
@ -143,7 +138,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
};
// See QueueManager.prototype.wantedGatherRates()
this.queues =
queues =
{
"firstTurn": {
"food": 10,
@ -163,15 +158,15 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
}
};
this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 };
garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 };
this.unusedNoAllyTechs = [
unusedNoAllyTechs = [
"Player/sharedLos",
"Market/InternationalBonus",
"Player/sharedDropsites"
];
this.criticalPopulationFactors = [
criticalPopulationFactors = [
0.8,
0.8,
0.7,
@ -180,7 +175,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
0.35
];
this.criticalStructureFactors = [
criticalStructureFactors = [
0.8,
0.8,
0.7,
@ -189,7 +184,7 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
0.35
];
this.criticalRootFactors = [
criticalRootFactors = [
0.8,
0.8,
0.67,
@ -197,10 +192,17 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
0.35,
0.2
];
}
Config.prototype.setConfig = function(gameState)
{
constructor(difficulty = difficultyLevel.MEDIUM, behavior)
{
this.difficulty = difficulty;
// for instance "balanced", "aggressive" or "defensive"
this.behavior = behavior || "random";
}
setConfig(gameState)
{
if (this.difficulty > difficultyLevel.SANDBOX)
{
// Setup personality traits according to the user choice:
@ -320,10 +322,10 @@ Config.prototype.setConfig = function(gameState)
if (this.debug < 2)
return;
aiWarn(" >>> Petra bot: personality = " + uneval(this.personality));
};
}
Config.prototype.Cheat = function(gameState)
{
Cheat(gameState)
{
// Sandbox, Very Easy, Easy, Medium, Hard, Very Hard
// rate apply on resource stockpiling as gathering and trading
// time apply on building, upgrading, packing, training and technologies
@ -335,19 +337,20 @@ Config.prototype.Cheat = function(gameState)
"Trader/GainMultiplier": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
"Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }],
}, gameState.playerData.entity);
};
}
Config.prototype.Serialize = function()
{
Serialize()
{
var data = {};
for (const key in this)
if (Object.hasOwn(this, key) && key != "debug")
data[key] = this[key];
return data;
};
}
Config.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -13,8 +13,10 @@ import { Worker } from "simulation/ai/petra/worker.js";
* "capturing": army set to capture a gaia building or recover capture points to one of its own structures
* It must contain only one foe (the building to capture) and never be merged
*/
export function DefenseArmy(gameState, foeEntities, type)
export class DefenseArmy
{
constructor(gameState, foeEntities, type)
{
this.ID = gameState.ai.uniqueIDs.armies++;
this.type = type || "default";
@ -46,16 +48,16 @@ export function DefenseArmy(gameState, foeEntities, type)
this.recalculatePosition(gameState, true);
return true;
}
}
/**
/**
* add an entity to the enemy army
* Will return true if the entity was added and false otherwise.
* won't recalculate our position but will dirty it.
* force is true at army creation or when merging armies, so in this case we should add it even if far
*/
DefenseArmy.prototype.addFoe = function(gameState, enemyId, force)
{
addFoe(gameState, enemyId, force)
{
if (this.foeEntities.indexOf(enemyId) !== -1)
return false;
const ent = gameState.getEntityById(enemyId);
@ -73,14 +75,14 @@ DefenseArmy.prototype.addFoe = function(gameState, enemyId, force)
ent.setMetadata(PlayerID, "PartOfArmy", this.ID);
return true;
};
}
/**
/**
* returns true if the entity was removed and false otherwise.
* TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen.
*/
DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity)
{
removeFoe(gameState, enemyId, enemyEntity)
{
const idx = this.foeEntities.indexOf(enemyId);
if (idx === -1)
return false;
@ -100,14 +102,14 @@ DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity)
}
return true;
};
}
/**
/**
* adds a defender but doesn't assign him yet.
* force is true when merging armies, so in this case we should add it even if no position as it can be in a ship
*/
DefenseArmy.prototype.addOwn = function(gameState, id, force)
{
addOwn(gameState, id, force)
{
if (this.ownEntities.indexOf(id) !== -1)
return false;
const ent = gameState.getEntityById(id);
@ -129,10 +131,10 @@ DefenseArmy.prototype.addOwn = function(gameState, id, force)
ent.setMetadata(PlayerID, "formerSubrole", subrole);
ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_DEFENDER);
return true;
};
}
DefenseArmy.prototype.removeOwn = function(gameState, id, Entity)
{
removeOwn(gameState, id, Entity)
{
const idx = this.ownEntities.indexOf(id);
if (idx === -1)
return false;
@ -188,17 +190,17 @@ DefenseArmy.prototype.removeOwn = function(gameState, id, Entity)
plan.cancelTransport(gameState);
}
}
*/
*/
return true;
};
}
/**
/**
* resets the army properly.
* assumes we already cleared dead units.
*/
DefenseArmy.prototype.clear = function(gameState)
{
clear(gameState)
{
while (this.foeEntities.length > 0)
this.removeFoe(gameState, this.foeEntities[0]);
@ -294,10 +296,10 @@ DefenseArmy.prototype.clear = function(gameState)
this.recalculateStrengths(gameState);
this.recalculatePosition(gameState);
};
}
DefenseArmy.prototype.assignUnit = function(gameState, entID)
{
assignUnit(gameState, entID)
{
// we'll assume this defender is ours already.
// we'll also override any previous assignment
@ -369,27 +371,27 @@ DefenseArmy.prototype.assignUnit = function(gameState, entID)
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition);
return true;
};
}
DefenseArmy.prototype.getType = function()
{
getType()
{
return this.type;
};
}
DefenseArmy.prototype.getState = function()
{
getState()
{
if (!this.foeEntities.length)
return 0;
return 1;
};
}
/**
/**
* merge this army with another properly.
* assumes units are in only one army.
* also assumes that all have been properly cleaned up (no dead units).
*/
DefenseArmy.prototype.merge = function(gameState, otherArmy)
{
merge(gameState, otherArmy)
{
// copy over all parameters.
for (const i in otherArmy.assignedAgainst)
{
@ -411,10 +413,10 @@ DefenseArmy.prototype.merge = function(gameState, otherArmy)
this.recalculateStrengths(gameState);
return true;
};
}
DefenseArmy.prototype.needsDefenders = function(gameState)
{
needsDefenders(gameState)
{
let defenseRatio;
const territoryOwner = gameState.ai.HQ.territoryMap.getOwner(this.foePosition);
if (territoryOwner == PlayerID)
@ -438,12 +440,12 @@ DefenseArmy.prototype.needsDefenders = function(gameState)
if (this.foeStrength * defenseRatio <= this.ownStrength)
return false;
return this.foeStrength * defenseRatio - this.ownStrength;
};
}
/** if not forced, will only recalculate if on a different turn. */
DefenseArmy.prototype.recalculatePosition = function(gameState, force)
{
/** if not forced, will only recalculate if on a different turn. */
recalculatePosition(gameState, force)
{
if (!force && this.positionLastUpdate === gameState.ai.elapsedTime)
return;
@ -467,10 +469,10 @@ DefenseArmy.prototype.recalculatePosition = function(gameState, force)
}
this.positionLastUpdate = gameState.ai.elapsedTime;
};
}
DefenseArmy.prototype.recalculateStrengths = function(gameState)
{
recalculateStrengths(gameState)
{
this.ownStrength = 0;
this.foeStrength = 0;
@ -478,11 +480,11 @@ DefenseArmy.prototype.recalculateStrengths = function(gameState)
this.evaluateStrength(gameState.getEntityById(id));
for (const id of this.ownEntities)
this.evaluateStrength(gameState.getEntityById(id), true);
};
}
/** adds or remove the strength of the entity either to the enemy or to our units. */
DefenseArmy.prototype.evaluateStrength = function(ent, isOwn, remove)
{
/** adds or remove the strength of the entity either to the enemy or to our units. */
evaluateStrength(ent, isOwn, remove)
{
if (!ent)
return;
@ -509,10 +511,10 @@ DefenseArmy.prototype.evaluateStrength = function(ent, isOwn, remove)
this.ownStrength += entStrength;
else
this.foeStrength += entStrength;
};
}
DefenseArmy.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
// Warning the metadata is already cloned in shared.js. Futhermore, changes should be done before destroyEvents
// otherwise it would remove the old entity from this army list
// TODO we should may-be reevaluate the strength
@ -567,10 +569,10 @@ DefenseArmy.prototype.checkEvents = function(gameState, events)
this.removeOwn(gameState, evt.entity);
this.removeFoe(gameState, evt.entity);
}
};
}
DefenseArmy.prototype.update = function(gameState)
{
update(gameState)
{
for (const entId of this.ownEntities)
{
const ent = gameState.getEntityById(entId);
@ -632,10 +634,10 @@ DefenseArmy.prototype.update = function(gameState)
}
return breakaways;
};
}
DefenseArmy.prototype.Serialize = function()
{
Serialize()
{
return {
"ID": this.ID,
"type": this.type,
@ -648,10 +650,11 @@ DefenseArmy.prototype.Serialize = function()
"ownEntities": this.ownEntities,
"ownStrength": this.ownStrength
};
};
}
DefenseArmy.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -7,8 +7,10 @@ import { allowCapture, getLandAccess, getMaxStrength, isSiegeUnit } from
import { GarrisonManager } from "simulation/ai/petra/garrisonManager.js";
import { Worker } from "simulation/ai/petra/worker.js";
export function DefenseManager(Config)
export class DefenseManager
{
constructor(Config)
{
// Array of "army" Objects.
this.armies = [];
this.Config = Config;
@ -21,10 +23,10 @@ export function DefenseManager(Config)
this.attackingArmies = {};
this.attackingUnits = {};
this.attackedAllies = {};
}
}
DefenseManager.prototype.update = function(gameState, events)
{
update(gameState, events)
{
Engine.ProfileStart("Defense Manager");
this.territoryMap = gameState.ai.HQ.territoryMap;
@ -72,10 +74,10 @@ DefenseManager.prototype.update = function(gameState, events)
this.assignDefenders(gameState);
Engine.ProfileStop();
};
}
DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default")
{
makeIntoArmy(gameState, entityID, type = "default")
{
if (type == "default")
{
for (const army of this.armies)
@ -84,15 +86,15 @@ DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "de
}
this.armies.push(new DefenseArmy(gameState, [entityID], type));
};
}
DefenseManager.prototype.getArmy = function(partOfArmy)
{
getArmy(partOfArmy)
{
return this.armies.find(army => army.ID == partOfArmy);
};
}
DefenseManager.prototype.isDangerous = function(gameState, entity)
{
isDangerous(gameState, entity)
{
if (!entity.position())
return false;
@ -201,10 +203,10 @@ DefenseManager.prototype.isDangerous = function(gameState, entity)
}
return false;
};
}
DefenseManager.prototype.checkEnemyUnits = function(gameState)
{
checkEnemyUnits(gameState)
{
const nbPlayers = gameState.sharedScript.playersData.length;
const i = gameState.ai.playedTurn % nbPlayers;
this.attackingUnits[i] = undefined;
@ -277,10 +279,10 @@ DefenseManager.prototype.checkEnemyUnits = function(gameState)
if (owner == PlayerID)
this.makeIntoArmy(gameState, ent.id(), "capturing");
}
};
}
DefenseManager.prototype.checkEnemyArmies = function(gameState)
{
checkEnemyArmies(gameState)
{
for (let i = 0; i < this.armies.length; ++i)
{
const army = this.armies[i];
@ -392,10 +394,10 @@ DefenseManager.prototype.checkEnemyArmies = function(gameState)
army.clear(gameState);
this.armies.splice(i--, 1);
}
};
}
DefenseManager.prototype.assignDefenders = function(gameState)
{
assignDefenders(gameState)
{
if (!this.armies.length)
return;
@ -520,10 +522,10 @@ DefenseManager.prototype.assignDefenders = function(gameState)
for (let a = 0; a < armiesNeeding.length; ++a)
armiesPos.push(armiesNeeding[a].army.foePosition);
gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos);
};
}
DefenseManager.prototype.abortArmy = function(gameState, army)
{
abortArmy(gameState, army)
{
army.clear(gameState);
for (let i = 0; i < this.armies.length; ++i)
{
@ -532,16 +534,16 @@ DefenseManager.prototype.abortArmy = function(gameState, army)
this.armies.splice(i, 1);
break;
}
};
}
/**
/**
* If our defense structures are attacked, garrison soldiers inside when possible
* and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure
* and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress.
* If our hero is attacked with regicide victory condition, the victoryManager will handle it.
*/
DefenseManager.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
// Must be called every turn for all armies.
for (const army of this.armies)
army.checkEvents(gameState, events);
@ -756,10 +758,10 @@ DefenseManager.prototype.checkEvents = function(gameState, events)
target.attack(attacker.id(), shouldCapture);
}
}
};
}
DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data)
{
garrisonUnitsInside(gameState, target, data)
{
if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints())
return false;
const minGarrison = data.min || target.garrisonMax();
@ -838,11 +840,11 @@ DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data)
ret = true;
}
return ret;
};
}
/** Garrison a attacked siege ranged unit inside the nearest fortress. */
DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit)
{
/** Garrison a attacked siege ranged unit inside the nearest fortress. */
garrisonSiegeUnit(gameState, unit)
{
let distmin = Math.min();
let nearest;
const unitAccess = getLandAccess(gameState, unit);
@ -868,15 +870,15 @@ DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit)
if (nearest)
garrisonManager.garrison(gameState, unit, nearest, GarrisonManager.TYPE_PROTECTION);
return nearest !== undefined;
};
}
/**
/**
* Garrison a hurt unit inside a player-owned or allied structure.
* If emergency is true, the unit will be garrisoned in the closest possible structure.
* Otherwise, it will garrison in the closest healing structure.
*/
DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false)
{
garrisonAttackedUnit(gameState, unit, emergency = false)
{
let distmin = Math.min();
let nearest;
const unitAccess = getLandAccess(gameState, unit);
@ -916,24 +918,24 @@ DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emerge
garrisonManager.garrison(gameState, unit, nearest,
nearest.buffHeal() ? GarrisonManager.TYPE_PROTECTION : GarrisonManager.TYPE_EMERGENCY);
return true;
};
}
/**
/**
* Be more inclined to help an ally attacked by several enemies.
*/
DefenseManager.prototype.GetCooperationLevel = function(ally)
{
GetCooperationLevel(ally)
{
let cooperation = this.Config.personality.cooperative;
if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1)
cooperation += 0.2 * (this.attackedAllies[ally] - 1);
return cooperation;
};
}
/**
/**
* Switch a defense army into an attack if needed.
*/
DefenseManager.prototype.switchToAttack = function(gameState, army)
{
switchToAttack(gameState, army)
{
if (!army)
return;
for (const targetId of this.targetList)
@ -954,10 +956,10 @@ DefenseManager.prototype.switchToAttack = function(gameState, army)
return;
}
}
};
}
DefenseManager.prototype.Serialize = function()
{
Serialize()
{
const properties = {
"targetList": this.targetList,
"armyMergeSize": this.armyMergeSize,
@ -971,10 +973,10 @@ DefenseManager.prototype.Serialize = function()
armies.push(army.Serialize());
return { "properties": properties, "armies": armies };
};
}
DefenseManager.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data.properties)
this[key] = data.properties[key];
@ -985,4 +987,5 @@ DefenseManager.prototype.Deserialize = function(gameState, data)
army.Deserialize(dataArmy);
this.armies.push(army);
}
};
}
}

View file

@ -24,8 +24,10 @@ import * as chat from "simulation/ai/petra/chatHelper.js";
* that we suggested within a period of time, or else the request will be deleted from this.sentDiplomacyRequests.
* When @c deserialized is true, do not call any random function inside constructor as that would cause oos.
*/
export function DiplomacyManager(Config, deserialized)
export class DiplomacyManager
{
constructor(Config, deserialized)
{
this.Config = Config;
this.nextTributeUpdate = 90;
this.nextTributeRequest = new Map();
@ -36,14 +38,14 @@ export function DiplomacyManager(Config, deserialized)
this.receivedDiplomacyRequests = new Map();
this.sentDiplomacyRequests = new Map();
this.sentDiplomacyRequestLapseTime = deserialized ? 175 : randFloat(130, 220);
}
}
/**
/**
* If there are any players that are allied/neutral with us but we are not allied/neutral with them,
* treat this situation like an ally/neutral request.
*/
DiplomacyManager.prototype.init = function(gameState)
{
init(gameState)
{
this.lastManStandingCheck(gameState);
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
@ -58,14 +60,14 @@ DiplomacyManager.prototype.init = function(gameState)
else if (gameState.sharedScript.playersData[i].isNeutral[PlayerID] && gameState.isPlayerEnemy(i))
this.handleDiplomacyRequest(gameState, i, "neutral");
}
};
}
/**
/**
* Check if any allied needs help (tribute) and sent it if we have enough resource
* or ask for a tribute if we are in need and one ally can help
*/
DiplomacyManager.prototype.tributes = function(gameState)
{
tributes(gameState)
{
this.nextTributeUpdate = gameState.ai.elapsedTime + 30;
const resTribCodes = Resources.GetTributableCodes();
if (!resTribCodes.length)
@ -124,10 +126,10 @@ DiplomacyManager.prototype.tributes = function(gameState)
chat.sentTribute(gameState, i);
Engine.PostCommand(PlayerID, { "type": "tribute", "player": i, "amounts": tribute });
}
};
}
DiplomacyManager.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
// Increase slowly the cooperative personality trait either when we receive tribute from our allies
// or if our allies attack enemies inside our territory
for (const evt of events.TributeExchanged)
@ -283,14 +285,14 @@ DiplomacyManager.prototype.checkEvents = function(gameState, events)
this.nextTributeUpdate = gameState.ai.elapsedTime + 15;
}
}
};
}
/**
/**
* If the "Last Man Standing" option is enabled, check if the only remaining players are allies or neutral.
* If so, turn against the strongest first, but be more likely to first turn against neutral players, if there are any.
*/
DiplomacyManager.prototype.lastManStandingCheck = function(gameState)
{
lastManStandingCheck(gameState)
{
if (gameState.sharedScript.playersData[PlayerID].teamsLocked || gameState.isCeasefireActive() ||
gameState.getAlliedVictory() && gameState.hasAllies())
return;
@ -377,14 +379,14 @@ DiplomacyManager.prototype.lastManStandingCheck = function(gameState)
}
this.betrayLapseTime = -1;
this.waitingToBetray = false;
};
}
/**
/**
* Do not become allies with a player if the game would be over.
* Overall, be reluctant to become allies with any one player, but be more likely to accept neutral requests.
*/
DiplomacyManager.prototype.handleDiplomacyRequest = function(gameState, player, requestType)
{
handleDiplomacyRequest(gameState, player, requestType)
{
if (gameState.sharedScript.playersData[PlayerID].teamsLocked)
return;
let response;
@ -445,10 +447,10 @@ DiplomacyManager.prototype.handleDiplomacyRequest = function(gameState, player,
}
}
chat.answerRequestDiplomacy(gameState, player, requestType, response, requiredTribute);
};
}
DiplomacyManager.prototype.changePlayerDiplomacy = function(gameState, player, newDiplomaticStance)
{
changePlayerDiplomacy(gameState, player, newDiplomaticStance)
{
if (gameState.isPlayerEnemy(player) && (newDiplomaticStance === "ally" || newDiplomaticStance === "neutral"))
gameState.ai.HQ.attackManager.cancelAttacksAgainstPlayer(gameState, player);
Engine.PostCommand(PlayerID, { "type": "diplomacy", "player": player, "to": newDiplomaticStance });
@ -456,10 +458,10 @@ DiplomacyManager.prototype.changePlayerDiplomacy = function(gameState, player, n
aiWarn("diplomacy stance with player " + player + " is now " + newDiplomaticStance);
if (this.Config.chat)
chat.newDiplomacy(gameState, player, newDiplomaticStance);
};
}
DiplomacyManager.prototype.checkRequestedTributes = function(gameState)
{
checkRequestedTributes(gameState)
{
for (const [player, data] of this.receivedDiplomacyRequests)
if (data.status === "waitingForTribute" && gameState.ai.elapsedTime > data.warnTime)
{
@ -478,14 +480,14 @@ DiplomacyManager.prototype.checkRequestedTributes = function(gameState)
});
}
}
};
}
/**
/**
* Try to become allies with a player who has a lot of mutual enemies in common with us.
* TODO: Possibly let human players demand tributes from AIs who send diplomacy requests.
*/
DiplomacyManager.prototype.sendDiplomacyRequest = function(gameState)
{
sendDiplomacyRequest(gameState)
{
let player;
let max = 0;
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
@ -524,10 +526,10 @@ DiplomacyManager.prototype.sendDiplomacyRequest = function(gameState)
aiWarn("Sending diplomacy request to player " + player + " with " + requestType);
Engine.PostCommand(PlayerID, { "type": "diplomacy-request", "source": PlayerID, "player": player, "to": requestType });
chat.newRequestDiplomacy(gameState, player, requestType, "sendRequest");
};
}
DiplomacyManager.prototype.checkSentDiplomacyRequests = function(gameState)
{
checkSentDiplomacyRequests(gameState)
{
for (const [player, data] of this.sentDiplomacyRequests)
if (gameState.ai.elapsedTime > data.timeSent + 60 && !gameState.ai.HQ.saveResources &&
gameState.getPopulation() > 70)
@ -535,10 +537,10 @@ DiplomacyManager.prototype.checkSentDiplomacyRequests = function(gameState)
chat.newRequestDiplomacy(gameState, player, data.requestType, "requestExpired");
this.sentDiplomacyRequests.delete(player);
}
};
}
DiplomacyManager.prototype.update = function(gameState, events)
{
update(gameState, events)
{
this.checkEvents(gameState, events);
if (Resources.GetTributableCodes().length && !gameState.ai.HQ.saveResources && gameState.ai.elapsedTime > this.nextTributeUpdate)
@ -563,10 +565,10 @@ DiplomacyManager.prototype.update = function(gameState, events)
}
this.checkSentDiplomacyRequests(gameState);
};
}
DiplomacyManager.prototype.Serialize = function()
{
Serialize()
{
return {
"nextTributeUpdate": this.nextTributeUpdate,
"nextTributeRequest": this.nextTributeRequest,
@ -577,10 +579,11 @@ DiplomacyManager.prototype.Serialize = function()
"sentDiplomacyRequests": this.sentDiplomacyRequests,
"sentDiplomacyRequestLapseTime": this.sentDiplomacyRequestLapseTime
};
};
}
DiplomacyManager.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -3,24 +3,27 @@ import { emergency as chatEmergency } from "simulation/ai/petra/chatHelper.js";
/**
* Checks for emergencies and acts accordingly
*/
export function EmergencyManager(Config)
export class EmergencyManager
{
this.Config = Config;
this.referencePopulation = 0;
this.referenceStructureCount = 0;
this.numRoots = 0;
this.hasEmergency = false;
}
referencePopulation = 0;
referenceStructureCount = 0;
numRoots = 0;
hasEmergency = false;
EmergencyManager.prototype.init = function(gameState)
{
constructor(Config)
{
this.Config = Config;
}
init(gameState)
{
this.referencePopulation = gameState.getPopulation();
this.referenceStructureCount = gameState.getOwnStructures().length;
this.numRoots = this.rootCount(gameState);
};
}
EmergencyManager.prototype.update = function(gameState)
{
update(gameState)
{
if (this.hasEmergency)
{
this.emergencyUpdate(gameState);
@ -42,10 +45,10 @@ EmergencyManager.prototype.update = function(gameState)
this.referenceStructureCount = nStructures;
if (nRoots > this.numRoots || this.hasEmergency)
this.numRoots = nRoots;
};
}
EmergencyManager.prototype.emergencyUpdate = function(gameState)
{
emergencyUpdate(gameState)
{
const pop = gameState.getPopulation();
const nStructures = gameState.getOwnStructures().length;
const nRoots = this.rootCount(gameState);
@ -60,10 +63,10 @@ EmergencyManager.prototype.emergencyUpdate = function(gameState)
this.referenceStructureCount = nStructures;
this.numRoots = nRoots;
}
};
}
EmergencyManager.prototype.rootCount = function(gameState)
{
rootCount(gameState)
{
let roots = 0;
gameState.getOwnStructures().toEntityArray().forEach(ent =>
{
@ -71,26 +74,27 @@ EmergencyManager.prototype.rootCount = function(gameState)
roots++;
});
return roots;
};
}
EmergencyManager.prototype.setEmergency = function(gameState, enable)
{
setEmergency(gameState, enable)
{
this.hasEmergency = enable;
chatEmergency(gameState, enable);
};
}
EmergencyManager.prototype.Serialize = function()
{
Serialize()
{
return {
"referencePopulation": this.referencePopulation,
"referenceStructureCount": this.referenceStructureCount,
"numRoots": this.numRoots,
"hasEmergency": this.hasEmergency
};
};
}
EmergencyManager.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -10,21 +10,24 @@ import { Worker } from "simulation/ai/petra/worker.js";
* Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...)
*/
export function GarrisonManager(Config)
export class GarrisonManager
{
holders = new Map();
decayingStructures = new Map();
constructor(Config)
{
this.Config = Config;
this.holders = new Map();
this.decayingStructures = new Map();
}
}
GarrisonManager.TYPE_FORCE = "force";
GarrisonManager.TYPE_TRADE = "trade";
GarrisonManager.TYPE_PROTECTION = "protection";
GarrisonManager.TYPE_DECAY = "decay";
GarrisonManager.TYPE_EMERGENCY = "emergency";
static TYPE_FORCE = "force";
static TYPE_TRADE = "trade";
static TYPE_PROTECTION = "protection";
static TYPE_DECAY = "decay";
static TYPE_EMERGENCY = "emergency";
GarrisonManager.prototype.update = function(gameState, events)
{
update(gameState, events)
{
// First check for possible upgrade of a structure
for (const evt of events.EntityRenamed)
{
@ -207,37 +210,37 @@ GarrisonManager.prototype.update = function(gameState, events)
else if (this.numberOfGarrisonedSlots(ent) < gmin)
gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": GarrisonManager.TYPE_DECAY });
}
};
}
/** TODO should add the units garrisoned inside garrisoned units */
GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder)
{
/** TODO should add the units garrisoned inside garrisoned units */
numberOfGarrisonedUnits(holder)
{
if (!this.holders.has(holder.id()))
return holder.garrisoned().length;
return holder.garrisoned().length + this.holders.get(holder.id()).list.length;
};
}
/** TODO should add the units garrisoned inside garrisoned units */
GarrisonManager.prototype.numberOfGarrisonedSlots = function(holder)
{
/** TODO should add the units garrisoned inside garrisoned units */
numberOfGarrisonedSlots(holder)
{
if (!this.holders.has(holder.id()))
return holder.garrisonedSlots();
return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length;
};
}
GarrisonManager.prototype.allowMelee = function(holder)
{
allowMelee(holder)
{
if (!this.holders.has(holder.id()))
return undefined;
return this.holders.get(holder.id()).allowMelee;
};
}
/** This is just a pre-garrison state, while the entity walk to the garrison holder */
GarrisonManager.prototype.garrison = function(gameState, ent, holder, type)
{
/** This is just a pre-garrison state, while the entity walk to the garrison holder */
garrison(gameState, ent, holder, type)
{
if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison())
return;
@ -259,27 +262,27 @@ GarrisonManager.prototype.garrison = function(gameState, ent, holder, type)
ent.setMetadata(PlayerID, "garrisonHolder", holder.id());
ent.setMetadata(PlayerID, "garrisonType", type);
ent.garrison(holder);
};
}
/**
/**
This is the end of the pre-garrison state, either because the entity is really garrisoned
or because it has changed its order (i.e. because the garrisonHolder was destroyed)
This function is for internal use inside garrisonManager. From outside, you should also update
the holder and then using cancelGarrison should be the preferred solution
*/
GarrisonManager.prototype.leaveGarrison = function(ent)
{
leaveGarrison(ent)
{
ent.setMetadata(PlayerID, "subrole", undefined);
if (ent.getMetadata(PlayerID, "plan") === -2)
ent.setMetadata(PlayerID, "plan", -1);
else
ent.setMetadata(PlayerID, "plan", undefined);
ent.setMetadata(PlayerID, "garrisonHolder", undefined);
};
}
/** Cancel a pre-garrison state */
GarrisonManager.prototype.cancelGarrison = function(ent)
{
/** Cancel a pre-garrison state */
cancelGarrison(ent)
{
ent.stopMoving();
this.leaveGarrison(ent);
const holderId = ent.getMetadata(PlayerID, "garrisonHolder");
@ -289,10 +292,10 @@ GarrisonManager.prototype.cancelGarrison = function(ent)
const index = list.indexOf(ent.id());
if (index !== -1)
list.splice(index, 1);
};
}
GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around)
{
keepGarrisoned(ent, holder, around)
{
switch (ent.getMetadata(PlayerID, "garrisonType"))
{
case GarrisonManager.TYPE_FORCE: // force the ungarrisoning
@ -340,24 +343,24 @@ GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around)
ent.setMetadata(PlayerID, "garrisonType", GarrisonManager.TYPE_PROTECTION);
return true;
}
};
}
/** Add this holder in the list managed by the garrisonManager */
GarrisonManager.prototype.registerHolder = function(gameState, holder)
{
/** Add this holder in the list managed by the garrisonManager */
registerHolder(gameState, holder)
{
if (this.holders.has(holder.id())) // already registered
return;
this.holders.set(holder.id(), { "list": [], "allowMelee": true });
holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime);
};
}
/**
/**
* Garrison units in decaying structures to stop their decay
* do it only for structures useful for defense, except if we are expanding (justCaptured=true)
* in which case we also do it for structures useful for unit trainings (TODO only Barracks are done)
*/
GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured)
{
addDecayingStructure(gameState, entId, justCaptured)
{
if (this.decayingStructures.has(entId))
return true;
const ent = gameState.getEntityById(entId);
@ -368,22 +371,23 @@ GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, just
const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate());
this.decayingStructures.set(entId, gmin);
return true;
};
}
GarrisonManager.prototype.removeDecayingStructure = function(entId)
{
removeDecayingStructure(entId)
{
if (!this.decayingStructures.has(entId))
return;
this.decayingStructures.delete(entId);
};
}
GarrisonManager.prototype.Serialize = function()
{
Serialize()
{
return { "holders": this.holders, "decayingStructures": this.decayingStructures };
};
}
GarrisonManager.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -39,8 +39,10 @@ import { Worker } from "simulation/ai/petra/worker.js";
* -picking new CC locations.
*/
export function Headquarters(config, deserialized)
export class Headquarters
{
constructor(config, deserialized)
{
this.Config = config;
this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i
@ -76,11 +78,11 @@ export function Headquarters(config, deserialized)
this.capturableTargets = new Map();
this.capturableTargetsTime = 0;
}
}
/** More initialisation for stuff that needs the gameState */
Headquarters.prototype.init = function(gameState, queues)
{
/** More initialisation for stuff that needs the gameState */
init(gameState, queues)
{
this.territoryMap = createTerritoryMap(gameState);
// create borderMap: flag cells on the border of the map
// then this map will be completed with our frontier in updateTerritories
@ -96,23 +98,23 @@ Headquarters.prototype.init = function(gameState, queues)
this.currentPhase = gameState.currentPhase();
this.decayingStructures = new Set();
this.emergencyManager.init(gameState);
};
}
/**
/**
* initialization needed after deserialization (only called when deserialization)
*/
Headquarters.prototype.postinit = function(gameState)
{
postinit(gameState)
{
this.basesManager.postinit(gameState);
};
}
/**
/**
* returns the sea index linking regions 1 and region 2 (supposed to be different land region)
* otherwise return undefined
* for the moment, only the case land-sea-land is supported
*/
Headquarters.prototype.getSeaBetweenIndices = function(gameState, index1, index2)
{
getSeaBetweenIndices(gameState, index1, index2)
{
const path = gameState.ai.accessibility.getTrajectToIndex(index1, index2);
if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water")
return path[1];
@ -124,10 +126,10 @@ Headquarters.prototype.getSeaBetweenIndices = function(gameState, index1, index2
aiWarn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2]));
}
return undefined;
};
}
Headquarters.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
this.buildManager.checkEvents(gameState, events);
this.updateTerritories(gameState);
@ -297,10 +299,10 @@ Headquarters.prototype.checkEvents = function(gameState, events)
}
this.decayingStructures.delete(entId);
}
};
}
Headquarters.prototype.handleNewBase = function(gameState)
{
handleNewBase(gameState)
{
if (!this.firstBaseConfig)
// This is our first base, let us configure our starting resources.
configFirstBase(this, gameState);
@ -311,11 +313,11 @@ Headquarters.prototype.handleNewBase = function(gameState)
this.saveSpace = undefined;
this.maxFields = false;
}
};
}
/** Ensure that all requirements are met when phasing up*/
Headquarters.prototype.checkPhaseRequirements = function(gameState, queues)
{
/** Ensure that all requirements are met when phasing up*/
checkPhaseRequirements(gameState, queues)
{
if (gameState.getNumberOfPhases() == this.currentPhase)
return;
@ -386,16 +388,16 @@ Headquarters.prototype.checkPhaseRequirements = function(gameState, queues)
return;
}
}
};
}
/** Called by any "phase" research plan once it's started */
Headquarters.prototype.OnPhaseUp = function(gameState, phase)
{
};
/** Called by any "phase" research plan once it's started */
OnPhaseUp(gameState, phase)
{
}
/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */
Headquarters.prototype.trainMoreWorkers = function(gameState, queues)
{
/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */
trainMoreWorkers(gameState, queues)
{
// default template
const requirementsDef = [ ["costsResource", 1, "food"] ];
const classesDef = ["Support+Worker"];
@ -502,11 +504,11 @@ Headquarters.prototype.trainMoreWorkers = function(gameState, queues)
queues.villager.addPlan(new TrainingPlan(gameState, templateDef, { "role": Worker.ROLE_WORKER, "base": 0, "support": true }, size, size));
else if (template)
queues.citizenSoldier.addPlan(new TrainingPlan(gameState, template, { "role": Worker.ROLE_WORKER, "base": 0 }, size, size));
};
}
/** picks the best template based on parameters and classes */
Headquarters.prototype.findBestTrainableUnit = function(gameState, classes, requirements)
{
/** picks the best template based on parameters and classes */
findBestTrainableUnit(gameState, classes, requirements)
{
let units;
if (classes.indexOf("Hero") != -1)
units = gameState.findTrainableUnits(classes, []);
@ -585,43 +587,43 @@ Headquarters.prototype.findBestTrainableUnit = function(gameState, classes, requ
return -aValue/aCost + bValue/bCost;
});
return units[0][0];
};
}
/**
/**
* returns an entity collection of workers through BaseManager.pickBuilders
* TODO: when same accessIndex, sort by distance
*/
Headquarters.prototype.bulkPickWorkers = function(gameState, baseRef, number)
{
bulkPickWorkers(gameState, baseRef, number)
{
return this.basesManager.bulkPickWorkers(gameState, baseRef, number);
};
}
Headquarters.prototype.getTotalResourceLevel = function(gameState, resources, proximity)
{
getTotalResourceLevel(gameState, resources, proximity)
{
return this.basesManager.getTotalResourceLevel(gameState, resources, proximity);
};
}
/**
/**
* Returns the current gather rate
* This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that.
*/
Headquarters.prototype.GetCurrentGatherRates = function(gameState)
{
GetCurrentGatherRates(gameState)
{
return this.basesManager.GetCurrentGatherRates(gameState);
};
}
/**
/**
* Returns the wanted gather rate.
*/
Headquarters.prototype.GetWantedGatherRates = function(gameState)
{
GetWantedGatherRates(gameState)
{
if (!this.turnCache.wantedRates)
this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState);
return this.turnCache.wantedRates;
};
}
/**
/**
* Pick the resource which most needs another worker
* How this works:
* We get the rates we would want to have to be able to deal with our plans
@ -629,8 +631,8 @@ Headquarters.prototype.GetWantedGatherRates = function(gameState)
* We compare; we pick the one where the discrepancy is highest.
* Need to balance long-term needs and possible short-term needs.
*/
Headquarters.prototype.pickMostNeededResources = function(gameState, allowedResources = [])
{
pickMostNeededResources(gameState, allowedResources = [])
{
const wantedRates = this.GetWantedGatherRates(gameState);
const currentRates = this.GetCurrentGatherRates(gameState);
if (!allowedResources.length)
@ -659,14 +661,14 @@ Headquarters.prototype.pickMostNeededResources = function(gameState, allowedReso
return a.current - a.wanted - b.current + b.wanted;
});
return needed;
};
}
/**
/**
* Returns the best position to build a new Civil Center
* Whose primary function would be to reach new resources of type "resource".
*/
Headquarters.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic)
{
findEconomicCCLocation(gameState, template, resource, proximity, fromStrategic)
{
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then look for a good spot.
@ -860,14 +862,14 @@ Headquarters.prototype.findEconomicCCLocation = function(gameState, template, re
}
return [x, z];
};
}
/**
/**
* Returns the best position to build a new Civil Center
* Whose primary function would be to assure territorial continuity with our allies
*/
Headquarters.prototype.findStrategicCCLocation = function(gameState, template)
{
findStrategicCCLocation(gameState, template)
{
// This builds a map. The procedure is fairly simple.
// We minimize the Sum((dist - 300)^2) where the sum is on the three nearest allied CC
// with the constraints that all CC have dist > 200 and at least one have dist < 400
@ -1003,17 +1005,17 @@ Headquarters.prototype.findStrategicCCLocation = function(gameState, template)
}
return [x, z];
};
}
/**
/**
* Returns the best position to build a new market: if the allies already have a market, build it as far as possible
* from it, although not in our border to be able to defend it easily. If no allied market, our second market will
* follow the same logic.
* To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance
* for performance reasons.
*/
Headquarters.prototype.findMarketLocation = function(gameState, template)
{
findMarketLocation(gameState, template)
{
let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", filters.byClass("Trade"),
gameState.getExclusiveAllyEntities()).toEntityArray();
if (!markets.length)
@ -1127,14 +1129,14 @@ Headquarters.prototype.findMarketLocation = function(gameState, template)
const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, idx, expectedGain];
};
}
/**
/**
* Returns the best position to build defensive buildings (fortress and towers)
* Whose primary function is to defend our borders
*/
Headquarters.prototype.findDefensiveLocation = function(gameState, template)
{
findDefensiveLocation(gameState, template)
{
// We take the point in our territory which is the nearest to any enemy cc
// but requiring a minimal distance with our other defensive structures
// and not in range of any enemy defensive structure to avoid building under fire.
@ -1269,10 +1271,10 @@ Headquarters.prototype.findDefensiveLocation = function(gameState, template)
const x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
const z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.baseAtIndex(bestJdx)];
};
}
Headquarters.prototype.buildTemple = function(gameState, queues)
{
buildTemple(gameState, queues)
{
// at least one market (which have the same queue) should be build before any temple
if (queues.economicBuilding.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Temple", true).hasEntities() ||
@ -1288,10 +1290,10 @@ Headquarters.prototype.buildTemple = function(gameState, queues)
else if (!this.canBuild(gameState, templateName))
return;
queues.economicBuilding.addPlan(new ConstructionPlan(gameState, templateName));
};
}
Headquarters.prototype.buildMarket = function(gameState, queues)
{
buildMarket(gameState, queues)
{
if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}/market"))
return;
@ -1323,11 +1325,11 @@ Headquarters.prototype.buildMarket = function(gameState, queues)
const plan = new ConstructionPlan(gameState, "structures/{civ}/market");
plan.queueToReset = "economicBuilding";
queues.economicBuilding.addPlan(plan);
};
}
/** Build a farmstead */
Headquarters.prototype.buildFarmstead = function(gameState, queues)
{
/** Build a farmstead */
buildFarmstead(gameState, queues)
{
// Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs)
if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities())
return;
@ -1342,14 +1344,14 @@ Headquarters.prototype.buildFarmstead = function(gameState, queues)
return;
queues.economicBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/farmstead"));
};
}
/**
/**
* Try to build a wonder when required
* force = true when called from the victoryManager in case of Wonder victory condition.
*/
Headquarters.prototype.buildWonder = function(gameState, queues, force = false)
{
buildWonder(gameState, queues, force = false)
{
if (queues.wonder && queues.wonder.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}/wonder"))
@ -1375,11 +1377,11 @@ Headquarters.prototype.buildWonder = function(gameState, queues, force = false)
}
queues.wonder.addPlan(new ConstructionPlan(gameState, "structures/{civ}/wonder"));
};
}
/** Build a corral, and train animals there */
Headquarters.prototype.manageCorral = function(gameState, queues)
{
/** Build a corral, and train animals there */
manageCorral(gameState, queues)
{
if (queues.corral.hasQueuedUnits())
return;
@ -1419,14 +1421,14 @@ Headquarters.prototype.manageCorral = function(gameState, queues)
return;
}
}
};
}
/**
/**
* build more houses if needed.
* kinda ugly, lots of special cases to both build enough houses but not tooo many
*/
Headquarters.prototype.buildMoreHouses = function(gameState, queues)
{
buildMoreHouses(gameState, queues)
{
let houseTemplateString = "structures/{civ}/apartment";
if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) ||
!this.canBuild(gameState, houseTemplateString))
@ -1516,17 +1518,17 @@ Headquarters.prototype.buildMoreHouses = function(gameState, queues)
if (priority && priority != gameState.ai.queueManager.getPriority("house"))
gameState.ai.queueManager.changePriority("house", priority);
};
}
/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */
Headquarters.prototype.checkBaseExpansion = function(gameState, queues)
{
/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */
checkBaseExpansion(gameState, queues)
{
if (queues.civilCentre.hasQueuedUnits())
return;
// First build one cc if all have been destroyed
if (!this.hasPotentialBase())
{
buildFirstBase(this, gameState);
this.buildFirstBase(gameState);
return;
}
// Then expand if we have not enough room available for buildings
@ -1553,10 +1555,10 @@ Headquarters.prototype.checkBaseExpansion = function(gameState, queues)
}
this.buildNewBase(gameState, queues);
}
};
}
Headquarters.prototype.buildNewBase = function(gameState, queues, resource)
{
buildNewBase(gameState, queues, resource)
{
if (this.hasPotentialBase() && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))
return false;
if (gameState.getOwnFoundations().filter(filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits())
@ -1586,11 +1588,11 @@ Headquarters.prototype.buildNewBase = function(gameState, queues, resource)
aiWarn("new base " + gameState.applyCiv(template) + " planned with resource " + resource);
queues.civilCentre.addPlan(new ConstructionPlan(gameState, template, { "base": -1, "resource": resource }));
return true;
};
}
/** Deals with building fortresses and towers along our border with enemies. */
Headquarters.prototype.buildDefenses = function(gameState, queues)
{
/** Deals with building fortresses and towers along our border with enemies. */
buildDefenses(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits())
return;
@ -1646,10 +1648,10 @@ Headquarters.prototype.buildDefenses = function(gameState, queues)
plan.queueToReset = "defenseBuilding";
queues.defenseBuilding.addPlan(plan);
}
};
}
Headquarters.prototype.buildForge = function(gameState, queues)
{
buildForge(gameState, queues)
{
if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge ||
queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length)
return;
@ -1659,14 +1661,14 @@ Headquarters.prototype.buildForge = function(gameState, queues)
if (this.canBuild(gameState, "structures/{civ}/forge"))
queues.militaryBuilding.addPlan(new ConstructionPlan(gameState, "structures/{civ}/forge"));
};
}
/**
/**
* Deals with constructing military buildings (e.g. barracks, stable).
* They are mostly defined by Config.js. This is unreliable since changes could be done easily.
*/
Headquarters.prototype.constructTrainingBuildings = function(gameState, queues)
{
constructTrainingBuildings(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits())
return;
@ -1775,13 +1777,13 @@ Headquarters.prototype.constructTrainingBuildings = function(gameState, queues)
return;
}
}
};
}
/**
/**
* Find base nearest to ennemies for military buildings.
*/
Headquarters.prototype.findBestBaseForMilitary = function(gameState)
{
findBestBaseForMilitary(gameState)
{
const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre")).toEntityArray();
let bestBase;
let enemyFound = false;
@ -1810,14 +1812,14 @@ Headquarters.prototype.findBestBaseForMilitary = function(gameState)
}
}
return bestBase;
};
}
/**
/**
* train with highest priority ranged infantry in the nearest civil center from a given set of positions
* and garrison them there for defense
*/
Headquarters.prototype.trainEmergencyUnits = function(gameState, positions)
{
trainEmergencyUnits(gameState, positions)
{
if (gameState.ai.queues.emergency.hasQueuedUnits())
return false;
@ -1919,10 +1921,10 @@ Headquarters.prototype.trainEmergencyUnits = function(gameState, positions)
metadata.garrisonType = GarrisonManager.TYPE_PROTECTION;
gameState.ai.queues.emergency.addPlan(new TrainingPlan(gameState, templateFound[0], metadata, 1, 1));
return true;
};
}
Headquarters.prototype.canBuild = function(gameState, structure)
{
canBuild(gameState, structure)
{
const type = gameState.applyCiv(structure);
if (this.buildManager.isUnbuildable(gameState, type))
return false;
@ -1973,10 +1975,10 @@ Headquarters.prototype.canBuild = function(gameState, structure)
}
return true;
};
}
Headquarters.prototype.updateTerritories = function(gameState)
{
updateTerritories(gameState)
{
const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ];
const alliedVictory = gameState.getAlliedVictory();
const passabilityMap = gameState.getPassabilityMap();
@ -2045,57 +2047,57 @@ Headquarters.prototype.updateTerritories = function(gameState)
const cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize;
if (expansion * cellArea > 960)
this.tradeManager.routeProspection = true;
};
}
/**
/**
* returns the base corresponding to baseID
*/
Headquarters.prototype.getBaseByID = function(baseID)
{
getBaseByID(baseID)
{
return this.basesManager.getBaseByID(baseID);
};
}
/**
/**
* returns the number of bases with a cc
* ActiveBases includes only those with a built cc
* PotentialBases includes also those with a cc in construction
*/
Headquarters.prototype.numActiveBases = function()
{
numActiveBases()
{
return this.basesManager.numActiveBases();
};
}
Headquarters.prototype.hasActiveBase = function()
{
hasActiveBase()
{
return this.basesManager.hasActiveBase();
};
}
Headquarters.prototype.numPotentialBases = function()
{
numPotentialBases()
{
return this.basesManager.numPotentialBases();
};
}
Headquarters.prototype.hasPotentialBase = function()
{
hasPotentialBase()
{
return this.basesManager.hasPotentialBase();
};
}
Headquarters.prototype.isDangerousLocation = function(gameState, pos, radius)
{
isDangerousLocation(gameState, pos, radius)
{
return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius);
};
}
/** Check that the chosen position is not too near from an invading army */
Headquarters.prototype.isNearInvadingArmy = function(pos)
{
/** Check that the chosen position is not too near from an invading army */
isNearInvadingArmy(pos)
{
for (const army of this.defenseManager.armies)
if (army.foePosition && SquareVectorDistance(army.foePosition, pos) < 12000)
return true;
return false;
};
}
Headquarters.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0)
{
isUnderEnemyFire(gameState, pos, radius = 0)
{
if (!this.turnCache.firingStructures)
{
this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures",
@ -2108,11 +2110,11 @@ Headquarters.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0)
return true;
}
return false;
};
}
/** Compute the capture strength of all units attacking a capturable target */
Headquarters.prototype.updateCaptureStrength = function(gameState)
{
/** Compute the capture strength of all units attacking a capturable target */
updateCaptureStrength(gameState)
{
this.capturableTargets.clear();
for (const ent of gameState.getOwnUnits().values())
{
@ -2159,25 +2161,25 @@ Headquarters.prototype.updateCaptureStrength = function(gameState)
}
this.capturableTargetsTime = gameState.ai.elapsedTime;
};
}
/**
/**
* Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around)
*/
Headquarters.prototype.isDefendable = function(ent)
{
isDefendable(ent)
{
if (!this.turnCache.numAround)
this.turnCache.numAround = {};
if (this.turnCache.numAround[ent.id()] === undefined)
this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130);
return +this.turnCache.numAround[ent.id()] > 8;
};
}
/**
/**
* Get the number of population already accounted for
*/
Headquarters.prototype.getAccountedPopulation = function(gameState)
{
getAccountedPopulation(gameState)
{
if (this.turnCache.accountedPopulation == undefined)
{
let pop = gameState.getPopulation();
@ -2195,13 +2197,13 @@ Headquarters.prototype.getAccountedPopulation = function(gameState)
this.turnCache.accountedPopulation = pop;
}
return this.turnCache.accountedPopulation;
};
}
/**
/**
* Get the number of workers already accounted for
*/
Headquarters.prototype.getAccountedWorkers = function(gameState)
{
getAccountedWorkers(gameState)
{
if (this.turnCache.accountedWorkers == undefined)
{
let workers = gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length;
@ -2217,28 +2219,28 @@ Headquarters.prototype.getAccountedWorkers = function(gameState)
this.turnCache.accountedWorkers = workers;
}
return this.turnCache.accountedWorkers;
};
}
Headquarters.prototype.baseManagers = function()
{
baseManagers()
{
return this.basesManager.baseManagers;
};
}
/**
/**
* @param {number} territoryIndex - The index to get the map for.
* @return {number} - The ID of the base at the given territory index.
*/
Headquarters.prototype.baseAtIndex = function(territoryIndex)
{
baseAtIndex(territoryIndex)
{
return this.basesManager.baseAtIndex(territoryIndex);
};
}
/**
/**
* Some functions are run every turn
* Others once in a while
*/
Headquarters.prototype.update = function(gameState, queues, events)
{
update(gameState, queues, events)
{
Engine.ProfileStart("Headquarters update");
this.emergencyManager.update(gameState);
this.turnCache = {};
@ -2345,10 +2347,10 @@ Headquarters.prototype.update = function(gameState, queues, events)
this.updateCaptureStrength(gameState);
Engine.ProfileStop();
};
}
Headquarters.prototype.Serialize = function()
{
Serialize()
{
const properties = {
"phasing": this.phasing,
"lastFailedGather": this.lastFailedGather,
@ -2407,10 +2409,10 @@ Headquarters.prototype.Serialize = function()
"victoryManager": this.victoryManager.Serialize(),
"emergencyManager": this.emergencyManager.Serialize(),
};
};
}
Headquarters.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data.properties)
this[key] = data.properties[key];
@ -2452,4 +2454,5 @@ Headquarters.prototype.Deserialize = function(gameState, data)
this.emergencyManager = new EmergencyManager(this.Config);
this.emergencyManager.Deserialize(data.emergencyManager);
};
}
}

View file

@ -16,33 +16,35 @@ import { Worker } from "simulation/ai/petra/worker.js";
* -Scouting, ultimately.
* Also deals with handling docks, making sure we have access and stuffs like that.
*/
export function NavalManager(Config)
export class NavalManager
{
this.Config = Config;
// ship subCollections. Also exist for land zones, idem, not caring.
this.seaShips = [];
this.seaTransportShips = [];
this.seaWarShips = [];
this.seaFishShips = [];
seaShips = [];
seaTransportShips = [];
seaWarShips = [];
seaFishShips = [];
// wanted NB per zone.
this.wantedTransportShips = [];
this.wantedWarShips = [];
this.wantedFishShips = [];
wantedTransportShips = [];
wantedWarShips = [];
wantedFishShips = [];
// needed NB per zone.
this.neededTransportShips = [];
this.neededWarShips = [];
neededTransportShips = [];
neededWarShips = [];
this.transportPlans = [];
transportPlans = [];
// shore-line regions where we can load and unload units
this.landingZones = {};
}
landingZones = {};
/** More initialisation for stuff that needs the gameState */
NavalManager.prototype.init = function(gameState, deserializing)
{
constructor(Config)
{
this.Config = Config;
}
/** More initialisation for stuff that needs the gameState */
init(gameState, deserializing)
{
// docks
this.docks = gameState.getOwnStructures().filter(filters.byClasses(["Dock", "Shipyard"]));
this.docks.registerUpdates();
@ -166,25 +168,25 @@ NavalManager.prototype.init = function(gameState, deserializing)
setSeaAccess(gameState, ship);
for (const dock of this.docks.values())
setSeaAccess(gameState, dock);
};
}
NavalManager.prototype.updateFishingBoats = function(sea, num)
{
updateFishingBoats(sea, num)
{
if (this.wantedFishShips[sea])
this.wantedFishShips[sea] = num;
};
}
NavalManager.prototype.resetFishingBoats = function(gameState, sea)
{
resetFishingBoats(gameState, sea)
{
if (sea !== undefined)
this.wantedFishShips[sea] = 0;
else
this.wantedFishShips.fill(0);
};
}
/** Get the sea, cache it if not yet done and check if in opensea */
NavalManager.prototype.getFishSea = function(gameState, fish)
{
/** Get the sea, cache it if not yet done and check if in opensea */
getFishSea(gameState, fish)
{
let sea = fish.getMetadata(PlayerID, "sea");
if (sea)
return sea;
@ -218,11 +220,11 @@ NavalManager.prototype.getFishSea = function(gameState, fish)
}))
fish.setMetadata(PlayerID, "opensea", true);
return sea;
};
}
/** check if we can safely fish at the fish position */
NavalManager.prototype.canFishSafely = function(gameState, fish)
{
/** check if we can safely fish at the fish position */
canFishSafely(gameState, fish)
{
if (fish.getMetadata(PlayerID, "opensea"))
return true;
const ntry = 2;
@ -245,11 +247,11 @@ NavalManager.prototype.canFishSafely = function(gameState, fish)
}
return true;
});
};
}
/** get the list of seas (or lands) around this region not connected by a dock */
NavalManager.prototype.getUnconnectedSeas = function(gameState, region)
{
/** get the list of seas (or lands) around this region not connected by a dock */
getUnconnectedSeas(gameState, region)
{
const seas = gameState.ai.accessibility.regionLinks[region].slice();
this.docks.forEach(dock =>
{
@ -260,10 +262,10 @@ NavalManager.prototype.getUnconnectedSeas = function(gameState, region)
seas.splice(i, 1);
});
return seas;
};
}
NavalManager.prototype.checkEvents = function(gameState, queues, events)
{
checkEvents(gameState, queues, events)
{
for (const evt of events.Create)
{
if (!evt.entity)
@ -342,29 +344,29 @@ NavalManager.prototype.checkEvents = function(gameState, queues, events)
if (ent && ent.hasClasses(["Dock", "Shipyard"]))
setSeaAccess(gameState, ent);
}
};
}
NavalManager.prototype.getPlan = function(ID)
{
getPlan(ID)
{
for (const plan of this.transportPlans)
if (plan.ID === ID)
return plan;
return undefined;
};
}
NavalManager.prototype.addPlan = function(plan)
{
addPlan(plan)
{
this.transportPlans.push(plan);
};
}
/**
/**
* complete already existing plan or create a new one for this requirement
* (many units can then call this separately and end up in the same plan)
* TODO check garrison classes
*/
NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos)
{
requireTransport(gameState, ent, startIndex, endIndex, endPos)
{
if (!ent.canGarrison())
return false;
@ -409,11 +411,11 @@ NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, e
plan.init(gameState);
this.transportPlans.push(plan);
return true;
};
}
/** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */
NavalManager.prototype.splitTransport = function(gameState, plan)
{
/** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */
splitTransport(gameState, plan)
{
if (this.Config.debug > 1)
aiWarn(">>>> split of transport plan started <<<<");
const newplan = new TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos);
@ -436,14 +438,14 @@ NavalManager.prototype.splitTransport = function(gameState, plan)
if (newplan.units.length)
this.transportPlans.push(newplan);
return newplan.units.length != 0;
};
}
/**
/**
* create a transport from a garrisoned ship to a land location
* needed at start game when starting with a garrisoned ship
*/
NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess)
{
createTransportIfNeeded(gameState, fromPos, toPos, toAccess)
{
const fromAccess = gameState.ai.accessibility.getAccessValue(fromPos);
if (fromAccess !== 1)
return;
@ -466,20 +468,20 @@ NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, to
plan.init(gameState);
this.transportPlans.push(plan);
}
};
}
// set minimal number of needed ships when a new event (new base or new attack plan)
NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number)
{
// set minimal number of needed ships when a new event (new base or new attack plan)
setMinimalTransportShips(gameState, sea, number)
{
if (!sea)
return;
if (this.wantedTransportShips[sea] < number)
this.wantedTransportShips[sea] = number;
};
}
// bumps up the number of ships we want if we need more.
NavalManager.prototype.checkLevels = function(gameState, queues)
{
// bumps up the number of ships we want if we need more.
checkLevels(gameState, queues)
{
if (queues.ships.hasQueuedUnits())
return;
@ -505,10 +507,10 @@ NavalManager.prototype.checkLevels = function(gameState, queues)
for (let sea = 0; sea < this.neededTransportShips.length; sea++)
if (this.neededTransportShips[sea] > 2)
++this.wantedTransportShips[sea];
};
}
NavalManager.prototype.maintainFleet = function(gameState, queues)
{
maintainFleet(gameState, queues)
{
if (queues.ships.hasQueuedUnits())
return;
if (!this.docks.filter(filters.isBuilt()).hasEntities())
@ -543,33 +545,33 @@ NavalManager.prototype.maintainFleet = function(gameState, queues)
}
}
}
};
}
/** assigns free ships to plans that need some */
NavalManager.prototype.assignShipsToPlans = function(gameState)
{
/** assigns free ships to plans that need some */
assignShipsToPlans(gameState)
{
for (const plan of this.transportPlans)
if (plan.needTransportShips)
plan.assignShip(gameState);
};
}
/** Return true if this ship is likeky (un)garrisoning units */
NavalManager.prototype.isShipBoarding = function(ship)
{
/** Return true if this ship is likeky (un)garrisoning units */
isShipBoarding(ship)
{
if (!ship.position())
return false;
const plan = this.getPlan(ship.getMetadata(PlayerID, "transporter"));
if (!plan || !plan.boardingPos[ship.id()])
return false;
return SquareVectorDistance(plan.boardingPos[ship.id()], ship.position()) < plan.boardingRange;
};
}
/** let blocking ships move apart from active ships (waiting for a better pathfinder)
/** let blocking ships move apart from active ships (waiting for a better pathfinder)
* TODO Ships entity collections are currently in two parts as the trader ships are dealt with
* in the tradeManager. That should be modified to avoid dupplicating all the code here.
*/
NavalManager.prototype.moveApart = function(gameState)
{
moveApart(gameState)
{
const blockedShips = [];
const blockedIds = [];
@ -730,10 +732,10 @@ NavalManager.prototype.moveApart = function(gameState)
blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35);
}
}
};
}
NavalManager.prototype.buildNavalStructures = function(gameState, queues)
{
buildNavalStructures(gameState, queues)
{
if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.hasPotentialBase())
return;
@ -793,11 +795,11 @@ NavalManager.prototype.buildNavalStructures = function(gameState, queues)
const sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea");
queues.militaryBuilding.addPlan(
new ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea }));
};
}
/** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */
NavalManager.prototype.getBestShip = function(gameState, sea, goal)
{
/** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */
getBestShip(gameState, sea, goal)
{
const civ = gameState.getPlayerCiv();
const trainableShips = [];
gameState.getOwnTrainingFacilities().filter(filters.byMetadata(PlayerID, "sea", sea)).forEach(
@ -853,10 +855,10 @@ NavalManager.prototype.getBestShip = function(gameState, sea, goal)
bestShip = trainable;
}
return bestShip;
};
}
NavalManager.prototype.update = function(gameState, queues, events)
{
update(gameState, queues, events)
{
Engine.ProfileStart("Naval Manager update");
// close previous transport plans if finished
@ -884,10 +886,10 @@ NavalManager.prototype.update = function(gameState, queues, events)
this.moveApart(gameState);
Engine.ProfileStop();
};
}
NavalManager.prototype.Serialize = function()
{
Serialize()
{
const properties = {
"wantedTransportShips": this.wantedTransportShips,
"wantedWarShips": this.wantedWarShips,
@ -902,10 +904,10 @@ NavalManager.prototype.Serialize = function()
transports[plan] = this.transportPlans[plan].Serialize();
return { "properties": properties, "transports": transports };
};
}
NavalManager.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data.properties)
this[key] = data.properties[key];
@ -918,4 +920,5 @@ NavalManager.prototype.Deserialize = function(gameState, data)
plan.init(gameState);
this.transportPlans.push(plan);
}
};
}
}

View file

@ -7,20 +7,19 @@ import { TrainingPlan } from "simulation/ai/petra/queueplanTraining.js";
/**
* Holds a list of wanted plans to train or construct
*/
export function Queue()
export class Queue
{
this.plans = [];
this.paused = false;
this.switched = 0;
}
plans = [];
paused = false;
switched = 0;
Queue.prototype.empty = function()
{
empty()
{
this.plans = [];
};
}
Queue.prototype.addPlan = function(newPlan)
{
addPlan(newPlan)
{
if (!newPlan)
return;
for (const plan of this.plans)
@ -34,10 +33,10 @@ Queue.prototype.addPlan = function(newPlan)
return;
}
this.plans.push(newPlan);
};
}
Queue.prototype.check= function(gameState)
{
check(gameState)
{
while (this.plans.length > 0)
{
if (!this.plans[0].isInvalid(gameState))
@ -46,31 +45,31 @@ Queue.prototype.check= function(gameState)
if (plan.queueToReset)
gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]);
}
};
}
Queue.prototype.getNext = function()
{
getNext()
{
if (this.plans.length > 0)
return this.plans[0];
return null;
};
}
Queue.prototype.startNext = function(gameState)
{
startNext(gameState)
{
if (this.plans.length > 0)
{
this.plans.shift().start(gameState);
return true;
}
return false;
};
}
/**
/**
* returns the maximal account we'll accept for this queue.
* Currently all the cost of the first element and fraction of that of the second
*/
Queue.prototype.maxAccountWanted = function(gameState, fraction)
{
maxAccountWanted(gameState, fraction)
{
const cost = new ResourcesManager();
if (this.plans.length > 0 && this.plans[0].isGo(gameState))
cost.add(this.plans[0].getCost());
@ -81,68 +80,68 @@ Queue.prototype.maxAccountWanted = function(gameState, fraction)
cost.add(costs);
}
return cost;
};
}
Queue.prototype.queueCost = function()
{
queueCost()
{
const cost = new ResourcesManager();
for (const plan of this.plans)
cost.add(plan.getCost());
return cost;
};
}
Queue.prototype.length = function()
{
length()
{
return this.plans.length;
};
}
Queue.prototype.hasQueuedUnits = function()
{
hasQueuedUnits()
{
return this.plans.length > 0;
};
}
Queue.prototype.countQueuedUnits = function()
{
countQueuedUnits()
{
let count = 0;
for (const plan of this.plans)
count += plan.number;
return count;
};
}
Queue.prototype.hasQueuedUnitsWithClass = function(classe)
{
hasQueuedUnitsWithClass(classe)
{
return this.plans.some(plan => plan.template && plan.template.hasClass(classe));
};
}
Queue.prototype.countQueuedUnitsWithClass = function(classe)
{
countQueuedUnitsWithClass(classe)
{
let count = 0;
for (const plan of this.plans)
if (plan.template && plan.template.hasClass(classe))
count += plan.number;
return count;
};
}
Queue.prototype.countQueuedUnitsWithMetadata = function(data, value)
{
countQueuedUnitsWithMetadata(data, value)
{
let count = 0;
for (const plan of this.plans)
if (plan.metadata[data] && plan.metadata[data] == value)
count += plan.number;
return count;
};
}
Queue.prototype.Serialize = function()
{
Serialize()
{
const plans = [];
for (const plan of this.plans)
plans.push(plan.Serialize());
return { "plans": plans, "paused": this.paused, "switched": this.switched };
};
}
Queue.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
this.paused = data.paused;
this.switched = data.switched;
this.plans = [];
@ -163,4 +162,5 @@ Queue.prototype.Deserialize = function(gameState, data)
plan.Deserialize(gameState, dataPlan);
this.plans.push(plan);
}
};
}
}

View file

@ -40,8 +40,10 @@ function sortQueues(queueArrays, priorities)
});
}
export function QueueManager(Config, queues)
export class QueueManager
{
constructor(Config, queues)
{
this.Config = Config;
this.queues = queues;
this.priorities = {};
@ -57,26 +59,26 @@ export function QueueManager(Config, queues)
this.queueArrays.push([q, this.queues[q]]);
}
sortQueues(this.queueArrays, this.priorities);
}
}
QueueManager.prototype.getAvailableResources = function(gameState)
{
getAvailableResources(gameState)
{
const resources = gameState.getResources();
for (const key in this.queues)
resources.subtract(this.accounts[key]);
return resources;
};
}
QueueManager.prototype.getTotalAccountedResources = function()
{
getTotalAccountedResources()
{
const resources = new ResourcesManager();
for (const key in this.queues)
resources.add(this.accounts[key]);
return resources;
};
}
QueueManager.prototype.currentNeeds = function(gameState)
{
currentNeeds(gameState)
{
const needed = new ResourcesManager();
// queueArrays because it's faster.
for (const q of this.queueArrays)
@ -93,12 +95,12 @@ QueueManager.prototype.currentNeeds = function(gameState)
needed[res] = Math.max(0, needed[res] - current[res]);
return needed;
};
}
// calculate the gather rates we'd want to be able to start all elements in our queues
// TODO: many things.
QueueManager.prototype.wantedGatherRates = function(gameState)
{
// calculate the gather rates we'd want to be able to start all elements in our queues
// TODO: many things.
wantedGatherRates(gameState)
{
// default values for first turn when we have not yet set our queues.
if (gameState.ai.playedTurn === 0)
{
@ -172,10 +174,10 @@ QueueManager.prototype.wantedGatherRates = function(gameState)
}
return rates;
};
}
QueueManager.prototype.printQueues = function(gameState)
{
printQueues(gameState)
{
let numWorkers = 0;
gameState.getOwnUnits().forEach(ent =>
{
@ -214,19 +216,19 @@ QueueManager.prototype.printQueues = function(gameState)
aiWarn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState)));
aiWarn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState)));
aiWarn("------------------------------------");
};
}
QueueManager.prototype.clear = function()
{
clear()
{
for (const i in this.queues)
this.queues[i].empty();
};
}
/**
/**
* set accounts of queue i from the unaccounted resources
*/
QueueManager.prototype.setAccounts = function(gameState, cost, i)
{
setAccounts(gameState, cost, i)
{
const available = this.getAvailableResources(gameState);
for (const res of Resources.GetCodes())
{
@ -234,13 +236,13 @@ QueueManager.prototype.setAccounts = function(gameState, cost, i)
continue;
this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]);
}
};
}
/**
/**
* transfer accounts from queue i to queue j
*/
QueueManager.prototype.transferAccounts = function(cost, i, j)
{
transferAccounts(cost, i, j)
{
for (const res of Resources.GetCodes())
{
if (this.accounts[j][res] >= cost[res])
@ -249,13 +251,13 @@ QueueManager.prototype.transferAccounts = function(cost, i, j)
this.accounts[i][res] -= diff;
this.accounts[j][res] += diff;
}
};
}
/**
/**
* distribute the resources between the different queues according to their priorities
*/
QueueManager.prototype.distributeResources = function(gameState)
{
distributeResources(gameState)
{
const availableRes = this.getAvailableResources(gameState);
for (const res of Resources.GetCodes())
{
@ -341,10 +343,10 @@ QueueManager.prototype.distributeResources = function(gameState)
if (available < 0)
aiWarn("Petra: problem with remaining " + res + " in queueManager " + available);
}
};
}
QueueManager.prototype.switchResource = function(gameState, res)
{
switchResource(gameState, res)
{
// We have no available resources, see if we can't "compact" them in one queue.
// compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it.
// TODO: this isn't perfect compression.
@ -380,11 +382,11 @@ QueueManager.prototype.switchResource = function(gameState, res)
break;
}
}
};
}
// Start the next item in the queue if we can afford it.
QueueManager.prototype.startNextItems = function(gameState)
{
// Start the next item in the queue if we can afford it.
startNextItems(gameState)
{
for (const q of this.queueArrays)
{
const name = q[0];
@ -410,10 +412,10 @@ QueueManager.prototype.startNextItems = function(gameState)
queue.switched = 0;
}
}
};
}
QueueManager.prototype.update = function(gameState)
{
update(gameState)
{
Engine.ProfileStart("Queue Manager");
for (const i in this.queues)
@ -439,11 +441,11 @@ QueueManager.prototype.update = function(gameState)
this.printQueues(gameState);
Engine.ProfileStop();
};
}
// Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training
QueueManager.prototype.checkPausedQueues = function(gameState)
{
// Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training
checkPausedQueues(gameState)
{
const numWorkers = gameState.countOwnEntitiesAndQueuedWithRole(Worker.ROLE_WORKER);
const workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2);
for (const q in this.queues)
@ -503,32 +505,32 @@ QueueManager.prototype.checkPausedQueues = function(gameState)
queue.plans[1].number = 1;
}
}
};
}
QueueManager.prototype.canAfford = function(queue, cost)
{
canAfford(queue, cost)
{
if (!this.accounts[queue])
return false;
return this.accounts[queue].canAfford(cost);
};
}
QueueManager.prototype.pauseQueue = function(queue, scrapAccounts)
{
pauseQueue(queue, scrapAccounts)
{
if (!this.queues[queue])
return;
this.queues[queue].paused = true;
if (scrapAccounts)
this.accounts[queue].reset();
};
}
QueueManager.prototype.unpauseQueue = function(queue)
{
unpauseQueue(queue)
{
if (this.queues[queue])
this.queues[queue].paused = false;
};
}
QueueManager.prototype.pauseAll = function(scrapAccounts, but)
{
pauseAll(scrapAccounts, but)
{
for (const q in this.queues)
{
if (q == but)
@ -537,18 +539,18 @@ QueueManager.prototype.pauseAll = function(scrapAccounts, but)
this.accounts[q].reset();
this.queues[q].paused = true;
}
};
}
QueueManager.prototype.unpauseAll = function(but)
{
unpauseAll(but)
{
for (const q in this.queues)
if (q != but)
this.queues[q].paused = false;
};
}
QueueManager.prototype.addQueue = function(queueName, priority)
{
addQueue(queueName, priority)
{
if (this.queues[queueName] !== undefined)
return;
@ -560,10 +562,10 @@ QueueManager.prototype.addQueue = function(queueName, priority)
for (const q in this.queues)
this.queueArrays.push([q, this.queues[q]]);
sortQueues(this.queueArrays, this.priorities);
};
}
QueueManager.prototype.removeQueue = function(queueName)
{
removeQueue(queueName)
{
if (this.queues[queueName] === undefined)
return;
@ -575,15 +577,15 @@ QueueManager.prototype.removeQueue = function(queueName)
for (const q in this.queues)
this.queueArrays.push([q, this.queues[q]]);
sortQueues(this.queueArrays, this.priorities);
};
}
QueueManager.prototype.getPriority = function(queueName)
{
getPriority(queueName)
{
return this.priorities[queueName];
};
}
QueueManager.prototype.changePriority = function(queueName, newPriority)
{
changePriority(queueName, newPriority)
{
if (this.Config.debug > 1)
{
aiWarn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] +
@ -592,10 +594,10 @@ QueueManager.prototype.changePriority = function(queueName, newPriority)
if (this.queues[queueName] !== undefined)
this.priorities[queueName] = newPriority;
sortQueues(this.queueArrays, this.priorities);
};
}
QueueManager.prototype.Serialize = function()
{
Serialize()
{
const accounts = {};
const queues = {};
for (const q in this.queues)
@ -614,10 +616,10 @@ QueueManager.prototype.Serialize = function()
"queues": queues,
"accounts": accounts
};
};
}
QueueManager.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
this.priorities = data.priorities;
this.queues = {};
this.accounts = {};
@ -633,4 +635,5 @@ QueueManager.prototype.Deserialize = function(gameState, data)
this.queueArrays.push([q, this.queues[q]]);
}
sortQueues(this.queueArrays, data.priorities);
};
}
}

View file

@ -4,16 +4,18 @@ import { Worker } from "simulation/ai/petra/worker.js";
/**
* Manage the research
*/
export function ResearchManager(Config)
export class ResearchManager
{
constructor(Config)
{
this.Config = Config;
}
}
/**
/**
* Check if we can go to the next phase
*/
ResearchManager.prototype.checkPhase = function(gameState, queues)
{
checkPhase(gameState, queues)
{
if (queues.majorTech.hasQueuedUnits())
return;
// Don't try to phase up if already trying to gather resources for a civil-centre or wonder
@ -36,10 +38,10 @@ ResearchManager.prototype.checkPhase = function(gameState, queues)
gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech);
queues.majorTech.addPlan(new ResearchPlan(gameState, nextPhaseName, true));
}
};
}
ResearchManager.prototype.researchPopulationBonus = function(gameState, queues)
{
researchPopulationBonus(gameState, queues)
{
if (queues.minorTech.hasQueuedUnits())
return;
@ -54,10 +56,10 @@ ResearchManager.prototype.researchPopulationBonus = function(gameState, queues)
queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0]));
break;
}
};
}
ResearchManager.prototype.researchTradeBonus = function(gameState, queues)
{
researchTradeBonus(gameState, queues)
{
if (queues.minorTech.hasQueuedUnits())
return;
@ -75,11 +77,11 @@ ResearchManager.prototype.researchTradeBonus = function(gameState, queues)
queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0]));
break;
}
};
}
/** Techs to be searched for as soon as they are available */
ResearchManager.prototype.researchWantedTechs = function(gameState, techs)
{
/** Techs to be searched for as soon as they are available */
researchWantedTechs(gameState, techs)
{
const phase1 = gameState.currentPhase() === 1;
const available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null;
const numWorkers = phase1 ? gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length : 0;
@ -119,11 +121,11 @@ ResearchManager.prototype.researchWantedTechs = function(gameState, techs)
}
}
return null;
};
}
/** Techs to be searched for as soon as they are available, but only after phase 2 */
ResearchManager.prototype.researchPreferredTechs = function(gameState, techs)
{
/** Techs to be searched for as soon as they are available, but only after phase 2 */
researchPreferredTechs(gameState, techs)
{
const phase2 = gameState.currentPhase() === 2;
const available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null;
const numWorkers = phase2 ? gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length : 0;
@ -156,10 +158,10 @@ ResearchManager.prototype.researchPreferredTechs = function(gameState, techs)
}
}
return null;
};
}
ResearchManager.prototype.update = function(gameState, queues)
{
update(gameState, queues)
{
if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits())
return;
@ -225,21 +227,22 @@ ResearchManager.prototype.update = function(gameState, queues)
// randomly pick one. No worries about pairs in that case.
queues.minorTech.addPlan(new ResearchPlan(gameState, pickRandom(techs)[0]));
};
}
ResearchManager.prototype.CostSum = function(cost)
{
CostSum(cost)
{
let costSum = 0;
for (const res in cost)
costSum += cost[res];
return costSum;
};
}
ResearchManager.prototype.Serialize = function()
{
Serialize()
{
return {};
};
}
ResearchManager.prototype.Deserialize = function(data)
{
};
Deserialize(data)
{
}
}

View file

@ -12,37 +12,39 @@ import { Worker } from "simulation/ai/petra/worker.js";
/**
* Manage the trade
*/
export function TradeManager(config)
export class TradeManager
{
constructor(config)
{
this.Config = config;
this.tradeRoute = undefined;
this.potentialTradeRoute = undefined;
this.routeProspection = false;
this.targetNumTraders = this.Config.Economy.targetNumTraders;
this.warnedAllies = {};
}
}
TradeManager.prototype.init = function(gameState)
{
init(gameState)
{
this.traders = gameState.getOwnUnits().filter(
filters.byMetadata(PlayerID, "role", Worker.ROLE_TRADER));
this.traders.registerUpdates();
this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5;
};
}
TradeManager.prototype.hasTradeRoute = function()
{
hasTradeRoute()
{
return this.tradeRoute !== undefined;
};
}
TradeManager.prototype.assignTrader = function(ent)
{
assignTrader(ent)
{
ent.setMetadata(PlayerID, "role", Worker.ROLE_TRADER);
this.traders.updateEnt(ent);
};
}
TradeManager.prototype.trainMoreTraders = function(gameState, queues)
{
trainMoreTraders(gameState, queues)
{
if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits())
return;
@ -122,10 +124,10 @@ TradeManager.prototype.trainMoreTraders = function(gameState, queues)
return;
}
queues.trader.addPlan(new TrainingPlan(gameState, template, metadata, 1, 1));
};
}
TradeManager.prototype.updateTrader = function(gameState, ent)
{
updateTrader(gameState, ent)
{
if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 &&
!ent.unitAIState().startsWith("INDIVIDUAL.COLLECTTREASURE") &&
gatherTreasure(gameState, ent, true))
@ -177,10 +179,10 @@ TradeManager.prototype.updateTrader = function(gameState, ent)
ent.tradeRoute(routeSource, routeTarget);
ent.setMetadata(PlayerID, "route", route);
Engine.ProfileStop();
};
}
TradeManager.prototype.setTradingGoods = function(gameState)
{
setTradingGoods(gameState)
{
const resTradeCodes = Resources.GetTradableCodes();
if (!resTradeCodes.length)
return;
@ -230,14 +232,14 @@ TradeManager.prototype.setTradingGoods = function(gameState)
Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods });
if (this.Config.debug > 2)
aiWarn(" trading goods set to " + uneval(tradingGoods));
};
}
/**
/**
* Try to barter unneeded resources for needed resources.
* only once per turn because the info is not updated within a turn
*/
TradeManager.prototype.performBarter = function(gameState)
{
performBarter(gameState)
{
const barterers = gameState.getOwnEntitiesByClass("Barter", true).filter(filters.isBuilt())
.toEntityArray();
if (barterers.length == 0)
@ -355,10 +357,10 @@ TradeManager.prototype.performBarter = function(gameState)
}
return false;
};
}
TradeManager.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
// check if one market from a traderoute is renamed, change the route accordingly
for (const evt of events.EntityRenamed)
{
@ -425,21 +427,21 @@ TradeManager.prototype.checkEvents = function(gameState, events)
}
return false;
};
}
TradeManager.prototype.activateProspection = function(gameState)
{
activateProspection(gameState)
{
this.routeProspection = true;
gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}/market"));
gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}/dock"));
};
}
/**
/**
* fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute
* If an index is given, it returns the best route with this index or the best land route if index is a land index
*/
TradeManager.prototype.checkRoutes = function(gameState, accessIndex)
{
checkRoutes(gameState, accessIndex)
{
// If we cannot trade, do not bother checking routes.
if (!Resources.GetTradableCodes().length)
{
@ -579,11 +581,11 @@ TradeManager.prototype.checkRoutes = function(gameState, accessIndex)
return false;
}
return true;
};
}
/** Called when a market was built or destroyed, and checks if trader orders should be changed */
TradeManager.prototype.checkTrader = function(gameState, ent)
{
/** Called when a market was built or destroyed, and checks if trader orders should be changed */
checkTrader(gameState, ent)
{
const presentRoute = ent.getMetadata(PlayerID, "route");
if (!presentRoute)
return;
@ -616,10 +618,10 @@ TradeManager.prototype.checkTrader = function(gameState, ent)
}
ent.stopMoving();
}
};
}
TradeManager.prototype.prospectForNewMarket = function(gameState, queues)
{
prospectForNewMarket(gameState, queues)
{
if (queues.economicBuilding.hasQueuedUnitsWithClass("Trade") || queues.dock.hasQueuedUnitsWithClass("Trade"))
return;
if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market"))
@ -668,10 +670,10 @@ TradeManager.prototype.prospectForNewMarket = function(gameState, queues)
if (!this.tradeRoute)
plan.queueToReset = "economicBuilding";
queues.economicBuilding.addPlan(plan);
};
}
TradeManager.prototype.isNewMarketWorth = function(expectedGain)
{
isNewMarketWorth(expectedGain)
{
if (!Resources.GetTradableCodes().length)
return false;
if (expectedGain < this.minimalGain)
@ -680,10 +682,10 @@ TradeManager.prototype.isNewMarketWorth = function(expectedGain)
expectedGain < this.potentialTradeRoute.gain + 20)
return false;
return true;
};
}
TradeManager.prototype.update = function(gameState, events, queues)
{
update(gameState, events, queues)
{
if (gameState.ai.HQ.canBarter && Resources.GetBarterableCodes().length)
this.performBarter(gameState);
@ -709,10 +711,10 @@ TradeManager.prototype.update = function(gameState, events, queues)
if (this.routeProspection)
this.prospectForNewMarket(gameState, queues);
};
}
TradeManager.prototype.Serialize = function()
{
Serialize()
{
return {
"tradeRoute": this.tradeRoute,
"potentialTradeRoute": this.potentialTradeRoute,
@ -720,10 +722,11 @@ TradeManager.prototype.Serialize = function()
"targetNumTraders": this.targetNumTraders,
"warnedAllies": this.warnedAllies
};
};
}
TradeManager.prototype.Deserialize = function(gameState, data)
{
Deserialize(gameState, data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -25,8 +25,10 @@ import { Worker } from "simulation/ai/petra/worker.js";
* transporter = this.ID
*/
export function TransportPlan(gameState, units, startIndex, endIndex, endPos, ship)
export class TransportPlan
{
constructor(gameState, units, startIndex, endIndex, endPos, ship)
{
this.ID = gameState.ai.uniqueIDs.transports++;
this.debug = gameState.ai.Config.debug;
this.flotilla = false; // when false, only one ship per transport ... not yet tested when true
@ -82,19 +84,19 @@ export function TransportPlan(gameState, units, startIndex, endIndex, endPos, sh
this.needTransportShips = ship === undefined;
this.nTry = {};
return true;
}
}
/**
/**
* We're trying to board units onto our ships.
*/
TransportPlan.BOARDING = "boarding";
/**
static BOARDING = "boarding";
/**
* We're moving ships and eventually unload units.
*/
TransportPlan.SAILING = "sailing";
static SAILING = "sailing";
TransportPlan.prototype.init = function(gameState)
{
init(gameState)
{
this.units = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "transport", this.ID));
this.ships = gameState.ai.HQ.navalManager.ships.filter(filters.byMetadata(PlayerID, "transporter",
this.ID));
@ -106,28 +108,28 @@ TransportPlan.prototype.init = function(gameState)
this.transportShips.registerUpdates();
this.boardingRange = 18*18; // TODO compute it from the ship clearance and garrison range
};
}
/** count available slots */
TransportPlan.prototype.countFreeSlots = function()
{
/** count available slots */
countFreeSlots()
{
let slots = 0;
for (const ship of this.transportShips.values())
slots += this.countFreeSlotsOnShip(ship);
return slots;
};
}
TransportPlan.prototype.countFreeSlotsOnShip = function(ship)
{
countFreeSlotsOnShip(ship)
{
if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints())
return 0;
const occupied = ship.garrisoned().length +
this.units.filter(filters.byMetadata(PlayerID, "onBoard", ship.id())).length;
return Math.max(ship.garrisonMax() - occupied, 0);
};
}
TransportPlan.prototype.assignUnitToShip = function(gameState, ent)
{
assignUnitToShip(gameState, ent)
{
if (this.needTransportShips)
return;
@ -156,10 +158,10 @@ TransportPlan.prototype.assignUnitToShip = function(gameState, ent)
this.needSplit = [ent];
else
this.needSplit.push(ent);
};
}
TransportPlan.prototype.assignShip = function(gameState)
{
assignShip(gameState)
{
let pos;
// choose a unit of this plan not yet assigned to a ship
for (const ent of this.units.values())
@ -196,19 +198,19 @@ TransportPlan.prototype.assignShip = function(gameState)
this.transportShips.updateEnt(nearest);
this.needTransportShips = false;
return true;
};
}
/** add a unit to this plan */
TransportPlan.prototype.addUnit = function(unit, endPos)
{
/** add a unit to this plan */
addUnit(unit, endPos)
{
unit.setMetadata(PlayerID, "transport", this.ID);
unit.setMetadata(PlayerID, "endPos", endPos);
this.units.updateEnt(unit);
};
}
/** remove a unit from this plan, if not yet on board */
TransportPlan.prototype.removeUnit = function(gameState, unit)
{
/** remove a unit from this plan, if not yet on board */
removeUnit(gameState, unit)
{
const shipId = unit.getMetadata(PlayerID, "onBoard");
if (shipId == "onBoard")
return; // too late, already onBoard
@ -229,10 +231,10 @@ TransportPlan.prototype.removeUnit = function(gameState, unit)
this.transportShips.updateEnt(ship);
}
}
};
}
TransportPlan.prototype.releaseShip = function(ship)
{
releaseShip(ship)
{
if (ship.getMetadata(PlayerID, "transporter") != this.ID)
{
aiWarn(" Petra: try removing a transporter ship with " +
@ -248,10 +250,10 @@ TransportPlan.prototype.releaseShip = function(ship)
ship.setMetadata(PlayerID, "transporter", undefined);
if (ship.getMetadata(PlayerID, "role") === Worker.ROLE_SWITCH_TO_TRADER)
ship.setMetadata(PlayerID, "role", Worker.ROLE_TRADER);
};
}
TransportPlan.prototype.releaseAll = function()
{
releaseAll()
{
for (const ship of this.ships.values())
this.releaseShip(ship);
@ -267,11 +269,11 @@ TransportPlan.prototype.releaseAll = function()
this.transportShips.unregister();
this.ships.unregister();
this.units.unregister();
};
}
/** TODO not currently used ... to be fixed */
TransportPlan.prototype.cancelTransport = function(gameState)
{
/** TODO not currently used ... to be fixed */
cancelTransport(gameState)
{
const ent = this.units.toEntityArray()[0];
let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base"));
if (!base.anchor || !base.anchor.position())
@ -292,24 +294,24 @@ TransportPlan.prototype.cancelTransport = function(gameState)
this.endPos = base.anchor.position();
this.canceled = true;
return true;
};
}
/**
/**
* Try to move on and then clear the plan.
*/
TransportPlan.prototype.update = function(gameState)
{
update(gameState)
{
if (this.state === TransportPlan.BOARDING)
this.onBoarding(gameState);
else if (this.state === TransportPlan.SAILING)
this.onSailing(gameState);
return this.units.length;
};
}
TransportPlan.prototype.onBoarding = function(gameState)
{
onBoarding(gameState)
{
let ready = true;
const time = gameState.ai.elapsedTime;
const shipTested = {};
@ -448,11 +450,11 @@ TransportPlan.prototype.onBoarding = function(gameState)
this.nTry = {};
this.unloaded = [];
this.recovered = [];
};
}
/** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */
TransportPlan.prototype.isOnBoard = function(ent)
{
/** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */
isOnBoard(ent)
{
for (const ship of this.transportShips.values())
{
if (ship.garrisoned().indexOf(ent.id()) == -1)
@ -461,12 +463,12 @@ TransportPlan.prototype.isOnBoard = function(ent)
return true;
}
return false;
};
}
/** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */
TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, seaIndex, destination,
/** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */
getBoardingPos(gameState, ship, landIndex, seaIndex, destination,
avoidEnnemy)
{
{
if (!gameState.ai.HQ.navalManager.landingZones[landIndex])
{
aiWarn(" >>> no landing zone for land " + landIndex);
@ -530,10 +532,10 @@ TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, se
if (!posmin && this.boardingPos[ship.id()])
posmin = this.boardingPos[ship.id()];
return posmin;
};
}
TransportPlan.prototype.onSailing = function(gameState)
{
onSailing(gameState)
{
// Check that the units recovered on the previous turn have been reloaded
for (const recov of this.recovered)
{
@ -700,10 +702,10 @@ TransportPlan.prototype.onSailing = function(gameState)
ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]);
}
}
};
}
TransportPlan.prototype.resetUnit = function(gameState, ent)
{
resetUnit(gameState, ent)
{
ent.setMetadata(PlayerID, "transport", undefined);
ent.setMetadata(PlayerID, "onBoard", undefined);
ent.setMetadata(PlayerID, "endPos", undefined);
@ -720,10 +722,10 @@ TransportPlan.prototype.resetUnit = function(gameState, ent)
if (army)
army.removeOwn(gameState, ent.id());
}
};
}
TransportPlan.prototype.Serialize = function()
{
Serialize()
{
return {
"ID": this.ID,
"flotilla": this.flotilla,
@ -739,12 +741,13 @@ TransportPlan.prototype.Serialize = function()
"unloaded": this.unloaded,
"recovered": this.recovered
};
};
}
TransportPlan.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
this.failed = false;
};
}
}

View file

@ -13,8 +13,10 @@ import { Worker } from "simulation/ai/petra/worker.js";
* in wonder, train military guards.
*/
export function VictoryManager(Config)
export class VictoryManager
{
constructor(Config)
{
this.Config = Config;
this.criticalEnts = new Map();
// Holds ids of all ents who are (or can be) guarding and if the ent is currently guarding
@ -24,13 +26,13 @@ export function VictoryManager(Config)
this.tryCaptureGaiaRelicLapseTime = -1;
// Gaia relics which we are targeting currently and have not captured yet
this.targetedGaiaRelics = new Map();
}
}
/**
/**
* Cache the ids of any inital victory-critical entities.
*/
VictoryManager.prototype.init = function(gameState)
{
init(gameState)
{
if (gameState.getVictoryConditions().has("wonder"))
{
for (const wonder of gameState.getOwnEntitiesByClass("Wonder", true).values())
@ -62,15 +64,15 @@ VictoryManager.prototype.init = function(gameState)
this.criticalEnts.set(relic.id(), { "guardsAssigned": new Set(), "guards": new Map() });
}
}
};
}
/**
/**
* In regicide victory condition, if the hero has less than 70% health, try to garrison it in a healing structure
* If it is less than 40%, try to garrison in the closest possible structure
* If the hero cannot garrison, retreat it to the closest base
*/
VictoryManager.prototype.checkEvents = function(gameState, events)
{
checkEvents(gameState, events)
{
if (gameState.getVictoryConditions().has("wonder"))
{
for (const evt of events.Create)
@ -303,10 +305,10 @@ VictoryManager.prototype.checkEvents = function(gameState, events)
this.pickCriticalEntRetreatLocation(gameState, ent, false);
}
}
};
}
VictoryManager.prototype.removeCriticalEnt = function(gameState, criticalEntId)
{
removeCriticalEnt(gameState, criticalEntId)
{
for (const [guardId, role] of this.criticalEnts.get(criticalEntId).guards)
{
const guardEnt = gameState.getEntityById(guardId);
@ -326,13 +328,13 @@ VictoryManager.prototype.removeCriticalEnt = function(gameState, criticalEntId)
guardEnt.setMetadata(PlayerID, "guardedEnt", undefined);
}
this.criticalEnts.delete(criticalEntId);
};
}
/**
/**
* Train more healers to be later affected to critical entities if needed
*/
VictoryManager.prototype.manageCriticalEntHealers = function(gameState, queues)
{
manageCriticalEntHealers(gameState, queues)
{
if (gameState.ai.HQ.saveResources || queues.healer.hasQueuedUnits() ||
!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() ||
this.guardEnts.size > Math.min(gameState.getPopulationMax() / 10, gameState.getPopulation() / 4))
@ -350,15 +352,15 @@ VictoryManager.prototype.manageCriticalEntHealers = function(gameState, queues)
{ "role": Worker.ROLE_CRITICAL_ENT_HEALER, "base": 0 }, 1, 1));
return;
}
};
}
/**
/**
* Try to keep some military units guarding any criticalEnts, if we can afford it.
* If we have too low a population and require units for other needs, remove guards so they can be reassigned.
* TODO: Swap citizen soldier guards with champions if they become available.
*/
VictoryManager.prototype.manageCriticalEntGuards = function(gameState)
{
manageCriticalEntGuards(gameState)
{
let numWorkers = gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).length;
if (numWorkers < 20)
{
@ -460,10 +462,10 @@ VictoryManager.prototype.manageCriticalEntGuards = function(gameState)
}
}
}
};
}
VictoryManager.prototype.tryAssignMilitaryGuard = function(gameState, guardEnt, criticalEnt, checkForSameAccess)
{
tryAssignMilitaryGuard(gameState, guardEnt, criticalEnt, checkForSameAccess)
{
if (guardEnt.getMetadata(PlayerID, "plan") !== undefined ||
guardEnt.getMetadata(PlayerID, "transport") !== undefined || this.criticalEnts.has(guardEnt.id()) ||
checkForSameAccess && (!guardEnt.position() || !criticalEnt.position() ||
@ -476,10 +478,10 @@ VictoryManager.prototype.tryAssignMilitaryGuard = function(gameState, guardEnt,
guardEnt.setMetadata(PlayerID, "plan", -2);
guardEnt.setMetadata(PlayerID, "role", Worker.ROLE_CRITICAL_ENT_GUARD);
return true;
};
}
VictoryManager.prototype.pickCriticalEntRetreatLocation = function(gameState, criticalEnt, emergency)
{
pickCriticalEntRetreatLocation(gameState, criticalEnt, emergency)
{
gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, criticalEnt, emergency);
const plan = criticalEnt.getMetadata(PlayerID, "plan");
@ -500,9 +502,9 @@ VictoryManager.prototype.pickCriticalEntRetreatLocation = function(gameState, cr
criticalEnt.moveToRange(bestBasePos[0], bestBasePos[1],
0, bestBase.anchor.obstructionRadius().max);
}
};
}
/**
/**
* Only send the guard command if the guard's accessIndex is the same as the critical ent
* and the critical ent has a position (i.e. not garrisoned).
* Request a transport if the accessIndex value is different, and if a transport is needed,
@ -510,8 +512,8 @@ VictoryManager.prototype.pickCriticalEntRetreatLocation = function(gameState, cr
* which will be used once its transport has finished.
* Return false if the guardEnt is not a valid guard unit (i.e. cannot guard or is being transported).
*/
VictoryManager.prototype.assignGuardToCriticalEnt = function(gameState, guardEnt, criticalEntId)
{
assignGuardToCriticalEnt(gameState, guardEnt, criticalEntId)
{
if (guardEnt.getMetadata(PlayerID, "transport") !== undefined || !guardEnt.canGuard())
return false;
@ -580,17 +582,17 @@ VictoryManager.prototype.assignGuardToCriticalEnt = function(gameState, guardEnt
this.guardEnts.set(guardEnt.id(), guardEntAccess == criticalEntAccess);
return true;
};
}
VictoryManager.prototype.resetCaptureGaiaRelic = function(gameState)
{
resetCaptureGaiaRelic(gameState)
{
// Do not capture gaia relics too frequently as the ai has access to the entire map
this.tryCaptureGaiaRelicLapseTime = gameState.ai.elapsedTime + 240 - 30 * (this.Config.difficulty - 3);
this.tryCaptureGaiaRelic = false;
};
}
VictoryManager.prototype.update = function(gameState, events, queues)
{
update(gameState, events, queues)
{
// Wait a turn for trigger scripts to spawn any critical ents (i.e. in regicide)
if (gameState.ai.playedTurn == 1)
this.init(gameState);
@ -665,13 +667,13 @@ VictoryManager.prototype.update = function(gameState, events, queues)
}
}
}
};
}
/**
/**
* Send an expedition to capture a gaia relic, or reinforce an existing one.
*/
VictoryManager.prototype.captureGaiaRelic = function(gameState, relic)
{
captureGaiaRelic(gameState, relic)
{
let capture = -relic.defaultRegenRate();
const sumCapturePoints = relic.capturePoints().reduce((a, b) => a + b);
const plans = this.targetedGaiaRelics.get(relic.id());
@ -736,10 +738,10 @@ VictoryManager.prototype.captureGaiaRelic = function(gameState, relic)
}
attack.forceStart();
this.targetedGaiaRelics.get(relic.id()).push(plan);
};
}
VictoryManager.prototype.abortCaptureGaiaRelic = function(gameState, relicId)
{
abortCaptureGaiaRelic(gameState, relicId)
{
for (const plan of this.targetedGaiaRelics.get(relicId))
{
const attack = gameState.ai.HQ.attackManager.getPlan(plan);
@ -747,10 +749,10 @@ VictoryManager.prototype.abortCaptureGaiaRelic = function(gameState, relicId)
attack.Abort(gameState);
}
this.targetedGaiaRelics.delete(relicId);
};
}
VictoryManager.prototype.Serialize = function()
{
Serialize()
{
return {
"criticalEnts": this.criticalEnts,
"guardEnts": this.guardEnts,
@ -759,10 +761,11 @@ VictoryManager.prototype.Serialize = function()
"tryCaptureGaiaRelicLapseTime": this.tryCaptureGaiaRelicLapseTime,
"targetedGaiaRelics": this.targetedGaiaRelics
};
};
}
VictoryManager.prototype.Deserialize = function(data)
{
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
};
}
}

View file

@ -7,33 +7,35 @@ import { TransportPlan } from "simulation/ai/petra/transportPlan.js";
/**
* This class makes a worker do as instructed by the economy manager
*/
export function Worker(base)
export class Worker
{
constructor(base)
{
this.ent = undefined;
this.base = base;
this.baseID = base.ID;
}
}
Worker.ROLE_ATTACK = "attack";
Worker.ROLE_TRADER = "trader";
Worker.ROLE_SWITCH_TO_TRADER = "switchToTrader";
Worker.ROLE_WORKER = "worker";
Worker.ROLE_CRITICAL_ENT_GUARD = "criticalEntGuard";
Worker.ROLE_CRITICAL_ENT_HEALER = "criticalEntHealer";
static ROLE_ATTACK = "attack";
static ROLE_TRADER = "trader";
static ROLE_SWITCH_TO_TRADER = "switchToTrader";
static ROLE_WORKER = "worker";
static ROLE_CRITICAL_ENT_GUARD = "criticalEntGuard";
static ROLE_CRITICAL_ENT_HEALER = "criticalEntHealer";
Worker.SUBROLE_DEFENDER = "defender";
Worker.SUBROLE_IDLE = "idle";
Worker.SUBROLE_BUILDER = "builder";
Worker.SUBROLE_COMPLETING = "completing";
Worker.SUBROLE_WALKING = "walking";
Worker.SUBROLE_ATTACKING = "attacking";
Worker.SUBROLE_GATHERER = "gatherer";
Worker.SUBROLE_HUNTER = "hunter";
Worker.SUBROLE_FISHER = "fisher";
Worker.SUBROLE_GARRISONING = "garrisoning";
static SUBROLE_DEFENDER = "defender";
static SUBROLE_IDLE = "idle";
static SUBROLE_BUILDER = "builder";
static SUBROLE_COMPLETING = "completing";
static SUBROLE_WALKING = "walking";
static SUBROLE_ATTACKING = "attacking";
static SUBROLE_GATHERER = "gatherer";
static SUBROLE_HUNTER = "hunter";
static SUBROLE_FISHER = "fisher";
static SUBROLE_GARRISONING = "garrisoning";
Worker.prototype.update = function(gameState, ent)
{
update(gameState, ent)
{
if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return;
@ -439,10 +441,10 @@ Worker.prototype.update = function(gameState, ent)
this.startFishing(gameState);
}
}
};
}
Worker.prototype.retryWorking = function(gameState, subrole)
{
retryWorking(gameState, subrole)
{
switch (subrole)
{
case Worker.SUBROLE_GATHERER:
@ -456,10 +458,10 @@ Worker.prototype.retryWorking = function(gameState, subrole)
default:
return false;
}
};
}
Worker.prototype.startBuilding = function(gameState)
{
startBuilding(gameState)
{
const target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() == false)
return false;
@ -467,10 +469,10 @@ Worker.prototype.startBuilding = function(gameState)
return false;
this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses
return true;
};
}
Worker.prototype.startGathering = function(gameState)
{
startGathering(gameState)
{
// First look for possible treasure if any
if (gatherTreasure(gameState, this.ent))
return true;
@ -761,14 +763,14 @@ Worker.prototype.startGathering = function(gameState)
aiWarn(" >>>>> worker with gather-type " + resource + " with nothing to gather ");
this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE);
return false;
};
}
/**
/**
* if position is given, we only check if we could hunt from this position but do nothing
* otherwise the position of the entity is taken, and if something is found, we directly start the hunt
*/
Worker.prototype.startHunting = function(gameState, position)
{
startHunting(gameState, position)
{
// First look for possible treasure if any
if (!position && gatherTreasure(gameState, this.ent))
return true;
@ -873,10 +875,10 @@ Worker.prototype.startHunting = function(gameState, position)
return true;
}
return false;
};
}
Worker.prototype.startFishing = function(gameState)
{
startFishing(gameState)
{
if (!this.ent.position())
return false;
@ -969,10 +971,10 @@ Worker.prototype.startFishing = function(gameState)
if (this.ent.getMetadata(PlayerID, "subrole") === Worker.SUBROLE_FISHER)
this.ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_IDLE);
return false;
};
}
Worker.prototype.gatherNearestField = function(gameState, baseID)
{
gatherNearestField(gameState, baseID)
{
const ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(filters.isBuilt())
.filter(filters.byMetadata(PlayerID, "base", baseID));
let bestFarm;
@ -1009,14 +1011,14 @@ Worker.prototype.gatherNearestField = function(gameState, baseID)
this.base.AddTCGatherer(bestFarm.ent.id());
this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id());
return bestFarm.ent;
};
}
/**
/**
* WARNING with the present options of AI orders, the unit will not gather after building the farm.
* This is done by calling the gatherNearestField function when construction is completed.
*/
Worker.prototype.buildAnyField = function(gameState, baseID)
{
buildAnyField(gameState, baseID)
{
if (!this.ent.isBuilder())
return false;
let bestFarmEnt = false;
@ -1037,15 +1039,15 @@ Worker.prototype.buildAnyField = function(gameState, baseID)
bestFarmDist = dist;
}
return bestFarmEnt;
};
}
/**
/**
* Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions.
* For the time being, we move towards the nearest gatherer (providing him a dropsite).
* BaseManager does also use that function to deal with its mobile dropsites.
*/
Worker.prototype.moveToGatherer = function(gameState, ent, forced)
{
moveToGatherer(gameState, ent, forced)
{
const pos = ent.position();
if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
@ -1075,15 +1077,15 @@ Worker.prototype.moveToGatherer = function(gameState, ent, forced)
ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5));
if (destination && dist > 10)
ent.move(destination[0], destination[1]);
};
}
/**
/**
* Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes
* inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case
* it will be cleared later).
*/
Worker.prototype.isInaccessibleSupply = function(gameState)
{
isInaccessibleSupply(gameState)
{
if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target)
return false;
const targetId = this.ent.unitAIOrderData()[0].target;
@ -1147,4 +1149,5 @@ Worker.prototype.isInaccessibleSupply = function(gameState)
}
}
return false;
};
}
}