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 17384) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 17385) @@ -1,946 +1,946 @@ 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) { var value = this._template; if (this._auraTemplateModif && this._auraTemplateModif.has(string)) return this._auraTemplateModif.get(string); else if (this._techModif && this._techModif.has(string)) return this._techModif.get(string); else { if (!this._tpCache.has(string)) { var 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() { if (!this.get("Identity") || !this.get("Identity/GenericName")) return undefined; return this.get("Identity/GenericName"); }, rank: function() { if (!this.get("Identity")) return undefined; return this.get("Identity/Rank"); }, classes: function() { var template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, requiredTech: function() { return this.get("Identity/RequiredTechnology"); }, available: function(gameState) { if (this.requiredTech() === undefined) return true; return gameState.isResearched(this.get("Identity/RequiredTechnology")); }, // specifically phase: function() { if (!this.get("Identity/RequiredTechnology")) return 0; if (this.get("Identity/RequiredTechnology") == "phase_village") return 1; if (this.get("Identity/RequiredTechnology") == "phase_town") return 2; if (this.get("Identity/RequiredTechnology") == "phase_city") return 3; return 0; }, hasClass: function(name) { if (!this._classes) this._classes = this.classes(); var 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() { if (!this.get("Cost")) return undefined; var ret = {}; for (var type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, costSum: function() { if (!this.get("Cost")) return undefined; var ret = 0; for (var type in this.get("Cost/Resources")) ret += +this.get("Cost/Resources/" + type); return ret; }, /** * 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")) { var w = +this.get("Obstruction/Static/@width"); var 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")) { var w = +this.get("Footprint/Square/@width"); var 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() { if (this.get("Health") !== undefined) return +this.get("Health/Max"); return 0; }, isHealable: function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, isRepairable: function() { if (this.get("Health") !== undefined) return this.get("Health/Repairable") === "true"; return false; }, 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; var ret = []; for (var 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; var Classes = []; for (var i in this.get("Attack")) { if (!this.get("Attack/" + i + "/Bonuses")) continue; for (var o in this.get("Attack/" + i + "/Bonuses")) if (this.get("Attack/" + i + "/Bonuses/" + o + "/Classes")) Classes.push([this.get("Attack/" + i +"/Bonuses/" + o +"/Classes").split(" "), +this.get("Attack/" + i +"/Bonuses" +o +"/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; var mcounter = []; for (var i in this.get("Attack")) { if (!this.get("Attack/" + i + "/Bonuses")) continue; for (var o in this.get("Attack/" + i + "/Bonuses")) if (this.get("Attack/" + i + "/Bonuses/" + o + "/Classes")) mcounter.concat(this.get("Attack/" + i + "/Bonuses/" + o + "/Classes").split(" ")); } for (var 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 (var o in this.get("Attack/" + type + "/Bonuses")) { if (!this.get("Attack/" + type + "/Bonuses/" + o + "/Classes")) continue; var total = this.get("Attack/" + type + "/Bonuses/" + o + "/Classes").split(" "); for (var j in total) if (total[j] === againstClass) return +this.get("Attack/" + type + "/Bonuses/" + o + "/Multiplier"); } return 1; }, // returns true if the entity can attack the given class canAttackClass: function(saidClass) { if (!this.get("Attack")) return false; for (var i in this.get("Attack")) { if (!this.get("Attack/" + i + "/RestrictedClasses") || !this.get("Attack/" + i + "/RestrictedClasses/_string")) continue; var cannotAttack = this.get("Attack/" + i + "/RestrictedClasses/_string").split(" "); if (cannotAttack.indexOf(saidClass) !== -1) return false; } return true; }, buildableEntities: function() { if (!this.get("Builder/Entities/_string")) return []; var civ = this.civ(); var templates = this.get("Builder/Entities/_string").replace(/\{civ\}/g, civ).split(/\s+/); return templates; // TODO: map to Entity? }, trainableEntities: function(civ) { if (!this.get("ProductionQueue/Entities/_string")) return undefined; var templates = this.get("ProductionQueue/Entities/_string").replace(/\{civ\}/g, civ).split(/\s+/); return templates; }, researchableTechs: function(civ) { if (this.civ() !== civ) // techs can only be researched in structure from the player civ return undefined; if (!this.get("ProductionQueue/Technologies/_string")) return undefined; var templates = this.get("ProductionQueue/Technologies/_string").split(/\s+/); return templates; }, resourceSupplyType: function() { if (!this.get("ResourceSupply")) return undefined; var [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; var [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, resourceSupplyMax: function() { if (!this.get("ResourceSupply")) return undefined; return +this.get("ResourceSupply/Amount"); }, maxGatherers: function() { if (this.get("ResourceSupply") !== undefined) return +this.get("ResourceSupply/MaxGatherers"); return 0; }, resourceGatherRates: function() { if (!this.get("ResourceGatherer")) return undefined; var ret = {}; var baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (var 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() { if (!this.get("GarrisonHolder")) return undefined; return this.get("GarrisonHolder/List/_string"); }, garrisonMax: function() { if (!this.get("GarrisonHolder")) return undefined; return this.get("GarrisonHolder/Max"); }, garrisonEjectHealth: function() { if (!this.get("GarrisonHolder")) return undefined; return +this.get("GarrisonHolder/EjectHealth"); }, getDefaultArrow: function() { if (!this.get("BuildingAI")) return undefined; return +this.get("BuildingAI/DefaultArrowCount"); }, getArrowMultiplier: function() { if (!this.get("BuildingAI")) return undefined; return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, getGarrisonArrowClasses: function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, buffHeal: function() { if (!this.get("GarrisonHolder")) return undefined; return +this.get("GarrisonHolder/BuffHeal"); }, promotion: function() { if (!this.get("Promotion")) return undefined; return this.get("Promotion/Entity"); }, /** * Returns whether this is an animal that is too difficult to hunt. * (Any non domestic currently.) */ isUnhuntable: function() { // used by Aegis if (!this.get("UnitAI") || !this.get("UnitAI/NaturalBehaviour")) return false; // only attack domestic animals since they won't flee nor retaliate. return this.get("UnitAI/NaturalBehaviour") !== "domestic"; }, isHuntable: function() { // used by Petra if(!this.get("ResourceSupply") || !this.get("ResourceSupply/KillBeforeGather")) return false; // special case: rabbits too difficult to hunt for such a small food amount if (this.get("Identity") && this.get("Identity/SpecificName") && this.get("Identity/SpecificName") === "Rabbit") return false; // do not hunt retaliating animals (animals without UnitAI are dead animals) return !this.get("UnitAI") || !(this.get("UnitAI/NaturalBehaviour") === "violent" || this.get("UnitAI/NaturalBehaviour") === "aggressive" || this.get("UnitAI/NaturalBehaviour") === "defensive"); }, walkSpeed: function() { if (!this.get("UnitMotion") || !this.get("UnitMotion/WalkSpeed")) return undefined; return +this.get("UnitMotion/WalkSpeed"); }, trainingCategory: function() { if (!this.get("TrainingRestrictions") || !this.get("TrainingRestrictions/Category")) return undefined; return this.get("TrainingRestrictions/Category"); }, buildCategory: function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Category")) return undefined; return this.get("BuildRestrictions/Category"); }, buildTime: function() { if (!this.get("Cost") || !this.get("Cost/BuildTime")) return undefined; return +this.get("Cost/BuildTime"); }, buildDistance: function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Distance")) return undefined; - return +this.get("BuildRestrictions/Distance"); + return this.get("BuildRestrictions/Distance"); }, buildPlacementType: function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/PlacementType")) return undefined; 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) { var territories = this.buildTerritories(); return (territories && territories.indexOf(territory) != -1); }, hasTerritoryInfluence: function() { return (this.get("TerritoryInfluence") !== undefined); }, hasDefensiveFire: function() { if (!this.get("Attack") || !this.get("Attack/Ranged")) return false; return (this.getDefaultArrow() || this.getArrowMultiplier()); }, territoryInfluenceRadius: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); else return -1; }, territoryInfluenceWeight: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); else return -1; }, territoryDecayRate: function() { return (this.get("TerritoryDecay") ? +this.get("TerritoryDecay/DecayRate") : 0); }, defaultRegenRate: function() { return (this.get("Capturable") ? +this.get("Capturable/RegenRate") : 0); }, garrisonRegenRate: function() { return (this.get("Capturable") ? +this.get("Capturable/GarrisonRegenRate") : 0); }, visionRange: function() { return +this.get("Vision/Range"); } }); // 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._auraTemplateModif = new Map(); // template modification from auras. this is only for this entity. this._ai = sharedAI; if (!sharedAI._techModifications[entity.owner][this._templateName]) sharedAI._techModifications[entity.owner][this._templateName] = new Map(); this._techModif = sharedAI._techModifications[entity.owner][this._templateName]; // save a reference to the template tech modifications }, 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; }, 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); }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ trainingQueue: function() { var queue = this._entity.trainingQueue; return queue; }, trainingQueueTime: function() { var queue = this._entity.trainingQueue; if (!queue) return undefined; var time = 0; for (var 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; }, isGarrisonHolder: function() { return this.get("GarrisonHolder"); }, garrisoned: function() { return this._entity.garrisoned; }, canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, // TODO: visibility 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-own", "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) { - Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": false}); + 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) { var direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; var 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) { var FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],this.position()[1] - unitToFleeFrom.position()[1]]; var dist = m.VectorDistance(unitToFleeFrom.position(), this.position() ); FleeDirection[0] = (FleeDirection[0]/dist) * 8; FleeDirection[1] = (FleeDirection[1]/dist) * 8; Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0]*5, "z": this.position()[1] + FleeDirection[1]*5, "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) { var 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) { var 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) { var queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (var 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/ai/petra/defenseArmy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 17384) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 17385) @@ -1,145 +1,148 @@ var PETRA = function(m) { // Specialization of Armies used by the defense manager. m.DefenseArmy = function(gameState, ownEntities, foeEntities) { if (!m.Army.call(this, gameState, ownEntities, foeEntities)) return false; return true; }; m.DefenseArmy.prototype = Object.create(m.Army.prototype); m.DefenseArmy.prototype.assignUnit = function (gameState, entID) { // we'll assume this defender is ours already. // we'll also override any previous assignment var ent = gameState.getEntityById(entID); if (!ent || !ent.position()) return false; - + + // try to return its resources, and if any, the attack order will be queued + var queued = m.returnResources(gameState, ent); + var idMin; var distMin; var idMinAll; var distMinAll; for (let id of this.foeEntities) { let eEnt = gameState.getEntityById(id); if (!eEnt || !eEnt.position()) // probably can't happen. continue; if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length && eEnt.unitAIOrderData()[0]["target"] && eEnt.unitAIOrderData()[0]["target"] == entID) { // being attacked >>> target the unit idMin = id; break; } // already enough units against it if (this.assignedAgainst[id].length > 8 || (this.assignedAgainst[id].length > 5 && !eEnt.hasClass("Hero") && !eEnt.hasClass("Siege"))) continue; let dist = API3.SquareVectorDistance(ent.position(), eEnt.position()); if (idMinAll === undefined || dist < distMinAll) { idMinAll = id; distMinAll = dist; } if (this.assignedAgainst[id].length > 2) continue; if (idMin === undefined || dist < distMin) { idMin = id; distMin = dist; } } if (idMin !== undefined) var idFoe = idMin; else if (idMinAll !== undefined) var idFoe = idMinAll; else return false; let ownIndex = gameState.ai.accessibility.getAccessValue(ent.position()); let foeEnt = gameState.getEntityById(idFoe); let foePosition = foeEnt.position(); let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); if (ownIndex == foeIndex || ent.hasClass("Ship")) { this.assignedTo[entID] = idFoe; this.assignedAgainst[idFoe].push(entID); - ent.attack(idFoe, !foeEnt.hasClass("Siege")); + ent.attack(idFoe, !foeEnt.hasClass("Siege"), queued); } else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); return true; }; // TODO: this should return cleverer results ("needs anti-elephant"…) m.DefenseArmy.prototype.needsDefenders = function (gameState) { // some preliminary checks because we don't update for tech so entStrength removed can be > entStrength added if (this.foeStrength <= 0 || this.ownStrength <= 0) this.recalculateStrengths(gameState); if (this.foeStrength * this.defenseRatio <= this.ownStrength) return false; return this.foeStrength * this.defenseRatio - this.ownStrength; }; m.DefenseArmy.prototype.getState = function () { if (this.foeEntities.length == 0) return 0; return 1; }; m.DefenseArmy.prototype.update = function (gameState) { for (let entId of this.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent) continue; let orderData = ent.unitAIOrderData(); if (!orderData.length && !ent.getMetadata(PlayerID, "transport")) this.assignUnit(gameState, entId); else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") { let target = gameState.getEntityById(orderData[0].target); if (target && target.hasClass("Siege")) ent.attack(orderData[0].target, false); } } return this.onUpdate(gameState); }; m.DefenseArmy.prototype.Serialize = function() { return { "ID": this.ID, "foePosition": this.foePosition, "positionLastUpdate": this.positionLastUpdate, "assignedAgainst": this.assignedAgainst, "assignedTo": this.assignedTo, "foeEntities": this.foeEntities, "foeStrength": this.foeStrength, "ownEntities": this.ownEntities, "ownStrength": this.ownStrength, }; }; m.DefenseArmy.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entity-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entity-extend.js (revision 17384) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entity-extend.js (revision 17385) @@ -1,191 +1,192 @@ var PETRA = function(m) { // returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. m.getMaxStrength = function(ent, againstClass) { var strength = 0.0; var attackTypes = ent.attackTypes(); if (!attackTypes) return strength; var armourStrength = ent.armourStrengths(); var hp = ent.maxHitpoints() / 100.0; // some normalization for (let type of attackTypes) { if (type == "Slaughter" || type == "Charged") continue; var attackStrength = ent.attackStrengths(type); var attackRange = ent.attackRange(type); var attackTimes = ent.attackTimes(type); for (var str in attackStrength) { var val = parseFloat(attackStrength[str]); if (againstClass) val *= ent.getMultiplierAgainst(type, againstClass); switch (str) { case "crush": strength += (val * 0.085) / 3; break; case "hack": strength += (val * 0.075) / 3; break; case "pierce": strength += (val * 0.065) / 3; break; } } if (attackRange){ strength += (attackRange.max * 0.0125) ; } for (var str in attackTimes) { var val = parseFloat(attackTimes[str]); switch (str){ case "repeat": strength += (val / 100000); break; case "prepare": strength -= (val / 100000); break; } } } - for (var str in armourStrength) { + for (var str in armourStrength) + { var val = parseFloat(armourStrength[str]); switch (str) { case "crush": strength += (val * 0.085) / 3; break; case "hack": strength += (val * 0.075) / 3; break; case "pierce": strength += (val * 0.065) / 3; break; } } return strength * hp; }; // Makes the worker deposit the currently carried resources at the closest accessible dropsite m.returnResources = function(gameState, ent) { if (!ent.resourceCarrying() || ent.resourceCarrying().length == 0 || !ent.position()) return false; var resource = ent.resourceCarrying()[0].type; var closestDropsite; var distmin = Math.min(); var access = gameState.ai.accessibility.getAccessValue(ent.position()); gameState.getOwnDropsites(resource).forEach(function(dropsite) { if (!dropsite.position() || dropsite.getMetadata(PlayerID, "access") !== access) return; var dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist > distmin) return; distmin = dist; closestDropsite = dropsite; }); if (!closestDropsite) return false; ent.returnResources(closestDropsite); return true; }; // is supply full taking into account gatherers affected during this turn m.IsSupplyFull = function(gameState, ent) { if (ent.isFull() === true) return true; var turnCache = gameState.ai.HQ.turnCache; var count = ent.resourceSupplyNumGatherers(); if (turnCache["resourceGatherer"] && turnCache["resourceGatherer"][ent.id()]) count += turnCache["resourceGatherer"][ent.id()]; if (count >= ent.maxGatherers()) return true; return false; }; /** * get the best base (in terms of distance and accessIndex) for an entity */ m.getBestBase = function(gameState, ent) { var pos = ent.position(); if (!pos) { var holder = m.getHolder(gameState, ent); if (!holder || !holder.position()) { API3.warn("Petra error: entity without position, but not garrisoned"); m.dumpEntity(ent); return gameState.ai.HQ.baseManagers[0]; } pos = holder.position(); } var distmin = Math.min(); var bestbase; var accessIndex = gameState.ai.accessibility.getAccessValue(pos); for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (base.accessIndex !== accessIndex) dist += 100000000; if (dist > distmin) continue; distmin = dist; bestbase = base; } if (!bestbase) bestbase = gameState.ai.HQ.baseManagers[0]; return bestbase; }; m.getHolder = function(gameState, ent) { var found; gameState.getEntities().forEach(function (holder) { if (found || !holder.isGarrisonHolder()) return; if (holder.garrisoned().indexOf(ent.id()) !== -1) found = holder; }); return found; }; /** * return true if it is not worth finishing this building (it would surely decay) * TODO implement the other conditions */ m.isNotWorthBuilding = function(gameState, ent) { if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) { let buildTerritories = ent.buildTerritories(); if (buildTerritories && (!buildTerritories.length || (buildTerritories.length === 1 && buildTerritories[0] === "own"))) return true; } return false; }; m.dumpEntity = function(ent) { if (!ent) return; API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + " state " + ent.unitAIState()); API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + " subrole " + ent.getMetadata(PlayerID, "subrole")); API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints()); API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport") + " gather-type " + ent.getMetadata(PlayerID, "gather-type") + " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 17384) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 17385) @@ -1,2168 +1,2164 @@ var PETRA = function(m) { /* Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: -defining RESS needs -BO decisions. > training workers > building stuff (though we'll send that to bases) > researching -picking strategy (specific manager?) -diplomacy (specific manager?) -planning attacks -picking new CC locations. */ m.HQ = function(Config) { this.Config = Config; this.econState = "growth"; // existing values: growth, townPhasing. this.currentPhase = undefined; // cache the rates. this.turnCache = {}; this.wantedRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.currentRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.lastFailedGather = { "wood": undefined, "stone": undefined, "metal": undefined }; // workers configuration this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.femaleRatio = this.Config.Economy.femaleRatio; this.lastTerritoryUpdate = -1; this.stopBuilding = new Map(); // list of buildings to stop (temporarily) production because no room this.fortStartTime = 180; // wooden defense towers, will start at fortStartTime + towerLapseTime this.towerStartTime = 0; // stone defense towers, will start as soon as available this.towerLapseTime = this.Config.Military.towerLapseTime; this.fortressStartTime = 0; // will start as soon as available this.fortressLapseTime = this.Config.Military.fortressLapseTime; this.baseManagers = []; this.attackManager = new m.AttackManager(this.Config); this.defenseManager = new m.DefenseManager(this.Config); this.tradeManager = new m.TradeManager(this.Config); this.navalManager = new m.NavalManager(this.Config); this.researchManager = new m.ResearchManager(this.Config); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.garrisonManager = new m.GarrisonManager(); }; // More initialisation for stuff that needs the gameState m.HQ.prototype.init = function(gameState, queues) { this.territoryMap = m.createTerritoryMap(gameState); // initialize base map. Each pixel is a base ID, or 0 if not or not accessible this.basesMap = new API3.Map(gameState.sharedScript, "territory"); // area of n cells on the border of the map : 0=inside map, 1=border map, 2=border+inaccessible this.borderMap = m.createBorderMap(gameState); // initialize frontier map. Each cell is 2 if on the near frontier, 1 on the frontier and 0 otherwise this.frontierMap = m.createFrontierMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; this.treasures = gameState.getEntities().filter(function (ent) { var type = ent.resourceSupplyType(); if (type && type.generic === "treasure") return true; return false; }); this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); }; /** * initialization needed after deserialization (only called when deserialization) */ m.HQ.prototype.postinit = function(gameState) { // Rebuild the base maps from the territory indices of each base this.basesMap = new API3.Map(gameState.sharedScript, "territory"); for (let base of this.baseManagers) for (let j of base.territoryIndices) this.basesMap.map[j] = base.ID; for (let ent of gameState.getOwnEntities().values()) { if (!ent.resourceDropsiteTypes() || ent.hasClass("Elephant")) continue; let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.assignResourceToDropsite(gameState, ent); } }; // returns the sea index linking regions 1 and region 2 (supposed to be different land region) // otherwise return undefined // for the moment, only the case land-sea-land is supported m.HQ.prototype.getSeaIndex = function (gameState, index1, index2) { var path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] === "water") return path[1]; else { if (this.Config.debug > 1) { API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); } return undefined; } }; m.HQ.prototype.checkEvents = function (gameState, events, queues) { for (let evt of events["Create"]) { // Let's check if we have a building set to create a new base. let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) continue; if (ent.getMetadata(PlayerID, "base") == -1) { // Okay so let's try to create a new base around this. let newbase = new m.BaseManager(gameState, this.Config); newbase.init(gameState, "unconstructed"); newbase.setAnchor(gameState, ent); this.baseManagers.push(newbase); // Let's get a few units from other bases there to build this. let builders = this.bulkPickWorkers(gameState, newbase, 10); if (builders !== false) { builders.forEach(function (worker) { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } else if (ent.hasClass("Wonder") && gameState.getGameType() === "wonder") { // Let's get a few units from other bases there to build this. let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); let builders = this.bulkPickWorkers(gameState, base, 10); if (builders !== false) { builders.forEach(function (worker) { worker.setMetadata(PlayerID, "base", base.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } for (let evt of events["ConstructionFinished"]) { // Let's check if we have a building set to create a new base. // TODO: move to the base manager. if (evt.newentity) { if (evt.newentity === evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.isOwn(PlayerID)) continue; if (ent.getMetadata(PlayerID, "baseAnchor") == true) { let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (base.constructing) base.constructing = false; base.anchor = ent; base.anchorId = evt.newentity; base.buildings.updateEnt(ent); this.updateTerritories(gameState); if (base.ID === this.baseManagers[1].ID) { // this is our first base, let us configure our starting resources this.configFirstBase(gameState); } else { // let us hope this new base will fix our possible resource shortage this.saveResources = undefined; this.saveSpace = undefined; } } else if (ent.hasTerritoryInfluence()) this.updateTerritories(gameState); } } for (let evt of events["OwnershipChanged"]) // capture events { if (evt.to !== PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; if (ent.position()) ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(ent.position())); if (ent.hasClass("Unit")) { m.getBestBase(gameState, ent).assignEntity(gameState, ent); ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.hasClass("Trader")) { ent.setMetadata(PlayerID, "role", "trader"); ent.setMetadata(PlayerID, "route", undefined); } if (ent.hasClass("Female") || ent.hasClass("CitizenSoldier")) { ent.setMetadata(PlayerID, "role", "worker"); ent.setMetadata(PlayerID, "subrole", "idle"); } if (ent.hasClass("Ship")) ent.setMetadata(PlayerID, "sea", gameState.ai.accessibility.getAccessValue(ent.position(), true)); if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) ent.setMetadata(PlayerID, "plan", -1); continue; } if (ent.hasClass("CivCentre")) // build a new base around it { let newbase = new m.BaseManager(gameState, this.Config); if (ent.foundationProgress() !== undefined) newbase.init(gameState, "unconstructed"); else newbase.init(gameState, "captured"); newbase.setAnchor(gameState, ent); this.baseManagers.push(newbase); this.updateTerritories(gameState); newbase.assignEntity(gameState, ent); } else { // TODO should be reassigned later if a better base is captured m.getBestBase(gameState, ent).assignEntity(gameState, ent); if (ent.hasTerritoryInfluence()) this.updateTerritories(gameState); if (ent.decaying()) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } } } // deal with the different rally points of training units: the rally point is set when the training starts // for the time being, only autogarrison is used for (let evt of events["TrainingStarted"]) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) continue; let metadata = ent._entity.trainingQueue[0].metadata; if (metadata && metadata.garrisonType) ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison else ent.unsetRallyPoint(); } for (let evt of events["TrainingFinished"]) { for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent.position()) { // we are autogarrisoned, check that the holder is registered in the garrisonManager let holderId = ent.unitAIOrderData()[0]["target"]; let holder = gameState.getEntityById(holderId); if (holder) this.garrisonManager.registerHolder(gameState, holder); } else if (ent.getMetadata(PlayerID, "garrisonType")) { // we were supposed to be autogarrisoned, but this has failed (may-be full) ent.setMetadata(PlayerID, "garrisonType", undefined); } // Check if this unit is no more needed in its attack plan // (happen when the training ends after the attack is started or aborted) let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = this.attackManager.getPlan(plan); if (!attack || attack.state !== "unexecuted") ent.setMetadata(PlayerID, "plan", -1); } // Assign it immediately to something useful to do if (ent.getMetadata(PlayerID, "role") === "worker") { let base; if (ent.getMetadata(PlayerID, "base") === undefined) { base = m.getBestBase(gameState, ent); base.assignEntity(gameState, ent); } else base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.reassignIdleWorkers(gameState, [ent]); base.workerObject.update(gameState, ent); } } } for (let evt of events["TerritoryDecayChanged"]) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) continue; if (evt.to) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } else if (ent.isGarrisonHolder()) this.garrisonManager.removeDecayingStructure(evt.entity); } // then deals with decaying structures for (let entId of this.decayingStructures) { let ent = gameState.getEntityById(entId); if (ent && ent.decaying() && ent.isOwn(PlayerID)) { let capture = ent.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio < 0.50) continue; let decayToGaia = true; for (let i = 1; i < capture.length; ++i) { if (gameState.isPlayerAlly(i) || !capture[i]) continue; decayToGaia = false; break; } if (decayToGaia) continue; let ratioMax = 0.70; for (let evt of events["Attacked"]) { if (ent.id() != evt.target) continue; ratioMax = 0.90; break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; // Called by the "town phase" research plan once it's started m.HQ.prototype.OnTownPhase = function(gameState) { if (this.Config.difficulty > 2 && this.femaleRatio > 0.4) this.femaleRatio = 0.4; var phaseName = gameState.getTemplate(gameState.townPhase()).name(); m.chatNewPhase(gameState, phaseName, true); }; // Called by the "city phase" research plan once it's started m.HQ.prototype.OnCityPhase = function(gameState) { if (this.Config.difficulty > 2 && this.femaleRatio > 0.3) this.femaleRatio = 0.3; // increase the priority of defense buildings to free this queue for our first fortress gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); var phaseName = gameState.getTemplate(gameState.cityPhase()).name(); m.chatNewPhase(gameState, phaseName, true); }; // This code trains females and citizen workers, trying to keep close to a ratio of females/CS // TODO: this should choose a base depending on which base need workers // TODO: also there are several things that could be greatly improved here. m.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // Count the workers in the world and in progress var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen"), true); // counting the workers that aren't part of a plan var numWorkers = 0; gameState.getOwnUnits().forEach (function (ent) { if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") == undefined) ++numWorkers; }); var numInTraining = 0; gameState.getOwnTrainingFacilities().forEach(function(ent) { ent.trainingQueue().forEach(function(item) { if (item.metadata && item.metadata.role && item.metadata.role == "worker" && item.metadata.plan == undefined) numWorkers += item.count; numInTraining += item.count; }); }); // Anticipate the optimal batch size when this queue will start // and adapt the batch size of the first and second queued workers and females to the present population // to ease a possible recovery if our population was drastically reduced by an attack // (need to go up to second queued as it is accounted in queueManager) if (numWorkers < 12) var size = 1; else var size = Math.min(5, Math.ceil(numWorkers / 10)); if (queues.villager.queue[0]) { queues.villager.queue[0].number = Math.min(queues.villager.queue[0].number, size); if (queues.villager.queue[1]) queues.villager.queue[1].number = Math.min(queues.villager.queue[1].number, size); } if (queues.citizenSoldier.queue[0]) { queues.citizenSoldier.queue[0].number = Math.min(queues.citizenSoldier.queue[0].number, size); if (queues.citizenSoldier.queue[1]) queues.citizenSoldier.queue[1].number = Math.min(queues.citizenSoldier.queue[1].number, size); } var numQueuedF = queues.villager.countQueuedUnits(); var numQueuedS = queues.citizenSoldier.countQueuedUnits(); var numQueued = numQueuedS + numQueuedF; var numTotal = numWorkers + numQueued; if (this.saveResources && numTotal > this.Config.Economy.popForTown + 10) return; if (numTotal > this.targetNumWorkers || (numTotal >= this.Config.Economy.popForTown && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase()))) return; if (numQueued > 50 || (numQueuedF > 20 && numQueuedS > 20) || numInTraining > 15) return; // default template var templateDef = gameState.applyCiv("units/{civ}_support_female_citizen"); // Choose whether we want soldiers instead. let femaleRatio = (gameState.isDisabledTemplates(gameState.applyCiv("structures/{civ}_field")) ? Math.min(this.femaleRatio, 0.2) : this.femaleRatio); let template; if (!gameState.templates[templateDef] || ((numFemales+numQueuedF) > 8 && (numFemales+numQueuedF)/numTotal > femaleRatio)) { if (numTotal < 45) var requirements = [ ["cost", 1], ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"]]; else var requirements = [ ["strength", 1] ]; var proba = Math.random(); if (proba < 0.6) { // we require at least 30% ranged and 30% melee if (proba < 0.3) var classes = ["CitizenSoldier", "Infantry", "Ranged"]; else var classes = ["CitizenSoldier", "Infantry", "Melee"]; template = this.findBestTrainableUnit(gameState, classes, requirements); } if (!template) template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], requirements); } if (!template && gameState.templates[templateDef]) template = templateDef; // base "0" means "auto" if (template === gameState.applyCiv("units/{civ}_support_female_citizen")) queues.villager.addItem(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); else if (template) queues.citizenSoldier.addItem(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); }; // picks the best template based on parameters and classes m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) { if (classes.indexOf("Hero") != -1) var units = gameState.findTrainableUnits(classes, []); else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it var units = gameState.findTrainableUnits(classes, ["SiegeTower"]); else // We do not want hero when not explicitely specified var units = gameState.findTrainableUnits(classes, ["Hero"]); if (units.length == 0) return undefined; var parameters = requirements.slice(); var remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory var availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources for (var type in remainingResources) { if (type === "food") continue; if (availableResources[type] > 800) continue; if (remainingResources[type] > 800) continue; else if (remainingResources[type] > 400) var costsResource = 0.6; else var costsResource = 0.2; var toAdd = true; for (var param of parameters) { if (param[0] !== "costsResource" || param[2] !== type) continue; param[1] = Math.min( param[1], costsResource ); toAdd = false; break; } if (toAdd) parameters.push( [ "costsResource", costsResource, type ] ); } units.sort(function(a, b) {// }) { var aDivParam = 0, bDivParam = 0; var aTopParam = 0, bTopParam = 0; for (let param of parameters) { if (param[0] == "base") { aTopParam = param[1]; bTopParam = param[1]; } if (param[0] == "strength") { aTopParam += m.getMaxStrength(a[1]) * param[1]; bTopParam += m.getMaxStrength(b[1]) * param[1]; } if (param[0] == "siegeStrength") { aTopParam += m.getMaxStrength(a[1], "Structure") * param[1]; bTopParam += m.getMaxStrength(b[1], "Structure") * param[1]; } if (param[0] == "speed") { aTopParam += a[1].walkSpeed() * param[1]; bTopParam += b[1].walkSpeed() * param[1]; } if (param[0] == "cost") { aDivParam += a[1].costSum() * param[1]; bDivParam += b[1].costSum() * param[1]; } // requires a third parameter which is the resource if (param[0] == "costsResource") { if (a[1].cost()[param[2]]) aTopParam *= param[1]; if (b[1].cost()[param[2]]) bTopParam *= param[1]; } if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aTopParam *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bTopParam *= param[1]; } } return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); }); return units[0][0]; }; // returns an entity collection of workers through BaseManager.pickBuilders // TODO: when same accessIndex, sort by distance m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) { var accessIndex = baseRef.accessIndex; if (!accessIndex) return false; // sorting bases by whether they are on the same accessindex or not. var baseBest = this.baseManagers.slice().sort(function (a,b) { if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) return -1; else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) return 1; return 0; }); var needed = number; var workers = new API3.EntityCollection(gameState.sharedScript); for (let base of baseBest) { if (base.ID === baseRef.ID) continue; base.pickBuilders(gameState, workers, needed); if (workers.length < number) needed = number - workers.length; else break; } if (workers.length === 0) return false; return workers; }; m.HQ.prototype.getTotalResourceLevel = function(gameState) { var total = { "food": 0, "wood": 0, "stone": 0, "metal": 0 }; for (var base of this.baseManagers) for (var type in total) total[type] += base.getResourceLevel(gameState, type); return total; }; // returns the current gather rate // This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. m.HQ.prototype.GetCurrentGatherRates = function(gameState) { if (!this.turnCache["gatherRates"]) { for (let res in this.currentRates) this.currentRates[res] = 0.5 * this.GetTCResGatherer(res); for (let base of this.baseManagers) base.getGatherRates(gameState, this.currentRates); for (let res in this.currentRates) { if (this.currentRates[res] < 0) { if (this.Config.debug > 0) API3.warn("Petra: current rate for " + res + " < 0 with " + this.GetTCResGatherer(res) + " moved gatherers"); this.currentRates[res] = 0; } } this.turnCache["gatherRates"] = true; } return this.currentRates; }; /* Pick the resource which most needs another worker * How this works: * We get the rates we would want to have to be able to deal with our plans * We get our current rates * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ m.HQ.prototype.pickMostNeededResources = function(gameState) { this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); var currentRates = this.GetCurrentGatherRates(gameState); var needed = []; for (var res in this.wantedRates) needed.push({ "type": res, "wanted": this.wantedRates[res], "current": currentRates[res] }); needed.sort(function(a, b) { var va = (Math.max(0, a.wanted - a.current))/ (a.current+1); var vb = (Math.max(0, b.wanted - b.current))/ (b.current+1); // If they happen to be equal (generally this means "0" aka no need), make it fair. if (va === vb) return (a.current - b.current); return (vb - va); }); return needed; }; // Returns the best position to build a new Civil Centre // Whose primary function would be to reach new resources of type "resource". m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) { // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. Engine.ProfileStart("findEconomicCCLocation"); // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); var halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); var ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); var dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"]))); var ccList = []; for (let cc of ccEnts.values()) ccList.push({"pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner())}); var dpList = []; for (let dp of dpEnts.values()) dpList.push({"pos": dp.position()}); var bestIdx; var bestVal; var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); var scale = 250 * 250; var proxyAccess; var nbShips = this.navalManager.transportShips.length; if (proximity) // this is our first base { // if our first base, ensure room around radius = Math.ceil((template.obstructionRadius() + 8) / obstructions.cellSize); // scale is the typical scale at which we want to find a location for our first base // look for bigger scale if we start from a ship (access < 2) or from a small island var cellArea = gameState.getMap().cellSize * gameState.getMap().cellSize; proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) scale = 400 * 400; } var width = this.territoryMap.width; var cellSize = this.territoryMap.cellSize; for (var j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // with enough room around to build the cc var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible var index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; if (proxyAccess && nbShips === 0 && proxyAccess !== index) continue; var norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // checking distance to other cc var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; if (proximity) // this is our first cc, let's do it near our units { let dist = API3.SquareVectorDistance(proximity, pos); norm /= (1 + dist/scale); } else { var minDist = Math.min(); for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { norm = 0; break; } if (!cc.ally) continue; if (dist < 40000) // Reject if too near from an allied cc { norm = 0; break; } if (dist < 62000) // Disfavor if quite near an allied cc norm *= 0.5; if (dist < minDist) minDist = dist; } if (norm == 0) continue; if (minDist > 170000 && !this.navalMap) // Reject if too far from any allied cc (not connected) { norm = 0; continue; } else if (minDist > 130000) // Disfavor if quite far from any allied cc { if (this.navalMap) { if (minDist > 250000) norm *= 0.5; else norm *= 0.8; } else norm *= 0.5; } for (let dp of dpList) { let dist = API3.SquareVectorDistance(dp.pos, pos); if (dist < 3600) { norm = 0; break; } else if (dist < 6400) norm *= 0.5; } if (norm == 0) continue; } if (this.borderMap.map[j] > 0) // disfavor the borders of the map norm *= 0.5; var val = 2*gameState.sharedScript.CCResourceMaps[resource].map[j] + gameState.sharedScript.CCResourceMaps["wood"].map[j] + gameState.sharedScript.CCResourceMaps["stone"].map[j] + gameState.sharedScript.CCResourceMaps["metal"].map[j]; val *= norm; if (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); var cut = 60; if (fromStrategic || proximity) // be less restrictive cut = 30; if (this.Config.debug > 1) API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); // not good enough. if (bestVal < cut) return false; var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base var index = gameState.ai.accessibility.landPassMap[bestIdx]; for (var base of this.baseManagers) { if (!base.anchor || base.accessIndex === index) continue; var sea = this.getSeaIndex(gameState, base.accessIndex, index); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; // Returns the best position to build a new Civil Centre // Whose primary function would be to assure territorial continuity with our allies m.HQ.prototype.findStrategicCCLocation = function(gameState, template) { // This builds a map. The procedure is fairly simple. // We minimize the Sum((dist-300)**2) where the sum is on the three nearest allied CC // with the constraints that all CC have dist > 200 and at least one have dist < 400 // This needs at least 2 CC. Otherwise, go back to economic CC. var ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); var ccList = []; var numAllyCC = 0; for (let cc of ccEnts.values()) { let ally = gameState.isPlayerAlly(cc.owner()); ccList.push({"pos": cc.position(), "ally": ally}); if (ally) ++numAllyCC; } if (numAllyCC < 2) return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); Engine.ProfileStart("findStrategicCCLocation"); // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); var halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); var bestIdx; var bestVal; var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); var width = this.territoryMap.width; var cellSize = this.territoryMap.cellSize; var currentVal, delta; var distcc0, distcc1, distcc2; for (var j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // with enough room around to build the cc var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible var index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; // checking distances to other cc var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; var minDist = Math.min(); distcc0 = undefined; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { minDist = 0; break; } if (!cc.ally) continue; if (dist < 62000) // Reject if quite near from ally cc { minDist = 0; break; } if (dist < minDist) minDist = dist; if (!distcc0 || dist < distcc0) { distcc2 = distcc1; distcc1 = distcc0; distcc0 = dist; } else if (!distcc1 || dist < distcc1) { distcc2 = distcc1; distcc1 = dist; } else if (!distcc2 || dist < distcc2) distcc2 = dist; } if (minDist < 1 || (minDist > 170000 && !this.navalMap)) continue; delta = Math.sqrt(distcc0) - 300; // favor a distance of 300 currentVal = delta*delta; delta = Math.sqrt(distcc1) - 300; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - 300; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] > 0) currentVal += 10000; if (bestVal !== undefined && currentVal > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = currentVal; bestIdx = i; } if (this.Config.debug > 1) API3.warn("We've found a strategic base with bestVal = " + bestVal); Engine.ProfileStop(); if (bestVal === undefined) return undefined; var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base var index = gameState.ai.accessibility.landPassMap[bestIdx]; for (var base of this.baseManagers) { if (!base.anchor || base.accessIndex === index) continue; var sea = this.getSeaIndex(gameState, base.accessIndex, index); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; // Returns the best position to build a new market: if the allies already have a market, build it as far as possible // from it, although not in our border to be able to defend it easily. If no allied market, our second market will // follow the same logic // TODO check that it is on same accessIndex m.HQ.prototype.findMarketLocation = function(gameState, template) { var markets = gameState.updatingCollection("ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (!markets.length) markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray(); if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); var halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); var bestIdx; var bestJdx; var bestVal; var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); var isNavalMarket = template.hasClass("NavalMarket"); var width = this.territoryMap.width; var cellSize = this.territoryMap.cellSize; for (var j = 0; j < this.territoryMap.length; ++j) { // do not try on the border of our territory if (this.frontierMap.map[j] == 2) continue; if (this.basesMap.map[j] == 0) // only in our territory continue; // with enough room around to build the cc var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; var index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other markets var maxDist = 0; for (let market of markets) { if (isNavalMarket && market.hasClass("NavalMarket")) { // TODO check that there are on the same sea. For the time being, we suppose it is true } else if (gameState.ai.accessibility.getAccessValue(market.position()) != index) continue; let dist = API3.SquareVectorDistance(market.position(), pos); if (dist > maxDist) maxDist = dist; } if (maxDist == 0) continue; if (bestVal !== undefined && maxDist < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxDist; bestIdx = i; bestJdx = j; } if (this.Config.debug > 1) API3.warn("We found a market position with bestVal = " + bestVal); if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; var expectedGain = Math.round(bestVal / this.Config.distUnitGain); if (this.Config.debug > 1) API3.warn("this would give a trading gain of " + expectedGain); // do not keep it if gain is too small, except if this is our first BarterMarket if (expectedGain < this.tradeManager.minimalGain || (expectedGain < 8 && (!template.hasClass("BarterMarket") || gameState.getOwnEntitiesByClass("BarterMarket", true).length > 0))) return false; var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.basesMap.map[bestJdx], expectedGain]; }; // Returns the best position to build defensive buildings (fortress and towers) // Whose primary function is to defend our borders m.HQ.prototype.findDefensiveLocation = function(gameState, template) { // We take the point in our territory which is the nearest to any enemy cc // but requiring a minimal distance with our other defensive structures // and not in range of any enemy defensive structure to avoid building under fire. var ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); var enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])).toEntityArray(); var wonderMode = (gameState.getGameType() === "wonder"); var wonderDistmin; if (wonderMode) { var wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); wonderMode = (wonders.length != 0); if (wonderMode) wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); } // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); var halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); var bestIdx; var bestJdx; var bestVal; var width = this.territoryMap.width; var cellSize = this.territoryMap.cellSize; var isTower = template.hasClass("Tower"); var isFortress = template.hasClass("Fortress"); if (isFortress) var radius = Math.floor((template.obstructionRadius() + 8) / obstructions.cellSize); else var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); for (var j = 0; j < this.territoryMap.length; ++j) { if (!wonderMode) { // do not try if well inside or outside territory if (this.frontierMap.map[j] == 0) continue; if (this.frontierMap.map[j] == 1 && isTower) continue; } if (this.basesMap.map[j] == 0) // inaccessible cell continue; // with enough room around to build the cc var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other structures var minDist = Math.min(); var dista = 0; if (wonderMode) { dista = API3.SquareVectorDistance(wonders[0].position(), pos); if (dista < wonderDistmin) continue; dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder } for (let str of enemyStructures) { if (str.foundationProgress() !== undefined) continue; let strPos = str.position(); if (!strPos) continue; let dist = API3.SquareVectorDistance(strPos, pos); if (dist < 6400) // TODO check on true attack range instead of this 80*80 { minDist = -1; break; } if (str.hasClass("CivCentre") && dist + dista < minDist) minDist = dist + dista; } if (minDist < 0) continue; + var cutDist = 900; // 30*30 TODO maybe increase it for (let str of ownStructures) { let strPos = str.position(); if (!strPos) continue; - var dist = API3.SquareVectorDistance(strPos, pos); - if ((isTower && str.hasClass("Tower")) || (isFortress && str.hasClass("Fortress"))) - var cutDist = 4225; // TODO check on true buildrestrictions instead of this 65*65 - else - var cutDist = 900; // 30*30 TODO maybe increase it - if (dist < cutDist) + if (API3.SquareVectorDistance(strPos, pos) < cutDist) { minDist = -1; break; } } if (minDist < 0) continue; if (bestVal !== undefined && minDist > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = minDist; bestIdx = i; bestJdx = j; } if (bestVal === undefined) return undefined; var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.basesMap.map[bestJdx]]; }; m.HQ.prototype.buildTemple = function(gameState, queues) { // at least one market (which have the same queue) should be build before any temple if (gameState.currentPhase() < 3 || queues.economicBuilding.countQueuedUnits() != 0 || gameState.getOwnEntitiesByClass("Temple", true).length != 0 || gameState.getOwnEntitiesByClass("BarterMarket", true).length == 0) return; if (!this.canBuild(gameState, "structures/{civ}_temple")) return; queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_temple")); }; m.HQ.prototype.buildMarket = function(gameState, queues) { if (gameState.getOwnEntitiesByClass("BarterMarket", true).length > 0 || !this.canBuild(gameState, "structures/{civ}_market")) return; if (queues.economicBuilding.countQueuedUnitsWithClass("BarterMarket") > 0) { if (!this.navalMap && !queues.economicBuilding.paused) { // Put available resources in this market when not a naval map let queueManager = gameState.ai.queueManager; let cost = queues.economicBuilding.queue[0].getCost(); queueManager.setAccounts(gameState, cost, "economicBuilding"); if (!queueManager.accounts["economicBuilding"].canAfford(cost)) { for (let p in queueManager.queues) { if (p === "economicBuilding") continue; queueManager.transferAccounts(cost, p, "economicBuilding"); if (queueManager.accounts["economicBuilding"].canAfford(cost)) break; } } } return; } if (gameState.getPopulation() < this.Config.Economy.popForMarket) return; gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding); var plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); plan.onStart = function(gameState) { gameState.ai.queueManager.changePriority("economicBuilding", gameState.ai.Config.priorities.economicBuilding); }; queues.economicBuilding.addItem(plan); }; // Build a farmstead m.HQ.prototype.buildFarmstead = function(gameState, queues) { // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) if (gameState.getOwnEntitiesByClass("Farmstead", true).length > 0) return; // Wait to have at least one dropsite and house before the farmstead if (gameState.getOwnEntitiesByClass("Storehouse", true).length == 0) return; if (gameState.getOwnEntitiesByClass("House", true).length == 0) return; if (queues.economicBuilding.countQueuedUnitsWithClass("DropsiteFood") > 0) return; if (!this.canBuild(gameState, "structures/{civ}_farmstead")) return; queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); }; // build more houses if needed. // kinda ugly, lots of special cases to both build enough houses but not tooo many… m.HQ.prototype.buildMoreHouses = function(gameState,queues) { if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return; var numPlanned = queues.house.length(); if (numPlanned < 3 || (numPlanned < 5 && gameState.getPopulation() > 80)) { var plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); // change the starting condition according to the situation. plan.isGo = function (gameState) { if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house")) return false; if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return false; var HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; // TODO popBonus may have different values if we can ever capture foundations from another civ // and take into account other structures than houses var popBonus = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house")).getPopulationBonus(); var freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - gameState.getPopulation(); if (gameState.ai.HQ.saveResources) return (freeSlots <= 10); else if (gameState.getPopulation() > 55) return (freeSlots <= 21); else if (gameState.getPopulation() > 30) return (freeSlots <= 15); else return (freeSlots <= 10); }; queues.house.addItem(plan); } if (numPlanned > 0 && this.econState == "townPhasing" && gameState.getPhaseRequirements(2)) { let requirements = gameState.getPhaseRequirements(2); var count = gameState.getOwnStructures().filter(API3.Filters.byClass(requirements["class"])).length; if (requirements && count < requirements["number"] && this.stopBuilding.has(gameState.applyCiv("structures/{civ}_house"))) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.stopBuilding.delete(gameState.applyCiv("structures/{civ}_house")); this.requireHouses = true; } var houseQueue = queues.house.queue; for (var i = 0; i < numPlanned; ++i) { if (houseQueue[i].isGo(gameState)) ++count; else if (count < requirements["number"]) { houseQueue[i].isGo = function () { return true; }; ++count; } } } if (this.requireHouses) { let requirements = gameState.getPhaseRequirements(2); if (gameState.getOwnStructures().filter(API3.Filters.byClass(requirements["class"])).length >= requirements["number"]) this.requireHouses = undefined; } // When population limit too tight // - if no room to build, try to improve with technology // - otherwise increase temporarily the priority of houses var house = gameState.applyCiv("structures/{civ}_house"); var HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; var popBonus = gameState.getTemplate(house).getPopulationBonus(); var freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - gameState.getPopulation(); if (freeSlots < 5) { if (this.stopBuilding.has(house)) { if (this.stopBuilding.get(house) > gameState.ai.elapsedTime) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to improve with technology"); this.researchManager.researchPopulationBonus(gameState, queues); } else { this.stopBuilding.delete(house); var priority = 2*this.Config.priorities.house; } } else var priority = 2*this.Config.priorities.house; } else var priority = this.Config.priorities.house; if (priority && priority != gameState.ai.queueManager.getPriority("house")) gameState.ai.queueManager.changePriority("house", priority); }; // checks the status of the territory expansion. If no new economic bases created, build some strategic ones. m.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.length() > 0) return; // first build one cc if all have been destroyed let activeBases = this.numActiveBase(); if (activeBases == 0) { this.buildFirstBase(gameState); return; } // then expand if we have not enough room available for buildings if (this.stopBuilding.size > 1) { if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build " + uneval(this.stopBuilding)); this.buildNewBase(gameState, queues); return; } // then expand if we have lots of units let numUnits = gameState.getOwnUnits().length; if (numUnits > activeBases * (70 + 15*(activeBases-1)) || (this.saveResources && numUnits > 50)) { if (this.Config.debug > 2) API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); this.buildNewBase(gameState, queues); } }; m.HQ.prototype.buildNewBase = function(gameState, queues, resource) { if (this.numActiveBase() > 0 && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase())) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).length > 0 || queues.civilCentre.length() > 0) return false; var template = (this.numActiveBase() > 0) ? this.bBase[0] : gameState.applyCiv("structures/{civ}_civil_centre"); if (!this.canBuild(gameState, template)) return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base planned with resource " + resource); queues.civilCentre.addItem(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; // Deals with building fortresses and towers along our border with enemies. m.HQ.prototype.buildDefenses = function(gameState, queues) { if (this.saveResources || queues.defenseBuilding.length() !== 0) return; if (gameState.currentPhase() > 2 || gameState.isResearching(gameState.cityPhase())) { // try to build fortresses if (this.canBuild(gameState, "structures/{civ}_fortress")) { let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; if ((numFortresses == 0 || gameState.ai.elapsedTime > (1 + 0.10*numFortresses)*this.fortressLapseTime + this.fortressStartTime) && gameState.getOwnFoundationsByClass("Fortress").length < 2) { this.fortressStartTime = gameState.ai.elapsedTime; if (numFortresses == 0) gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_fortress"); plan.onStart = function(gameState) { gameState.ai.queueManager.changePriority("defenseBuilding", gameState.ai.Config.priorities.defenseBuilding); }; queues.defenseBuilding.addItem(plan); } } } if (this.Config.Military.numWoodenTowers && gameState.currentPhase() < 2 && this.canBuild(gameState, "structures/{civ}_wooden_tower")) { let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers if (numTowers < this.Config.Military.numWoodenTowers && gameState.ai.elapsedTime > this.towerLapseTime + this.fortStartTime) { this.fortStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_wooden_tower")); } return; } if (gameState.currentPhase() < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("DefenseTower", true).filter(API3.Filters.byClass("Town")).length; if ((numTowers == 0 || gameState.ai.elapsedTime > (1 + 0.10*numTowers)*this.towerLapseTime + this.towerStartTime) && gameState.getOwnFoundationsByClass("DefenseTower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower")); } // TODO otherwise protect markets and civilcentres }; m.HQ.prototype.buildBlacksmith = function(gameState, queues) { if (gameState.getPopulation() < this.Config.Military.popForBlacksmith || queues.militaryBuilding.length() != 0 || gameState.getOwnEntitiesByClass("Blacksmith", true).length > 0) return; // build a market before the blacksmith if (gameState.getOwnEntitiesByClass("BarterMarket", true).length == 0) return; if (this.canBuild(gameState, "structures/{civ}_blacksmith")) queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); }; m.HQ.prototype.buildWonder = function(gameState, queues) { if (!this.canBuild(gameState, "structures/{civ}_wonder")) return; if (gameState.ai.queues["wonder"] && gameState.ai.queues["wonder"].length() > 0) return; if (gameState.getOwnEntitiesByClass("Wonder", true).length > 0) return; if (!gameState.ai.queues["wonder"]) gameState.ai.queueManager.addQueue("wonder", 1000); gameState.ai.queues["wonder"].addItem(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); }; // Deals with constructing military buildings (barracks, stables…) // They are mostly defined by Config.js. This is unreliable since changes could be done easily. // TODO: We need to determine these dynamically. Also doesn't build fortresses since the above function does that. // TODO: building placement is bad. Choice of buildings is also fairly dumb. m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { if (this.canBuild(gameState, "structures/{civ}_barracks") && queues.militaryBuilding.length() == 0) { var barrackNb = gameState.getOwnEntitiesByClass("Barracks", true).length; // first barracks. if (barrackNb == 0 && (gameState.getPopulation() > this.Config.Military.popForBarracks1 || (this.econState == "townPhasing" && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5))) { gameState.ai.queueManager.changePriority("militaryBuilding", 2*this.Config.priorities.militaryBuilding); var preferredBase = this.findBestBaseForMilitary(gameState); var plan = new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase }); plan.onStart = function(gameState) { gameState.ai.queueManager.changePriority("militaryBuilding", gameState.ai.Config.priorities.militaryBuilding); }; queues.militaryBuilding.addItem(plan); } // second barracks, then 3rd barrack, and optional 4th for some civs as they rely on barracks more. else if (barrackNb == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2) { var preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); } else if (barrackNb == 2 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 20) { var preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); } else if (barrackNb == 3 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 50 && (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber")) { var preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); } } //build advanced military buildings if (gameState.currentPhase() > 2 && gameState.getPopulation() > 80 && queues.militaryBuilding.length() == 0 && this.bAdvanced.length != 0) { var nAdvanced = 0; for (var advanced of this.bAdvanced) nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); if (nAdvanced == 0 || (nAdvanced < this.bAdvanced.length && gameState.getPopulation() > 120)) { for (var advanced of this.bAdvanced) { if (gameState.countEntitiesAndQueuedByType(advanced, true) < 1 && this.canBuild(gameState, advanced)) { var preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, advanced, { "preferredBase": preferredBase })); break; } } } } }; /** * Construct military building in bases nearest to the ennemies TODO revisit as the nearest one may not be accessible */ m.HQ.prototype.findBestBaseForMilitary = function(gameState) { var ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); var bestBase = 1; var distMin = Math.min(); for (var cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; for (var cc of ccEnts) { if (cc.owner() != PlayerID) continue; var dist = API3.SquareVectorDistance(cc.position(), cce.position()); if (dist > distMin) continue; bestBase = cc.getMetadata(PlayerID, "base"); distMin = dist; } } return bestBase; }; /** * train with highest priority ranged infantry in the nearest civil centre from a given set of positions * and garrison them there for defense */ m.HQ.prototype.trainEmergencyUnits = function(gameState, positions) { if (gameState.ai.queues.emergency.countQueuedUnits() !== 0) return false; var civ = gameState.civ(); // find nearest base anchor var distcut = 20000; var nearestAnchor; var distmin; for (let pos of positions) { let access = gameState.ai.accessibility.getAccessValue(pos); // check nearest base anchor for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.anchor.getMetadata(PlayerID, "access") !== access) continue; if (!base.anchor.trainableEntities(civ)) // base still in construction continue; let queue = base.anchor._entity.trainingQueue; if (queue) { let time = 0; for (let item of queue) if (item.progress > 0 || (item.metadata && item.metadata.garrisonType)) time += item.timeRemaining; if (time/1000 > 5) continue; } let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (nearestAnchor && dist > distmin) continue; distmin = dist; nearestAnchor = base.anchor; } } if (!nearestAnchor || distmin > distcut) return false; // We will choose randomly ranged and melee units, except when garrisonHolder is full // in which case we prefer melee units var numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor); if (nearestAnchor._entity.trainingQueue) { for (var item of nearestAnchor._entity.trainingQueue) { if (item.metadata && item.metadata["garrisonType"]) numGarrisoned += item.count; else if (!item.progress && (!item.metadata || !item.metadata.trainer)) nearestAnchor.stopProduction(item.id); } } var autogarrison = (numGarrisoned < nearestAnchor.garrisonMax() && nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints()); var rangedWanted = (Math.random() > 0.5 && autogarrison); var total = gameState.getResources(); var templateFound; var trainables = nearestAnchor.trainableEntities(civ); var garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); for (let trainable of trainables) { if (gameState.isDisabledTemplates(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier")) continue; if (autogarrison && !MatchesClassList(garrisonArrowClasses, template.classes())) continue; if (!total.canAfford(new API3.Resources(template.cost()))) continue; templateFound = [trainable, template]; if (template.hasClass("Ranged") === rangedWanted) break; } if (!templateFound) return false; // Check first if we can afford it without touching the other accounts // and if not, take some of other accounted resources // TODO sort the queues to be substracted let queueManager = gameState.ai.queueManager; let cost = new API3.Resources(templateFound[1].cost()); queueManager.setAccounts(gameState, cost, "emergency"); if (!queueManager.accounts["emergency"].canAfford(cost)) { for (let p in queueManager.queues) { if (p === "emergency") continue; queueManager.transferAccounts(cost, p, "emergency"); if (queueManager.accounts["emergency"].canAfford(cost)) break; } } var metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; if (autogarrison) metadata.garrisonType = "protection"; gameState.ai.queues.emergency.addItem(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); return true; }; m.HQ.prototype.canBuild = function(gameState, structure) { var type = gameState.applyCiv(structure); // available room to build it if (this.stopBuilding.has(type)) { if (this.stopBuilding.get(type) > gameState.ai.elapsedTime) return false; else this.stopBuilding.delete(type); } if (gameState.isDisabledTemplates(type)) { this.stopBuilding.set(type, Infinity); return false; } var template = gameState.getTemplate(type); if (!template) { this.stopBuilding.set(type, Infinity); if (this.Config.debug > 0) API3.warn("Petra error: trying to build " + structure + " for civ " + gameState.civ() + " but no template found."); } if (!template || !template.available(gameState)) return false; if (this.numActiveBase() < 1) { // if no base, check that we can build outside our territory var buildTerritories = template.buildTerritories(); if (buildTerritories && (!buildTerritories.length || (buildTerritories.length === 1 && buildTerritories[0] === "own"))) { this.stopBuilding.set(type, gameState.ai.elapsedTime + 180); return false; } } // build limits var limits = gameState.getEntityLimits(); var category = template.buildCategory(); if (category && limits[category] && gameState.getEntityCounts()[category] >= limits[category]) return false; return true; }; m.HQ.prototype.stopBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.stopBuilding.has(type)) this.stopBuilding.set(type, Math.max(this.stopBuilding.get(type), gameState.ai.elapsedTime + 180)); else this.stopBuilding.set(type, gameState.ai.elapsedTime + 180); }; m.HQ.prototype.restartBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.stopBuilding.has(type)) this.stopBuilding.delete(type); }; m.HQ.prototype.updateTerritories = function(gameState) { // TODO may-be update also when territory decreases. For the moment, only increases are taking into account if (this.lastTerritoryUpdate == gameState.ai.playedTurn) return; this.lastTerritoryUpdate = gameState.ai.playedTurn; var passabilityMap = gameState.getMap(); var width = this.territoryMap.width; var cellSize = this.territoryMap.cellSize; var expansion = 0; for (var j = 0; j < this.territoryMap.length; ++j) { if (this.borderMap.map[j] > 1) continue; if (this.territoryMap.getOwnerIndex(j) != PlayerID) { if (this.basesMap.map[j] == 0) continue; var base = this.getBaseByID(this.basesMap.map[j]); var index = base.territoryIndices.indexOf(j); if (index == -1) { API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); continue; } base.territoryIndices.splice(index, 1); this.basesMap.map[j] = 0; } else if (this.basesMap.map[j] == 0) { var landPassable = false; var ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); var access; for (var k of ind) { if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) continue; landPassable = true; access = gameState.ai.accessibility.landPassMap[k]; break; } if (!landPassable) continue; var distmin = Math.min(); var baseID = undefined; var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (var base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; var dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (!baseID) continue; this.getBaseByID(baseID).territoryIndices.push(j); this.basesMap.map[j] = baseID; expansion++; } } this.frontierMap = m.createFrontierMap(gameState); if (!expansion) return; // We've increased our territory, so we may have some new room to build this.stopBuilding.clear(); // And if sufficient expansion, check if building a new market would improve our present trade routes var cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; if (expansion * cellArea > 960) this.tradeManager.routeProspection = true; }; /** * returns the base corresponding to baseID */ m.HQ.prototype.getBaseByID = function(baseID) { for (let base of this.baseManagers) if (base.ID === baseID) return base; API3.warn("Petra error: no base found with ID " + baseID); return undefined; }; /** * returns the number of active (i.e. with one cc) bases * TODO should be cached */ m.HQ.prototype.numActiveBase = function() { let num = 0; for (let base of this.baseManagers) if (base.anchor) ++num; return num; }; // Count gatherers returning resources in the number of gatherers of resourceSupplies // to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps). m.HQ.prototype.assignGatherers = function() { for (let base of this.baseManagers) { for (let worker of base.workers.values()) { if (worker.unitAIState().split(".")[1] !== "RETURNRESOURCE") continue; let orders = worker.unitAIOrderData(); if (orders.length < 2 || !orders[1].target || orders[1].target !== worker.getMetadata(PlayerID, "supply")) continue; this.AddTCGatherer(orders[1].target); } } }; m.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) { return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); }; // Check that the chosen position is not too near from an invading army m.HQ.prototype.isNearInvadingArmy = function(pos) { for (let army of this.defenseManager.armies) if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) return true; return false; }; m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) { if (!this.turnCache["firingStructures"]) this.turnCache["firingStructures"] = gameState.updatingCollection("FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); for (let ent of this.turnCache["firingStructures"].values()) { let range = radius + ent.attackRange("Ranged").max; if (API3.SquareVectorDistance(ent.position(), pos) < range*range) return true; } return false; }; // Some functions that register that we assigned a gatherer to a resource this turn // add a gatherer to the turn cache for this supply. m.HQ.prototype.AddTCGatherer = function(supplyID) { if (this.turnCache["resourceGatherer"] && this.turnCache["resourceGatherer"][supplyID] !== undefined) ++this.turnCache["resourceGatherer"][supplyID]; else { if (!this.turnCache["resourceGatherer"]) this.turnCache["resourceGatherer"] = {}; this.turnCache["resourceGatherer"][supplyID] = 1; } }; // remove a gatherer to the turn cache for this supply. m.HQ.prototype.RemoveTCGatherer = function(supplyID) { if (this.turnCache["resourceGatherer"] && this.turnCache["resourceGatherer"][supplyID]) --this.turnCache["resourceGatherer"][supplyID]; else { if (!this.turnCache["resourceGatherer"]) this.turnCache["resourceGatherer"] = {}; this.turnCache["resourceGatherer"][supplyID] = -1; } }; m.HQ.prototype.GetTCGatherer = function(supplyID) { if (this.turnCache["resourceGatherer"] && this.turnCache["resourceGatherer"][supplyID]) return this.turnCache["resourceGatherer"][supplyID]; else return 0; }; // The next two are to register that we assigned a gatherer to a resource this turn. m.HQ.prototype.AddTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) ++this.turnCache["resourceGatherer-" + resource]; else this.turnCache["resourceGatherer-" + resource] = 1; this.turnCache["gatherRates"] = false; }; m.HQ.prototype.GetTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) return this.turnCache["resourceGatherer-" + resource]; else return 0; }; // Some functions are run every turn // Others once in a while m.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.turnCache = {}; this.territoryMap = m.createTerritoryMap(gameState); if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; m.dumpEntity(ent); }); } this.checkEvents(gameState, events, queues); this.researchManager.checkPhase(gameState, queues); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) { this.currentPhase = gameState.currentPhase(); let phaseName = "Unknown Phase"; if (this.currentPhase == 2) phaseName = gameState.getTemplate(gameState.townPhase()).name(); else if (this.currentPhase == 3) phaseName = gameState.getTemplate(gameState.cityPhase()).name(); m.chatNewPhase(gameState, phaseName, false); this.updateTerritories(gameState); } else if (gameState.ai.playedTurn - this.lastTerritoryUpdate > 100) this.updateTerritories(gameState); if (gameState.getGameType() === "wonder") this.buildWonder(gameState, queues); if (this.numActiveBase() > 0) { this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 2 == 1) this.buildMoreHouses(gameState,queues); if (gameState.ai.playedTurn % 4 == 2 && !this.saveResources) this.buildFarmstead(gameState, queues); if (queues.minorTech.length() == 0 && gameState.ai.playedTurn % 5 == 1) this.researchManager.update(gameState, queues); } if (this.numActiveBase() < 1 || (this.Config.difficulty > 0 && gameState.ai.playedTurn % 10 == 7 && gameState.currentPhase() > 1)) this.checkBaseExpansion(gameState, queues); if (gameState.currentPhase() > 1) { if (!this.saveResources) { this.buildMarket(gameState, queues); this.buildBlacksmith(gameState, queues); this.buildTemple(gameState, queues); } if (this.Config.difficulty > 1) this.tradeManager.update(gameState, events, queues); } this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); if (!this.saveResources) this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > 0) this.buildDefenses(gameState, queues); this.assignGatherers(); for (let i = 0; i < this.baseManagers.length; ++i) { this.baseManagers[i].checkEvents(gameState, events, queues); if (((i + gameState.ai.playedTurn)%this.baseManagers.length) === 0) this.baseManagers[i].update(gameState, queues, events); } this.navalManager.update(gameState, queues, events); if (this.Config.difficulty > 0 && (this.numActiveBase() > 0 || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); Engine.ProfileStop(); }; m.HQ.prototype.Serialize = function() { let properties = { "econState": this.econState, "currentPhase": this.currentPhase, "wantedRates": this.wantedRates, "currentRates": this.currentRates, "lastFailedGather": this.lastFailedGather, "femaleRatio": this.femaleRatio, "targetNumWorkers": this.targetNumWorkers, "lastTerritoryUpdate": this.lastTerritoryUpdate, "stopBuilding": this.stopBuilding, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "towerLapseTime": this.towerLapseTime, "fortressStartTime": this.fortressStartTime, "fortressLapseTime": this.fortressLapseTime, "bBase": this.bBase, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "canBuildUnits": this.canBuildUnits, "navalMap": this.navalMap, "landRegions": this.landRegions, "navalRegions": this.navalRegions, "decayingStructures": this.decayingStructures }; let baseManagers = []; for (let base of this.baseManagers) baseManagers.push(base.Serialize()); if (this.Config.debug == -100) { API3.warn(" HQ serialization ---------------------"); API3.warn(" properties " + uneval(properties)); API3.warn(" baseManagers " + uneval(baseManagers)); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); } return { "properties": properties, "baseManagers": baseManagers, "attackManager": this.attackManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), "tradeManager": this.tradeManager.Serialize(), "navalManager": this.navalManager.Serialize(), "researchManager": this.researchManager.Serialize(), "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), }; }; m.HQ.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.baseManagers = []; for (let base of data.baseManagers) { // the first call to deserialize set the ID base needed by entitycollections var newbase = new m.BaseManager(gameState, this.Config); newbase.Deserialize(gameState, base); newbase.init(gameState); newbase.Deserialize(gameState, base); this.baseManagers.push(newbase); } this.navalManager = new m.NavalManager(this.Config); this.navalManager.init(gameState, true); this.navalManager.Deserialize(gameState, data.navalManager); this.attackManager = new m.AttackManager(this.Config); this.attackManager.Deserialize(gameState, data.attackManager); this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); this.defenseManager = new m.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); this.tradeManager = new m.TradeManager(this.Config); this.tradeManager.init(gameState); this.tradeManager.Deserialize(gameState, data.tradeManager); this.researchManager = new m.ResearchManager(this.Config); this.researchManager.Deserialize(data.researchManager); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.diplomacyManager.Deserialize(data.diplomacyManager); this.garrisonManager = new m.GarrisonManager(); this.garrisonManager.Deserialize(data.garrisonManager); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/map-module.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/map-module.js (revision 17384) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/map-module.js (revision 17385) @@ -1,268 +1,271 @@ var PETRA = function(m) { // other map functions m.TERRITORY_PLAYER_MASK = 0x1F; m.createObstructionMap = function(gameState, accessIndex, template) { var passabilityMap = gameState.getMap(); var territoryMap = gameState.ai.territoryMap; var ratio = territoryMap.cellSize / passabilityMap.cellSize; // default values var placementType = "land"; var buildOwn = true; var buildAlly = true; var buildNeutral = true; var buildEnemy = false; // If there is a template then replace the defaults if (template) { placementType = template.buildPlacementType(); buildOwn = template.hasBuildTerritory("own"); buildAlly = template.hasBuildTerritory("ally"); buildNeutral = template.hasBuildTerritory("neutral"); buildEnemy = template.hasBuildTerritory("enemy"); } var obstructionTiles = new Uint8Array(passabilityMap.data.length); if (placementType == "shore") { var passMap = gameState.ai.accessibility.navalPassMap; var obstructionMask = gameState.getPassabilityClassMask("building-shore"); } else { var passMap = gameState.ai.accessibility.landPassMap; var obstructionMask = gameState.getPassabilityClassMask("building-land"); } for (var k = 0; k < territoryMap.data.length; ++k) { let tilePlayer = (territoryMap.data[k] & m.TERRITORY_PLAYER_MASK); if ((!buildNeutral && tilePlayer == 0) || (!buildOwn && tilePlayer == PlayerID) || (!buildAlly && tilePlayer != PlayerID && gameState.isPlayerAlly(tilePlayer)) || (!buildEnemy && tilePlayer != 0 && gameState.isPlayerEnemy(tilePlayer))) continue; let x = ratio * (k % territoryMap.width); let y = ratio * (Math.floor(k / territoryMap.width)); for (let ix = 0; ix < ratio; ++ix) { for (let iy = 0; iy < ratio; ++iy) { let i = x + ix + (y + iy)*passabilityMap.width; if (placementType != "shore" && accessIndex && accessIndex !== passMap[i]) continue; if (!(passabilityMap.data[i] & obstructionMask)) obstructionTiles[i] = 255; } } } var map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles); map.setMaxVal(255); - + if (template && template.buildDistance()) { - var minDist = template.buildDistance().MinDistance; - var category = template.buildDistance().FromCategory; - if (minDist !== undefined && category !== undefined) + let minDist = +template.buildDistance().MinDistance; + let fromClass = template.buildDistance().FromClass; + if (minDist && fromClass) { - gameState.getOwnStructures().forEach(function(ent) { - if (ent.buildCategory() === category && ent.position()) - { - var pos = ent.position(); - var x = Math.round(pos[0] / passabilityMap.cellSize); - var z = Math.round(pos[1] / passabilityMap.cellSize); - map.addInfluence(x, z, minDist/passability.cellSize, -255, "constant"); - } - }); + let cellSize = passabilityMap.cellSize; + let cellDist = 1 + minDist / cellSize; + let structures = gameState.getOwnStructures().filter(API3.Filters.byClass(fromClass)); + for (let ent of structures.values()) + { + if (!ent.position()) + continue; + let pos = ent.position(); + let x = Math.round(pos[0] / cellSize); + let z = Math.round(pos[1] / cellSize); + map.addInfluence(x, z, cellDist, -255, "constant"); + } } } return map; }; m.createTerritoryMap = function(gameState) { var map = gameState.ai.territoryMap; var ret = new API3.Map(gameState.sharedScript, "territory", map.data); ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; }; ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; }; return ret; }; // flag cells around the border of the map (2 if all points into that cell are inaccessible, 1 otherwise) m.createBorderMap = function(gameState) { var map = new API3.Map(gameState.sharedScript, "territory"); var width = map.width; var border = Math.round(80 / map.cellSize); var passabilityMap = gameState.sharedScript.passabilityMap; var obstructionMask = gameState.getPassabilityClassMask("unrestricted"); if (gameState.ai.circularMap) { let ic = (width - 1) / 2; let radcut = (ic - border) * (ic - border); for (let j = 0; j < map.length; ++j) { let dx = j%width - ic; let dy = Math.floor(j/width) - ic; let radius = dx*dx + dy*dy; if (radius < radcut) continue; map.map[j] = 2; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = 1; break; } } } else { let borderCut = width - border; for (let j = 0; j < map.length; ++j) { let ix = j%width; let iy = Math.floor(j/width); if (ix < border || ix >= borderCut || iy < border || iy >= borderCut) { map.map[j] = 2; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = 1; break; } } } } // map.dumpIm("border.png", 5); return map; }; // map of our frontier : 2 means narrow border, 1 means large border m.createFrontierMap = function(gameState) { var territoryMap = gameState.ai.HQ.territoryMap; var borderMap = gameState.ai.HQ.borderMap; const around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ]; var map = new API3.Map(gameState.sharedScript, "territory"); var width = map.width; var insideSmall = Math.round(45 / map.cellSize); var insideLarge = Math.round(80 / map.cellSize); // should be about the range of towers for (var j = 0; j < territoryMap.length; ++j) { if (territoryMap.getOwnerIndex(j) !== PlayerID || borderMap.map[j] > 1) continue; var ix = j%width; var iz = Math.floor(j/width); for (var a of around) { var jx = ix + Math.round(insideSmall*a[0]); if (jx < 0 || jx >= width) continue; var jz = iz + Math.round(insideSmall*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] > 1) continue; if (!gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) { map.map[j] = 2; break; } jx = ix + Math.round(insideLarge*a[0]); if (jx < 0 || jx >= width) continue; jz = iz + Math.round(insideLarge*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] > 1) continue; if (!gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) map.map[j] = 1; } } // m.debugMap(gameState, map); return map; }; // return a measure of the proximity to our frontier (including our allies) // 0=inside, 1=less than 16m, 2= less than 32m, 3= less than 48m, 4=less than 64m, 5=above 64m m.getFrontierProximity = function(gameState, j) { var territoryMap = gameState.ai.HQ.territoryMap; var borderMap = gameState.ai.HQ.borderMap; const around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ]; var width = territoryMap.width; var step = Math.round(16 / territoryMap.cellSize); if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(j))) return 0; var ix = j%width; var iz = Math.floor(j/width); var best = 5; for (let a of around) { for (let i = 1; i < 5; ++i) { let jx = ix + Math.round(i*step*a[0]); if (jx < 0 || jx >= width) continue; var jz = iz + Math.round(i*step*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] > 1) continue; if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) { best = Math.min(best, i); break; } } if (best === 1) break; } return best; }; m.debugMap = function(gameState, map) { var width = map.width; var cell = map.cellSize; gameState.getEntities().forEach( function (ent) { var pos = ent.position(); if (!pos) return; var x = Math.round(pos[0] / cell); var z = Math.round(pos[1] / cell); var id = x + width*z; if (map.map[id] == 1) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); else if (map.map[id] == 2) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); else if (map.map[id] == 3) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,2]}); }); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js (revision 17384) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js (revision 17385) @@ -1,706 +1,705 @@ var PETRA = function(m) { // Defines a construction plan, ie a building. // We'll try to fing a good position if non has been provided m.ConstructionPlan = function(gameState, type, metadata, position) { if (!m.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); // checks other than resource ones. // TODO: change this. // TODO: if there are specific requirements here, maybe try to do them? m.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.ai.HQ.turnCache["buildingBuilt"]) // do not start another building if already one this turn return false; if (!this.isGo(gameState)) return false; if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) return false; return (gameState.findBuilders(this.type).length > 0); }; m.ConstructionPlan.prototype.start = function(gameState) { Engine.ProfileStart("Building construction start"); var builders = gameState.findBuilders(this.type).toEntityArray(); // We don't care which builder we assign, since they won't actually // do the building themselves - all we care about is that there is // some unit that can start the foundation var pos = this.findGoodPosition(gameState); if (!pos) { gameState.ai.HQ.stopBuild(gameState, this.type); Engine.ProfileStop(); return; } else if (this.metadata && this.metadata.expectedGain) { // Check if this market is still worth building (others may have been built making it useless) let tradeManager = gameState.ai.HQ.tradeManager; tradeManager.checkRoutes(gameState); if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { Engine.ProfileStop(); return; } } gameState.ai.HQ.turnCache["buildingBuilt"] = true; if (this.metadata === undefined) this.metadata = { "base": pos.base }; else if (this.metadata.base === undefined) this.metadata.base = pos.base; if (pos.access) this.metadata.access = pos.access; // needed for Docks whose position is on water else this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); if (this.template.buildCategory() === "Dock") { // adjust a bit the position if needed // TODO we would need groundLevel and waterLevel to do it properly let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builders[0].construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builders[0].construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } else if (pos.xx === undefined || (pos.x == pos.xx && pos.z == pos.zz)) builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata); else // try with the lowest, move towards us unless we're same { for (let step = 0; step <= 1; step += 0.2) builders[0].construct(this.type, (step*pos.x + (1-step)*pos.xx), (step*pos.z + (1-step)*pos.zz), pos.angle, this.metadata); } this.onStart(gameState); Engine.ProfileStop(); // TODO should have a ConstructionStarted even in case the construct order fails if (this.metadata && this.metadata.proximity) gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); }; // TODO for dock, we should allow building them outside territory, and we should check that we are along the right sea m.ConstructionPlan.prototype.findGoodPosition = function(gameState) { var template = this.template; if (template.buildCategory() === "Dock") return this.findDockPosition(gameState); if (template.hasClass("Storehouse") && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = gameState.ai.HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; let newpos = base.findBestDropsiteLocation(gameState, type); if (newpos && newpos.quality > 0) { let pos = newpos.pos; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; } } if (!this.position) { if (template.hasClass("CivCentre")) { if (this.metadata && this.metadata.resource) { var proximity = this.metadata.proximity ? this.metadata.proximity : undefined; var pos = gameState.ai.HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else var pos = gameState.ai.HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; else return false; } else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) { var pos = gameState.ai.HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; else if (template.hasClass("DefenseTower") || gameState.civ() === "mace" || gameState.civ() === "maur" || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_fortress"), true) + gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_army_camp"), true) > 0) return false; // if this fortress is our first siege unit builder, just try the standard placement as we want siege units } else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before { var pos = gameState.ai.HQ.findMarketLocation(gameState, template); if (pos && pos[2] > 0) { if (!this.metadata) this.metadata = {}; this.metadata.expectedGain = pos[3]; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; } else if (!pos) return false; } } // Compute each tile's closeness to friendly structures: var placement = new API3.Map(gameState.sharedScript, "territory"); var cellSize = placement.cellSize; // size of each tile var alreadyHasHouses = false; if (this.position) // If a position was specified then place the building as close to it as possible { var x = Math.floor(this.position[0] / cellSize); var z = Math.floor(this.position[1] / cellSize); placement.addInfluence(x, z, 255); } else // No position was specified so try and find a sensible place to build { // give a small > 0 level as the result of addInfluence is constrained to be > 0 // if we really need houses (i.e. townPhasing without enough village building), do not apply these constraints if (this.metadata && this.metadata.base !== undefined) { var base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] == base) placement.map[j] = 45; } else { for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] != 0) placement.map[j] = 45; } if (!gameState.ai.HQ.requireHouses || !template.hasClass("House")) { gameState.getOwnStructures().forEach(function(ent) { var pos = ent.position(); var x = Math.round(pos[0] / cellSize); var z = Math.round(pos[1] / cellSize); if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1) { if (template.hasClass("Field")) placement.addInfluence(x, z, 80/cellSize, 50); else // If this is not a field add a negative influence because we want to leave this area for fields placement.addInfluence(x, z, 80/cellSize, -20); } else if (template.hasClass("House")) { if (ent.hasClass("House")) { placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses alreadyHasHouses = true; } else placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs } else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && (!ent.hasClass("StoneWall") || ent.hasClass("Gates")))) placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.genericName() == "House") placement.addInfluence(x, z, 120/cellSize, -50); else if (template.hasClass("Military")) placement.addInfluence(x, z, 40/cellSize, -40); }); } if (template.hasClass("Farmstead")) { for (let j = 0; j < placement.map.length; ++j) { var value = placement.map[j] - (gameState.sharedScript.resourceMaps["wood"].map[j])/3; placement.map[j] = value >= 0 ? value : 0; if (gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] /= 2; // we need space around farmstead, so disfavor map border } } } // requires to be inside our territory, and inside our base territory if required // and if our first market, put it on border if possible to maximize distance with next market var favorBorder = template.hasClass("BarterMarket"); var disfavorBorder = (gameState.currentPhase() > 1 && !template.hasDefensiveFire()); var preferredBase = (this.metadata && this.metadata.preferredBase); if (this.metadata && this.metadata.base !== undefined) { var base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] != base) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] += 50; else if (disfavorBorder && gameState.ai.HQ.borderMap.map[j] == 0 && placement.map[j] > 0) placement.map[j] += 10; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] == 0) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] += 50; else if (disfavorBorder && gameState.ai.HQ.borderMap.map[j] == 0 && placement.map[j] > 0) placement.map[j] += 10; if (preferredBase && gameState.ai.HQ.basesMap.map[j] == this.metadata.preferredBase) placement.map[j] += 200; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } // Find the best non-obstructed: // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, // this allows room for units to walk between buildings. // note: not for houses and dropsites who ought to be closer to either each other or a resource. // also not for fields who can be stacked quite a bit var obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); var radius = 0; if (template.hasClass("Fortress") || this.type === gameState.applyCiv("structures/{civ}_siege_workshop") || this.type === gameState.applyCiv("structures/{civ}_elephant_stables")) radius = Math.floor((template.obstructionRadius() + 12) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && !template.hasClass("Field")) radius = Math.ceil((template.obstructionRadius() + 4) / obstructions.cellSize); else radius = Math.ceil((template.obstructionRadius() + 0.5) / obstructions.cellSize); if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first var bestTile = placement.findBestTile(3*radius, obstructions); var bestIdx = bestTile[0]; var bestVal = bestTile[1]; } if (bestVal === undefined || bestVal == -1) { var bestTile = placement.findBestTile(radius, obstructions); var bestIdx = bestTile[0]; var bestVal = bestTile[1]; } if (bestVal <= 0) return false; var x = ((bestIdx % obstructions.width) + 0.5) * obstructions.cellSize; var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined) { let secondBest = obstructions.findNearestObstructed(bestIdx, radius); if (secondBest >= 0) { x = ((secondBest % obstructions.width) + 0.5) * obstructions.cellSize; z = (Math.floor(secondBest / obstructions.width) + 0.5) * obstructions.cellSize; } } let territorypos = placement.gamePosToMapPos([x, z]); let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": gameState.ai.HQ.basesMap.map[territoryIndex] }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory */ m.ConstructionPlan.prototype.findDockPosition = function(gameState) { var template = this.template; var territoryMap = gameState.ai.HQ.territoryMap; var obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); var bestIdx; var bestJdx; var bestAngle; var bestLand; var bestVal = -1; - var landPassMap = gameState.ai.accessibility.landPassMap; var navalPassMap = gameState.ai.accessibility.navalPassMap; var width = gameState.ai.HQ.territoryMap.width; var cellSize = gameState.ai.HQ.territoryMap.cellSize; var nbShips = gameState.ai.HQ.navalManager.transportShips.length; var proxyAccess; if (this.metadata.proximity) proxyAccess = gameState.ai.accessibility.getAccessValue(this.metadata.proximity); var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); var halfSize = 0; // used for dock angle var halfDepth = 0; // used by checkPlacement var halfWidth = 0; // used by checkPlacement if (template.get("Footprint/Square")) { halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; halfDepth = +template.get("Footprint/Square/@depth") / 2; halfWidth = +template.get("Footprint/Square/@width") / 2; } else if (template.get("Footprint/Circle")) { halfSize = +template.get("Footprint/Circle/@radius"); halfDepth = halfSize; halfWidth = halfSize; } var maxres = 10; for (let j = 0; j < territoryMap.length; ++j) { var i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; var landAccess = this.getLandAccess(gameState, i, radius+1, obstructions.width); if (landAccess.size == 0) continue; if (this.metadata) { if (this.metadata.land && !landAccess.has(+this.metadata.land)) continue; if (this.metadata.sea && navalPassMap[i] != +this.metadata.sea) continue; if (nbShips === 0 && proxyAccess && proxyAccess > 1 && !landAccess.has(proxyAccess)) continue; } var dist; var res = Math.min(maxres, this.getResourcesAround(gameState, j, 80)); var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; if (this.metadata.proximity) { // if proximity is given, we look for the nearest point dist = API3.SquareVectorDistance(this.metadata.proximity, pos); dist = Math.sqrt(dist) + 15 * (maxres - res); } else { // if not in our (or allied) territory, we do not want it too far to be able to defend it dist = m.getFrontierProximity(gameState, j); if (dist > 4) continue; dist += 0.4 * (maxres - res); } // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] > 0) dist += 2; if (bestIdx !== undefined && dist > bestVal) continue; let x = ((i % obstructions.width) + 0.5) * obstructions.cellSize; let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; let angle = this.getDockAngle(gameState, x, z, halfSize); if (angle === false) continue; let land = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); if (land < 2 || !gameState.ai.HQ.landRegions[land]) continue; if (this.metadata.proximity && gameState.ai.accessibility.regionSize[land] < 4000) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = dist; bestIdx = i; bestJdx = j; bestAngle = angle; bestLand = land; } if (bestVal < 0) return false; var x = ((bestIdx % obstructions.width) + 0.5) * obstructions.cellSize; var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Assign this dock to a base var baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; if (!baseIndex) { for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex !== bestLand) continue; baseIndex = base.ID; break; } if (!baseIndex) { if (gameState.ai.HQ.numActiveBase() > 0) API3.warn("Petra: dock constructed without base index " + baseIndex); else baseIndex = gameState.ai.HQ.baseManagers[0].ID; } } return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; // Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { var pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); var j = pos[0] + pos[1]*gameState.ai.accessibility.width; var seaRef = gameState.ai.accessibility.navalPassMap[j]; if (seaRef < 2) return false; const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { let angle = (i/numPoints)*2*Math.PI; pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; pos = gameState.ai.accessibility.gamePosToMapPos(pos); let j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.navalPassMap[j] == seaRef) waterPoints.push(i); } var length = waterPoints.length; if (!length) continue; var consec = []; for (var i = 0; i < length; ++i) { var count = 0; for (var j = 0; j < (length-1); ++j) { if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } var start = 0; var count = 0; for (var c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; } return false; }; // Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js // to determine the special dock requirements m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) { let sz = halfDepth * Math.sin(angle); let cz = halfDepth * Math.cos(angle); // center back position let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); let j = pos[0] + pos[1]*gameState.ai.accessibility.width; let ret = gameState.ai.accessibility.landPassMap[j]; if (ret < 2) return 0; // center front position pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) return 0; // additional constraints compared to BuildRestriction.js to assure we have enough place to build let sw = halfWidth * Math.cos(angle) * 3 / 4; let cw = halfWidth * Math.sin(angle) * 3 / 4; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != ret) return 0; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != ret) return 0; return ret; }; // get the list of all the land access from this position m.ConstructionPlan.prototype.getLandAccess = function(gameState, i, radius, w) { var access = new Set(); var landPassMap = gameState.ai.accessibility.landPassMap; var kx = i % w; var ky = Math.floor(i / w); var land; for (let dy = 0; dy <= radius; ++dy) { let dxmax = radius - dy; let xp = kx + (ky + dy)*w; let xm = kx + (ky - dy)*w; for (let dx = -dxmax; dx <= dxmax; ++dx) { if (kx + dx < 0 || kx + dx >= w) continue; if (ky + dy >= 0 && ky + dy < w) { land = landPassMap[xp + dx]; if (land > 1 && !access.has(land)) access.add(land); } if (ky - dy >= 0 && ky - dy < w) { land = landPassMap[xm + dx]; if (land > 1 && !access.has(land)) access.add(land); } } } return access; }; // get the sum of the resources (except food) around, inside a given radius // resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood m.ConstructionPlan.prototype.getResourcesAround = function(gameState, i, radius) { let resourceMaps = gameState.sharedScript.resourceMaps; let w = resourceMaps["wood"].width; let cellSize = resourceMaps["wood"].cellSize; let size = Math.floor(radius / cellSize); let ix = i % w; let iy = Math.floor(i / w); let total = 0; let nbcell = 0; for (let k in resourceMaps) { if (k === "food") continue; let weigh0 = (k === "wood") ? 2 : 1; for (let dy = 0; dy <= size; ++dy) { let dxmax = size - dy; let ky = iy + dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = (dx > 0) ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } if (dy == 0) continue; ky = iy - dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = (dx > 0) ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } } } return (nbcell ? (total / nbcell) : 0); }; m.ConstructionPlan.prototype.Serialize = function() { let prop = { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position, "lastIsGo": this.lastIsGo, }; let func = { "isGo": uneval(this.isGo), "onStart": uneval(this.onStart) }; return { "prop": prop, "func": func }; }; m.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.prop) this[key] = data.prop[key]; let cost = new API3.Resources(); cost.Deserialize(data.prop.cost); this.cost = cost; for (let fun in data.func) this[fun] = eval(data.func[fun]); }; return m; }(PETRA);