Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 18740)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 18741)
@@ -1,938 +1,937 @@
var API3 = function(m)
{
// defines a template.
// It's completely raw data, except it's slightly cleverer now and then.
m.Template = m.Class({
_init: function(template)
{
this._template = template;
this._tpCache = new Map();
},
// helper function to return a template value, optionally adjusting for tech.
// TODO: there's no support for "_string" values here.
get: function(string)
{
let value = this._template;
if (this._entityModif && this._entityModif.has(string))
return this._entityModif.get(string);
else if (this._templateModif && this._templateModif.has(string))
return this._templateModif.get(string);
if (!this._tpCache.has(string))
{
let args = string.split("/");
for (let arg of args)
{
if (value[arg])
value = value[arg];
else
{
value = undefined;
break;
}
}
this._tpCache.set(string, value);
}
return this._tpCache.get(string);
},
genericName: function() {
return this.get("Identity/GenericName");
},
rank: function() {
return this.get("Identity/Rank");
},
classes: function() {
let template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
},
requiredTech: function() {
return this.get("Identity/RequiredTechnology");
},
available: function(gameState) {
let techRequired = this.requiredTech();
if (!techRequired)
return true;
return gameState.isResearched(techRequired);
},
// specifically
phase: function() {
let techRequired = this.requiredTech();
if (!techRequired)
return 0;
if (techRequired === "phase_village")
return 1;
if (techRequired === "phase_town")
return 2;
if (techRequired === "phase_city")
return 3;
return 0;
},
hasClass: function(name) {
if (!this._classes)
this._classes = this.classes();
let classes = this._classes;
return classes && classes.indexOf(name) !== -1;
},
hasClasses: function(array) {
if (!this._classes)
this._classes = this.classes();
let classes = this._classes;
if (!classes)
return false;
for (let cls of array)
if (classes.indexOf(cls) === -1)
return false;
return true;
},
civ: function() {
return this.get("Identity/Civ");
},
"cost": function(productionQueue) {
if (!this.get("Cost"))
return undefined;
let ret = {};
for (let type in this.get("Cost/Resources"))
ret[type] = +this.get("Cost/Resources/" + type);
return ret;
},
"costSum": function(productionQueue) {
let cost = this.cost(productionQueue);
if (!cost)
return undefined;
let ret = 0;
for (let type in cost)
ret += cost[type];
return ret;
},
"techCostMultiplier": function(type) {
return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1);
},
/**
* Returns the radius of a circle surrounding this entity's
* obstruction shape, or undefined if no obstruction.
*/
obstructionRadius: function() {
if (!this.get("Obstruction"))
return undefined;
if (this.get("Obstruction/Static"))
{
let w = +this.get("Obstruction/Static/@width");
let h = +this.get("Obstruction/Static/@depth");
return Math.sqrt(w*w + h*h) / 2;
}
if (this.get("Obstruction/Unit"))
return +this.get("Obstruction/Unit/@radius");
return 0; // this should never happen
},
/**
* Returns the radius of a circle surrounding this entity's
* footprint.
*/
footprintRadius: function() {
if (!this.get("Footprint"))
return undefined;
if (this.get("Footprint/Square"))
{
let w = +this.get("Footprint/Square/@width");
let h = +this.get("Footprint/Square/@depth");
return Math.sqrt(w*w + h*h) / 2;
}
if (this.get("Footprint/Circle"))
return +this.get("Footprint/Circle/@radius");
return 0; // this should never happen
},
maxHitpoints: function()
{
return +(this.get("Health/Max") || 0);
},
isHealable: function()
{
if (this.get("Health") !== undefined)
return this.get("Health/Unhealable") !== "true";
return false;
},
isRepairable: function()
{
return this.get("Repairable") !== undefined;
},
getPopulationBonus: function() {
return +this.get("Cost/PopulationBonus");
},
armourStrengths: function() {
if (!this.get("Armour"))
return undefined;
return {
hack: +this.get("Armour/Hack"),
pierce: +this.get("Armour/Pierce"),
crush: +this.get("Armour/Crush")
};
},
attackTypes: function() {
if (!this.get("Attack"))
return undefined;
let ret = [];
for (let type in this.get("Attack"))
ret.push(type);
return ret;
},
attackRange: function(type) {
if (!this.get("Attack/" + type +""))
return undefined;
return {
max: +this.get("Attack/" + type +"/MaxRange"),
min: +(this.get("Attack/" + type +"/MinRange") || 0)
};
},
attackStrengths: function(type) {
if (!this.get("Attack/" + type +""))
return undefined;
return {
hack: +(this.get("Attack/" + type + "/Hack") || 0),
pierce: +(this.get("Attack/" + type + "/Pierce") || 0),
crush: +(this.get("Attack/" + type + "/Crush") || 0)
};
},
captureStrength: function() {
if (!this.get("Attack/Capture"))
return undefined;
return +this.get("Attack/Capture/Value") || 0;
},
attackTimes: function(type) {
if (!this.get("Attack/" + type +""))
return undefined;
return {
prepare: +(this.get("Attack/" + type + "/PrepareTime") || 0),
repeat: +(this.get("Attack/" + type + "/RepeatTime") || 1000)
};
},
// returns the classes this templates counters:
// Return type is [ [-neededClasses- , multiplier], … ].
getCounteredClasses: function() {
if (!this.get("Attack"))
return undefined;
let Classes = [];
for (let type in this.get("Attack"))
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses" + b +"/Multiplier")]);
}
}
return Classes;
},
// returns true if the entity counters those classes.
// TODO: refine using the multiplier
countersClasses: function(classes) {
if (!this.get("Attack"))
return false;
let mcounter = [];
for (let type in this.get("Attack"))
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
mcounter.concat(bonusClasses.split(" "));
}
}
for (let i in classes)
{
if (mcounter.indexOf(classes[i]) !== -1)
return true;
}
return false;
},
// returns, if it exists, the multiplier from each attack against a given class
getMultiplierAgainst: function(type, againstClass) {
if (!this.get("Attack/" + type +""))
return undefined;
if (this.get("Attack/" + type + "/Bonuses"))
{
for (let b in this.get("Attack/" + type + "/Bonuses"))
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (!bonusClasses)
continue;
for (let bcl of bonusesClasses.split(" "))
if (bcl === againstClass)
return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
}
}
return 1;
},
// returns true if the entity can attack the given class
canAttackClass: function(saidClass) {
if (!this.get("Attack"))
return false;
for (let type in this.get("Attack"))
{
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
- if (!restrictedClasses)
- continue;
- if (restrictedClasses.split(" ").indexOf(saidClass) !== -1)
+ if (restrictedClasses && !MatchesClassList([saidClass], restrictedClasses))
return false;
}
+
return true;
},
"buildableEntities": function() {
let templates = this.get("Builder/Entities/_string");
if (!templates)
return [];
let civ = this.civ();
return templates.replace(/\{civ\}/g, civ).split(/\s+/);
},
"trainableEntities": function(civ) {
let templates = this.get("ProductionQueue/Entities/_string");
if (!templates)
return undefined;
if (civ)
templates = templates.replace(/\{civ\}/g, civ);
return templates.split(/\s+/);
},
"researchableTechs": function(civ) {
if (this.civ() !== civ) // techs can only be researched in structures from the player civ TODO no more true
return undefined;
let templates = this.get("ProductionQueue/Technologies/_string");
if (!templates)
return undefined;
return templates.split(/\s+/);
},
resourceSupplyType: function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
},
// will return either "food", "wood", "stone", "metal" and not treasure.
getResourceType: function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
if (type == "treasure")
return subtype;
return type;
},
resourceSupplyMax: function() {
return +this.get("ResourceSupply/Amount");
},
maxGatherers: function() {
return +(this.get("ResourceSupply/MaxGatherers") || 0);
},
resourceGatherRates: function() {
if (!this.get("ResourceGatherer"))
return undefined;
let ret = {};
let baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
for (let r in this.get("ResourceGatherer/Rates"))
ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
return ret;
},
resourceDropsiteTypes: function() {
if (!this.get("ResourceDropsite"))
return undefined;
let types = this.get("ResourceDropsite/Types");
return types ? types.split(/\s+/) : [];
},
garrisonableClasses: function() {
return this.get("GarrisonHolder/List/_string");
},
garrisonMax: function() {
return this.get("GarrisonHolder/Max");
},
garrisonEjectHealth: function() {
return +this.get("GarrisonHolder/EjectHealth");
},
getDefaultArrow: function() {
return +this.get("BuildingAI/DefaultArrowCount");
},
getArrowMultiplier: function() {
return +this.get("BuildingAI/GarrisonArrowMultiplier");
},
getGarrisonArrowClasses: function() {
if (!this.get("BuildingAI"))
return undefined;
return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
},
buffHeal: function() {
return +this.get("GarrisonHolder/BuffHeal");
},
promotion: function() {
return this.get("Promotion/Entity");
},
/**
* Returns whether this is an animal that is too difficult to hunt.
* (Any non domestic currently.)
*/
isHuntable: function() {
if(!this.get("ResourceSupply/KillBeforeGather"))
return false;
// special case: rabbits too difficult to hunt for such a small food amount
let specificName = this.get("Identity/SpecificName");
if (specificName && specificName === "Rabbit")
return false;
// do not hunt retaliating animals (animals without UnitAI are dead animals)
let behaviour = this.get("UnitAI/NaturalBehaviour");
return !this.get("UnitAI") ||
!(behaviour === "violent" || behaviour === "aggressive" || behaviour === "defensive");
},
walkSpeed: function() {
return +this.get("UnitMotion/WalkSpeed");
},
trainingCategory: function() {
return this.get("TrainingRestrictions/Category");
},
buildCategory: function() {
return this.get("BuildRestrictions/Category");
},
"buildTime": function(productionQueue) {
let time = +this.get("Cost/BuildTime");
if (productionQueue)
time *= productionQueue.techCostMultiplier("time");
return time;
},
buildDistance: function() {
return this.get("BuildRestrictions/Distance");
},
buildPlacementType: function() {
return this.get("BuildRestrictions/PlacementType");
},
buildTerritories: function() {
if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory"))
return undefined;
return this.get("BuildRestrictions/Territory").split(/\s+/);
},
hasBuildTerritory: function(territory) {
let territories = this.buildTerritories();
return territories && territories.indexOf(territory) !== -1;
},
hasTerritoryInfluence: function() {
return this.get("TerritoryInfluence") !== undefined;
},
hasDefensiveFire: function() {
if (!this.get("Attack/Ranged"))
return false;
return this.getDefaultArrow() || this.getArrowMultiplier();
},
territoryInfluenceRadius: function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Radius");
return -1;
},
territoryInfluenceWeight: function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Weight");
return -1;
},
territoryDecayRate: function() {
return +(this.get("TerritoryDecay/DecayRate") || 0);
},
defaultRegenRate: function() {
return +(this.get("Capturable/RegenRate") || 0);
},
garrisonRegenRate: function() {
return +(this.get("Capturable/GarrisonRegenRate") || 0);
},
visionRange: function() {
return +this.get("Vision/Range");
},
gainMultiplier: function() {
return +this.get("Trader/GainMultiplier");
}
});
// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
m.Entity = m.Class({
_super: m.Template,
_init: function(sharedAI, entity)
{
this._super.call(this, sharedAI.GetTemplate(entity.template));
this._templateName = entity.template;
this._entity = entity;
this._ai = sharedAI;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[entity.owner][this._templateName])
sharedAI._templatesModifications[entity.owner][this._templateName] = new Map();
this._templateModif = sharedAI._templatesModifications[entity.owner][this._templateName];
// save a reference to the entity tech/aura modifications
if (!sharedAI._entitiesModifications.has(entity.id))
sharedAI._entitiesModifications.set(entity.id, new Map());
this._entityModif = sharedAI._entitiesModifications.get(entity.id);
},
toString: function() {
return "[Entity " + this.id() + " " + this.templateName() + "]";
},
id: function() {
return this._entity.id;
},
templateName: function() {
return this._templateName;
},
/**
* Returns extra data that the AI scripts have associated with this entity,
* for arbitrary local annotations.
* (This data should not be shared with any other AI scripts.)
*/
getMetadata: function(player, key) {
return this._ai.getMetadata(player, this, key);
},
/**
* Sets extra data to be associated with this entity.
*/
setMetadata: function(player, key, value) {
this._ai.setMetadata(player, this, key, value);
},
deleteAllMetadata: function(player) {
delete this._ai._entityMetadata[player][this.id()];
},
deleteMetadata: function(player, key) {
this._ai.deleteMetadata(player, this, key);
},
position: function() { return this._entity.position; },
angle: function() { return this._entity.angle; },
isIdle: function() {
if (typeof this._entity.idle === "undefined")
return undefined;
return this._entity.idle;
},
unitAIState: function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; },
unitAIOrderData: function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; },
hitpoints: function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; },
isHurt: function() { return this.hitpoints() < this.maxHitpoints(); },
healthLevel: function() { return this.hitpoints() / this.maxHitpoints(); },
needsHeal: function() { return this.isHurt() && this.isHealable(); },
needsRepair: function() { return this.isHurt() && this.isRepairable(); },
decaying: function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; },
capturePoints: function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; },
"isSharedDropsite": function() { return this._entity.sharedDropsite === true; },
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
trainingQueue: function() {
let queue = this._entity.trainingQueue;
return queue;
},
trainingQueueTime: function() {
let queue = this._entity.trainingQueue;
if (!queue)
return undefined;
let time = 0;
for (let item of queue)
time += item.timeRemaining;
return time/1000;
},
foundationProgress: function() {
if (this._entity.foundationProgress === undefined)
return undefined;
return this._entity.foundationProgress;
},
getBuilders: function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return [];
return this._entity.foundationBuilders;
},
getBuildersNb: function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return 0;
return this._entity.foundationBuilders.length;
},
owner: function() {
return this._entity.owner;
},
isOwn: function(player) {
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === player;
},
resourceSupplyAmount: function() {
if (this._entity.resourceSupplyAmount === undefined)
return undefined;
return this._entity.resourceSupplyAmount;
},
resourceSupplyNumGatherers: function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this._entity.resourceSupplyNumGatherers;
return undefined;
},
isFull: function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this.maxGatherers() === this._entity.resourceSupplyNumGatherers;
return undefined;
},
resourceCarrying: function() {
if (this._entity.resourceCarrying === undefined)
return undefined;
return this._entity.resourceCarrying;
},
currentGatherRate: function() {
// returns the gather rate for the current target if applicable.
if (!this.get("ResourceGatherer"))
return undefined;
if (this.unitAIOrderData().length &&
(this.unitAIState().split(".")[1] === "GATHER" || this.unitAIState().split(".")[1] === "RETURNRESOURCE"))
{
let res;
// this is an abuse of "_ai" but it works.
if (this.unitAIState().split(".")[1] === "GATHER" && this.unitAIOrderData()[0].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[0].target);
else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[1].target);
if (!res)
return 0;
let type = res.resourceSupplyType();
if (!type)
return 0;
if (type.generic === "treasure")
return 1000;
let tstring = type.generic + "." + type.specific;
let rate = +this.get("ResourceGatherer/BaseSpeed");
rate *= +this.get("ResourceGatherer/Rates/" +tstring);
if (rate)
return rate;
return 0;
}
return undefined;
},
isBuilder: function() { return this.get("Builder") !== undefined; },
isGatherer: function() { return this.get("ResourceGatherer") !== undefined; },
canGather: function(type) {
let gatherRates = this.get("ResourceGatherer/Rates");
if (!gatherRates)
return false;
for (let r in gatherRates)
if (r.split('.')[0] === type)
return true;
return false;
},
isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; },
garrisoned: function() { return this._entity.garrisoned; },
canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); },
move: function(x, z, queued = false) {
Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued });
return this;
},
moveToRange: function(x, z, min, max, queued = false) {
Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
return this;
},
attackMove: function(x, z, targetClasses, queued = false) {
Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued });
return this;
},
// violent, aggressive, defensive, passive, standground
setStance: function(stance, queued = false) {
Engine.PostCommand(PlayerID,{"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued });
return this;
},
stopMoving: function() {
Engine.PostCommand(PlayerID,{"type": "stop", "entities": [this.id()], "queued": false});
},
unload: function(id) {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID,{"type": "unload", "garrisonHolder": this.id(), "entities": [id]});
return this;
},
// Unloads all owned units, don't unload allies
unloadAll: function() {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID,{"type": "unload-all-by-owner", "garrisonHolders": [this.id()]});
return this;
},
garrison: function(target, queued = false) {
Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": queued});
return this;
},
attack: function(unitId, allowCapture = true, queued = false) {
Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued});
return this;
},
// moveApart from a point in the opposite direction with a distance dist
moveApart: function(point, dist) {
if (this.position() !== undefined) {
let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
let norm = m.VectorDistance(point, this.position());
if (norm === 0)
direction = [1, 0];
else
{
direction[0] /= norm;
direction[1] /= norm;
}
Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false});
}
return this;
},
// Flees from a unit in the opposite direction.
flee: function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) {
let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
this.position()[1] - unitToFleeFrom.position()[1]];
let dist = m.VectorDistance(unitToFleeFrom.position(), this.position() );
FleeDirection[0] = 40 * FleeDirection[0]/dist;
FleeDirection[1] = 40 * FleeDirection[1]/dist;
Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false});
}
return this;
},
gather: function(target, queued = false) {
Engine.PostCommand(PlayerID,{"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued});
return this;
},
repair: function(target, autocontinue = false, queued = false) {
Engine.PostCommand(PlayerID,{"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued});
return this;
},
returnResources: function(target, queued = false) {
Engine.PostCommand(PlayerID,{"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued});
return this;
},
destroy: function() {
Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": [this.id()] });
return this;
},
barter: function(buyType, sellType, amount) {
Engine.PostCommand(PlayerID,{"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount });
return this;
},
tradeRoute: function(target, source) {
Engine.PostCommand(PlayerID,{"type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false });
return this;
},
setRallyPoint: function(target, command) {
let data = {"command": command, "target": target.id()};
Engine.PostCommand(PlayerID, {"type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data});
return this;
},
unsetRallyPoint: function() {
Engine.PostCommand(PlayerID, {"type": "unset-rallypoint", "entities": [this.id()]});
return this;
},
train: function(civ, type, count, metadata, promotedTypes)
{
let trainable = this.trainableEntities(civ);
if (!trainable)
{
error("Called train("+type+", "+count+") on non-training entity "+this);
return this;
}
if (trainable.indexOf(type) === -1)
{
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
return this;
}
Engine.PostCommand(PlayerID,{
"type": "train",
"entities": [this.id()],
"template": type,
"count": count,
"metadata": metadata,
"promoted": promotedTypes
});
return this;
},
construct: function(template, x, z, angle, metadata) {
// TODO: verify this unit can construct this, just for internal
// sanity-checking and error reporting
Engine.PostCommand(PlayerID,{
"type": "construct",
"entities": [this.id()],
"template": template,
"x": x,
"z": z,
"angle": angle,
"autorepair": false,
"autocontinue": false,
"queued": false,
"metadata" : metadata // can be undefined
});
return this;
},
research: function(template) {
Engine.PostCommand(PlayerID,{ "type": "research", "entity": this.id(), "template": template });
return this;
},
stopProduction: function(id) {
Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": id });
return this;
},
stopAllProduction: function(percentToStopAt) {
let queue = this._entity.trainingQueue;
if (!queue)
return true; // no queue, so technically we stopped all production.
for (let item of queue)
if (item.progress < percentToStopAt)
Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": item.id });
return this;
}
});
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 18740)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 18741)
@@ -1,608 +1,608 @@
function Attack() {}
Attack.prototype.bonusesSchema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Attack.prototype.preferredClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.restrictedClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.Schema =
"Controls the attack abilities and strengths of the unit." +
"" +
"" +
"10.0" +
"0.0" +
"5.0" +
"4.0" +
"1000" +
"" +
"" +
"pers" +
"Infantry" +
"1.5" +
"" +
"" +
"Cavalry Melee" +
"1.5" +
"" +
"" +
"Champion" +
"Cavalry Infantry" +
"" +
"" +
"0.0" +
"10.0" +
"0.0" +
"44.0" +
"20.0" +
"15.0" +
"800" +
"1600" +
"50.0" +
"2.5" +
"" +
"" +
"Cavalry" +
"2" +
"" +
"" +
"Champion" +
"" +
"Circular" +
"20" +
"false" +
"0.0" +
"10.0" +
"0.0" +
"" +
"" +
"" +
"1000.0" +
"0.0" +
"0.0" +
"4.0" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Attack.prototype.bonusesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" + // TODO: how do these work?
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"";
Attack.prototype.Init = function()
{
};
Attack.prototype.Serialize = null; // we have no dynamic state to save
Attack.prototype.GetAttackTypes = function()
{
return ["Melee", "Ranged", "Capture"].filter(type => !!this.template[type]);
};
Attack.prototype.GetPreferredClasses = function(type)
{
if (this.template[type] && this.template[type].PreferredClasses &&
this.template[type].PreferredClasses._string)
return this.template[type].PreferredClasses._string.split(/\s+/);
return [];
};
Attack.prototype.GetRestrictedClasses = function(type)
{
if (this.template[type] && this.template[type].RestrictedClasses &&
this.template[type].RestrictedClasses._string)
return this.template[type].RestrictedClasses._string.split(/\s+/);
return [];
};
Attack.prototype.CanAttack = function(target)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
return true;
let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
return false;
// Check if the relative height difference is larger than the attack range
// If the relative height is bigger, it means they will never be able to
// reach each other, no matter how close they come.
let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
const targetClasses = cmpIdentity.GetClassesList();
for (let type of this.GetAttackTypes())
{
if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))
continue;
if (heightDiff > this.GetRange(type).max)
continue;
let restrictedClasses = this.GetRestrictedClasses(type);
if (!restrictedClasses.length)
return true;
- if (targetClasses.every(c => restrictedClasses.indexOf(c) == -1))
+ if (!MatchesClassList(targetClasses, restrictedClasses))
return true;
}
return false;
};
/**
* Returns null if we have no preference or the lowest index of a preferred class.
*/
Attack.prototype.GetPreference = function(target)
{
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
const targetClasses = cmpIdentity.GetClassesList();
let minPref = null;
for (let type of this.GetAttackTypes())
{
let preferredClasses = this.GetPreferredClasses(type);
for (let targetClass of targetClasses)
{
let pref = preferredClasses.indexOf(targetClass);
if (pref === 0)
return pref;
if (pref != -1 && (minPref === null || minPref > pref))
minPref = pref;
}
}
return minPref;
};
/**
* Get the full range of attack using all available attack types.
*/
Attack.prototype.GetFullAttackRange = function()
{
let ret = { "min": Infinity, "max": 0 };
for (let type of this.GetAttackTypes())
{
let range = this.GetRange(type);
ret.min = Math.min(ret.min, range.min);
ret.max = Math.max(ret.max, range.max);
}
return ret;
};
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
{
// TODO: Formation against formation needs review
let types = this.GetAttackTypes();
return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1);
}
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
let targetClasses = cmpIdentity.GetClassesList();
let isTargetClass = className => targetClasses.indexOf(className) != -1;
// Always slaughter domestic animals instead of using a normal attack
if (isTargetClass("Domestic") && this.template.Slaughter)
return "Slaughter";
let attack = this;
let types = this.GetAttackTypes().filter(type => !attack.GetRestrictedClasses(type).some(isTargetClass));
// check if the target is capturable
let captureIndex = types.indexOf("Capture");
if (captureIndex != -1)
{
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
let cmpPlayer = QueryOwnerInterface(this.entity);
if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
return "Capture";
// not captureable, so remove this attack
types.splice(captureIndex, 1);
}
let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass);
return types.sort((a, b) =>
(types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
(types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
};
Attack.prototype.CompareEntitiesByPreference = function(a, b)
{
let aPreference = this.GetPreference(a);
let bPreference = this.GetPreference(b);
if (aPreference === null && bPreference === null) return 0;
if (aPreference === null) return 1;
if (bPreference === null) return -1;
return aPreference - bPreference;
};
Attack.prototype.GetTimers = function(type)
{
let prepare = +(this.template[type].PrepareTime || 0);
prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity);
let repeat = +(this.template[type].RepeatTime || 1000);
repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity);
return { "prepare": prepare, "repeat": repeat };
};
Attack.prototype.GetAttackStrengths = function(type)
{
// Work out the attack values with technology effects
let template = this.template[type];
let splash = "";
if (!template)
{
template = this.template[type.split(".")[0]].Splash;
splash = "/Splash";
}
let applyMods = damageType =>
ApplyValueModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), this.entity);
if (type == "Capture")
return { "value": applyMods("Value") };
return {
"hack": applyMods("Hack"),
"pierce": applyMods("Pierce"),
"crush": applyMods("Crush")
};
};
Attack.prototype.GetSplashDamage = function(type)
{
if (!this.template[type].Splash)
return false;
let splash = this.GetAttackStrengths(type + ".Splash");
splash.friendlyFire = this.template[type].Splash.FriendlyFire != "false";
return splash;
};
Attack.prototype.GetRange = function(type)
{
let max = +this.template[type].MaxRange;
max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity);
let min = +(this.template[type].MinRange || 0);
min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
let elevationBonus = +(this.template[type].ElevationBonus || 0);
elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity);
return { "max": max, "min": min, "elevationBonus": elevationBonus };
};
// Calculate the attack damage multiplier against a target
Attack.prototype.GetAttackBonus = function(type, target)
{
let attackBonus = 1;
let template = this.template[type];
if (!template)
template = this.template[type.split(".")[0]].Splash;
if (template.Bonuses)
{
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return 1;
// Multiply the bonuses for all matching classes
for (let key in template.Bonuses)
{
let bonus = template.Bonuses[key];
let hasClasses = true;
if (bonus.Classes){
let classes = bonus.Classes.split(/\s+/);
for (let key in classes)
hasClasses = hasClasses && cmpIdentity.HasClass(classes[key]);
}
if (hasClasses && (!bonus.Civ || bonus.Civ === cmpIdentity.GetCiv()))
attackBonus *= bonus.Multiplier;
}
}
return attackBonus;
};
// Returns a 2d random distribution scaled for a spread of scale 1.
// The current implementation is a 2d gaussian with sigma = 1
Attack.prototype.GetNormalDistribution = function(){
// Use the Box-Muller transform to get a gaussian distribution
let a = Math.random();
let b = Math.random();
let c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b);
let d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b);
return [c, d];
};
/**
* Attack the target entity. This should only be called after a successful range check,
* and should only be called after GetTimers().repeat msec has passed since the last
* call to PerformAttack.
*/
Attack.prototype.PerformAttack = function(type, target)
{
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
// If this is a ranged attack, then launch a projectile
if (type == "Ranged")
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let turnLength = cmpTimer.GetLatestTurnLength()/1000;
// In the future this could be extended:
// * Obstacles like trees could reduce the probability of the target being hit
// * Obstacles like walls should block projectiles entirely
// Get some data about the entity
let horizSpeed = +this.template[type].ProjectileSpeed;
let gravity = 9.81; // this affects the shape of the curve; assume it's constant for now
let spread = +this.template.Ranged.Spread;
spread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", spread, this.entity);
//horizSpeed /= 2; gravity /= 2; // slow it down for testing
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
let selfPosition = cmpPosition.GetPosition();
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
let targetPosition = cmpTargetPosition.GetPosition();
let relativePosition = Vector3D.sub(targetPosition, selfPosition);
let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength);
// The component of the targets velocity radially away from the archer
let radialSpeed = relativePosition.dot(targetVelocity) / relativePosition.length();
let horizDistance = targetPosition.horizDistanceTo(selfPosition);
// This is an approximation of the time ot the target, it assumes that the target has a constant radial
// velocity, but since units move in straight lines this is not true. The exact value would be more
// difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was
// about 5% of the units radius out in the worst case)
let timeToTarget = horizDistance / (horizSpeed - radialSpeed);
// Predict where the unit is when the missile lands.
let predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
// Compute the real target point (based on spread and target speed)
let range = this.GetRange(type);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let elevationAdaptedMaxRange = cmpRangeManager.GetElevationAdaptedRange(selfPosition, cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
let distanceModifiedSpread = spread * horizDistance/elevationAdaptedMaxRange;
let randNorm = this.GetNormalDistribution();
let offsetX = randNorm[0] * distanceModifiedSpread * (1 + targetVelocity.length() / 20);
let offsetZ = randNorm[1] * distanceModifiedSpread * (1 + targetVelocity.length() / 20);
let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ);
// Calculate when the missile will hit the target position
let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition);
timeToTarget = realHorizDistance / horizSpeed;
let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance);
// Launch the graphical projectile
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
let id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let data = {
"type": type,
"attacker": this.entity,
"target": target,
"strengths": this.GetAttackStrengths(type),
"position": realTargetPosition,
"direction": missileDirection,
"projectileId": id,
"multiplier": this.GetAttackBonus(type, target),
"isSplash": false,
"attackerOwner": attackerOwner
};
if (this.template.Ranged.Splash)
{
data.friendlyFire = this.template.Ranged.Splash.FriendlyFire;
data.radius = +this.template.Ranged.Splash.Range;
data.shape = this.template.Ranged.Splash.Shape;
data.isSplash = true;
}
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000, data);
}
else if (type == "Capture")
{
if (attackerOwner == -1)
return;
let multiplier = this.GetAttackBonus(type, target);
let cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.GetHitpoints() == 0)
return;
multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints());
let cmpCapturable = Engine.QueryInterface(target, IID_Capturable);
if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner))
return;
let strength = this.GetAttackStrengths("Capture").value * multiplier;
if (cmpCapturable.Reduce(strength, attackerOwner))
Engine.PostMessage(target, MT_Attacked, {
"attacker": this.entity,
"target": target,
"type": type,
"damage": strength,
"attackerOwner": attackerOwner
});
}
else
{
// Melee attack - hurt the target immediately
cmpDamage.CauseDamage({
"strengths": this.GetAttackStrengths(type),
"target": target,
"attacker": this.entity,
"multiplier": this.GetAttackBonus(type, target),
"type": type,
"attackerOwner": attackerOwner
});
}
};
Attack.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")
return;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (!cmpUnitAI)
return;
if (this.GetAttackTypes().some(type =>
msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1))
cmpUnitAI.UpdateRangeQueries();
};
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml (revision 18740)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml (revision 18741)
@@ -1,67 +1,67 @@
2
5
2
10.0
0.0
0.0
5.0
1000
- Ship Structure Human Elephant Domestic
+ !SeaCreature
6.0
1
0
Female Infantry
Support Infantry
1
10
true
Fishing Boat
Fish the waters for Food. Garrison a support or infantry unit inside to boost fishing rate.
FishingBoat
1
10
0
6.0
1.0
1.8
actor/ship/boat_move.xml
actor/ship/boat_move.xml
passive
false
ship-small
10
24