mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-25 14:53:29 +00:00
@@ -1,60 +1,63 @@
|
||||
globalThis.PlayerID = -1;
|
||||
|
||||
export function BaseAI(settings)
|
||||
export class BaseAI
|
||||
{
|
||||
if (!settings)
|
||||
return;
|
||||
constructor(settings)
|
||||
{
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
this.player = settings.player;
|
||||
this.player = settings.player;
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
Deserialize(data, sharedScript)
|
||||
{
|
||||
}
|
||||
|
||||
Init(playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.accessibility = sharedAI.accessibility;
|
||||
|
||||
this.gameState = sharedAI.gameState[this.player];
|
||||
this.gameState.ai = this;
|
||||
|
||||
this.timeElapsed = sharedAI.timeElapsed;
|
||||
|
||||
this.CustomInit(this.gameState);
|
||||
}
|
||||
|
||||
/** AIs override this function */
|
||||
CustomInit()
|
||||
{
|
||||
}
|
||||
|
||||
HandleMessage(state, playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.OnUpdate(sharedAI);
|
||||
}
|
||||
|
||||
/** AIs override this function */
|
||||
OnUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
chat(message)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "aichat", "message": message });
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a simple object (using no classes etc) that will be serialized into saved games */
|
||||
BaseAI.prototype.Serialize = function()
|
||||
{
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Called after the constructor when loading a saved game, with 'data' being
|
||||
* whatever Serialize() returned
|
||||
*/
|
||||
BaseAI.prototype.Deserialize = function(data, sharedScript)
|
||||
{
|
||||
};
|
||||
|
||||
BaseAI.prototype.Init = function(playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.accessibility = sharedAI.accessibility;
|
||||
|
||||
this.gameState = sharedAI.gameState[this.player];
|
||||
this.gameState.ai = this;
|
||||
|
||||
this.timeElapsed = sharedAI.timeElapsed;
|
||||
|
||||
this.CustomInit(this.gameState);
|
||||
};
|
||||
|
||||
/** AIs override this function */
|
||||
BaseAI.prototype.CustomInit = function()
|
||||
{
|
||||
};
|
||||
|
||||
BaseAI.prototype.HandleMessage = function(state, playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.OnUpdate(sharedAI);
|
||||
};
|
||||
|
||||
/** AIs override this function */
|
||||
BaseAI.prototype.OnUpdate = function()
|
||||
{
|
||||
};
|
||||
|
||||
BaseAI.prototype.chat = function(message)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "aichat", "message": message });
|
||||
};
|
||||
|
||||
@@ -1,368 +1,371 @@
|
||||
import { SquareVectorDistance } from "simulation/ai/common-api/utils.js";
|
||||
|
||||
export function EntityCollection(sharedAI, entities = new Map(), filters = [])
|
||||
export class EntityCollection
|
||||
{
|
||||
this._ai = sharedAI;
|
||||
this._entities = entities;
|
||||
this._filters = filters;
|
||||
this.dynamicProp = [];
|
||||
for (const filter of this._filters)
|
||||
if (filter.dynamicProperties.length)
|
||||
this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties);
|
||||
|
||||
Object.defineProperty(this, "length", { "get": () => this._entities.size });
|
||||
this.frozen = false;
|
||||
}
|
||||
|
||||
EntityCollection.prototype.Serialize = function()
|
||||
{
|
||||
const filters = [];
|
||||
for (const f of this._filters)
|
||||
filters.push(uneval(f));
|
||||
return {
|
||||
"ents": this.toIdArray(),
|
||||
"frozen": this.frozen,
|
||||
"filters": filters
|
||||
};
|
||||
};
|
||||
|
||||
EntityCollection.prototype.Deserialize = function(data, sharedAI)
|
||||
{
|
||||
this._ai = sharedAI;
|
||||
for (const id of data.ents)
|
||||
this._entities.set(id, sharedAI._entities.get(id));
|
||||
|
||||
for (const f of data.filters)
|
||||
this._filters.push(eval(f));
|
||||
|
||||
if (data.frozen)
|
||||
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()
|
||||
{
|
||||
this.frozen = true;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.defreeze = function()
|
||||
{
|
||||
this.frozen = false;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toIdArray = function()
|
||||
{
|
||||
return Array.from(this._entities.keys());
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toEntityArray = function()
|
||||
{
|
||||
return Array.from(this._entities.values());
|
||||
};
|
||||
|
||||
EntityCollection.prototype.values = function()
|
||||
{
|
||||
return this._entities.values();
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toString = function()
|
||||
{
|
||||
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
|
||||
};
|
||||
|
||||
EntityCollection.prototype.filter = function(filter, thisp)
|
||||
{
|
||||
if (typeof filter === "function")
|
||||
filter = { "func": filter, "dynamicProperties": [] };
|
||||
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
if (filter.func.call(thisp, ent, id, this))
|
||||
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)
|
||||
{
|
||||
// Compute the distance of each entity
|
||||
const data = []; // [ [id, ent, distance], ... ]
|
||||
for (const [id, ent] of this._entities)
|
||||
if (ent.position())
|
||||
data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]);
|
||||
|
||||
// Sort by increasing distance
|
||||
data.sort((a, b) => a[2] - b[2]);
|
||||
|
||||
if (n === undefined)
|
||||
n = data.length;
|
||||
else
|
||||
n = Math.min(n, data.length);
|
||||
|
||||
// Extract the first n
|
||||
const ret = new Map();
|
||||
for (let i = 0; i < n; ++i)
|
||||
ret.set(data[i][0], data[i][1]);
|
||||
|
||||
return new EntityCollection(this._ai, ret);
|
||||
};
|
||||
|
||||
EntityCollection.prototype.filter_raw = function(callback, thisp)
|
||||
{
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
constructor(sharedAI, entities = new Map(), filters = [])
|
||||
{
|
||||
const val = ent._entity;
|
||||
if (callback.call(thisp, val, id, this))
|
||||
ret.set(id, ent);
|
||||
this._ai = sharedAI;
|
||||
this._entities = entities;
|
||||
this._filters = filters;
|
||||
this.dynamicProp = [];
|
||||
for (const filter of this._filters)
|
||||
if (filter.dynamicProperties.length)
|
||||
this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties);
|
||||
|
||||
Object.defineProperty(this, "length", { "get": () => this._entities.size });
|
||||
this.frozen = false;
|
||||
}
|
||||
return new EntityCollection(this._ai, ret);
|
||||
};
|
||||
|
||||
EntityCollection.prototype.forEach = function(callback)
|
||||
{
|
||||
for (const ent of this._entities.values())
|
||||
callback(ent);
|
||||
return this;
|
||||
};
|
||||
Serialize()
|
||||
{
|
||||
const filters = [];
|
||||
for (const f of this._filters)
|
||||
filters.push(uneval(f));
|
||||
return {
|
||||
"ents": this.toIdArray(),
|
||||
"frozen": this.frozen,
|
||||
"filters": filters
|
||||
};
|
||||
}
|
||||
|
||||
EntityCollection.prototype.hasEntities = function()
|
||||
{
|
||||
return this._entities.size !== 0;
|
||||
};
|
||||
Deserialize(data, sharedAI)
|
||||
{
|
||||
this._ai = sharedAI;
|
||||
for (const id of data.ents)
|
||||
this._entities.set(id, sharedAI._entities.get(id));
|
||||
|
||||
EntityCollection.prototype.move = function(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
for (const f of data.filters)
|
||||
this._filters.push(eval(f));
|
||||
|
||||
EntityCollection.prototype.moveToRange = function(x, z, min, max, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk-to-range",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
if (data.frozen)
|
||||
this.freeze();
|
||||
else
|
||||
this.defreeze();
|
||||
}
|
||||
|
||||
EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack-walk",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"targetClasses": targetClasses,
|
||||
"allowCapture": allowCapture,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
freeze()
|
||||
{
|
||||
this.frozen = true;
|
||||
}
|
||||
|
||||
EntityCollection.prototype.moveIndiv = function(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
for (const id of this._entities.keys())
|
||||
defreeze()
|
||||
{
|
||||
this.frozen = false;
|
||||
}
|
||||
|
||||
toIdArray()
|
||||
{
|
||||
return Array.from(this._entities.keys());
|
||||
}
|
||||
|
||||
toEntityArray()
|
||||
{
|
||||
return Array.from(this._entities.values());
|
||||
}
|
||||
|
||||
values()
|
||||
{
|
||||
return this._entities.values();
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
|
||||
}
|
||||
|
||||
filter(filter, thisp)
|
||||
{
|
||||
if (typeof filter === "function")
|
||||
filter = { "func": filter, "dynamicProperties": [] };
|
||||
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
if (filter.func.call(thisp, ent, id, this))
|
||||
ret.set(id, ent);
|
||||
|
||||
return new EntityCollection(this._ai, ret, this._filters.concat([filter]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (at most) n entities nearest to targetPos.
|
||||
*/
|
||||
filterNearest(targetPos, n)
|
||||
{
|
||||
// Compute the distance of each entity
|
||||
const data = []; // [ [id, ent, distance], ... ]
|
||||
for (const [id, ent] of this._entities)
|
||||
if (ent.position())
|
||||
data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]);
|
||||
|
||||
// Sort by increasing distance
|
||||
data.sort((a, b) => a[2] - b[2]);
|
||||
|
||||
if (n === undefined)
|
||||
n = data.length;
|
||||
else
|
||||
n = Math.min(n, data.length);
|
||||
|
||||
// Extract the first n
|
||||
const ret = new Map();
|
||||
for (let i = 0; i < n; ++i)
|
||||
ret.set(data[i][0], data[i][1]);
|
||||
|
||||
return new EntityCollection(this._ai, ret);
|
||||
}
|
||||
|
||||
filter_raw(callback, thisp)
|
||||
{
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
{
|
||||
const val = ent._entity;
|
||||
if (callback.call(thisp, val, id, this))
|
||||
ret.set(id, ent);
|
||||
}
|
||||
return new EntityCollection(this._ai, ret);
|
||||
}
|
||||
|
||||
forEach(callback)
|
||||
{
|
||||
for (const ent of this._entities.values())
|
||||
callback(ent);
|
||||
return this;
|
||||
}
|
||||
|
||||
hasEntities()
|
||||
{
|
||||
return this._entities.size !== 0;
|
||||
}
|
||||
|
||||
move(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk",
|
||||
"entities": [id],
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.garrison = function(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "garrison",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.occupyTurret = function(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "occupy-turret",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.destroy = function()
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() });
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.attack = function(unitId, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack",
|
||||
"entities": this.toIdArray(),
|
||||
"target": unitId,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/** violent, aggressive, defensive, passive, standground */
|
||||
EntityCollection.prototype.setStance = function(stance)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "stance",
|
||||
"entities": this.toIdArray(),
|
||||
"name": stance
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/** Returns the average position of all units */
|
||||
EntityCollection.prototype.getCentrePosition = function()
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let count = 0;
|
||||
for (const ent of this._entities.values())
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
count++;
|
||||
return this;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let i = 0;
|
||||
for (const ent of this._entities.values())
|
||||
moveToRange(x, z, min, max, queued = false, pushFront = false)
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
i++;
|
||||
if (i === sample)
|
||||
break;
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk-to-range",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
return i ? [sumPos[0]/i, sumPos[1]/i] : undefined;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.hasEntId = function(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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (this._entities.has(ent.id()))
|
||||
return false;
|
||||
this._entities.set(ent.id(), ent);
|
||||
const temp = this.toEntityArray();
|
||||
temp.sort((a, b) => a.id() - b.id());
|
||||
this._entities.clear();
|
||||
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)
|
||||
{
|
||||
let passesFilters = true;
|
||||
for (const filter of this._filters)
|
||||
passesFilters = passesFilters && filter.func(ent);
|
||||
|
||||
if (passesFilters)
|
||||
attackMove(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
|
||||
{
|
||||
if (!force && this.frozen)
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack-walk",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"targetClasses": targetClasses,
|
||||
"allowCapture": allowCapture,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
moveIndiv(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
for (const id of this._entities.keys())
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk",
|
||||
"entities": [id],
|
||||
"x": x,
|
||||
"z": z,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
garrison(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "garrison",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
occupyTurret(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "occupy-turret",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
destroy()
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() });
|
||||
return this;
|
||||
}
|
||||
|
||||
attack(unitId, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack",
|
||||
"entities": this.toIdArray(),
|
||||
"target": unitId,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
getCentrePosition()
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let count = 0;
|
||||
for (const ent of this._entities.values())
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
count++;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
getApproximatePosition(sample)
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let i = 0;
|
||||
for (const ent of this._entities.values())
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
i++;
|
||||
if (i === sample)
|
||||
break;
|
||||
}
|
||||
|
||||
return i ? [sumPos[0]/i, sumPos[1]/i] : undefined;
|
||||
}
|
||||
|
||||
hasEntId(id)
|
||||
{
|
||||
return this._entities.has(id);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
return this.addEnt(ent);
|
||||
this._entities.delete(ent.id());
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.removeEnt(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);
|
||||
const temp = this.toEntityArray();
|
||||
temp.sort((a, b) => a.id() - b.id());
|
||||
this._entities.clear();
|
||||
for (const e of temp)
|
||||
this._entities.set(e.id(), e);
|
||||
return true;
|
||||
}
|
||||
|
||||
EntityCollection.prototype.registerUpdates = function()
|
||||
{
|
||||
this._ai.registerUpdatingEntityCollection(this);
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
updateEnt(ent, force)
|
||||
{
|
||||
let passesFilters = true;
|
||||
for (const filter of this._filters)
|
||||
passesFilters = passesFilters && filter.func(ent);
|
||||
|
||||
EntityCollection.prototype.unregister = function()
|
||||
{
|
||||
this._ai.removeUpdatingEntityCollection(this);
|
||||
};
|
||||
if (passesFilters)
|
||||
{
|
||||
if (!force && this.frozen)
|
||||
return false;
|
||||
return this.addEnt(ent);
|
||||
}
|
||||
|
||||
EntityCollection.prototype.dynamicProperties = function()
|
||||
{
|
||||
return this.dynamicProp;
|
||||
};
|
||||
return this.removeEnt(ent);
|
||||
}
|
||||
|
||||
EntityCollection.prototype.setUID = function(id)
|
||||
{
|
||||
this._UID = id;
|
||||
};
|
||||
registerUpdates()
|
||||
{
|
||||
this._ai.registerUpdatingEntityCollection(this);
|
||||
}
|
||||
|
||||
EntityCollection.prototype.getUID = function()
|
||||
{
|
||||
return this._UID;
|
||||
};
|
||||
unregister()
|
||||
{
|
||||
this._ai.removeUpdatingEntityCollection(this);
|
||||
}
|
||||
|
||||
dynamicProperties()
|
||||
{
|
||||
return this.dynamicProp;
|
||||
}
|
||||
|
||||
setUID(id)
|
||||
{
|
||||
this._UID = id;
|
||||
}
|
||||
|
||||
getUID()
|
||||
{
|
||||
return this._UID;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,63 @@
|
||||
Resources = new Resources();
|
||||
|
||||
export function ResourcesManager(amounts = {}, population = 0)
|
||||
export class ResourcesManager
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = amounts[key] || 0;
|
||||
constructor(amounts = {}, population = 0)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = amounts[key] || 0;
|
||||
|
||||
this.population = population > 0 ? population : 0;
|
||||
this.population = population > 0 ? population : 0;
|
||||
}
|
||||
|
||||
reset()
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = 0;
|
||||
this.population = 0;
|
||||
}
|
||||
|
||||
canAfford(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
if (this[key] < that[key])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
add(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] += that[key];
|
||||
this.population += that.population;
|
||||
}
|
||||
|
||||
subtract(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] -= that[key];
|
||||
this.population += that.population;
|
||||
}
|
||||
|
||||
multiply(n)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] *= n;
|
||||
this.population *= n;
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
const amounts = {};
|
||||
for (const key of Resources.GetCodes())
|
||||
amounts[key] = this[key];
|
||||
return { "amounts": amounts, "population": this.population };
|
||||
}
|
||||
|
||||
Deserialize(data)
|
||||
{
|
||||
for (const key in data.amounts)
|
||||
this[key] = data.amounts[key];
|
||||
this.population = data.population;
|
||||
}
|
||||
}
|
||||
|
||||
ResourcesManager.prototype.reset = function()
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = 0;
|
||||
this.population = 0;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.canAfford = function(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
if (this[key] < that[key])
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.add = function(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] += that[key];
|
||||
this.population += that.population;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.subtract = function(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] -= that[key];
|
||||
this.population += that.population;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.multiply = function(n)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] *= n;
|
||||
this.population *= n;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.Serialize = function()
|
||||
{
|
||||
const amounts = {};
|
||||
for (const key of Resources.GetCodes())
|
||||
amounts[key] = this[key];
|
||||
return { "amounts": amounts, "population": this.population };
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.Deserialize = function(data)
|
||||
{
|
||||
for (const key in data.amounts)
|
||||
this[key] = data.amounts[key];
|
||||
this.population = data.population;
|
||||
};
|
||||
|
||||
@@ -5,448 +5,451 @@ 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
|
||||
{
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players)
|
||||
this._templates = settings.templates;
|
||||
|
||||
this._entityMetadata = {};
|
||||
for (const player of this._players)
|
||||
this._entityMetadata[player] = {};
|
||||
|
||||
// array of entity collections
|
||||
this._entityCollections = new Map();
|
||||
this._entitiesModifications = new Map(); // entities modifications
|
||||
this._templatesModifications = {}; // template modifications
|
||||
// each name is a reference to the actual one.
|
||||
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 {
|
||||
"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)
|
||||
{
|
||||
this._players = data.players;
|
||||
this._templatesModifications = data.templatesModifications;
|
||||
this._entitiesModifications = data.entitiesModifications;
|
||||
this._entityMetadata = data.metadata;
|
||||
|
||||
this.isDeserialized = true;
|
||||
};
|
||||
|
||||
SharedScript.prototype.GetTemplate = function(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)
|
||||
{
|
||||
if (!deserialization)
|
||||
this._entitiesModifications = new Map();
|
||||
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.circularMap = state.circularMap;
|
||||
this.mapSize = state.mapSize;
|
||||
this.victoryConditions = new Set(state.victoryConditions);
|
||||
this.alliedVictory = state.alliedVictory;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
if (this.mapSize % this.passabilityMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width);
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
if (this.mapSize % this.territoryMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width);
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
/*
|
||||
let landPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let waterPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let obstructionMaskLand = this.passabilityClasses["default-terrain-only"];
|
||||
let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"];
|
||||
for (let i = 0; i < this.passabilityMap.data.length; ++i)
|
||||
constructor(settings)
|
||||
{
|
||||
landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
|
||||
waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
|
||||
}
|
||||
Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
|
||||
Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
|
||||
*/
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
this._entities = new Map();
|
||||
if (state.entities)
|
||||
for (const id in state.entities)
|
||||
this._entities.set(+id, new Entity(this, state.entities[id]));
|
||||
// entity collection updated on create/destroy event.
|
||||
this.entities = new EntityCollection(this, this._entities);
|
||||
this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players)
|
||||
this._templates = settings.templates;
|
||||
|
||||
// create the terrain analyzer
|
||||
this.terrainAnalyzer = new TerrainAnalysis(this, state);
|
||||
this.accessibility = new Accessibility(state, this.terrainAnalyzer);
|
||||
this._entityMetadata = {};
|
||||
for (const player of this._players)
|
||||
this._entityMetadata[player] = {};
|
||||
|
||||
// Resource types: ignore = not used for resource maps
|
||||
// abundant = abundant resource with small amount each
|
||||
// sparse = sparse resource, but huge amount each
|
||||
// The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites)
|
||||
// They are updated by checking for create and destroy events for all resources
|
||||
this.normalizationFactor = { "abundant": 50, "sparse": 90 };
|
||||
this.influenceRadius = { "abundant": 36, "sparse": 48 };
|
||||
this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 };
|
||||
|
||||
this.resources = []; // Contains entityIds of all resources in the maps.
|
||||
this.resourceMaps = {}; // Contains maps showing the density of resources
|
||||
this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement.
|
||||
this.createResourceMaps();
|
||||
|
||||
this.gameState = {};
|
||||
for (const player of this._players)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
{
|
||||
this.init(state, true);
|
||||
this.isDeserialized = false;
|
||||
// array of entity collections
|
||||
this._entityCollections = new Map();
|
||||
this._entitiesModifications = new Map(); // entities modifications
|
||||
this._templatesModifications = {}; // template modifications
|
||||
// each name is a reference to the actual one.
|
||||
this._entityCollectionsName = new Map();
|
||||
this._entityCollectionsByDynProp = {};
|
||||
this._entityCollectionsUID = 0;
|
||||
}
|
||||
|
||||
// deals with updating based on create and destroy messages.
|
||||
this.ApplyEntitiesDelta(state);
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
Engine.ProfileStart("onUpdate");
|
||||
|
||||
// those are dynamic and need to be reset as the "state" object moves in memory.
|
||||
this.events = state.events;
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.barterPrices = state.barterPrices;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
for (const i in this.gameState)
|
||||
this.gameState[i].update(this);
|
||||
|
||||
// TODO: merge this with "ApplyEntitiesDelta" since after all they do the same.
|
||||
this.updateResourceMaps(this.events);
|
||||
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
||||
SharedScript.prototype.ApplyEntitiesDelta = function(state)
|
||||
{
|
||||
Engine.ProfileStart("Shared ApplyEntitiesDelta");
|
||||
|
||||
const foundationFinished = {};
|
||||
|
||||
// by order of updating:
|
||||
// we "Destroy" last because we want to be able to switch Metadata first.
|
||||
|
||||
for (const evt of state.events.Create)
|
||||
/** Return a simple object (using no classes etc) that will be serialized into saved games */
|
||||
Serialize()
|
||||
{
|
||||
if (!state.entities[evt.entity])
|
||||
continue; // Sometimes there are things like foundations which get destroyed too fast
|
||||
|
||||
const entity = new Entity(this, state.entities[evt.entity]);
|
||||
this._entities.set(evt.entity, entity);
|
||||
this.entities.addEnt(entity);
|
||||
|
||||
// Update all the entity collections since the create operation affects static properties as well as dynamic
|
||||
for (const entCol of this._entityCollections.values())
|
||||
entCol.updateEnt(entity);
|
||||
return {
|
||||
"players": this._players,
|
||||
"templatesModifications": this._templatesModifications,
|
||||
"entitiesModifications": this._entitiesModifications,
|
||||
"metadata": this._entityMetadata
|
||||
};
|
||||
}
|
||||
|
||||
for (const evt of state.events.EntityRenamed)
|
||||
{ // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly
|
||||
/**
|
||||
* Called after the constructor when loading a saved game, with 'data' being
|
||||
* whatever Serialize() returned
|
||||
*/
|
||||
Deserialize(data)
|
||||
{
|
||||
this._players = data.players;
|
||||
this._templatesModifications = data.templatesModifications;
|
||||
this._entitiesModifications = data.entitiesModifications;
|
||||
this._entityMetadata = data.metadata;
|
||||
|
||||
this.isDeserialized = true;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
init(state, deserialization)
|
||||
{
|
||||
if (!deserialization)
|
||||
this._entitiesModifications = new Map();
|
||||
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.circularMap = state.circularMap;
|
||||
this.mapSize = state.mapSize;
|
||||
this.victoryConditions = new Set(state.victoryConditions);
|
||||
this.alliedVictory = state.alliedVictory;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
if (this.mapSize % this.passabilityMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width);
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
if (this.mapSize % this.territoryMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width);
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
/*
|
||||
let landPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let waterPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let obstructionMaskLand = this.passabilityClasses["default-terrain-only"];
|
||||
let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"];
|
||||
for (let i = 0; i < this.passabilityMap.data.length; ++i)
|
||||
{
|
||||
landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
|
||||
waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
|
||||
}
|
||||
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)
|
||||
for (const id in state.entities)
|
||||
this._entities.set(+id, new Entity(this, state.entities[id]));
|
||||
// entity collection updated on create/destroy event.
|
||||
this.entities = new EntityCollection(this, this._entities);
|
||||
|
||||
// create the terrain analyzer
|
||||
this.terrainAnalyzer = new TerrainAnalysis(this, state);
|
||||
this.accessibility = new Accessibility(state, this.terrainAnalyzer);
|
||||
|
||||
// Resource types: ignore = not used for resource maps
|
||||
// abundant = abundant resource with small amount each
|
||||
// sparse = sparse resource, but huge amount each
|
||||
// The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites)
|
||||
// They are updated by checking for create and destroy events for all resources
|
||||
this.normalizationFactor = { "abundant": 50, "sparse": 90 };
|
||||
this.influenceRadius = { "abundant": 36, "sparse": 48 };
|
||||
this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 };
|
||||
|
||||
this.resources = []; // Contains entityIds of all resources in the maps.
|
||||
this.resourceMaps = {}; // Contains maps showing the density of resources
|
||||
this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement.
|
||||
this.createResourceMaps();
|
||||
|
||||
this.gameState = {};
|
||||
for (const player of this._players)
|
||||
{
|
||||
this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity];
|
||||
this._entityMetadata[player][evt.entity] = {};
|
||||
this.gameState[player] = new GameState();
|
||||
this.gameState[player].init(this, state, player);
|
||||
}
|
||||
}
|
||||
|
||||
for (const evt of state.events.TrainingFinished)
|
||||
{ // Apply metadata stored in training queues
|
||||
for (const entId of evt.entities)
|
||||
if (this._entities.has(entId))
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
for (const evt of state.events.ConstructionFinished)
|
||||
/**
|
||||
* General update of the shared script, before each AI's update
|
||||
* applies entity deltas, and each gamestate.
|
||||
*/
|
||||
onUpdate(state)
|
||||
{
|
||||
// metada are already moved by EntityRenamed when needed (i.e. construction, not repair)
|
||||
if (evt.entity != evt.newentity)
|
||||
foundationFinished[evt.entity] = true;
|
||||
}
|
||||
|
||||
for (const evt of state.events.AIMetadata)
|
||||
{
|
||||
if (!this._entities.has(evt.id))
|
||||
continue; // might happen in some rare cases of foundations getting destroyed, perhaps.
|
||||
// Apply metadata (here for buildings for example)
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
for (const evt of state.events.Destroy)
|
||||
{
|
||||
if (foundationFinished[evt.entity])
|
||||
evt.SuccessfulFoundation = true;
|
||||
|
||||
// The entity was destroyed but its data may still be useful, so
|
||||
// remember the AI's metadata concerning it
|
||||
evt.metadata = {};
|
||||
for (const player of this._players)
|
||||
evt.metadata[player] = this._entityMetadata[player][evt.entity];
|
||||
|
||||
const entity = this._entities.get(evt.entity);
|
||||
if (entity)
|
||||
if (this.isDeserialized)
|
||||
{
|
||||
this.init(state, true);
|
||||
this.isDeserialized = false;
|
||||
}
|
||||
|
||||
// deals with updating based on create and destroy messages.
|
||||
this.ApplyEntitiesDelta(state);
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
Engine.ProfileStart("onUpdate");
|
||||
|
||||
// those are dynamic and need to be reset as the "state" object moves in memory.
|
||||
this.events = state.events;
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.barterPrices = state.barterPrices;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
for (const i in this.gameState)
|
||||
this.gameState[i].update(this);
|
||||
|
||||
// TODO: merge this with "ApplyEntitiesDelta" since after all they do the same.
|
||||
this.updateResourceMaps(this.events);
|
||||
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
|
||||
ApplyEntitiesDelta(state)
|
||||
{
|
||||
Engine.ProfileStart("Shared ApplyEntitiesDelta");
|
||||
|
||||
const foundationFinished = {};
|
||||
|
||||
// by order of updating:
|
||||
// we "Destroy" last because we want to be able to switch Metadata first.
|
||||
|
||||
for (const evt of state.events.Create)
|
||||
{
|
||||
if (!state.entities[evt.entity])
|
||||
continue; // Sometimes there are things like foundations which get destroyed too fast
|
||||
|
||||
const entity = new Entity(this, state.entities[evt.entity]);
|
||||
this._entities.set(evt.entity, entity);
|
||||
this.entities.addEnt(entity);
|
||||
|
||||
// Update all the entity collections since the create operation affects static properties as well as dynamic
|
||||
for (const entCol of this._entityCollections.values())
|
||||
entCol.removeEnt(entity);
|
||||
this.entities.removeEnt(entity);
|
||||
entCol.updateEnt(entity);
|
||||
}
|
||||
|
||||
this._entities.delete(evt.entity);
|
||||
this._entitiesModifications.delete(evt.entity);
|
||||
for (const player of this._players)
|
||||
delete this._entityMetadata[player][evt.entity];
|
||||
}
|
||||
|
||||
for (const id in state.entities)
|
||||
{
|
||||
const changes = state.entities[id];
|
||||
const entity = this._entities.get(+id);
|
||||
for (const prop in changes)
|
||||
{
|
||||
entity._entity[prop] = changes[prop];
|
||||
this.updateEntityCollections(prop, entity);
|
||||
for (const evt of state.events.EntityRenamed)
|
||||
{ // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly
|
||||
for (const player of this._players)
|
||||
{
|
||||
this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity];
|
||||
this._entityMetadata[player][evt.entity] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply per-entity aura-related changes.
|
||||
// this supersedes tech-related changes.
|
||||
for (const id in state.changedEntityTemplateInfo)
|
||||
{
|
||||
if (!this._entities.has(+id))
|
||||
continue; // dead, presumably.
|
||||
const changes = state.changedEntityTemplateInfo[id];
|
||||
if (!this._entitiesModifications.has(+id))
|
||||
this._entitiesModifications.set(+id, new Map());
|
||||
const modif = this._entitiesModifications.get(+id);
|
||||
for (const change of changes)
|
||||
modif.set(change.variable, change.value);
|
||||
}
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
for (const evt of state.events.TrainingFinished)
|
||||
{ // Apply metadata stored in training queues
|
||||
for (const entId of evt.entities)
|
||||
if (this._entities.has(entId))
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
SharedScript.prototype.ApplyTemplatesDelta = function(state)
|
||||
{
|
||||
Engine.ProfileStart("Shared ApplyTemplatesDelta");
|
||||
|
||||
for (const player in state.changedTemplateInfo)
|
||||
{
|
||||
const playerDiff = state.changedTemplateInfo[player];
|
||||
for (const template in playerDiff)
|
||||
for (const evt of state.events.ConstructionFinished)
|
||||
{
|
||||
const changes = playerDiff[template];
|
||||
if (!this._templatesModifications[template])
|
||||
this._templatesModifications[template] = {};
|
||||
if (!this._templatesModifications[template][player])
|
||||
this._templatesModifications[template][player] = new Map();
|
||||
const modif = this._templatesModifications[template][player];
|
||||
// metada are already moved by EntityRenamed when needed (i.e. construction, not repair)
|
||||
if (evt.entity != evt.newentity)
|
||||
foundationFinished[evt.entity] = true;
|
||||
}
|
||||
|
||||
for (const evt of state.events.AIMetadata)
|
||||
{
|
||||
if (!this._entities.has(evt.id))
|
||||
continue; // might happen in some rare cases of foundations getting destroyed, perhaps.
|
||||
// Apply metadata (here for buildings for example)
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
for (const evt of state.events.Destroy)
|
||||
{
|
||||
if (foundationFinished[evt.entity])
|
||||
evt.SuccessfulFoundation = true;
|
||||
|
||||
// The entity was destroyed but its data may still be useful, so
|
||||
// remember the AI's metadata concerning it
|
||||
evt.metadata = {};
|
||||
for (const player of this._players)
|
||||
evt.metadata[player] = this._entityMetadata[player][evt.entity];
|
||||
|
||||
const entity = this._entities.get(evt.entity);
|
||||
if (entity)
|
||||
{
|
||||
for (const entCol of this._entityCollections.values())
|
||||
entCol.removeEnt(entity);
|
||||
this.entities.removeEnt(entity);
|
||||
}
|
||||
|
||||
this._entities.delete(evt.entity);
|
||||
this._entitiesModifications.delete(evt.entity);
|
||||
for (const player of this._players)
|
||||
delete this._entityMetadata[player][evt.entity];
|
||||
}
|
||||
|
||||
for (const id in state.entities)
|
||||
{
|
||||
const changes = state.entities[id];
|
||||
const entity = this._entities.get(+id);
|
||||
for (const prop in changes)
|
||||
{
|
||||
entity._entity[prop] = changes[prop];
|
||||
this.updateEntityCollections(prop, entity);
|
||||
}
|
||||
}
|
||||
|
||||
// apply per-entity aura-related changes.
|
||||
// this supersedes tech-related changes.
|
||||
for (const id in state.changedEntityTemplateInfo)
|
||||
{
|
||||
if (!this._entities.has(+id))
|
||||
continue; // dead, presumably.
|
||||
const changes = state.changedEntityTemplateInfo[id];
|
||||
if (!this._entitiesModifications.has(+id))
|
||||
this._entitiesModifications.set(+id, new Map());
|
||||
const modif = this._entitiesModifications.get(+id);
|
||||
for (const change of changes)
|
||||
modif.set(change.variable, change.value);
|
||||
}
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
this._templatesModifications =
|
||||
Object.fromEntries(Object.entries(this._templatesModifications).sort());
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
||||
SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection)
|
||||
{
|
||||
entCollection.setUID(this._entityCollectionsUID);
|
||||
this._entityCollections.set(this._entityCollectionsUID, entCollection);
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
ApplyTemplatesDelta(state)
|
||||
{
|
||||
if (!this._entityCollectionsByDynProp[prop])
|
||||
this._entityCollectionsByDynProp[prop] = new Map();
|
||||
this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection);
|
||||
Engine.ProfileStart("Shared ApplyTemplatesDelta");
|
||||
|
||||
for (const player in state.changedTemplateInfo)
|
||||
{
|
||||
const playerDiff = state.changedTemplateInfo[player];
|
||||
for (const template in playerDiff)
|
||||
{
|
||||
const changes = playerDiff[template];
|
||||
if (!this._templatesModifications[template])
|
||||
this._templatesModifications[template] = {};
|
||||
if (!this._templatesModifications[template][player])
|
||||
this._templatesModifications[template][player] = new Map();
|
||||
const modif = this._templatesModifications[template][player];
|
||||
for (const change of changes)
|
||||
modif.set(change.variable, change.value);
|
||||
}
|
||||
}
|
||||
this._templatesModifications =
|
||||
Object.fromEntries(Object.entries(this._templatesModifications).sort());
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
this._entityCollectionsUID++;
|
||||
};
|
||||
|
||||
SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
|
||||
{
|
||||
const uid = entCollection.getUID();
|
||||
|
||||
if (this._entityCollections.has(uid))
|
||||
this._entityCollections.delete(uid);
|
||||
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
if (this._entityCollectionsByDynProp[prop].has(uid))
|
||||
this._entityCollectionsByDynProp[prop].delete(uid);
|
||||
};
|
||||
|
||||
SharedScript.prototype.updateEntityCollections = function(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)
|
||||
{
|
||||
let metadata = this._entityMetadata[player][ent.id()];
|
||||
if (!metadata)
|
||||
registerUpdatingEntityCollection(entCollection)
|
||||
{
|
||||
this._entityMetadata[player][ent.id()] = {};
|
||||
metadata = this._entityMetadata[player][ent.id()];
|
||||
entCollection.setUID(this._entityCollectionsUID);
|
||||
this._entityCollections.set(this._entityCollectionsUID, entCollection);
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
{
|
||||
if (!this._entityCollectionsByDynProp[prop])
|
||||
this._entityCollectionsByDynProp[prop] = new Map();
|
||||
this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection);
|
||||
}
|
||||
this._entityCollectionsUID++;
|
||||
}
|
||||
metadata[key] = value;
|
||||
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
};
|
||||
removeUpdatingEntityCollection(entCollection)
|
||||
{
|
||||
const uid = entCollection.getUID();
|
||||
|
||||
SharedScript.prototype.getMetadata = function(player, ent, key)
|
||||
{
|
||||
return this._entityMetadata[player][ent.id()]?.[key];
|
||||
};
|
||||
if (this._entityCollections.has(uid))
|
||||
this._entityCollections.delete(uid);
|
||||
|
||||
SharedScript.prototype.deleteMetadata = function(player, ent, key)
|
||||
{
|
||||
const metadata = this._entityMetadata[player][ent.id()];
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
if (this._entityCollectionsByDynProp[prop].has(uid))
|
||||
this._entityCollectionsByDynProp[prop].delete(uid);
|
||||
}
|
||||
|
||||
if (!metadata || !(key in metadata))
|
||||
updateEntityCollections(property, ent)
|
||||
{
|
||||
if (this._entityCollectionsByDynProp[property] === undefined)
|
||||
return;
|
||||
|
||||
for (const entCol of this._entityCollectionsByDynProp[property].values())
|
||||
entCol.updateEnt(ent);
|
||||
}
|
||||
|
||||
setMetadata(player, ent, key, value)
|
||||
{
|
||||
let metadata = this._entityMetadata[player][ent.id()];
|
||||
if (!metadata)
|
||||
{
|
||||
this._entityMetadata[player][ent.id()] = {};
|
||||
metadata = this._entityMetadata[player][ent.id()];
|
||||
}
|
||||
metadata[key] = value;
|
||||
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
}
|
||||
|
||||
getMetadata(player, ent, key)
|
||||
{
|
||||
return this._entityMetadata[player][ent.id()]?.[key];
|
||||
}
|
||||
|
||||
deleteMetadata(player, ent, key)
|
||||
{
|
||||
const metadata = this._entityMetadata[player][ent.id()];
|
||||
|
||||
if (!metadata || !(key in metadata))
|
||||
return true;
|
||||
metadata[key] = undefined;
|
||||
delete metadata[key];
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
return true;
|
||||
metadata[key] = undefined;
|
||||
delete metadata[key];
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
return true;
|
||||
};
|
||||
|
||||
/** creates a map of resource density */
|
||||
SharedScript.prototype.createResourceMaps = function()
|
||||
{
|
||||
for (const resource of Resources.GetCodes())
|
||||
{
|
||||
if (this.resourceMaps[resource] ||
|
||||
!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
|
||||
continue;
|
||||
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
|
||||
// But at that point the precision is not really important anyway. And it saves memory.
|
||||
this.resourceMaps[resource] = new InfoMap(this, "resource");
|
||||
this.ccResourceMaps[resource] = new InfoMap(this, "resource");
|
||||
}
|
||||
for (const ent of this._entities.values())
|
||||
this.addEntityToResourceMap(ent);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} events - The events from a turn.
|
||||
*/
|
||||
SharedScript.prototype.updateResourceMaps = function(events)
|
||||
{
|
||||
if (events.Destroy.some(e => this.resources.includes(e.entity)))
|
||||
{
|
||||
this.resources = [];
|
||||
this.resourceMaps = {};
|
||||
this.createResourceMaps();
|
||||
}
|
||||
|
||||
for (const e of events.Create)
|
||||
if (e.entity && this._entities.has(e.entity))
|
||||
this.addEntityToResourceMap(this._entities.get(e.entity));
|
||||
};
|
||||
/** creates a map of resource density */
|
||||
createResourceMaps()
|
||||
{
|
||||
for (const resource of Resources.GetCodes())
|
||||
{
|
||||
if (this.resourceMaps[resource] ||
|
||||
!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
|
||||
continue;
|
||||
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
|
||||
// But at that point the precision is not really important anyway. And it saves memory.
|
||||
this.resourceMaps[resource] = new InfoMap(this, "resource");
|
||||
this.ccResourceMaps[resource] = new InfoMap(this, "resource");
|
||||
}
|
||||
for (const ent of this._entities.values())
|
||||
this.addEntityToResourceMap(ent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} entity - The entity to add to the resource map.
|
||||
*/
|
||||
SharedScript.prototype.addEntityToResourceMap = function(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, 1);
|
||||
this.resources.push(entity.id());
|
||||
};
|
||||
/**
|
||||
* @param {Object} events - The events from a turn.
|
||||
*/
|
||||
updateResourceMaps(events)
|
||||
{
|
||||
if (events.Destroy.some(e => this.resources.includes(e.entity)))
|
||||
{
|
||||
this.resources = [];
|
||||
this.resourceMaps = {};
|
||||
this.createResourceMaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} entity - The entity to remove from the resource map.
|
||||
*/
|
||||
SharedScript.prototype.removeEntityFromResourceMap = function(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, -1);
|
||||
};
|
||||
for (const e of events.Create)
|
||||
if (e.entity && this._entities.has(e.entity))
|
||||
this.addEntityToResourceMap(this._entities.get(e.entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} ent - The entity to add to the resource map.
|
||||
*/
|
||||
SharedScript.prototype.changeEntityInResourceMapHelper = function(ent, multiplication = 1)
|
||||
{
|
||||
if (!ent)
|
||||
return;
|
||||
const entPos = ent.position();
|
||||
if (!entPos)
|
||||
return;
|
||||
const resource = ent.resourceSupplyType()?.generic;
|
||||
if (!resource || !this.resourceMaps[resource])
|
||||
return;
|
||||
const cellSize = this.resourceMaps[resource].cellSize;
|
||||
const x = Math.floor(entPos[0] / cellSize);
|
||||
const y = Math.floor(entPos[1] / cellSize);
|
||||
const grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
|
||||
const strength = multiplication * ent.resourceSupplyMax() / this.normalizationFactor[grp];
|
||||
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");
|
||||
};
|
||||
/**
|
||||
* @param {entity} entity - The entity to add to the resource map.
|
||||
*/
|
||||
addEntityToResourceMap(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, 1);
|
||||
this.resources.push(entity.id());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} entity - The entity to remove from the resource map.
|
||||
*/
|
||||
removeEntityFromResourceMap(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} ent - The entity to add to the resource map.
|
||||
*/
|
||||
changeEntityInResourceMapHelper(ent, multiplication = 1)
|
||||
{
|
||||
if (!ent)
|
||||
return;
|
||||
const entPos = ent.position();
|
||||
if (!entPos)
|
||||
return;
|
||||
const resource = ent.resourceSupplyType()?.generic;
|
||||
if (!resource || !this.resourceMaps[resource])
|
||||
return;
|
||||
const cellSize = this.resourceMaps[resource].cellSize;
|
||||
const x = Math.floor(entPos[0] / cellSize);
|
||||
const y = Math.floor(entPos[1] / cellSize);
|
||||
const grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
|
||||
const strength = multiplication * ent.resourceSupplyMax() / this.normalizationFactor[grp];
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +1,127 @@
|
||||
LoadModificationTemplates();
|
||||
|
||||
/** Wrapper around a technology template */
|
||||
export function Technology(templateName)
|
||||
export class Technology
|
||||
{
|
||||
this._templateName = templateName;
|
||||
const template = TechnologyTemplates.Get(templateName);
|
||||
|
||||
// check if this is one of two paired technologies.
|
||||
if (template.partOfPair)
|
||||
constructor(templateName)
|
||||
{
|
||||
const parentTech = TechnologyTemplates.Get(template.partOfPair);
|
||||
this._pairedWith = parentTech.pair[0] == templateName ? parentTech.pair[1] : parentTech.pair[0];
|
||||
this._templateName = templateName;
|
||||
const template = TechnologyTemplates.Get(templateName);
|
||||
|
||||
// check if this is one of two paired technologies.
|
||||
if (template.partOfPair)
|
||||
{
|
||||
const parentTech = TechnologyTemplates.Get(template.partOfPair);
|
||||
this._pairedWith = parentTech.pair[0] == templateName ? parentTech.pair[1] : parentTech.pair[0];
|
||||
}
|
||||
|
||||
// check if it only defines a pair:
|
||||
this._definesPair = !!template.pair;
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
// check if it only defines a pair:
|
||||
this._definesPair = !!template.pair;
|
||||
this._template = template;
|
||||
/** 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];
|
||||
}
|
||||
|
||||
pairDef()
|
||||
{
|
||||
return this._definesPair;
|
||||
}
|
||||
|
||||
/** 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));
|
||||
}
|
||||
|
||||
pair()
|
||||
{
|
||||
return this._template.partOfPair;
|
||||
}
|
||||
|
||||
pairedWith()
|
||||
{
|
||||
if (!this._template.partOfPair)
|
||||
return undefined;
|
||||
return this._pairedWith;
|
||||
}
|
||||
|
||||
cost(researcher)
|
||||
{
|
||||
if (!this._template.cost)
|
||||
return undefined;
|
||||
const cost = {};
|
||||
for (const type in this._template.cost)
|
||||
{
|
||||
cost[type] = +this._template.cost[type];
|
||||
if (researcher)
|
||||
cost[type] *= researcher.techCostMultiplier(type);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
costSum(researcher)
|
||||
{
|
||||
const cost = this.cost(researcher);
|
||||
if (!cost)
|
||||
return 0;
|
||||
let ret = 0;
|
||||
for (const type in cost)
|
||||
ret += cost[type];
|
||||
return ret;
|
||||
}
|
||||
|
||||
researchTime()
|
||||
{
|
||||
return this._template.researchTime || 0;
|
||||
}
|
||||
|
||||
requirements(civ)
|
||||
{
|
||||
return DeriveTechnologyRequirements(this._template, civ);
|
||||
}
|
||||
|
||||
autoResearch()
|
||||
{
|
||||
if (!this._template.autoResearch)
|
||||
return undefined;
|
||||
return this._template.autoResearch;
|
||||
}
|
||||
|
||||
supersedes()
|
||||
{
|
||||
if (!this._template.supersedes)
|
||||
return undefined;
|
||||
return this._template.supersedes;
|
||||
}
|
||||
|
||||
modifications()
|
||||
{
|
||||
if (!this._template.modifications)
|
||||
return undefined;
|
||||
return this._template.modifications;
|
||||
}
|
||||
|
||||
affects()
|
||||
{
|
||||
if (!this._template.affects)
|
||||
return undefined;
|
||||
return this._template.affects;
|
||||
}
|
||||
|
||||
isAffected(classes)
|
||||
{
|
||||
return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect));
|
||||
}
|
||||
}
|
||||
|
||||
/** returns generic, or specific if civ provided. */
|
||||
Technology.prototype.name = function(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()
|
||||
{
|
||||
return this._definesPair;
|
||||
};
|
||||
|
||||
/** in case this defines a pair only, returns the two paired technologies. */
|
||||
Technology.prototype.getPairedTechs = function()
|
||||
{
|
||||
if (!this._definesPair)
|
||||
return undefined;
|
||||
|
||||
return this._template.pair.map(name => new Technology(name));
|
||||
};
|
||||
|
||||
Technology.prototype.pair = function()
|
||||
{
|
||||
return this._template.partOfPair;
|
||||
};
|
||||
|
||||
Technology.prototype.pairedWith = function()
|
||||
{
|
||||
if (!this._template.partOfPair)
|
||||
return undefined;
|
||||
return this._pairedWith;
|
||||
};
|
||||
|
||||
Technology.prototype.cost = function(researcher)
|
||||
{
|
||||
if (!this._template.cost)
|
||||
return undefined;
|
||||
const cost = {};
|
||||
for (const type in this._template.cost)
|
||||
{
|
||||
cost[type] = +this._template.cost[type];
|
||||
if (researcher)
|
||||
cost[type] *= researcher.techCostMultiplier(type);
|
||||
}
|
||||
return cost;
|
||||
};
|
||||
|
||||
Technology.prototype.costSum = function(researcher)
|
||||
{
|
||||
const cost = this.cost(researcher);
|
||||
if (!cost)
|
||||
return 0;
|
||||
let ret = 0;
|
||||
for (const type in cost)
|
||||
ret += cost[type];
|
||||
return ret;
|
||||
};
|
||||
|
||||
Technology.prototype.researchTime = function()
|
||||
{
|
||||
return this._template.researchTime || 0;
|
||||
};
|
||||
|
||||
Technology.prototype.requirements = function(civ)
|
||||
{
|
||||
return DeriveTechnologyRequirements(this._template, civ);
|
||||
};
|
||||
|
||||
Technology.prototype.autoResearch = function()
|
||||
{
|
||||
if (!this._template.autoResearch)
|
||||
return undefined;
|
||||
return this._template.autoResearch;
|
||||
};
|
||||
|
||||
Technology.prototype.supersedes = function()
|
||||
{
|
||||
if (!this._template.supersedes)
|
||||
return undefined;
|
||||
return this._template.supersedes;
|
||||
};
|
||||
|
||||
Technology.prototype.modifications = function()
|
||||
{
|
||||
if (!this._template.modifications)
|
||||
return undefined;
|
||||
return this._template.modifications;
|
||||
};
|
||||
|
||||
Technology.prototype.affects = function()
|
||||
{
|
||||
if (!this._template.affects)
|
||||
return undefined;
|
||||
return this._template.affects;
|
||||
};
|
||||
|
||||
Technology.prototype.isAffected = function(classes)
|
||||
{
|
||||
return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect));
|
||||
};
|
||||
|
||||
@@ -6,171 +6,173 @@ 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);
|
||||
|
||||
// played turn, because Petra doesn't play every turn.
|
||||
this.turn = 0;
|
||||
this.playedTurn = 0;
|
||||
this.elapsedTime = 0;
|
||||
|
||||
this.uniqueIDs = {
|
||||
"armies": 1, // starts at 1 to allow easier tests on armies ID existence
|
||||
"bases": 1, // base manager ID starts at one because "0" means "no base" on the map
|
||||
"plans": 0, // training/building/research plans
|
||||
"transports": 1 // transport plans start at 1 because 0 might be used as none
|
||||
};
|
||||
|
||||
this.Config = new Config(settings.difficulty, settings.behavior);
|
||||
|
||||
this.savedEvents = {};
|
||||
}
|
||||
|
||||
PetraBot.prototype = Object.create(BaseAI.prototype);
|
||||
|
||||
PetraBot.prototype.CustomInit = function(gameState)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
constructor(settings)
|
||||
{
|
||||
// WARNING: the deserializations should not modify the metadatas infos inside their init functions
|
||||
this.canPlay = this.data.canPlay;
|
||||
this.turn = this.data.turn;
|
||||
this.playedTurn = this.data.playedTurn;
|
||||
this.elapsedTime = this.data.elapsedTime;
|
||||
this.savedEvents = this.data.savedEvents;
|
||||
super(settings);
|
||||
|
||||
// played turn, because Petra doesn't play every turn.
|
||||
this.turn = 0;
|
||||
this.playedTurn = 0;
|
||||
this.elapsedTime = 0;
|
||||
|
||||
this.uniqueIDs = {
|
||||
"armies": 1, // starts at 1 to allow easier tests on armies ID existence
|
||||
"bases": 1, // base manager ID starts at one because "0" means "no base" on the map
|
||||
"plans": 0, // training/building/research plans
|
||||
"transports": 1 // transport plans start at 1 because 0 might be used as none
|
||||
};
|
||||
|
||||
this.Config = new Config(settings.difficulty, settings.behavior);
|
||||
|
||||
this.savedEvents = {};
|
||||
}
|
||||
|
||||
CustomInit(gameState)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
{
|
||||
// WARNING: the deserializations should not modify the metadatas infos inside their init functions
|
||||
this.canPlay = this.data.canPlay;
|
||||
this.turn = this.data.turn;
|
||||
this.playedTurn = this.data.playedTurn;
|
||||
this.elapsedTime = this.data.elapsedTime;
|
||||
this.savedEvents = this.data.savedEvents;
|
||||
for (const key in this.savedEvents)
|
||||
{
|
||||
for (const i in this.savedEvents[key])
|
||||
{
|
||||
const evt = this.savedEvents[key][i];
|
||||
const evtmod = {};
|
||||
for (const keyevt in evt)
|
||||
{
|
||||
evtmod[keyevt] = evt[keyevt];
|
||||
this.savedEvents[key][i] = evtmod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.Config.Deserialize(this.data.config);
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, {});
|
||||
this.queueManager.Deserialize(gameState, this.data.queueManager);
|
||||
this.queues = this.queueManager.queues;
|
||||
|
||||
this.HQ = new Headquarters(this.Config, true);
|
||||
this.HQ.init(gameState, this.queues);
|
||||
this.HQ.Deserialize(gameState, this.data.HQ);
|
||||
|
||||
this.uniqueIDs = this.data.uniqueIDs;
|
||||
this.isDeserialized = false;
|
||||
this.data = undefined;
|
||||
|
||||
// initialisation needed after the completion of the deserialization
|
||||
this.HQ.postinit(gameState);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Config.setConfig(gameState);
|
||||
|
||||
// this.queues can only be modified by the queue manager or things will go awry.
|
||||
this.queues = {};
|
||||
for (const i in this.Config.priorities)
|
||||
this.queues[i] = new Queue();
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, this.queues);
|
||||
|
||||
this.HQ = new Headquarters(this.Config, false);
|
||||
|
||||
this.HQ.init(gameState, this.queues);
|
||||
|
||||
// Try to analyze our starting position and set a strategy.
|
||||
this.canPlay = gameAnalysis(this.HQ, gameState);
|
||||
}
|
||||
}
|
||||
|
||||
OnUpdate(sharedScript)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
this.Init(PlayerID, sharedScript);
|
||||
|
||||
if (this.gameFinished || this.gameState.playerData.state == "defeated")
|
||||
return;
|
||||
|
||||
for (const i in sharedScript.events)
|
||||
{
|
||||
if (i == "AIMetadata") // not used inside petra
|
||||
continue;
|
||||
if (this.savedEvents[i] !== undefined)
|
||||
this.savedEvents[i] = this.savedEvents[i].concat(sharedScript.events[i]);
|
||||
else
|
||||
this.savedEvents[i] = sharedScript.events[i];
|
||||
}
|
||||
|
||||
// Run the update every n turns, offset depending on player ID to balance the load
|
||||
this.elapsedTime = this.gameState.getTimeElapsed() / 1000;
|
||||
if (!this.playedTurn || (this.turn + this.player) % 8 == 5)
|
||||
{
|
||||
Engine.ProfileStart("PetraBot bot (player " + this.player +")");
|
||||
|
||||
this.playedTurn++;
|
||||
|
||||
if (!this.canPlay)
|
||||
{
|
||||
Engine.ProfileStop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.HQ.update(this.gameState, this.queues, this.savedEvents);
|
||||
|
||||
this.queueManager.update(this.gameState);
|
||||
|
||||
for (const i in this.savedEvents)
|
||||
this.savedEvents[i] = [];
|
||||
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
|
||||
this.turn++;
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
return this.data;
|
||||
|
||||
const savedEvents = {};
|
||||
for (const key in this.savedEvents)
|
||||
{
|
||||
for (const i in this.savedEvents[key])
|
||||
savedEvents[key] = this.savedEvents[key].slice();
|
||||
for (const i in savedEvents[key])
|
||||
{
|
||||
const evt = this.savedEvents[key][i];
|
||||
if (!savedEvents[key][i])
|
||||
continue;
|
||||
const evt = savedEvents[key][i];
|
||||
const evtmod = {};
|
||||
for (const keyevt in evt)
|
||||
{
|
||||
evtmod[keyevt] = evt[keyevt];
|
||||
this.savedEvents[key][i] = evtmod;
|
||||
}
|
||||
savedEvents[key][i] = evtmod;
|
||||
}
|
||||
}
|
||||
|
||||
this.Config.Deserialize(this.data.config);
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, {});
|
||||
this.queueManager.Deserialize(gameState, this.data.queueManager);
|
||||
this.queues = this.queueManager.queues;
|
||||
|
||||
this.HQ = new Headquarters(this.Config, true);
|
||||
this.HQ.init(gameState, this.queues);
|
||||
this.HQ.Deserialize(gameState, this.data.HQ);
|
||||
|
||||
this.uniqueIDs = this.data.uniqueIDs;
|
||||
this.isDeserialized = false;
|
||||
this.data = undefined;
|
||||
|
||||
// initialisation needed after the completion of the deserialization
|
||||
this.HQ.postinit(gameState);
|
||||
return {
|
||||
"canPlay": this.canPlay,
|
||||
"uniqueIDs": this.uniqueIDs,
|
||||
"turn": this.turn,
|
||||
"playedTurn": this.playedTurn,
|
||||
"elapsedTime": this.elapsedTime,
|
||||
"savedEvents": savedEvents,
|
||||
"config": this.Config.Serialize(),
|
||||
"queueManager": this.queueManager.Serialize(),
|
||||
"HQ": this.HQ.Serialize()
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
Deserialize(data, sharedScript)
|
||||
{
|
||||
this.Config.setConfig(gameState);
|
||||
|
||||
// this.queues can only be modified by the queue manager or things will go awry.
|
||||
this.queues = {};
|
||||
for (const i in this.Config.priorities)
|
||||
this.queues[i] = new Queue();
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, this.queues);
|
||||
|
||||
this.HQ = new Headquarters(this.Config, false);
|
||||
|
||||
this.HQ.init(gameState, this.queues);
|
||||
|
||||
// Try to analyze our starting position and set a strategy.
|
||||
this.canPlay = gameAnalysis(this.HQ, gameState);
|
||||
this.isDeserialized = true;
|
||||
this.data = data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
PetraBot.prototype.OnUpdate = function(sharedScript)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
this.Init(PlayerID, sharedScript);
|
||||
|
||||
if (this.gameFinished || this.gameState.playerData.state == "defeated")
|
||||
return;
|
||||
|
||||
for (const i in sharedScript.events)
|
||||
{
|
||||
if (i == "AIMetadata") // not used inside petra
|
||||
continue;
|
||||
if (this.savedEvents[i] !== undefined)
|
||||
this.savedEvents[i] = this.savedEvents[i].concat(sharedScript.events[i]);
|
||||
else
|
||||
this.savedEvents[i] = sharedScript.events[i];
|
||||
}
|
||||
|
||||
// Run the update every n turns, offset depending on player ID to balance the load
|
||||
this.elapsedTime = this.gameState.getTimeElapsed() / 1000;
|
||||
if (!this.playedTurn || (this.turn + this.player) % 8 == 5)
|
||||
{
|
||||
Engine.ProfileStart("PetraBot bot (player " + this.player +")");
|
||||
|
||||
this.playedTurn++;
|
||||
|
||||
if (!this.canPlay)
|
||||
{
|
||||
Engine.ProfileStop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.HQ.update(this.gameState, this.queues, this.savedEvents);
|
||||
|
||||
this.queueManager.update(this.gameState);
|
||||
|
||||
for (const i in this.savedEvents)
|
||||
this.savedEvents[i] = [];
|
||||
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
|
||||
this.turn++;
|
||||
};
|
||||
|
||||
PetraBot.prototype.Serialize = function()
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
return this.data;
|
||||
|
||||
const savedEvents = {};
|
||||
for (const key in this.savedEvents)
|
||||
{
|
||||
savedEvents[key] = this.savedEvents[key].slice();
|
||||
for (const i in savedEvents[key])
|
||||
{
|
||||
if (!savedEvents[key][i])
|
||||
continue;
|
||||
const evt = savedEvents[key][i];
|
||||
const evtmod = {};
|
||||
for (const keyevt in evt)
|
||||
evtmod[keyevt] = evt[keyevt];
|
||||
savedEvents[key][i] = evtmod;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"canPlay": this.canPlay,
|
||||
"uniqueIDs": this.uniqueIDs,
|
||||
"turn": this.turn,
|
||||
"playedTurn": this.playedTurn,
|
||||
"elapsedTime": this.elapsedTime,
|
||||
"savedEvents": savedEvents,
|
||||
"config": this.Config.Serialize(),
|
||||
"queueManager": this.queueManager.Serialize(),
|
||||
"HQ": this.HQ.Serialize()
|
||||
};
|
||||
};
|
||||
|
||||
PetraBot.prototype.Deserialize = function(data, sharedScript)
|
||||
{
|
||||
this.isDeserialized = true;
|
||||
this.data = data;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,153 +21,171 @@ 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
|
||||
{
|
||||
const civ = gameState.getPlayerCiv();
|
||||
for (const ent of gameState.getOwnUnits().values())
|
||||
addBuilder(this.builders, civ, ent);
|
||||
};
|
||||
// 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();
|
||||
|
||||
/** Update the builders counters */
|
||||
BuildManager.prototype.checkEvents = function(gameState, events)
|
||||
{
|
||||
this.elapsedTime = gameState.ai.elapsedTime;
|
||||
const civ = gameState.getPlayerCiv();
|
||||
|
||||
for (const evt of events.Create)
|
||||
/** Initialization at start of game */
|
||||
init(gameState)
|
||||
{
|
||||
if (events.Destroy.some(e => e.entity == evt.entity))
|
||||
continue;
|
||||
const ent = gameState.getEntityById(evt.entity);
|
||||
if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit"))
|
||||
addBuilder(this.builders, civ, ent);
|
||||
}
|
||||
|
||||
for (const evt of events.Destroy)
|
||||
removeBuilder(this.builders, evt.entity);
|
||||
|
||||
for (const evt of events.OwnershipChanged) // capture events
|
||||
{
|
||||
if (evt.from == PlayerID)
|
||||
{
|
||||
removeBuilder(this.builders, evt.entity);
|
||||
continue;
|
||||
}
|
||||
else if (evt.to != PlayerID)
|
||||
continue;
|
||||
|
||||
const ent = gameState.getEntityById(evt.entity);
|
||||
if (ent?.hasClass("Unit"))
|
||||
addBuilder(this.builders, civ, ent);
|
||||
}
|
||||
|
||||
for (const evt of events.ValueModification)
|
||||
{
|
||||
if (evt.component != "Builder" ||
|
||||
!evt.valueNames.some(val => val.startsWith("Builder/Entities/")))
|
||||
continue;
|
||||
|
||||
// Unfortunately there really is not an easy way to determine the changes
|
||||
// at this stage, so we simply have to dump the cache.
|
||||
this.builders = new Map();
|
||||
|
||||
const civ = gameState.getPlayerCiv();
|
||||
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)
|
||||
{
|
||||
const result = [];
|
||||
for (const [templateName, entities] of this.builders)
|
||||
/** Update the builders counters */
|
||||
checkEvents(gameState, events)
|
||||
{
|
||||
if (!entities.length || gameState.isTemplateDisabled(templateName))
|
||||
continue;
|
||||
const template = gameState.getTemplate(templateName);
|
||||
if (!template || !template.available(gameState))
|
||||
continue;
|
||||
if (filter.func(template))
|
||||
result.push(templateName);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
this.elapsedTime = gameState.ai.elapsedTime;
|
||||
const civ = gameState.getPlayerCiv();
|
||||
|
||||
/**
|
||||
* Get the first buildable structure with a given class
|
||||
* TODO when several available, choose the best one
|
||||
*/
|
||||
BuildManager.prototype.findStructureWithClass = function(gameState, classes)
|
||||
{
|
||||
return this.findStructuresByFilter(gameState, filters.byClasses(classes))[0];
|
||||
};
|
||||
|
||||
BuildManager.prototype.hasBuilder = function(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;
|
||||
};
|
||||
|
||||
BuildManager.prototype.setBuildable = function(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")
|
||||
{
|
||||
if (!this.unbuildables.has(template))
|
||||
this.unbuildables.set(template, { "reason": reason, "time": gameState.ai.elapsedTime + time });
|
||||
else
|
||||
{
|
||||
const unbuildable = this.unbuildables.get(template);
|
||||
if (unbuildable.time < gameState.ai.elapsedTime + time)
|
||||
for (const evt of events.Create)
|
||||
{
|
||||
unbuildable.reason = reason;
|
||||
unbuildable.time = gameState.ai.elapsedTime + time;
|
||||
if (events.Destroy.some(e => e.entity == evt.entity))
|
||||
continue;
|
||||
const ent = gameState.getEntityById(evt.entity);
|
||||
if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit"))
|
||||
addBuilder(this.builders, civ, ent);
|
||||
}
|
||||
|
||||
for (const evt of events.Destroy)
|
||||
removeBuilder(this.builders, evt.entity);
|
||||
|
||||
for (const evt of events.OwnershipChanged) // capture events
|
||||
{
|
||||
if (evt.from == PlayerID)
|
||||
{
|
||||
removeBuilder(this.builders, evt.entity);
|
||||
continue;
|
||||
}
|
||||
else if (evt.to != PlayerID)
|
||||
continue;
|
||||
|
||||
const ent = gameState.getEntityById(evt.entity);
|
||||
if (ent?.hasClass("Unit"))
|
||||
addBuilder(this.builders, civ, ent);
|
||||
}
|
||||
|
||||
for (const evt of events.ValueModification)
|
||||
{
|
||||
if (evt.component != "Builder" ||
|
||||
!evt.valueNames.some(val => val.startsWith("Builder/Entities/")))
|
||||
continue;
|
||||
|
||||
// Unfortunately there really is not an easy way to determine the changes
|
||||
// at this stage, so we simply have to dump the cache.
|
||||
this.builders = new Map();
|
||||
|
||||
for (const ent of gameState.getOwnUnits().values())
|
||||
addBuilder(this.builders, civ, ent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Return the number of unbuildables due to missing room */
|
||||
BuildManager.prototype.numberMissingRoom = function(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)
|
||||
{
|
||||
for (const [key, unbuildable] of this.unbuildables)
|
||||
if (unbuildable.reason == "room")
|
||||
this.unbuildables.delete(key);
|
||||
};
|
||||
/**
|
||||
* Get the buildable structures passing a filter.
|
||||
*/
|
||||
findStructuresByFilter(gameState, filter)
|
||||
{
|
||||
const result = [];
|
||||
for (const [templateName, entities] of this.builders)
|
||||
{
|
||||
if (!entities.length || gameState.isTemplateDisabled(templateName))
|
||||
continue;
|
||||
const template = gameState.getTemplate(templateName);
|
||||
if (!template || !template.available(gameState))
|
||||
continue;
|
||||
if (filter.func(template))
|
||||
result.push(templateName);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
BuildManager.prototype.Serialize = function()
|
||||
{
|
||||
return {
|
||||
"builders": this.builders,
|
||||
"unbuildables": this.unbuildables
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get the first buildable structure with a given class
|
||||
* TODO when several available, choose the best one
|
||||
*/
|
||||
findStructureWithClass(gameState, classes)
|
||||
{
|
||||
return this.findStructuresByFilter(gameState, filters.byClasses(classes))[0];
|
||||
}
|
||||
|
||||
BuildManager.prototype.Deserialize = function(data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
};
|
||||
hasBuilder(template)
|
||||
{
|
||||
const numBuilders = this.builders.get(template);
|
||||
return numBuilders && numBuilders.length > 0;
|
||||
}
|
||||
|
||||
isUnbuildable(gameState, template)
|
||||
{
|
||||
return this.unbuildables.has(template) &&
|
||||
this.unbuildables.get(template).time > gameState.ai.elapsedTime;
|
||||
}
|
||||
|
||||
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 */
|
||||
setUnbuildable(gameState, template, time = 90, reason = "room")
|
||||
{
|
||||
if (!this.unbuildables.has(template))
|
||||
{
|
||||
this.unbuildables.set(template,
|
||||
{ "reason": reason, "time": gameState.ai.elapsedTime + time });
|
||||
}
|
||||
else
|
||||
{
|
||||
const unbuildable = this.unbuildables.get(template);
|
||||
if (unbuildable.time < gameState.ai.elapsedTime + time)
|
||||
{
|
||||
unbuildable.reason = reason;
|
||||
unbuildable.time = gameState.ai.elapsedTime + time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
resetMissingRoom(gameState)
|
||||
{
|
||||
for (const [key, unbuildable] of this.unbuildables)
|
||||
if (unbuildable.reason == "room")
|
||||
this.unbuildables.delete(key);
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
return {
|
||||
"builders": this.builders,
|
||||
"unbuildables": this.unbuildables
|
||||
};
|
||||
}
|
||||
|
||||
Deserialize(data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,157 +192,165 @@ export function Config(difficulty = difficultyLevel.MEDIUM, behavior)
|
||||
0.35,
|
||||
0.2
|
||||
];
|
||||
}
|
||||
|
||||
Config.prototype.setConfig = function(gameState)
|
||||
{
|
||||
if (this.difficulty > difficultyLevel.SANDBOX)
|
||||
constructor(difficulty = difficultyLevel.MEDIUM, behavior)
|
||||
{
|
||||
// Setup personality traits according to the user choice:
|
||||
// The parameter used to define the personality is basically the aggressivity or (1-defensiveness)
|
||||
// as they are anticorrelated, although some small smearing to decorelate them will be added.
|
||||
// And for each user choice, this parameter can vary between min and max
|
||||
const personalityList = {
|
||||
"random": { "min": 0, "max": 1 },
|
||||
"defensive": { "min": 0, "max": 0.27 },
|
||||
"balanced": { "min": 0.37, "max": 0.63 },
|
||||
"aggressive": { "min": 0.73, "max": 1 }
|
||||
};
|
||||
const behavior = randFloat(-0.5, 0.5);
|
||||
// make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety
|
||||
const variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior));
|
||||
const aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5;
|
||||
const defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5;
|
||||
const min = personalityList[this.behavior].min;
|
||||
const max = personalityList[this.behavior].max;
|
||||
this.personality = {
|
||||
"aggressive": min + aggressive * (max - min),
|
||||
"defensive": 1 - max + defensive * (max - min),
|
||||
"cooperative": randFloat(0, 1)
|
||||
};
|
||||
this.difficulty = difficulty;
|
||||
|
||||
// for instance "balanced", "aggressive" or "defensive"
|
||||
this.behavior = behavior || "random";
|
||||
}
|
||||
// Petra usually uses the continuous values of personality.aggressive and personality.defensive
|
||||
// to define its behavior according to personality. But when discontinuous behavior is needed,
|
||||
// it uses the following personalityCut which should be set such that:
|
||||
// behavior="aggressive" => personality.aggressive > personalityCut.strong &&
|
||||
// personality.defensive < personalityCut.weak
|
||||
// and inversely for behavior="defensive"
|
||||
this.personalityCut = { "weak": 0.3, "medium": 0.5, "strong": 0.7 };
|
||||
|
||||
if (gameState.playerData.teamsLocked)
|
||||
this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30);
|
||||
else if (gameState.getAlliedVictory())
|
||||
this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15);
|
||||
|
||||
// changing settings based on difficulty or personality
|
||||
this.Military.towerLapseTime = Math.round(this.Military.towerLapseTime * (1.1 - 0.2 * this.personality.defensive));
|
||||
this.Military.fortressLapseTime = Math.round(this.Military.fortressLapseTime * (1.1 - 0.2 * this.personality.defensive));
|
||||
this.priorities.defenseBuilding = Math.round(this.priorities.defenseBuilding * (0.9 + 0.2 * this.personality.defensive));
|
||||
|
||||
if (this.difficulty < difficultyLevel.EASY)
|
||||
setConfig(gameState)
|
||||
{
|
||||
this.popScaling = 0.5;
|
||||
this.Economy.supportRatio = 0.5;
|
||||
this.Economy.provisionFields = 1;
|
||||
this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0;
|
||||
}
|
||||
else if (this.difficulty < difficultyLevel.MEDIUM)
|
||||
{
|
||||
this.popScaling = 0.7;
|
||||
this.Economy.supportRatio = 0.4;
|
||||
this.Economy.provisionFields = 1;
|
||||
this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.difficulty == difficultyLevel.MEDIUM)
|
||||
this.Military.numSentryTowers = 1;
|
||||
else
|
||||
this.Military.numSentryTowers = 2;
|
||||
if (this.personality.defensive > this.personalityCut.strong)
|
||||
++this.Military.numSentryTowers;
|
||||
else if (this.personality.defensive < this.personalityCut.weak)
|
||||
--this.Military.numSentryTowers;
|
||||
|
||||
if (this.personality.aggressive > this.personalityCut.strong)
|
||||
if (this.difficulty > difficultyLevel.SANDBOX)
|
||||
{
|
||||
this.Military.popForBarracks1 = 12;
|
||||
this.Economy.popPhase2 = 50;
|
||||
this.priorities.healer = 10;
|
||||
// Setup personality traits according to the user choice:
|
||||
// The parameter used to define the personality is basically the aggressivity or (1-defensiveness)
|
||||
// as they are anticorrelated, although some small smearing to decorelate them will be added.
|
||||
// And for each user choice, this parameter can vary between min and max
|
||||
const personalityList = {
|
||||
"random": { "min": 0, "max": 1 },
|
||||
"defensive": { "min": 0, "max": 0.27 },
|
||||
"balanced": { "min": 0.37, "max": 0.63 },
|
||||
"aggressive": { "min": 0.73, "max": 1 }
|
||||
};
|
||||
const behavior = randFloat(-0.5, 0.5);
|
||||
// make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety
|
||||
const variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior));
|
||||
const aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5;
|
||||
const defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5;
|
||||
const min = personalityList[this.behavior].min;
|
||||
const max = personalityList[this.behavior].max;
|
||||
this.personality = {
|
||||
"aggressive": min + aggressive * (max - min),
|
||||
"defensive": 1 - max + defensive * (max - min),
|
||||
"cooperative": randFloat(0, 1)
|
||||
};
|
||||
}
|
||||
// Petra usually uses the continuous values of personality.aggressive and personality.defensive
|
||||
// to define its behavior according to personality. But when discontinuous behavior is needed,
|
||||
// it uses the following personalityCut which should be set such that:
|
||||
// behavior="aggressive" => personality.aggressive > personalityCut.strong &&
|
||||
// personality.defensive < personalityCut.weak
|
||||
// and inversely for behavior="defensive"
|
||||
this.personalityCut = { "weak": 0.3, "medium": 0.5, "strong": 0.7 };
|
||||
|
||||
if (gameState.playerData.teamsLocked)
|
||||
this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30);
|
||||
else if (gameState.getAlliedVictory())
|
||||
this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15);
|
||||
|
||||
// changing settings based on difficulty or personality
|
||||
this.Military.towerLapseTime = Math.round(this.Military.towerLapseTime * (1.1 - 0.2 * this.personality.defensive));
|
||||
this.Military.fortressLapseTime = Math.round(this.Military.fortressLapseTime * (1.1 - 0.2 * this.personality.defensive));
|
||||
this.priorities.defenseBuilding = Math.round(this.priorities.defenseBuilding * (0.9 + 0.2 * this.personality.defensive));
|
||||
|
||||
if (this.difficulty < difficultyLevel.EASY)
|
||||
{
|
||||
this.popScaling = 0.5;
|
||||
this.Economy.supportRatio = 0.5;
|
||||
this.Economy.provisionFields = 1;
|
||||
this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0;
|
||||
}
|
||||
else if (this.difficulty < difficultyLevel.MEDIUM)
|
||||
{
|
||||
this.popScaling = 0.7;
|
||||
this.Economy.supportRatio = 0.4;
|
||||
this.Economy.provisionFields = 1;
|
||||
this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.difficulty == difficultyLevel.MEDIUM)
|
||||
this.Military.numSentryTowers = 1;
|
||||
else
|
||||
this.Military.numSentryTowers = 2;
|
||||
if (this.personality.defensive > this.personalityCut.strong)
|
||||
++this.Military.numSentryTowers;
|
||||
else if (this.personality.defensive < this.personalityCut.weak)
|
||||
--this.Military.numSentryTowers;
|
||||
|
||||
if (this.personality.aggressive > this.personalityCut.strong)
|
||||
{
|
||||
this.Military.popForBarracks1 = 12;
|
||||
this.Economy.popPhase2 = 50;
|
||||
this.priorities.healer = 10;
|
||||
}
|
||||
}
|
||||
|
||||
const maxPop = gameState.getPopulationMax();
|
||||
if (this.difficulty < difficultyLevel.EASY)
|
||||
this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop));
|
||||
else if (this.difficulty < difficultyLevel.MEDIUM)
|
||||
this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2)));
|
||||
else
|
||||
this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3)));
|
||||
this.Economy.targetNumTraders = 2 + this.difficulty;
|
||||
|
||||
|
||||
if (gameState.getVictoryConditions().has("wonder"))
|
||||
{
|
||||
this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3);
|
||||
this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4);
|
||||
}
|
||||
|
||||
if (maxPop < 300)
|
||||
this.popScaling *= Math.sqrt(maxPop / 300);
|
||||
|
||||
this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5));
|
||||
this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3));
|
||||
this.Military.popForForge = Math.min(Math.max(Math.floor(this.Military.popForForge * this.popScaling), 30), Math.floor(maxPop/2));
|
||||
this.Economy.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2));
|
||||
this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3));
|
||||
this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), Math.floor(maxPop*2/3));
|
||||
this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling);
|
||||
this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popPhase2);
|
||||
this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers);
|
||||
this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers);
|
||||
if (this.difficulty < difficultyLevel.EASY)
|
||||
this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase
|
||||
|
||||
this.emergencyValues = {
|
||||
"population": this.criticalPopulationFactors[this.difficulty],
|
||||
"structures": this.criticalStructureFactors[this.difficulty],
|
||||
"roots": this.criticalRootFactors[this.difficulty],
|
||||
};
|
||||
|
||||
this.Cheat(gameState);
|
||||
|
||||
if (this.debug < 2)
|
||||
return;
|
||||
aiWarn(" >>> Petra bot: personality = " + uneval(this.personality));
|
||||
}
|
||||
|
||||
const maxPop = gameState.getPopulationMax();
|
||||
if (this.difficulty < difficultyLevel.EASY)
|
||||
this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop));
|
||||
else if (this.difficulty < difficultyLevel.MEDIUM)
|
||||
this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2)));
|
||||
else
|
||||
this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3)));
|
||||
this.Economy.targetNumTraders = 2 + this.difficulty;
|
||||
|
||||
|
||||
if (gameState.getVictoryConditions().has("wonder"))
|
||||
Cheat(gameState)
|
||||
{
|
||||
this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3);
|
||||
this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4);
|
||||
// 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
|
||||
const rate = [ 0.42, 0.56, 0.75, 1.00, 1.25, 1.56 ];
|
||||
const time = [ 1.40, 1.25, 1.10, 1.00, 1.00, 1.00 ];
|
||||
const AIDiff = Math.min(this.difficulty, rate.length - 1);
|
||||
SimEngine.QueryInterface(Sim.SYSTEM_ENTITY, Sim.IID_ModifiersManager).AddModifiers("AI Bonus", {
|
||||
"ResourceGatherer/BaseSpeed": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
|
||||
"Trader/GainMultiplier": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
|
||||
"Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }],
|
||||
}, gameState.playerData.entity);
|
||||
}
|
||||
|
||||
if (maxPop < 300)
|
||||
this.popScaling *= Math.sqrt(maxPop / 300);
|
||||
Serialize()
|
||||
{
|
||||
var data = {};
|
||||
for (const key in this)
|
||||
if (Object.hasOwn(this, key) && key != "debug")
|
||||
data[key] = this[key];
|
||||
return data;
|
||||
}
|
||||
|
||||
this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5));
|
||||
this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3));
|
||||
this.Military.popForForge = Math.min(Math.max(Math.floor(this.Military.popForForge * this.popScaling), 30), Math.floor(maxPop/2));
|
||||
this.Economy.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2));
|
||||
this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3));
|
||||
this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), Math.floor(maxPop*2/3));
|
||||
this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling);
|
||||
this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popPhase2);
|
||||
this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers);
|
||||
this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers);
|
||||
if (this.difficulty < difficultyLevel.EASY)
|
||||
this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase
|
||||
|
||||
this.emergencyValues = {
|
||||
"population": this.criticalPopulationFactors[this.difficulty],
|
||||
"structures": this.criticalStructureFactors[this.difficulty],
|
||||
"roots": this.criticalRootFactors[this.difficulty],
|
||||
};
|
||||
|
||||
this.Cheat(gameState);
|
||||
|
||||
if (this.debug < 2)
|
||||
return;
|
||||
aiWarn(" >>> Petra bot: personality = " + uneval(this.personality));
|
||||
};
|
||||
|
||||
Config.prototype.Cheat = function(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
|
||||
const rate = [ 0.42, 0.56, 0.75, 1.00, 1.25, 1.56 ];
|
||||
const time = [ 1.40, 1.25, 1.10, 1.00, 1.00, 1.00 ];
|
||||
const AIDiff = Math.min(this.difficulty, rate.length - 1);
|
||||
SimEngine.QueryInterface(Sim.SYSTEM_ENTITY, Sim.IID_ModifiersManager).AddModifiers("AI Bonus", {
|
||||
"ResourceGatherer/BaseSpeed": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
|
||||
"Trader/GainMultiplier": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
|
||||
"Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }],
|
||||
}, gameState.playerData.entity);
|
||||
};
|
||||
|
||||
Config.prototype.Serialize = function()
|
||||
{
|
||||
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)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
};
|
||||
Deserialize(data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,94 +3,98 @@ 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;
|
||||
|
||||
constructor(Config)
|
||||
{
|
||||
this.Config = Config;
|
||||
}
|
||||
|
||||
init(gameState)
|
||||
{
|
||||
this.referencePopulation = gameState.getPopulation();
|
||||
this.referenceStructureCount = gameState.getOwnStructures().length;
|
||||
this.numRoots = this.rootCount(gameState);
|
||||
}
|
||||
|
||||
update(gameState)
|
||||
{
|
||||
if (this.hasEmergency)
|
||||
{
|
||||
this.emergencyUpdate(gameState);
|
||||
return;
|
||||
}
|
||||
const pop = gameState.getPopulation();
|
||||
const nStructures = gameState.getOwnStructures().length;
|
||||
const nRoots = this.rootCount(gameState);
|
||||
const factors = this.Config.emergencyValues;
|
||||
if (((pop / this.referencePopulation) < factors.population || pop == 0) &&
|
||||
((nStructures / this.referenceStructureCount) < factors.structures || nStructures == 0))
|
||||
this.setEmergency(gameState, true);
|
||||
else if ((nRoots / this.numRoots) <= factors.roots || (nRoots == 0 && this.numRoots != 0))
|
||||
this.setEmergency(gameState, true);
|
||||
|
||||
if (pop > this.referencePopulation || this.hasEmergency)
|
||||
this.referencePopulation = pop;
|
||||
if (nStructures > this.referenceStructureCount || this.hasEmergency)
|
||||
this.referenceStructureCount = nStructures;
|
||||
if (nRoots > this.numRoots || this.hasEmergency)
|
||||
this.numRoots = nRoots;
|
||||
}
|
||||
|
||||
emergencyUpdate(gameState)
|
||||
{
|
||||
const pop = gameState.getPopulation();
|
||||
const nStructures = gameState.getOwnStructures().length;
|
||||
const nRoots = this.rootCount(gameState);
|
||||
const factors = this.Config.emergencyValues;
|
||||
|
||||
if ((pop > this.referencePopulation * 1.2 &&
|
||||
nStructures > this.referenceStructureCount * 1.2) ||
|
||||
nRoots > this.numRoots)
|
||||
{
|
||||
this.setEmergency(gameState, false);
|
||||
this.referencePopulation = pop;
|
||||
this.referenceStructureCount = nStructures;
|
||||
this.numRoots = nRoots;
|
||||
}
|
||||
}
|
||||
|
||||
rootCount(gameState)
|
||||
{
|
||||
let roots = 0;
|
||||
gameState.getOwnStructures().toEntityArray().forEach(ent =>
|
||||
{
|
||||
if (ent?.get("TerritoryInfluence")?.Root === "true")
|
||||
roots++;
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
|
||||
setEmergency(gameState, enable)
|
||||
{
|
||||
this.hasEmergency = enable;
|
||||
chatEmergency(gameState, enable);
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
return {
|
||||
"referencePopulation": this.referencePopulation,
|
||||
"referenceStructureCount": this.referenceStructureCount,
|
||||
"numRoots": this.numRoots,
|
||||
"hasEmergency": this.hasEmergency
|
||||
};
|
||||
}
|
||||
|
||||
Deserialize(data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
EmergencyManager.prototype.init = function(gameState)
|
||||
{
|
||||
this.referencePopulation = gameState.getPopulation();
|
||||
this.referenceStructureCount = gameState.getOwnStructures().length;
|
||||
this.numRoots = this.rootCount(gameState);
|
||||
};
|
||||
|
||||
EmergencyManager.prototype.update = function(gameState)
|
||||
{
|
||||
if (this.hasEmergency)
|
||||
{
|
||||
this.emergencyUpdate(gameState);
|
||||
return;
|
||||
}
|
||||
const pop = gameState.getPopulation();
|
||||
const nStructures = gameState.getOwnStructures().length;
|
||||
const nRoots = this.rootCount(gameState);
|
||||
const factors = this.Config.emergencyValues;
|
||||
if (((pop / this.referencePopulation) < factors.population || pop == 0) &&
|
||||
((nStructures / this.referenceStructureCount) < factors.structures || nStructures == 0))
|
||||
this.setEmergency(gameState, true);
|
||||
else if ((nRoots / this.numRoots) <= factors.roots || (nRoots == 0 && this.numRoots != 0))
|
||||
this.setEmergency(gameState, true);
|
||||
|
||||
if (pop > this.referencePopulation || this.hasEmergency)
|
||||
this.referencePopulation = pop;
|
||||
if (nStructures > this.referenceStructureCount || this.hasEmergency)
|
||||
this.referenceStructureCount = nStructures;
|
||||
if (nRoots > this.numRoots || this.hasEmergency)
|
||||
this.numRoots = nRoots;
|
||||
};
|
||||
|
||||
EmergencyManager.prototype.emergencyUpdate = function(gameState)
|
||||
{
|
||||
const pop = gameState.getPopulation();
|
||||
const nStructures = gameState.getOwnStructures().length;
|
||||
const nRoots = this.rootCount(gameState);
|
||||
const factors = this.Config.emergencyValues;
|
||||
|
||||
if ((pop > this.referencePopulation * 1.2 &&
|
||||
nStructures > this.referenceStructureCount * 1.2) ||
|
||||
nRoots > this.numRoots)
|
||||
{
|
||||
this.setEmergency(gameState, false);
|
||||
this.referencePopulation = pop;
|
||||
this.referenceStructureCount = nStructures;
|
||||
this.numRoots = nRoots;
|
||||
}
|
||||
};
|
||||
|
||||
EmergencyManager.prototype.rootCount = function(gameState)
|
||||
{
|
||||
let roots = 0;
|
||||
gameState.getOwnStructures().toEntityArray().forEach(ent =>
|
||||
{
|
||||
if (ent?.get("TerritoryInfluence")?.Root === "true")
|
||||
roots++;
|
||||
});
|
||||
return roots;
|
||||
};
|
||||
|
||||
EmergencyManager.prototype.setEmergency = function(gameState, enable)
|
||||
{
|
||||
this.hasEmergency = enable;
|
||||
chatEmergency(gameState, enable);
|
||||
};
|
||||
|
||||
EmergencyManager.prototype.Serialize = function()
|
||||
{
|
||||
return {
|
||||
"referencePopulation": this.referencePopulation,
|
||||
"referenceStructureCount": this.referenceStructureCount,
|
||||
"numRoots": this.numRoots,
|
||||
"hasEmergency": this.hasEmergency
|
||||
};
|
||||
};
|
||||
|
||||
EmergencyManager.prototype.Deserialize = function(data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
};
|
||||
|
||||
@@ -10,38 +10,75 @@ 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
|
||||
{
|
||||
this.Config = Config;
|
||||
this.holders = new Map();
|
||||
this.decayingStructures = new Map();
|
||||
}
|
||||
holders = new Map();
|
||||
decayingStructures = new Map();
|
||||
|
||||
GarrisonManager.TYPE_FORCE = "force";
|
||||
GarrisonManager.TYPE_TRADE = "trade";
|
||||
GarrisonManager.TYPE_PROTECTION = "protection";
|
||||
GarrisonManager.TYPE_DECAY = "decay";
|
||||
GarrisonManager.TYPE_EMERGENCY = "emergency";
|
||||
|
||||
GarrisonManager.prototype.update = function(gameState, events)
|
||||
{
|
||||
// First check for possible upgrade of a structure
|
||||
for (const evt of events.EntityRenamed)
|
||||
constructor(Config)
|
||||
{
|
||||
for (const id of this.holders.keys())
|
||||
this.Config = Config;
|
||||
}
|
||||
|
||||
static TYPE_FORCE = "force";
|
||||
static TYPE_TRADE = "trade";
|
||||
static TYPE_PROTECTION = "protection";
|
||||
static TYPE_DECAY = "decay";
|
||||
static TYPE_EMERGENCY = "emergency";
|
||||
|
||||
update(gameState, events)
|
||||
{
|
||||
// First check for possible upgrade of a structure
|
||||
for (const evt of events.EntityRenamed)
|
||||
{
|
||||
if (id != evt.entity)
|
||||
continue;
|
||||
const data = this.holders.get(id);
|
||||
const newHolder = gameState.getEntityById(evt.newentity);
|
||||
if (newHolder && newHolder.isGarrisonHolder())
|
||||
for (const id of this.holders.keys())
|
||||
{
|
||||
this.holders.delete(id);
|
||||
this.holders.set(evt.newentity, data);
|
||||
if (id != evt.entity)
|
||||
continue;
|
||||
const data = this.holders.get(id);
|
||||
const newHolder = gameState.getEntityById(evt.newentity);
|
||||
if (newHolder && newHolder.isGarrisonHolder())
|
||||
{
|
||||
this.holders.delete(id);
|
||||
this.holders.set(evt.newentity, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const entId of data.list)
|
||||
{
|
||||
const ent = gameState.getEntityById(entId);
|
||||
if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id)
|
||||
continue;
|
||||
this.leaveGarrison(ent);
|
||||
ent.stopMoving();
|
||||
}
|
||||
this.holders.delete(id);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
for (const id of this.decayingStructures.keys())
|
||||
{
|
||||
for (const entId of data.list)
|
||||
if (id !== evt.entity)
|
||||
continue;
|
||||
this.decayingStructures.delete(id);
|
||||
if (this.decayingStructures.has(evt.newentity))
|
||||
continue;
|
||||
const ent = gameState.getEntityById(evt.newentity);
|
||||
if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate())
|
||||
continue;
|
||||
const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate());
|
||||
this.decayingStructures.set(evt.newentity, gmin);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, data] of this.holders.entries())
|
||||
{
|
||||
const list = data.list;
|
||||
const holder = gameState.getEntityById(id);
|
||||
if (!holder || !gameState.isPlayerAlly(holder.owner()))
|
||||
{
|
||||
// this holder was certainly destroyed or captured. Let's remove it
|
||||
for (const entId of list)
|
||||
{
|
||||
const ent = gameState.getEntityById(entId);
|
||||
if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id)
|
||||
@@ -50,340 +87,307 @@ GarrisonManager.prototype.update = function(gameState, events)
|
||||
ent.stopMoving();
|
||||
}
|
||||
this.holders.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of this.decayingStructures.keys())
|
||||
{
|
||||
if (id !== evt.entity)
|
||||
continue;
|
||||
this.decayingStructures.delete(id);
|
||||
if (this.decayingStructures.has(evt.newentity))
|
||||
continue;
|
||||
const ent = gameState.getEntityById(evt.newentity);
|
||||
if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate())
|
||||
continue;
|
||||
const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate());
|
||||
this.decayingStructures.set(evt.newentity, gmin);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, data] of this.holders.entries())
|
||||
{
|
||||
const list = data.list;
|
||||
const holder = gameState.getEntityById(id);
|
||||
if (!holder || !gameState.isPlayerAlly(holder.owner()))
|
||||
{
|
||||
// this holder was certainly destroyed or captured. Let's remove it
|
||||
for (const entId of list)
|
||||
{
|
||||
const ent = gameState.getEntityById(entId);
|
||||
if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id)
|
||||
continue;
|
||||
this.leaveGarrison(ent);
|
||||
ent.stopMoving();
|
||||
}
|
||||
this.holders.delete(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update the list of garrisoned units
|
||||
for (let j = 0; j < list.length; ++j)
|
||||
{
|
||||
for (const evt of events.EntityRenamed)
|
||||
if (evt.entity === list[j])
|
||||
list[j] = evt.newentity;
|
||||
// Update the list of garrisoned units
|
||||
for (let j = 0; j < list.length; ++j)
|
||||
{
|
||||
for (const evt of events.EntityRenamed)
|
||||
if (evt.entity === list[j])
|
||||
list[j] = evt.newentity;
|
||||
|
||||
const ent = gameState.getEntityById(list[j]);
|
||||
if (!ent) // unit must have been killed while garrisoning
|
||||
list.splice(j--, 1);
|
||||
else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned
|
||||
{
|
||||
this.leaveGarrison(ent);
|
||||
list.splice(j--, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ent.unitAIOrderData().some(order => order.target && order.target == id))
|
||||
continue;
|
||||
if (ent.getMetadata(PlayerID, "garrisonHolder") == id)
|
||||
const ent = gameState.getEntityById(list[j]);
|
||||
if (!ent) // unit must have been killed while garrisoning
|
||||
list.splice(j--, 1);
|
||||
else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned
|
||||
{
|
||||
// The garrison order must have failed
|
||||
this.leaveGarrison(ent);
|
||||
list.splice(j--, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gameState.ai.Config.debug > 0)
|
||||
if (ent.unitAIOrderData().some(order => order.target && order.target == id))
|
||||
continue;
|
||||
if (ent.getMetadata(PlayerID, "garrisonHolder") == id)
|
||||
{
|
||||
aiWarn("Petra garrison error: unit " + ent.id() + " (" +
|
||||
ent.genericName() + ") is expected to garrison in " + id + " (" +
|
||||
holder.genericName() + "), but has no such garrison order " +
|
||||
uneval(ent.unitAIOrderData()));
|
||||
dumpEntity(ent);
|
||||
// The garrison order must have failed
|
||||
this.leaveGarrison(ent);
|
||||
list.splice(j--, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gameState.ai.Config.debug > 0)
|
||||
{
|
||||
aiWarn("Petra garrison error: unit " + ent.id() + " (" +
|
||||
ent.genericName() + ") is expected to garrison in " + id + " (" +
|
||||
holder.genericName() + "), but has no such garrison order " +
|
||||
uneval(ent.unitAIOrderData()));
|
||||
dumpEntity(ent);
|
||||
}
|
||||
list.splice(j--, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!holder.position()) // could happen with siege unit inside a ship
|
||||
continue;
|
||||
|
||||
if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3)
|
||||
{
|
||||
const range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80;
|
||||
const around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false };
|
||||
for (const ent of gameState.getEnemyEntities().values())
|
||||
{
|
||||
if (ent.hasClass("Structure"))
|
||||
{
|
||||
if (!ent.attackRange("Ranged"))
|
||||
continue;
|
||||
}
|
||||
else if (ent.hasClass("Unit"))
|
||||
{
|
||||
if (ent.owner() == 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT"))
|
||||
continue;
|
||||
}
|
||||
else
|
||||
continue;
|
||||
if (!ent.position())
|
||||
continue;
|
||||
const dist = SquareVectorDistance(ent.position(), holder.position());
|
||||
if (dist > range*range)
|
||||
continue;
|
||||
if (ent.hasClass("Structure"))
|
||||
around.defenseStructure = true;
|
||||
else if (isSiegeUnit(ent))
|
||||
{
|
||||
if (ent.attackTypes().indexOf("Melee") !== -1)
|
||||
around.meleeSiege = true;
|
||||
else
|
||||
around.rangeSiege = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
around.unit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units
|
||||
data.allowMelee = around.defenseStructure || around.unit;
|
||||
|
||||
for (const entId of holder.garrisoned())
|
||||
{
|
||||
const ent = gameState.getEntityById(entId);
|
||||
if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around))
|
||||
holder.unload(entId);
|
||||
}
|
||||
for (let j = 0; j < list.length; ++j)
|
||||
{
|
||||
const ent = gameState.getEntityById(list[j]);
|
||||
if (this.keepGarrisoned(ent, holder, around))
|
||||
continue;
|
||||
if (ent.getMetadata(PlayerID, "garrisonHolder") == id)
|
||||
{
|
||||
this.leaveGarrison(ent);
|
||||
ent.stopMoving();
|
||||
}
|
||||
list.splice(j--, 1);
|
||||
}
|
||||
if (this.numberOfGarrisonedSlots(holder) === 0)
|
||||
this.holders.delete(id);
|
||||
else
|
||||
holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!holder.position()) // could happen with siege unit inside a ship
|
||||
continue;
|
||||
|
||||
if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3)
|
||||
// Warning new garrison orders (as in the following lines) should be done after having updated the holders
|
||||
// (or TODO we should add a test that the garrison order is from a previous turn when updating)
|
||||
for (const [id, gmin] of this.decayingStructures.entries())
|
||||
{
|
||||
const range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80;
|
||||
const around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false };
|
||||
for (const ent of gameState.getEnemyEntities().values())
|
||||
{
|
||||
if (ent.hasClass("Structure"))
|
||||
{
|
||||
if (!ent.attackRange("Ranged"))
|
||||
continue;
|
||||
}
|
||||
else if (ent.hasClass("Unit"))
|
||||
{
|
||||
if (ent.owner() == 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT"))
|
||||
continue;
|
||||
}
|
||||
else
|
||||
continue;
|
||||
if (!ent.position())
|
||||
continue;
|
||||
const dist = SquareVectorDistance(ent.position(), holder.position());
|
||||
if (dist > range*range)
|
||||
continue;
|
||||
if (ent.hasClass("Structure"))
|
||||
around.defenseStructure = true;
|
||||
else if (isSiegeUnit(ent))
|
||||
{
|
||||
if (ent.attackTypes().indexOf("Melee") !== -1)
|
||||
around.meleeSiege = true;
|
||||
else
|
||||
around.rangeSiege = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
around.unit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units
|
||||
data.allowMelee = around.defenseStructure || around.unit;
|
||||
|
||||
for (const entId of holder.garrisoned())
|
||||
{
|
||||
const ent = gameState.getEntityById(entId);
|
||||
if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around))
|
||||
holder.unload(entId);
|
||||
}
|
||||
for (let j = 0; j < list.length; ++j)
|
||||
{
|
||||
const ent = gameState.getEntityById(list[j]);
|
||||
if (this.keepGarrisoned(ent, holder, around))
|
||||
continue;
|
||||
if (ent.getMetadata(PlayerID, "garrisonHolder") == id)
|
||||
{
|
||||
this.leaveGarrison(ent);
|
||||
ent.stopMoving();
|
||||
}
|
||||
list.splice(j--, 1);
|
||||
}
|
||||
if (this.numberOfGarrisonedSlots(holder) === 0)
|
||||
this.holders.delete(id);
|
||||
else
|
||||
holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime);
|
||||
const ent = gameState.getEntityById(id);
|
||||
if (!ent || ent.owner() !== PlayerID)
|
||||
this.decayingStructures.delete(id);
|
||||
else if (this.numberOfGarrisonedSlots(ent) < gmin)
|
||||
gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": GarrisonManager.TYPE_DECAY });
|
||||
}
|
||||
}
|
||||
|
||||
// Warning new garrison orders (as in the following lines) should be done after having updated the holders
|
||||
// (or TODO we should add a test that the garrison order is from a previous turn when updating)
|
||||
for (const [id, gmin] of this.decayingStructures.entries())
|
||||
/** TODO should add the units garrisoned inside garrisoned units */
|
||||
numberOfGarrisonedUnits(holder)
|
||||
{
|
||||
const ent = gameState.getEntityById(id);
|
||||
if (!ent || ent.owner() !== PlayerID)
|
||||
this.decayingStructures.delete(id);
|
||||
else if (this.numberOfGarrisonedSlots(ent) < gmin)
|
||||
gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": GarrisonManager.TYPE_DECAY });
|
||||
}
|
||||
};
|
||||
if (!this.holders.has(holder.id()))
|
||||
return holder.garrisoned().length;
|
||||
|
||||
/** TODO should add the units garrisoned inside garrisoned units */
|
||||
GarrisonManager.prototype.numberOfGarrisonedUnits = function(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)
|
||||
{
|
||||
if (!this.holders.has(holder.id()))
|
||||
return holder.garrisonedSlots();
|
||||
|
||||
return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length;
|
||||
};
|
||||
|
||||
GarrisonManager.prototype.allowMelee = function(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)
|
||||
{
|
||||
if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison())
|
||||
return;
|
||||
|
||||
this.registerHolder(gameState, holder);
|
||||
this.holders.get(holder.id()).list.push(ent.id());
|
||||
|
||||
if (gameState.ai.Config.debug > 2)
|
||||
{
|
||||
warn("garrison unit " + ent.genericName() + " in " + holder.genericName() + " with type " + type);
|
||||
warn(" we try to garrison a unit with plan " + ent.getMetadata(PlayerID, "plan") + " and role " + ent.getMetadata(PlayerID, "role") +
|
||||
" and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport"));
|
||||
return holder.garrisoned().length + this.holders.get(holder.id()).list.length;
|
||||
}
|
||||
|
||||
if (ent.getMetadata(PlayerID, "plan") !== undefined)
|
||||
ent.setMetadata(PlayerID, "plan", -2);
|
||||
else
|
||||
ent.setMetadata(PlayerID, "plan", -3);
|
||||
ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_GARRISONING);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
ent.stopMoving();
|
||||
this.leaveGarrison(ent);
|
||||
const holderId = ent.getMetadata(PlayerID, "garrisonHolder");
|
||||
if (!holderId || !this.holders.has(holderId))
|
||||
return;
|
||||
const list = this.holders.get(holderId).list;
|
||||
const index = list.indexOf(ent.id());
|
||||
if (index !== -1)
|
||||
list.splice(index, 1);
|
||||
};
|
||||
|
||||
GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around)
|
||||
{
|
||||
switch (ent.getMetadata(PlayerID, "garrisonType"))
|
||||
/** TODO should add the units garrisoned inside garrisoned units */
|
||||
numberOfGarrisonedSlots(holder)
|
||||
{
|
||||
case GarrisonManager.TYPE_FORCE: // force the ungarrisoning
|
||||
return false;
|
||||
case GarrisonManager.TYPE_TRADE: // trader garrisoned in ship
|
||||
return true;
|
||||
case GarrisonManager.TYPE_PROTECTION: // hurt unit for healing or infantry for defense
|
||||
if (!this.holders.has(holder.id()))
|
||||
return holder.garrisonedSlots();
|
||||
|
||||
return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length;
|
||||
}
|
||||
|
||||
allowMelee(holder)
|
||||
{
|
||||
if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
|
||||
return true;
|
||||
const capture = ent.capturePoints();
|
||||
if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8)
|
||||
return true;
|
||||
if (ent.hasClasses(holder.getGarrisonArrowClasses()))
|
||||
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 */
|
||||
garrison(gameState, ent, holder, type)
|
||||
{
|
||||
if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison())
|
||||
return;
|
||||
|
||||
this.registerHolder(gameState, holder);
|
||||
this.holders.get(holder.id()).list.push(ent.id());
|
||||
|
||||
if (gameState.ai.Config.debug > 2)
|
||||
{
|
||||
if (around.unit || around.defenseStructure)
|
||||
warn("garrison unit " + ent.genericName() + " in " + holder.genericName() + " with type " + type);
|
||||
warn(" we try to garrison a unit with plan " + ent.getMetadata(PlayerID, "plan") + " and role " + ent.getMetadata(PlayerID, "role") +
|
||||
" and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport"));
|
||||
}
|
||||
|
||||
if (ent.getMetadata(PlayerID, "plan") !== undefined)
|
||||
ent.setMetadata(PlayerID, "plan", -2);
|
||||
else
|
||||
ent.setMetadata(PlayerID, "plan", -3);
|
||||
ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_GARRISONING);
|
||||
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
|
||||
*/
|
||||
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 */
|
||||
cancelGarrison(ent)
|
||||
{
|
||||
ent.stopMoving();
|
||||
this.leaveGarrison(ent);
|
||||
const holderId = ent.getMetadata(PlayerID, "garrisonHolder");
|
||||
if (!holderId || !this.holders.has(holderId))
|
||||
return;
|
||||
const list = this.holders.get(holderId).list;
|
||||
const index = list.indexOf(ent.id());
|
||||
if (index !== -1)
|
||||
list.splice(index, 1);
|
||||
}
|
||||
|
||||
keepGarrisoned(ent, holder, around)
|
||||
{
|
||||
switch (ent.getMetadata(PlayerID, "garrisonType"))
|
||||
{
|
||||
case GarrisonManager.TYPE_FORCE: // force the ungarrisoning
|
||||
return false;
|
||||
case GarrisonManager.TYPE_TRADE: // trader garrisoned in ship
|
||||
return true;
|
||||
case GarrisonManager.TYPE_PROTECTION: // hurt unit for healing or infantry for defense
|
||||
{
|
||||
if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
|
||||
return true;
|
||||
if (around.meleeSiege || around.rangeSiege)
|
||||
return ent.attackTypes().indexOf("Melee") === -1 || ent.healthLevel() < this.Config.garrisonHealthLevel.low;
|
||||
return false;
|
||||
const capture = ent.capturePoints();
|
||||
if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8)
|
||||
return true;
|
||||
if (ent.hasClasses(holder.getGarrisonArrowClasses()))
|
||||
{
|
||||
if (around.unit || around.defenseStructure)
|
||||
return true;
|
||||
if (around.meleeSiege || around.rangeSiege)
|
||||
return ent.attackTypes().indexOf("Melee") === -1 || ent.healthLevel() < this.Config.garrisonHealthLevel.low;
|
||||
return false;
|
||||
}
|
||||
if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1)
|
||||
return false;
|
||||
if (around.unit)
|
||||
return ent.hasClass("Support") || isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above
|
||||
if (isSiegeUnit(ent))
|
||||
return around.meleeSiege;
|
||||
return holder.buffHeal() && ent.needsHeal();
|
||||
}
|
||||
if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1)
|
||||
case GarrisonManager.TYPE_DECAY:
|
||||
return ent.captureStrength() && this.decayingStructures.has(holder.id());
|
||||
case GarrisonManager.TYPE_EMERGENCY: // f.e. hero in regicide mode
|
||||
if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
|
||||
return true;
|
||||
if (around.unit || around.defenseStructure || around.meleeSiege ||
|
||||
around.rangeSiege && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
|
||||
return true;
|
||||
return holder.buffHeal() && ent.needsHeal();
|
||||
default:
|
||||
if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager
|
||||
return true;
|
||||
aiWarn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") +
|
||||
" for " + ent.genericName() + " id " + ent.id() + " inside " + holder.genericName() +
|
||||
" id " + holder.id());
|
||||
ent.setMetadata(PlayerID, "garrisonType", GarrisonManager.TYPE_PROTECTION);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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)
|
||||
*/
|
||||
addDecayingStructure(gameState, entId, justCaptured)
|
||||
{
|
||||
if (this.decayingStructures.has(entId))
|
||||
return true;
|
||||
const ent = gameState.getEntityById(entId);
|
||||
if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire())
|
||||
return false;
|
||||
if (around.unit)
|
||||
return ent.hasClass("Support") || isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above
|
||||
if (isSiegeUnit(ent))
|
||||
return around.meleeSiege;
|
||||
return holder.buffHeal() && ent.needsHeal();
|
||||
}
|
||||
case GarrisonManager.TYPE_DECAY:
|
||||
return ent.captureStrength() && this.decayingStructures.has(holder.id());
|
||||
case GarrisonManager.TYPE_EMERGENCY: // f.e. hero in regicide mode
|
||||
if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
|
||||
return true;
|
||||
if (around.unit || around.defenseStructure || around.meleeSiege ||
|
||||
around.rangeSiege && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
|
||||
return true;
|
||||
return holder.buffHeal() && ent.needsHeal();
|
||||
default:
|
||||
if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager
|
||||
return true;
|
||||
aiWarn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") +
|
||||
" for " + ent.genericName() + " id " + ent.id() + " inside " + holder.genericName() +
|
||||
" id " + holder.id());
|
||||
ent.setMetadata(PlayerID, "garrisonType", GarrisonManager.TYPE_PROTECTION);
|
||||
if (!ent.territoryDecayRate() || !ent.garrisonRegenRate())
|
||||
return false;
|
||||
const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate());
|
||||
this.decayingStructures.set(entId, gmin);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/** Add this holder in the list managed by the garrisonManager */
|
||||
GarrisonManager.prototype.registerHolder = function(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);
|
||||
};
|
||||
removeDecayingStructure(entId)
|
||||
{
|
||||
if (!this.decayingStructures.has(entId))
|
||||
return;
|
||||
this.decayingStructures.delete(entId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if (this.decayingStructures.has(entId))
|
||||
return true;
|
||||
const ent = gameState.getEntityById(entId);
|
||||
if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire())
|
||||
return false;
|
||||
if (!ent.territoryDecayRate() || !ent.garrisonRegenRate())
|
||||
return false;
|
||||
const gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate());
|
||||
this.decayingStructures.set(entId, gmin);
|
||||
return true;
|
||||
};
|
||||
Serialize()
|
||||
{
|
||||
return { "holders": this.holders, "decayingStructures": this.decayingStructures };
|
||||
}
|
||||
|
||||
GarrisonManager.prototype.removeDecayingStructure = function(entId)
|
||||
{
|
||||
if (!this.decayingStructures.has(entId))
|
||||
return;
|
||||
this.decayingStructures.delete(entId);
|
||||
};
|
||||
|
||||
GarrisonManager.prototype.Serialize = function()
|
||||
{
|
||||
return { "holders": this.holders, "decayingStructures": this.decayingStructures };
|
||||
};
|
||||
|
||||
GarrisonManager.prototype.Deserialize = function(data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
};
|
||||
Deserialize(data)
|
||||
{
|
||||
for (const key in data)
|
||||
this[key] = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,160 +7,160 @@ 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;
|
||||
|
||||
empty()
|
||||
{
|
||||
this.plans = [];
|
||||
}
|
||||
|
||||
addPlan(newPlan)
|
||||
{
|
||||
if (!newPlan)
|
||||
return;
|
||||
for (const plan of this.plans)
|
||||
{
|
||||
if (newPlan.category === "unit" && plan.type == newPlan.type && plan.number + newPlan.number <= plan.maxMerge)
|
||||
{
|
||||
plan.addItem(newPlan.number);
|
||||
return;
|
||||
}
|
||||
else if (newPlan.category === "technology" && plan.type === newPlan.type)
|
||||
return;
|
||||
}
|
||||
this.plans.push(newPlan);
|
||||
}
|
||||
|
||||
check(gameState)
|
||||
{
|
||||
while (this.plans.length > 0)
|
||||
{
|
||||
if (!this.plans[0].isInvalid(gameState))
|
||||
return;
|
||||
const plan = this.plans.shift();
|
||||
if (plan.queueToReset)
|
||||
gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]);
|
||||
}
|
||||
}
|
||||
|
||||
getNext()
|
||||
{
|
||||
if (this.plans.length > 0)
|
||||
return this.plans[0];
|
||||
return null;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
maxAccountWanted(gameState, fraction)
|
||||
{
|
||||
const cost = new ResourcesManager();
|
||||
if (this.plans.length > 0 && this.plans[0].isGo(gameState))
|
||||
cost.add(this.plans[0].getCost());
|
||||
if (this.plans.length > 1 && this.plans[1].isGo(gameState) && fraction > 0)
|
||||
{
|
||||
const costs = this.plans[1].getCost();
|
||||
costs.multiply(fraction);
|
||||
cost.add(costs);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
queueCost()
|
||||
{
|
||||
const cost = new ResourcesManager();
|
||||
for (const plan of this.plans)
|
||||
cost.add(plan.getCost());
|
||||
return cost;
|
||||
}
|
||||
|
||||
length()
|
||||
{
|
||||
return this.plans.length;
|
||||
}
|
||||
|
||||
hasQueuedUnits()
|
||||
{
|
||||
return this.plans.length > 0;
|
||||
}
|
||||
|
||||
countQueuedUnits()
|
||||
{
|
||||
let count = 0;
|
||||
for (const plan of this.plans)
|
||||
count += plan.number;
|
||||
return count;
|
||||
}
|
||||
|
||||
hasQueuedUnitsWithClass(classe)
|
||||
{
|
||||
return this.plans.some(plan => plan.template && plan.template.hasClass(classe));
|
||||
}
|
||||
|
||||
countQueuedUnitsWithClass(classe)
|
||||
{
|
||||
let count = 0;
|
||||
for (const plan of this.plans)
|
||||
if (plan.template && plan.template.hasClass(classe))
|
||||
count += plan.number;
|
||||
return count;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
const plans = [];
|
||||
for (const plan of this.plans)
|
||||
plans.push(plan.Serialize());
|
||||
|
||||
return { "plans": plans, "paused": this.paused, "switched": this.switched };
|
||||
}
|
||||
|
||||
Deserialize(gameState, data)
|
||||
{
|
||||
this.paused = data.paused;
|
||||
this.switched = data.switched;
|
||||
this.plans = [];
|
||||
for (const dataPlan of data.plans)
|
||||
{
|
||||
let plan;
|
||||
if (dataPlan.category == "unit")
|
||||
plan = new TrainingPlan(gameState, dataPlan.type);
|
||||
else if (dataPlan.category == "building")
|
||||
plan = new ConstructionPlan(gameState, dataPlan.type);
|
||||
else if (dataPlan.category == "technology")
|
||||
plan = new ResearchPlan(gameState, dataPlan.type);
|
||||
else
|
||||
{
|
||||
aiWarn("Petra deserialization error: plan unknown " + uneval(dataPlan));
|
||||
continue;
|
||||
}
|
||||
plan.Deserialize(gameState, dataPlan);
|
||||
this.plans.push(plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Queue.prototype.empty = function()
|
||||
{
|
||||
this.plans = [];
|
||||
};
|
||||
|
||||
Queue.prototype.addPlan = function(newPlan)
|
||||
{
|
||||
if (!newPlan)
|
||||
return;
|
||||
for (const plan of this.plans)
|
||||
{
|
||||
if (newPlan.category === "unit" && plan.type == newPlan.type && plan.number + newPlan.number <= plan.maxMerge)
|
||||
{
|
||||
plan.addItem(newPlan.number);
|
||||
return;
|
||||
}
|
||||
else if (newPlan.category === "technology" && plan.type === newPlan.type)
|
||||
return;
|
||||
}
|
||||
this.plans.push(newPlan);
|
||||
};
|
||||
|
||||
Queue.prototype.check= function(gameState)
|
||||
{
|
||||
while (this.plans.length > 0)
|
||||
{
|
||||
if (!this.plans[0].isInvalid(gameState))
|
||||
return;
|
||||
const plan = this.plans.shift();
|
||||
if (plan.queueToReset)
|
||||
gameState.ai.queueManager.changePriority(plan.queueToReset, gameState.ai.Config.priorities[plan.queueToReset]);
|
||||
}
|
||||
};
|
||||
|
||||
Queue.prototype.getNext = function()
|
||||
{
|
||||
if (this.plans.length > 0)
|
||||
return this.plans[0];
|
||||
return null;
|
||||
};
|
||||
|
||||
Queue.prototype.startNext = function(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)
|
||||
{
|
||||
const cost = new ResourcesManager();
|
||||
if (this.plans.length > 0 && this.plans[0].isGo(gameState))
|
||||
cost.add(this.plans[0].getCost());
|
||||
if (this.plans.length > 1 && this.plans[1].isGo(gameState) && fraction > 0)
|
||||
{
|
||||
const costs = this.plans[1].getCost();
|
||||
costs.multiply(fraction);
|
||||
cost.add(costs);
|
||||
}
|
||||
return cost;
|
||||
};
|
||||
|
||||
Queue.prototype.queueCost = function()
|
||||
{
|
||||
const cost = new ResourcesManager();
|
||||
for (const plan of this.plans)
|
||||
cost.add(plan.getCost());
|
||||
return cost;
|
||||
};
|
||||
|
||||
Queue.prototype.length = function()
|
||||
{
|
||||
return this.plans.length;
|
||||
};
|
||||
|
||||
Queue.prototype.hasQueuedUnits = function()
|
||||
{
|
||||
return this.plans.length > 0;
|
||||
};
|
||||
|
||||
Queue.prototype.countQueuedUnits = function()
|
||||
{
|
||||
let count = 0;
|
||||
for (const plan of this.plans)
|
||||
count += plan.number;
|
||||
return count;
|
||||
};
|
||||
|
||||
Queue.prototype.hasQueuedUnitsWithClass = function(classe)
|
||||
{
|
||||
return this.plans.some(plan => plan.template && plan.template.hasClass(classe));
|
||||
};
|
||||
|
||||
Queue.prototype.countQueuedUnitsWithClass = function(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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.paused = data.paused;
|
||||
this.switched = data.switched;
|
||||
this.plans = [];
|
||||
for (const dataPlan of data.plans)
|
||||
{
|
||||
let plan;
|
||||
if (dataPlan.category == "unit")
|
||||
plan = new TrainingPlan(gameState, dataPlan.type);
|
||||
else if (dataPlan.category == "building")
|
||||
plan = new ConstructionPlan(gameState, dataPlan.type);
|
||||
else if (dataPlan.category == "technology")
|
||||
plan = new ResearchPlan(gameState, dataPlan.type);
|
||||
else
|
||||
{
|
||||
aiWarn("Petra deserialization error: plan unknown " + uneval(dataPlan));
|
||||
continue;
|
||||
}
|
||||
plan.Deserialize(gameState, dataPlan);
|
||||
this.plans.push(plan);
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,242 +4,245 @@ import { Worker } from "simulation/ai/petra/worker.js";
|
||||
/**
|
||||
* Manage the research
|
||||
*/
|
||||
export function ResearchManager(Config)
|
||||
export class ResearchManager
|
||||
{
|
||||
this.Config = Config;
|
||||
constructor(Config)
|
||||
{
|
||||
this.Config = Config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can go to the next phase
|
||||
*/
|
||||
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
|
||||
if (queues.civilCentre.hasQueuedUnits() || queues.wonder.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const currentPhaseIndex = gameState.currentPhase();
|
||||
const nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1);
|
||||
if (!nextPhaseName)
|
||||
return;
|
||||
|
||||
const petraRequirements =
|
||||
currentPhaseIndex == 1 && gameState.ai.HQ.getAccountedPopulation(gameState) >= this.Config.Economy.popPhase2 ||
|
||||
currentPhaseIndex == 2 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase3 ||
|
||||
currentPhaseIndex >= 3 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase4;
|
||||
if (petraRequirements && gameState.hasResearchers(nextPhaseName, true))
|
||||
{
|
||||
gameState.ai.HQ.phasing = currentPhaseIndex + 1;
|
||||
// Reset the queue priority in case it was changed during a previous phase update
|
||||
gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech);
|
||||
queues.majorTech.addPlan(new ResearchPlan(gameState, nextPhaseName, true));
|
||||
}
|
||||
}
|
||||
|
||||
researchPopulationBonus(gameState, queues)
|
||||
{
|
||||
if (queues.minorTech.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const techs = gameState.findAvailableTech();
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (!tech[1]._template.modifications)
|
||||
continue;
|
||||
// TODO may-be loop on all modifs and check if the effect if positive ?
|
||||
if (tech[1]._template.modifications[0].value !== "Population/Bonus")
|
||||
continue;
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
researchTradeBonus(gameState, queues)
|
||||
{
|
||||
if (queues.minorTech.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const techs = gameState.findAvailableTech();
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (!tech[1]._template.modifications || !tech[1]._template.affects)
|
||||
continue;
|
||||
if (tech[1]._template.affects.indexOf("Trader") === -1)
|
||||
continue;
|
||||
// TODO may-be loop on all modifs and check if the effect if positive ?
|
||||
if (tech[1]._template.modifications[0].value !== "UnitMotion/WalkSpeed" &&
|
||||
tech[1]._template.modifications[0].value !== "Trader/GainMultiplier")
|
||||
continue;
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (tech[0].indexOf("unlock_champion") == 0)
|
||||
return { "name": tech[0], "increasePriority": true };
|
||||
if (tech[0] == "traditional_army_sele" || tech[0] == "reformed_army_sele")
|
||||
return { "name": pickRandom(["traditional_army_sele", "reformed_army_sele"]), "increasePriority": true };
|
||||
|
||||
if (!tech[1]._template.modifications)
|
||||
continue;
|
||||
const template = tech[1]._template;
|
||||
if (phase1)
|
||||
{
|
||||
const cost = template.cost;
|
||||
let costMax = 0;
|
||||
for (const res in cost)
|
||||
costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
|
||||
if (10*numWorkers < costMax)
|
||||
continue;
|
||||
}
|
||||
for (const i in template.modifications)
|
||||
{
|
||||
if (gameState.ai.HQ.navalMap && template.modifications[i].value === "ResourceGatherer/Rates/food.fish")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/food.fruit")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/food.grain")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/wood.tree")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value.startsWith("ResourceGatherer/Capacities"))
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
else if (template.modifications[i].value === "Attack/Ranged/MaxRange")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (!tech[1]._template.modifications)
|
||||
continue;
|
||||
const template = tech[1]._template;
|
||||
if (phase2)
|
||||
{
|
||||
const cost = template.cost;
|
||||
let costMax = 0;
|
||||
for (const res in cost)
|
||||
costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
|
||||
if (10*numWorkers < costMax)
|
||||
continue;
|
||||
}
|
||||
for (const i in template.modifications)
|
||||
{
|
||||
if (template.modifications[i].value === "ResourceGatherer/Rates/stone.rock")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/metal.ore")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "BuildingAI/DefaultArrowCount")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "Health/RegenRate")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
else if (template.modifications[i].value === "Health/IdleRegenRate")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
update(gameState, queues)
|
||||
{
|
||||
if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const techs = gameState.findAvailableTech();
|
||||
|
||||
let techName = this.researchWantedTechs(gameState, techs);
|
||||
if (techName)
|
||||
{
|
||||
if (techName.increasePriority)
|
||||
{
|
||||
gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech);
|
||||
const plan = new ResearchPlan(gameState, techName.name);
|
||||
plan.queueToReset = "minorTech";
|
||||
queues.minorTech.addPlan(plan);
|
||||
}
|
||||
else
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.currentPhase() < 2)
|
||||
return;
|
||||
|
||||
techName = this.researchPreferredTechs(gameState, techs);
|
||||
if (techName)
|
||||
{
|
||||
if (techName.increasePriority)
|
||||
{
|
||||
gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech);
|
||||
const plan = new ResearchPlan(gameState, techName.name);
|
||||
plan.queueToReset = "minorTech";
|
||||
queues.minorTech.addPlan(plan);
|
||||
}
|
||||
else
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.currentPhase() < 3)
|
||||
return;
|
||||
|
||||
// remove some techs not yet used by this AI
|
||||
// remove also sharedLos if we have no ally
|
||||
for (let i = 0; i < techs.length; ++i)
|
||||
{
|
||||
const template = techs[i][1]._template;
|
||||
if (template.affects && template.affects.length === 1 &&
|
||||
(template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "Wall"))
|
||||
{
|
||||
techs.splice(i--, 1);
|
||||
continue;
|
||||
}
|
||||
if (template.modifications && template.modifications.length === 1 &&
|
||||
this.Config.unusedNoAllyTechs.includes(template.modifications[0].value) &&
|
||||
!gameState.hasAllies())
|
||||
{
|
||||
techs.splice(i--, 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!techs.length)
|
||||
return;
|
||||
|
||||
// randomly pick one. No worries about pairs in that case.
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, pickRandom(techs)[0]));
|
||||
}
|
||||
|
||||
CostSum(cost)
|
||||
{
|
||||
let costSum = 0;
|
||||
for (const res in cost)
|
||||
costSum += cost[res];
|
||||
return costSum;
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
Deserialize(data)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can go to the next phase
|
||||
*/
|
||||
ResearchManager.prototype.checkPhase = function(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
|
||||
if (queues.civilCentre.hasQueuedUnits() || queues.wonder.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const currentPhaseIndex = gameState.currentPhase();
|
||||
const nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1);
|
||||
if (!nextPhaseName)
|
||||
return;
|
||||
|
||||
const petraRequirements =
|
||||
currentPhaseIndex == 1 && gameState.ai.HQ.getAccountedPopulation(gameState) >= this.Config.Economy.popPhase2 ||
|
||||
currentPhaseIndex == 2 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase3 ||
|
||||
currentPhaseIndex >= 3 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase4;
|
||||
if (petraRequirements && gameState.hasResearchers(nextPhaseName, true))
|
||||
{
|
||||
gameState.ai.HQ.phasing = currentPhaseIndex + 1;
|
||||
// Reset the queue priority in case it was changed during a previous phase update
|
||||
gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech);
|
||||
queues.majorTech.addPlan(new ResearchPlan(gameState, nextPhaseName, true));
|
||||
}
|
||||
};
|
||||
|
||||
ResearchManager.prototype.researchPopulationBonus = function(gameState, queues)
|
||||
{
|
||||
if (queues.minorTech.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const techs = gameState.findAvailableTech();
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (!tech[1]._template.modifications)
|
||||
continue;
|
||||
// TODO may-be loop on all modifs and check if the effect if positive ?
|
||||
if (tech[1]._template.modifications[0].value !== "Population/Bonus")
|
||||
continue;
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, tech[0]));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ResearchManager.prototype.researchTradeBonus = function(gameState, queues)
|
||||
{
|
||||
if (queues.minorTech.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const techs = gameState.findAvailableTech();
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (!tech[1]._template.modifications || !tech[1]._template.affects)
|
||||
continue;
|
||||
if (tech[1]._template.affects.indexOf("Trader") === -1)
|
||||
continue;
|
||||
// TODO may-be loop on all modifs and check if the effect if positive ?
|
||||
if (tech[1]._template.modifications[0].value !== "UnitMotion/WalkSpeed" &&
|
||||
tech[1]._template.modifications[0].value !== "Trader/GainMultiplier")
|
||||
continue;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (tech[0].indexOf("unlock_champion") == 0)
|
||||
return { "name": tech[0], "increasePriority": true };
|
||||
if (tech[0] == "traditional_army_sele" || tech[0] == "reformed_army_sele")
|
||||
return { "name": pickRandom(["traditional_army_sele", "reformed_army_sele"]), "increasePriority": true };
|
||||
|
||||
if (!tech[1]._template.modifications)
|
||||
continue;
|
||||
const template = tech[1]._template;
|
||||
if (phase1)
|
||||
{
|
||||
const cost = template.cost;
|
||||
let costMax = 0;
|
||||
for (const res in cost)
|
||||
costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
|
||||
if (10*numWorkers < costMax)
|
||||
continue;
|
||||
}
|
||||
for (const i in template.modifications)
|
||||
{
|
||||
if (gameState.ai.HQ.navalMap && template.modifications[i].value === "ResourceGatherer/Rates/food.fish")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/food.fruit")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/food.grain")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/wood.tree")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value.startsWith("ResourceGatherer/Capacities"))
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
else if (template.modifications[i].value === "Attack/Ranged/MaxRange")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Techs to be searched for as soon as they are available, but only after phase 2 */
|
||||
ResearchManager.prototype.researchPreferredTechs = function(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;
|
||||
for (const tech of techs)
|
||||
{
|
||||
if (!tech[1]._template.modifications)
|
||||
continue;
|
||||
const template = tech[1]._template;
|
||||
if (phase2)
|
||||
{
|
||||
const cost = template.cost;
|
||||
let costMax = 0;
|
||||
for (const res in cost)
|
||||
costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
|
||||
if (10*numWorkers < costMax)
|
||||
continue;
|
||||
}
|
||||
for (const i in template.modifications)
|
||||
{
|
||||
if (template.modifications[i].value === "ResourceGatherer/Rates/stone.rock")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "ResourceGatherer/Rates/metal.ore")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "BuildingAI/DefaultArrowCount")
|
||||
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
|
||||
else if (template.modifications[i].value === "Health/RegenRate")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
else if (template.modifications[i].value === "Health/IdleRegenRate")
|
||||
return { "name": tech[0], "increasePriority": false };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
ResearchManager.prototype.update = function(gameState, queues)
|
||||
{
|
||||
if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits())
|
||||
return;
|
||||
|
||||
const techs = gameState.findAvailableTech();
|
||||
|
||||
let techName = this.researchWantedTechs(gameState, techs);
|
||||
if (techName)
|
||||
{
|
||||
if (techName.increasePriority)
|
||||
{
|
||||
gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech);
|
||||
const plan = new ResearchPlan(gameState, techName.name);
|
||||
plan.queueToReset = "minorTech";
|
||||
queues.minorTech.addPlan(plan);
|
||||
}
|
||||
else
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.currentPhase() < 2)
|
||||
return;
|
||||
|
||||
techName = this.researchPreferredTechs(gameState, techs);
|
||||
if (techName)
|
||||
{
|
||||
if (techName.increasePriority)
|
||||
{
|
||||
gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech);
|
||||
const plan = new ResearchPlan(gameState, techName.name);
|
||||
plan.queueToReset = "minorTech";
|
||||
queues.minorTech.addPlan(plan);
|
||||
}
|
||||
else
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, techName.name));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.currentPhase() < 3)
|
||||
return;
|
||||
|
||||
// remove some techs not yet used by this AI
|
||||
// remove also sharedLos if we have no ally
|
||||
for (let i = 0; i < techs.length; ++i)
|
||||
{
|
||||
const template = techs[i][1]._template;
|
||||
if (template.affects && template.affects.length === 1 &&
|
||||
(template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "Wall"))
|
||||
{
|
||||
techs.splice(i--, 1);
|
||||
continue;
|
||||
}
|
||||
if (template.modifications && template.modifications.length === 1 &&
|
||||
this.Config.unusedNoAllyTechs.includes(template.modifications[0].value) &&
|
||||
!gameState.hasAllies())
|
||||
{
|
||||
techs.splice(i--, 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!techs.length)
|
||||
return;
|
||||
|
||||
// randomly pick one. No worries about pairs in that case.
|
||||
queues.minorTech.addPlan(new ResearchPlan(gameState, pickRandom(techs)[0]));
|
||||
};
|
||||
|
||||
ResearchManager.prototype.CostSum = function(cost)
|
||||
{
|
||||
let costSum = 0;
|
||||
for (const res in cost)
|
||||
costSum += cost[res];
|
||||
return costSum;
|
||||
};
|
||||
|
||||
ResearchManager.prototype.Serialize = function()
|
||||
{
|
||||
return {};
|
||||
};
|
||||
|
||||
ResearchManager.prototype.Deserialize = function(data)
|
||||
{
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user